mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
The Music page is an extra, and lists every biome pool's tracks
The Music page becomes a Horizon extra like the others: off by default, a
switch on the Extras page (sound section), musicPage in horizon-extras.txt.
While it is off, Music & Sounds is exactly vanilla's page and no pool is
filtered; turning it on adds the Music... button and applies musicOff.
The page now covers every track the loaded sounds.json can play. After the
seven built-in situations, the tracks only a biome's pool names (the
overworld's deep dark, swamp, badlands and cave pieces, the Nether's) are
listed under Overworld biomes and Nether biomes: vanilla shares them between
several biomes, so no heading claims one. A track several pools share is
listed once, from the first pool that names it, so a row's "play now" always
has a pool that plays it and no track is left out.
That needs rows whose pool is not the group's, so MusicGroup holds
MusicTrack { event, id } and the page's headings are made at run time
(Row::Heading). track_groups is a pure function of the registry, tested with
a registry of base, biome, record and non-music events.
Co-Authored-By: JustKarkat's AI agent (ZCode) <[email protected]>
This commit is contained in:
1 parent
c6e49ac73c
commit
775981f9a8
6 files changed
+299
-87
No files matched your search
@@ -142,6 +142,10 @@ pub struct Extras {
|
||||
/// leave every pool they play in (`musicOff`, empty by default: vanilla's
|
||||
/// random selection).
|
||||
pub music_off: Vec<String>,
|
||||
/// The Music page: the Music... button on Music & Sounds, playing a
|
||||
/// built-in track and leaving the off ones out (`musicPage`, off by
|
||||
/// default: vanilla plays exactly as before).
|
||||
pub music_page: bool,
|
||||
}
|
||||
impl Default for Extras {
|
||||
fn default() -> Self {
|
||||
@@ -186,6 +190,7 @@ impl Default for Extras {
|
||||
splitscreen_names: true,
|
||||
wheel_cycles_settings: true,
|
||||
music_off: Vec::new(),
|
||||
music_page: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,6 +284,7 @@ impl Extras {
|
||||
.map(str::to_owned)
|
||||
.collect()
|
||||
}
|
||||
"musicPage" => e.music_page = flag(e.music_page),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -286,7 +292,7 @@ impl Extras {
|
||||
}
|
||||
pub fn serialize(&self) -> String {
|
||||
format!(
|
||||
"fullbright:{}\ndynamicLights:{}\nzoom:{}\nzoomKey:{}\nzoomLevel:{}\nzoomSmooth:{}\nzoomSensitivity:{}\nfps:{}\nfpsCorner:{}\nfpsLows:{}\nkeystrokes:{}\nkeystrokesCorner:{}\nkeystrokesCps:{}\nmovedToMods:{}\nminimap:{}\nminimapCorner:{}\nminimapSize:{}\nminimapShape:{}\nminimapZoom:{}\nminimapRotate:{}\nautoReconnect:{}\nuiSoundReserve:{}\nownNameTag:{}\nhideBlockParticles:{}\nmouseTweaks:{}\nmouseTweaksWheel:{}\nsort:{}\nsortKey:{}\nchatAnimation:{}\nchatIndicator:{}\n{}:{}\nagentControl:{}\nreproKeys:{}\ndiscordPresence:{}\nsplitscreen:{}\nsplitscreenTwo:{}\nsplitscreenThree:{}\nsplitscreenNames:{}\nwheelCyclesSettings:{}\nmusicOff:{}\n",
|
||||
"fullbright:{}\ndynamicLights:{}\nzoom:{}\nzoomKey:{}\nzoomLevel:{}\nzoomSmooth:{}\nzoomSensitivity:{}\nfps:{}\nfpsCorner:{}\nfpsLows:{}\nkeystrokes:{}\nkeystrokesCorner:{}\nkeystrokesCps:{}\nmovedToMods:{}\nminimap:{}\nminimapCorner:{}\nminimapSize:{}\nminimapShape:{}\nminimapZoom:{}\nminimapRotate:{}\nautoReconnect:{}\nuiSoundReserve:{}\nownNameTag:{}\nhideBlockParticles:{}\nmouseTweaks:{}\nmouseTweaksWheel:{}\nsort:{}\nsortKey:{}\nchatAnimation:{}\nchatIndicator:{}\n{}:{}\nagentControl:{}\nreproKeys:{}\ndiscordPresence:{}\nsplitscreen:{}\nsplitscreenTwo:{}\nsplitscreenThree:{}\nsplitscreenNames:{}\nwheelCyclesSettings:{}\nmusicOff:{}\nmusicPage:{}\n",
|
||||
bool_text(self.fullbright),
|
||||
bool_text(self.dynamic_lights),
|
||||
bool_text(self.zoom),
|
||||
@@ -332,6 +338,7 @@ impl Extras {
|
||||
bool_text(self.splitscreen_names),
|
||||
bool_text(self.wheel_cycles_settings),
|
||||
self.music_off.join(","),
|
||||
bool_text(self.music_page),
|
||||
)
|
||||
}
|
||||
/// Binds the extra key `name` (`SORT_KEY`); false for
|
||||
@@ -459,6 +466,8 @@ pub enum Item {
|
||||
HideBlockParticles,
|
||||
AutoReconnect,
|
||||
UiSoundReserve,
|
||||
/// The Music... page on Music & Sounds (`Music page`).
|
||||
MusicPage,
|
||||
MouseTweaks,
|
||||
MouseTweaksWheel,
|
||||
Sort,
|
||||
@@ -501,6 +510,7 @@ pub fn rows() -> Vec<Result<Item, &'static str>> {
|
||||
Ok(AutoReconnect),
|
||||
Err("horizon.extras.header.sound"),
|
||||
Ok(UiSoundReserve),
|
||||
Ok(MusicPage),
|
||||
Err("horizon.extras.header.menus"),
|
||||
Ok(WheelCyclesSettings),
|
||||
]
|
||||
@@ -515,6 +525,7 @@ impl Item {
|
||||
Item::HideBlockParticles => "horizon.extras.hide_block_particles",
|
||||
Item::AutoReconnect => "horizon.extras.auto_reconnect",
|
||||
Item::UiSoundReserve => "horizon.extras.ui_sound_reserve",
|
||||
Item::MusicPage => "horizon.extras.music_page",
|
||||
Item::MouseTweaks => "horizon.extras.mouse_tweaks",
|
||||
Item::MouseTweaksWheel => "horizon.extras.mouse_tweaks_wheel",
|
||||
Item::Sort => "horizon.extras.sort",
|
||||
@@ -538,6 +549,7 @@ impl Item {
|
||||
Item::HideBlockParticles => "horizon.extras.hide_block_particles.tooltip",
|
||||
Item::AutoReconnect => "horizon.extras.auto_reconnect.tooltip",
|
||||
Item::UiSoundReserve => "horizon.extras.ui_sound_reserve.tooltip",
|
||||
Item::MusicPage => "horizon.extras.music_page.tooltip",
|
||||
Item::MouseTweaks => "horizon.extras.mouse_tweaks.tooltip",
|
||||
Item::MouseTweaksWheel => "horizon.extras.mouse_tweaks_wheel.tooltip",
|
||||
Item::Sort => "horizon.extras.sort.tooltip",
|
||||
@@ -570,6 +582,7 @@ impl Item {
|
||||
Item::HideBlockParticles => "Hide block-break particles",
|
||||
Item::AutoReconnect => "Auto-reconnect",
|
||||
Item::UiSoundReserve => "Keep channels for UI sounds",
|
||||
Item::MusicPage => "Music page",
|
||||
Item::MouseTweaks => "Mouse Tweaks controls",
|
||||
Item::MouseTweaksWheel => "Scroll wheel moves items",
|
||||
Item::Sort => "Sort buttons",
|
||||
@@ -594,7 +607,7 @@ impl Item {
|
||||
ChatIndicator => ChatAnimation,
|
||||
Fullbright | DynamicLights | OwnNameTag | HideBlockParticles | MouseTweaks | Sort
|
||||
| AutoReconnect | NoAi | AgentControl | ReproKeys | DiscordPresence | Splitscreen
|
||||
| ChatAnimation | WheelCyclesSettings => {
|
||||
| ChatAnimation | WheelCyclesSettings | MusicPage => {
|
||||
return None;
|
||||
}
|
||||
})
|
||||
@@ -618,6 +631,9 @@ impl Item {
|
||||
Item::UiSoundReserve => {
|
||||
"Sounds in the world leave 16 of the 247 sound channels free, so a flood of them (a big explosion) can't silence menu clicks."
|
||||
}
|
||||
Item::MusicPage => {
|
||||
"Music & Sounds gets a Music... page: play any built-in track now, or turn one off so the game stops picking it. Nothing changes until you turn a track off."
|
||||
}
|
||||
Item::MouseTweaks => {
|
||||
"Right-drag places one per slot, left-drag gathers the held item, Shift + left-drag moves every slot."
|
||||
}
|
||||
@@ -660,6 +676,7 @@ impl Item {
|
||||
match self {
|
||||
Item::Fullbright => Some(Cost::None),
|
||||
Item::UiSoundReserve => Some(Cost::None),
|
||||
Item::MusicPage => Some(Cost::None),
|
||||
Item::DynamicLights => Some(Cost::Low),
|
||||
Item::ChatAnimation => Some(Cost::None),
|
||||
Item::OwnNameTag
|
||||
@@ -681,6 +698,7 @@ impl Item {
|
||||
Item::HideBlockParticles => e.hide_block_particles,
|
||||
Item::AutoReconnect => e.auto_reconnect,
|
||||
Item::UiSoundReserve => e.ui_sound_reserve,
|
||||
Item::MusicPage => e.music_page,
|
||||
Item::MouseTweaks => e.mouse_tweaks,
|
||||
Item::MouseTweaksWheel => e.mouse_tweaks_wheel,
|
||||
Item::Sort => e.sort,
|
||||
@@ -715,6 +733,7 @@ impl Item {
|
||||
Item::HideBlockParticles => e.hide_block_particles = !e.hide_block_particles,
|
||||
Item::AutoReconnect => e.auto_reconnect = !e.auto_reconnect,
|
||||
Item::UiSoundReserve => e.ui_sound_reserve = !e.ui_sound_reserve,
|
||||
Item::MusicPage => e.music_page = !e.music_page,
|
||||
Item::MouseTweaks => e.mouse_tweaks = !e.mouse_tweaks,
|
||||
Item::MouseTweaksWheel => e.mouse_tweaks_wheel = !e.mouse_tweaks_wheel,
|
||||
Item::Sort => e.sort = !e.sort,
|
||||
@@ -749,6 +768,7 @@ mod tests {
|
||||
assert!(!d.zoom && !d.fps && !d.keystrokes && !d.minimap && !d.auto_reconnect);
|
||||
assert!(!d.mouse_tweaks && !d.sort && !d.own_name_tag && !d.ui_sound_reserve);
|
||||
assert!(!d.hide_block_particles);
|
||||
assert!(!d.music_page && d.music_off.is_empty());
|
||||
assert!(d.discord_presence && d.wheel_cycles_settings);
|
||||
assert_eq!(Extras::parse(""), d);
|
||||
assert_eq!(Extras::parse(&d.serialize()), d);
|
||||
@@ -796,6 +816,9 @@ mod tests {
|
||||
"minecraft:music/game/calm1".into(),
|
||||
"minecraft:music.menu".into(),
|
||||
];
|
||||
assert!(!e.music_page);
|
||||
Item::MusicPage.cycle(&mut e, false);
|
||||
assert_eq!(Item::MusicPage.switch(&e), Some(true));
|
||||
assert_eq!(Extras::parse(&e.serialize()), e);
|
||||
}
|
||||
|
||||
|
||||
@@ -1464,15 +1464,21 @@ pub enum Event {
|
||||
/// A Music page row: play this track of this event now.
|
||||
PlayMusicTrack(String, String),
|
||||
}
|
||||
/// One situation's built-in tracks, for the Music page.
|
||||
/// A row of the Music page: the track, and the pool it is played from.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct MusicTrack {
|
||||
/// The music event the track belongs to (`minecraft:music.game`).
|
||||
pub event: String,
|
||||
/// The sound id (`minecraft:music/game/calm1`).
|
||||
pub id: String,
|
||||
}
|
||||
/// One group of rows on the Music page: a situation, or a dimension's biomes.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct MusicGroup {
|
||||
/// The heading the page shows ("Overworld").
|
||||
pub label: &'static str,
|
||||
/// The music event the tracks belong to (`minecraft:music.game`).
|
||||
pub event: String,
|
||||
/// The sound ids, in `sounds.json` order.
|
||||
pub tracks: Vec<String>,
|
||||
/// The heading the page shows ("Overworld", "Nether biomes").
|
||||
pub label: String,
|
||||
/// The rows, in `sounds.json` order.
|
||||
pub tracks: Vec<MusicTrack>,
|
||||
}
|
||||
/// `SliderableValueSet.applyValueImmediately`: these apply 600 ms after
|
||||
/// the last change (`OptionInstanceSliderButton.delayedApplyAt`).
|
||||
@@ -1814,6 +1820,8 @@ enum Row {
|
||||
Big(Opt),
|
||||
Pair(Opt, Option<Opt>),
|
||||
Header(&'static str),
|
||||
/// A heading the page makes at run time (the Music page's groups).
|
||||
Heading(String),
|
||||
/// A line of grey text (a path).
|
||||
Note(String),
|
||||
Buttons(Widget, Option<Widget>),
|
||||
@@ -2520,17 +2528,20 @@ impl OptionsUi {
|
||||
rows.extend(pairs(&[Opt::ShowSubtitles, Opt::DirectionalAudio]));
|
||||
rows.extend(pairs(&[Opt::MusicFrequency, Opt::MusicToast]));
|
||||
rows.push(Row::Big(Opt::SoundDevice));
|
||||
// Horizon's own music page: the built-in tracks, by situation.
|
||||
rows.push(Row::Buttons(
|
||||
button(
|
||||
0.0,
|
||||
0.0,
|
||||
310.0,
|
||||
tr("horizon.pages.open.music"),
|
||||
Act::Push(Page::Music),
|
||||
),
|
||||
None,
|
||||
));
|
||||
// Horizon's music page (an extra): the built-in tracks, by
|
||||
// situation, once it is on in Horizon Extras.
|
||||
if s.extras.music_page {
|
||||
rows.push(Row::Buttons(
|
||||
button(
|
||||
0.0,
|
||||
0.0,
|
||||
310.0,
|
||||
tr("horizon.pages.open.music"),
|
||||
Act::Push(Page::Music),
|
||||
),
|
||||
None,
|
||||
));
|
||||
}
|
||||
rows
|
||||
}
|
||||
Page::Music => {
|
||||
@@ -2539,24 +2550,24 @@ impl OptionsUi {
|
||||
}
|
||||
let mut rows = Vec::new();
|
||||
for group in &self.music_groups {
|
||||
rows.push(Row::Header(group.label));
|
||||
rows.push(Row::Heading(group.label.clone()));
|
||||
for track in &group.tracks {
|
||||
// Left: the track, played now. Right: whether its pool
|
||||
// still picks it (vanilla until one is turned off).
|
||||
let on = !s.extras.music_off.iter().any(|off| off == track);
|
||||
let on = !s.extras.music_off.contains(&track.id);
|
||||
let play = button(
|
||||
0.0,
|
||||
0.0,
|
||||
150.0,
|
||||
Text::literal(track_name(track).to_owned()),
|
||||
Act::PlayMusicTrack(group.event.clone(), track.clone()),
|
||||
Text::literal(track_name(&track.id).to_owned()),
|
||||
Act::PlayMusicTrack(track.event.clone(), track.id.clone()),
|
||||
);
|
||||
let toggle = button(
|
||||
0.0,
|
||||
0.0,
|
||||
150.0,
|
||||
on_off(on),
|
||||
Act::MusicTrackOn(track.clone(), on),
|
||||
Act::MusicTrackOn(track.id.clone(), on),
|
||||
);
|
||||
rows.push(Row::Buttons(play, Some(toggle)));
|
||||
}
|
||||
@@ -2760,6 +2771,33 @@ impl OptionsUi {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// A section heading of a list (`Row::Header`, `Row::Heading`), at `y`.
|
||||
fn heading_row(&self, out: &mut Vec<Widget>, label: Text, y: &mut f32, w: f32, top: f32) {
|
||||
let padding = if *y + self.scroll <= top + 2.0 {
|
||||
0.0
|
||||
} else {
|
||||
18.0
|
||||
};
|
||||
let label = label.with_style(&Style {
|
||||
bold: Some(true),
|
||||
underlined: Some(true),
|
||||
..Style::default()
|
||||
});
|
||||
out.push(Widget {
|
||||
x: 0.0,
|
||||
y: *y + 2.0 + padding + 4.0,
|
||||
w,
|
||||
h: 9.0,
|
||||
kind: WKind::Text {
|
||||
text: label,
|
||||
color: u32::MAX,
|
||||
centered: true,
|
||||
},
|
||||
list: false,
|
||||
row: 0,
|
||||
});
|
||||
*y += padding + 18.0;
|
||||
}
|
||||
/// Build every widget of the current page.
|
||||
fn layout(&mut self, s: &Settings, w: f32, h: f32) -> (Vec<Widget>, f32, f32, f32) {
|
||||
let mut out = Vec::new();
|
||||
@@ -3219,30 +3257,12 @@ impl OptionsUi {
|
||||
rows.begin(&out, y);
|
||||
match row {
|
||||
Row::Header(key) => {
|
||||
let padding = if y + self.scroll <= top + 2.0 {
|
||||
0.0
|
||||
} else {
|
||||
18.0
|
||||
};
|
||||
let label = tr(key).with_style(&Style {
|
||||
bold: Some(true),
|
||||
underlined: Some(true),
|
||||
..Style::default()
|
||||
});
|
||||
out.push(Widget {
|
||||
x: 0.0,
|
||||
y: y + 2.0 + padding + 4.0,
|
||||
w,
|
||||
h: 9.0,
|
||||
kind: WKind::Text {
|
||||
text: label,
|
||||
color: u32::MAX,
|
||||
centered: true,
|
||||
},
|
||||
list: false,
|
||||
row: 0,
|
||||
});
|
||||
y += padding + 18.0;
|
||||
let label = tr(key);
|
||||
self.heading_row(&mut out, label, &mut y, w, top);
|
||||
}
|
||||
Row::Heading(text) => {
|
||||
let label = Text::literal(text);
|
||||
self.heading_row(&mut out, label, &mut y, w, top);
|
||||
}
|
||||
Row::Note(text) => {
|
||||
out.push(Widget {
|
||||
@@ -7582,16 +7602,26 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn music_and_sounds_opens_the_music_page() {
|
||||
fn the_music_page_button_follows_the_extra() {
|
||||
let atlas = atlas();
|
||||
let size = (960.0, 540.0);
|
||||
let mut s = Settings::default();
|
||||
let mut ui = OptionsUi::default();
|
||||
ui.show(Page::Sound);
|
||||
// The button is the last row: scroll the list to its end first.
|
||||
let open = |ui: &mut OptionsUi, s: &Settings| {
|
||||
let (widgets, ..) = ui.layout(s, size.0, size.1);
|
||||
widgets.into_iter().find(
|
||||
|w| matches!(&w.kind, WKind::Button { act, .. } if *act == Act::Push(Page::Music)),
|
||||
)
|
||||
};
|
||||
// The extra is off by default: Music & Sounds is vanilla's page.
|
||||
assert!(open(&mut ui, &s).is_none());
|
||||
s.extras.music_page = true;
|
||||
assert!(open(&mut ui, &s).is_some(), "Music... once the extra is on");
|
||||
// It is the last row: scroll the list to its end before clicking it.
|
||||
ui.scroll(&s, size, -1000.0);
|
||||
let (widgets, ..) = ui.layout(&s, size.0, size.1);
|
||||
let open = widgets
|
||||
let open_button = widgets
|
||||
.into_iter()
|
||||
.find(
|
||||
|w| matches!(&w.kind, WKind::Button { act, .. } if *act == Act::Push(Page::Music)),
|
||||
@@ -7600,8 +7630,8 @@ mod tests {
|
||||
let events = ui.click(
|
||||
&mut s,
|
||||
&atlas,
|
||||
open.x + open.w / 2.0,
|
||||
open.y + open.h / 2.0,
|
||||
open_button.x + open_button.w / 2.0,
|
||||
open_button.y + open_button.h / 2.0,
|
||||
size,
|
||||
[854, 480],
|
||||
false,
|
||||
@@ -7618,11 +7648,16 @@ mod tests {
|
||||
let mut s = Settings::default();
|
||||
let mut ui = OptionsUi {
|
||||
music_groups: vec![MusicGroup {
|
||||
label: "Overworld",
|
||||
event: "minecraft:music.game".into(),
|
||||
label: "Overworld".into(),
|
||||
tracks: vec![
|
||||
"minecraft:music/game/calm1".into(),
|
||||
"minecraft:music/game/haggstrom".into(),
|
||||
MusicTrack {
|
||||
event: "minecraft:music.game".into(),
|
||||
id: "minecraft:music/game/calm1".into(),
|
||||
},
|
||||
MusicTrack {
|
||||
event: "minecraft:music.overworld.forest".into(),
|
||||
id: "minecraft:music/game/bromeliad".into(),
|
||||
},
|
||||
],
|
||||
}],
|
||||
..OptionsUi::default()
|
||||
@@ -7636,7 +7671,8 @@ mod tests {
|
||||
.expect("button");
|
||||
(w.x + w.w / 2.0, w.y + w.h / 2.0)
|
||||
};
|
||||
// The left of a row plays that track now.
|
||||
// The left of a row plays that track now, from its own pool (a biome
|
||||
// pool's track plays from that pool).
|
||||
let calm1 = "minecraft:music/game/calm1";
|
||||
let (x, y) = at(
|
||||
&mut ui,
|
||||
@@ -7648,6 +7684,17 @@ mod tests {
|
||||
"minecraft:music.game".into(),
|
||||
calm1.into()
|
||||
)));
|
||||
let bromeliad = "minecraft:music/game/bromeliad";
|
||||
let (x, y) = at(
|
||||
&mut ui,
|
||||
&s,
|
||||
&Act::PlayMusicTrack("minecraft:music.overworld.forest".into(), bromeliad.into()),
|
||||
);
|
||||
let events = ui.click(&mut s, &atlas, x, y, size, [854, 480], false, false);
|
||||
assert!(events.contains(&Event::PlayMusicTrack(
|
||||
"minecraft:music.overworld.forest".into(),
|
||||
bromeliad.into()
|
||||
)));
|
||||
// The right of a row turns it off, and the game saves that on `Changed`.
|
||||
let (x, y) = at(&mut ui, &s, &Act::MusicTrackOn(calm1.into(), true));
|
||||
let events = ui.click(&mut s, &atlas, x, y, size, [854, 480], false, false);
|
||||
|
||||
@@ -235,6 +235,7 @@
|
||||
"horizon.extras.hide_block_particles": "Hide block-break particles",
|
||||
"horizon.extras.auto_reconnect": "Auto-reconnect",
|
||||
"horizon.extras.ui_sound_reserve": "Keep channels for UI sounds",
|
||||
"horizon.extras.music_page": "Music page",
|
||||
"horizon.extras.mouse_tweaks": "Mouse Tweaks controls",
|
||||
"horizon.extras.mouse_tweaks_wheel": "Scroll wheel moves items",
|
||||
"horizon.extras.sort": "Sort buttons",
|
||||
@@ -252,6 +253,7 @@
|
||||
"horizon.extras.hide_block_particles.tooltip": "Hides the debris particles that burst from a broken block and crumble off the one you mine. Other particles stay.",
|
||||
"horizon.extras.auto_reconnect.tooltip": "Rejoins a server after a lost connection or a restart, never after a kick or ban.",
|
||||
"horizon.extras.ui_sound_reserve.tooltip": "Sounds in the world leave 16 of the 247 sound channels free, so a flood of them (a big explosion) can't silence menu clicks.",
|
||||
"horizon.extras.music_page.tooltip": "Music & Sounds gets a Music... page: play any built-in track now, or turn one off so the game stops picking it. Nothing changes until you turn a track off.",
|
||||
"horizon.extras.mouse_tweaks.tooltip": "Right-drag places one per slot, left-drag gathers the held item, Shift + left-drag moves every slot.",
|
||||
"horizon.extras.mouse_tweaks_wheel.tooltip": "Down sends one item of the slot to the other side, up brings one back.",
|
||||
"horizon.extras.sort.tooltip": "Sorts the inventory, chests, barrels, ender chests and shulker boxes in creative tab order, merging stacks.",
|
||||
|
||||
@@ -26,8 +26,14 @@ impl Game<'_> {
|
||||
if frequency != self.music.frequency {
|
||||
self.music.set_frequency(frequency, music.as_ref());
|
||||
}
|
||||
// The Music page's off tracks leave every pool they play in.
|
||||
self.music.set_off(&self.settings.extras.music_off);
|
||||
// The Music page's off tracks leave every pool they play in (the
|
||||
// extra off: nothing is filtered, vanilla's pools play as they are).
|
||||
let off = if self.settings.extras.music_page {
|
||||
&self.settings.extras.music_off[..]
|
||||
} else {
|
||||
&[][..]
|
||||
};
|
||||
self.music.set_off(off);
|
||||
// LevelLoadingScreen: no new track before the player is in the world.
|
||||
let loading = self.title.is_none() && self.player.is_none() && self.win.is_none();
|
||||
self.music
|
||||
@@ -50,21 +56,10 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// The Music page's groups: each built-in situation's tracks, from the
|
||||
/// sound registry (`sounds.json`).
|
||||
/// The Music page's groups: every track the loaded `sounds.json` can play,
|
||||
/// by situation (see `music::track_groups`).
|
||||
pub(super) fn music_groups(&self) -> Vec<MusicGroup> {
|
||||
let registry = &self.audio.engine.registry;
|
||||
Music::builtin()
|
||||
.into_iter()
|
||||
.filter_map(|(label, music)| {
|
||||
let tracks = registry.tracks(&music.sound);
|
||||
(!tracks.is_empty()).then_some(MusicGroup {
|
||||
label,
|
||||
event: music.sound,
|
||||
tracks,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
horizon_client::music::track_groups(&self.audio.engine.registry)
|
||||
}
|
||||
/// A Music page row: play this track of `event` now.
|
||||
pub(super) fn play_music_track(&mut self, event: &str, track: &str) {
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
//! `Minecraft.getSituationalMusic`, with the `minecraft:audio/background_music`
|
||||
//! and `minecraft:audio/music_volume` environment attributes of dimension
|
||||
//! types and biomes (a biome value overrides its dimension's).
|
||||
use crate::options_ui::{MusicGroup, MusicTrack};
|
||||
use crate::sound_engine::{InstanceId, MUSIC, PlayResult, SoundEngine, SoundInstance, Sounds};
|
||||
use crate::sound_events::Sentinel;
|
||||
use crate::sound_events::{Sentinel, SoundRegistry};
|
||||
use horizon_core::random::{LegacyRandom, RandomSource};
|
||||
use serde_json::Value;
|
||||
use std::{collections::HashMap, fs, path::Path};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
fs,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
/// `Music`: a sound event and the delay range (ticks) before it starts.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -78,7 +83,84 @@ impl Music {
|
||||
))
|
||||
}
|
||||
}
|
||||
/// `BackgroundMusic`: the attribute value.
|
||||
/// The Music page's groups: each built-in situation's own tracks first, then
|
||||
/// every other `music.` event's tracks that no earlier group listed, under
|
||||
/// its dimension's biomes. A track several pools share is listed once, from
|
||||
/// the first pool that names it, so "play now" always has a pool that plays
|
||||
/// it and no track is left without a row.
|
||||
pub fn track_groups(registry: &SoundRegistry) -> Vec<MusicGroup> {
|
||||
let mut groups: Vec<MusicGroup> = Vec::new();
|
||||
let mut listed: Vec<String> = Vec::new();
|
||||
let builtin = Music::builtin();
|
||||
for (label, music) in &builtin {
|
||||
let tracks = registry.tracks(&music.sound);
|
||||
if tracks.is_empty() {
|
||||
continue;
|
||||
}
|
||||
listed.extend(tracks.iter().cloned());
|
||||
groups.push(MusicGroup {
|
||||
label: (*label).to_owned(),
|
||||
tracks: tracks
|
||||
.into_iter()
|
||||
.map(|id| MusicTrack {
|
||||
event: music.sound.clone(),
|
||||
id,
|
||||
})
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
// `music_disc.*` (records) do not start with "music.".
|
||||
let mut events: Vec<&String> = registry
|
||||
.names()
|
||||
.filter(|event| event.starts_with("minecraft:music."))
|
||||
.collect();
|
||||
events.sort();
|
||||
// Keyed by the heading's rank, so the dimensions keep their order however
|
||||
// the event ids sort.
|
||||
let mut rest: BTreeMap<u8, (&'static str, Vec<MusicTrack>)> = BTreeMap::new();
|
||||
for event in events {
|
||||
if builtin.iter().any(|(_, music)| music.sound == *event) {
|
||||
continue;
|
||||
}
|
||||
let tracks: Vec<MusicTrack> = registry
|
||||
.tracks(event)
|
||||
.into_iter()
|
||||
.filter(|id| !listed.contains(id))
|
||||
.map(|id| MusicTrack {
|
||||
event: event.clone(),
|
||||
id,
|
||||
})
|
||||
.collect();
|
||||
if tracks.is_empty() {
|
||||
continue;
|
||||
}
|
||||
listed.extend(tracks.iter().map(|track| track.id.clone()));
|
||||
let (rank, label) = biome_group(event);
|
||||
rest.entry(rank)
|
||||
.or_insert_with(|| (label, Vec::new()))
|
||||
.1
|
||||
.extend(tracks);
|
||||
}
|
||||
groups.extend(rest.into_values().map(|(label, tracks)| MusicGroup {
|
||||
label: label.to_owned(),
|
||||
tracks,
|
||||
}));
|
||||
groups
|
||||
}
|
||||
/// The heading a biome-only pool's tracks go under: vanilla plays these
|
||||
/// tracks in several biomes, so no heading names a single one.
|
||||
fn biome_group(event: &str) -> (u8, &'static str) {
|
||||
let path = event.strip_prefix("minecraft:").unwrap_or(event);
|
||||
if path.starts_with("music.overworld.") {
|
||||
(1, "Overworld biomes")
|
||||
} else if path.starts_with("music.nether.") {
|
||||
(2, "Nether biomes")
|
||||
} else if path.starts_with("music.end.") {
|
||||
(3, "End biomes")
|
||||
} else {
|
||||
(4, "Other music")
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct BackgroundMusic {
|
||||
pub default: Option<Music>,
|
||||
@@ -800,6 +882,64 @@ mod tests {
|
||||
assert_eq!(bg.select(false, false).unwrap().sound, "a");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_music_page_lists_every_track_once_from_a_pool_that_plays_it() {
|
||||
let mut registry = SoundRegistry::default();
|
||||
registry.add_file(
|
||||
"minecraft",
|
||||
br#"{
|
||||
"music.game": {"sounds": ["music/game/shared", "music/game/second"]},
|
||||
"music.menu": {"sounds": ["music/menu/one"]},
|
||||
"music.overworld.forest": {"sounds": ["music/game/shared", "music/game/bromeliad"]},
|
||||
"music.overworld.swamp": {"sounds": ["music/game/bromeliad", "music/game/aerie"]},
|
||||
"music.nether.warped_forest": {"sounds": ["music/game/rubedo"]},
|
||||
"music_disc.cat": {"sounds": ["records/cat"]},
|
||||
"note": {"sounds": ["block/note/harp"]}
|
||||
}"#,
|
||||
&|_, _| true,
|
||||
);
|
||||
let groups = track_groups(®istry);
|
||||
let summary: Vec<(&str, Vec<&str>)> = groups
|
||||
.iter()
|
||||
.map(|group| {
|
||||
(
|
||||
group.label.as_str(),
|
||||
group
|
||||
.tracks
|
||||
.iter()
|
||||
.map(|track| track.id.rsplit('/').next().unwrap())
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
summary,
|
||||
[
|
||||
("Menu", vec!["one"]),
|
||||
("Overworld", vec!["shared", "second"]),
|
||||
// A biome pool's own track, from the pool that plays it.
|
||||
("Overworld biomes", vec!["bromeliad", "aerie"]),
|
||||
("Nether biomes", vec!["rubedo"]),
|
||||
]
|
||||
);
|
||||
// Each row plays from a pool that has the track; no track repeats.
|
||||
let mut seen = Vec::new();
|
||||
for group in &groups {
|
||||
for track in &group.tracks {
|
||||
assert!(!seen.contains(&track.id), "{} listed twice", track.id);
|
||||
seen.push(track.id.clone());
|
||||
assert!(
|
||||
registry.tracks(&track.event).contains(&track.id),
|
||||
"{} does not play {}",
|
||||
track.event,
|
||||
track.id
|
||||
);
|
||||
}
|
||||
}
|
||||
// Records and other events are not music.
|
||||
assert!(seen.iter().all(|id| !id.contains("records/")));
|
||||
}
|
||||
|
||||
/// The overworld pool, two tracks, and a menu track.
|
||||
fn two_tracks() -> SoundEngine {
|
||||
let mut registry = SoundRegistry::default();
|
||||
|
||||
+14
-9
@@ -1,7 +1,7 @@
|
||||
# Horizon extras
|
||||
|
||||
Optional conveniences built into the client: fullbright, dynamic lights, your own name tag, hiding block-break particles, auto-reconnect,
|
||||
Mouse Tweaks-style inventory controls, inventory sorting and a smooth chat. **Every one is off by default**: a fresh
|
||||
Mouse Tweaks-style inventory controls, inventory sorting, a smooth chat and a music page. **Every one is off by default**: a fresh
|
||||
install looks and behaves exactly like vanilla 26.3, and turning an extra off
|
||||
again restores vanilla's behaviour completely.
|
||||
|
||||
@@ -18,6 +18,7 @@ again restores vanilla's behaviour completely.
|
||||
| Game glue (input hooks, per-frame update) | `crates/horizon-client/src/game/extras.rs` |
|
||||
| Mouse Tweaks-style controls, sort buttons | `crates/horizon-client/src/game/tweaks.rs` |
|
||||
| Sort order and its clicks | `crates/horizon-client/src/inv_sort.rs` (pure, unit tested) |
|
||||
| The music page's rows, and the pools it filters | `options_ui.rs` (`Page::Music`), `crates/horizon-client/src/music.rs` (`track_groups`, `MusicManager::set_off`), `sound_events.rs` (`sound_except`) |
|
||||
| The settings page | `options_ui.rs` (`Page::Extras`): **Options → Horizon Settings... → Extras...** |
|
||||
|
||||
The settings are grouped by extra, under a header each, two buttons to a row as
|
||||
@@ -220,20 +221,24 @@ say **Leave** for disconnecting. `HORIZON_SPLIT_STATS=1` logs every 5 s where a
|
||||
goes (each player's turn and render, the presents, the GPU wait) and each player's frame rate.
|
||||
Joining P1 on an external server follows separately.
|
||||
|
||||
### Music page (`musicOff`, none off by default)
|
||||
- Options > Music & Sounds > **Music...** lists the game's built-in tracks by
|
||||
situation (menu, overworld, creative, underwater, the End, boss, credits),
|
||||
read from the loaded `sounds.json` (`SoundRegistry::tracks`).
|
||||
- **Left click** a track plays it now: it takes the music manager's slot, and the
|
||||
situation's own music waits until it ends.
|
||||
### Music page (`musicPage`, default off; `musicOff`, none off by default)
|
||||
- An extra like the rest: off, Music & Sounds is exactly vanilla's page and every
|
||||
pool plays as vanilla picks. On, it gains a **Music...** button.
|
||||
- The page lists every track the loaded `sounds.json` can play
|
||||
(`SoundRegistry::tracks`): the built-in situations' own tracks first (menu,
|
||||
overworld, creative, underwater, the End, boss, credits), then the tracks only a
|
||||
biome's pool names, under **Overworld biomes** and **Nether biomes** (vanilla
|
||||
shares those between several biomes, so no heading claims one). A track a few
|
||||
pools share is listed once, from the first pool that names it, so every row has
|
||||
a pool its "play now" can use.
|
||||
- **Left click** a track plays it now: it takes the music manager's slot (the track
|
||||
already playing stops), and the situation's own music waits until it ends.
|
||||
- **On/Off** at the right of a row takes a track out of every pool it plays in, so
|
||||
a situation with all its tracks off plays nothing. Every track is on by default,
|
||||
so vanilla's random selection is unchanged; the off ones are `musicOff` in
|
||||
`horizon-extras.txt`.
|
||||
- The pools are filtered where they are picked (`sound_except` in
|
||||
`sound_events.rs`), the manager is `music.rs`, and the page is in `options_ui.rs`.
|
||||
- Tracks only a single biome's pool names (the mangrove or deep-dark pieces) are
|
||||
not rows of their own yet.
|
||||
|
||||
## Shared rules
|
||||
|
||||
|
||||
Reference in new issue
Block a user