From 4ef7672008b4a6166c4e34890eb1e5b1f0e96264 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Fri, 6 Nov 2026 08:49:54 +0100 Subject: [PATCH] Music player in the sound settings: pick any calm track or automatic Co-Authored-By: Claude Opus 5.5 --- lang/cs.lang | 2 ++ lang/da.lang | 2 ++ lang/de.lang | 2 ++ lang/el.lang | 2 ++ lang/en.lang | 2 ++ lang/es.lang | 2 ++ lang/fi.lang | 2 ++ lang/hr.lang | 2 ++ lang/hu.lang | 2 ++ lang/it.lang | 2 ++ lang/nl.lang | 2 ++ lang/no.lang | 2 ++ lang/pt.lang | 2 ++ lang/ro.lang | 2 ++ lang/ru.lang | 2 ++ lang/sk.lang | 2 ++ lang/sr.lang | 2 ++ lang/sv.lang | 2 ++ src/audio.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 18 +++++++++++++++--- src/menu.rs | 12 +++++++++++- src/settings.rs | 2 ++ 22 files changed, 108 insertions(+), 4 deletions(-) diff --git a/lang/cs.lang b/lang/cs.lang index 8eaa416..58e2c5d 100644 --- a/lang/cs.lang +++ b/lang/cs.lang @@ -1115,6 +1115,8 @@ ov.summary = Tvorů: {0} - mzdy {1} zlata za výplatu - nálada {2} % ov.none = Zatím žádní tvorové. Postav doupě a houbovou zahradu a zaber portál. ov.hint = Úr., nálada, mzda, práce, zdraví - klik: přejít tam set.voice = Křivoprstův hlas +set.track = Skladba +val.track_auto = Automaticky # ---- campaign editor, block scripts, demo campaign menu.back_editor = Zpět do editoru diff --git a/lang/da.lang b/lang/da.lang index 061e2b9..10e493f 100644 --- a/lang/da.lang +++ b/lang/da.lang @@ -1114,6 +1114,8 @@ ov.summary = {0} væsner - løn {1} guld pr. lønningsdag - humør {2}% ov.none = Endnu ingen væsner. Byg et hi og en svampehave, og indtag en portal. ov.hint = Nv, humør, løn, job, liv - klik: gå derhen set.voice = Krumfingers stemme +set.track = Musiknummer +val.track_auto = Automatisk # ---- campaign editor, block scripts, demo campaign menu.back_editor = Tilbage til editoren diff --git a/lang/de.lang b/lang/de.lang index 6b16562..6dfb414 100644 --- a/lang/de.lang +++ b/lang/de.lang @@ -1114,6 +1114,8 @@ ov.summary = {0} Kreaturen - Lohn {1} Gold pro Zahltag - Laune {2}% ov.none = Noch keine Kreaturen. Baue ein Quartier und einen Pilzgarten und nimm ein Portal ein. ov.hint = St., Laune, Lohn, Tätigkeit, Leben - Klick: hingehen set.voice = Krummfingers Stimme +set.track = Musikstück +val.track_auto = Automatisch # ---- campaign editor, block scripts, demo campaign menu.back_editor = Zurück zum Editor diff --git a/lang/el.lang b/lang/el.lang index 35de2ee..4c794dd 100644 --- a/lang/el.lang +++ b/lang/el.lang @@ -1115,6 +1115,8 @@ ov.summary = {0} πλάσματα - μισθοί {1} χρυσός ανά πλη ov.none = Κανένα πλάσμα ακόμη. Χτίσε φωλιά και μανιταρόκηπο και κατάκτησε μια πύλη μετάβασης. ov.hint = Επ., διάθεση, μισθός, δουλειά, ζωή - κλικ: πήγαινε εκεί set.voice = Η φωνή του Στραβοδάχτυλου +set.track = Μουσικό κομμάτι +val.track_auto = Αυτόματα # ---- campaign editor, block scripts, demo campaign menu.back_editor = Πίσω στον επεξεργαστή diff --git a/lang/en.lang b/lang/en.lang index f71e392..a1c6e57 100644 --- a/lang/en.lang +++ b/lang/en.lang @@ -1115,6 +1115,8 @@ ov.summary = {0} creatures - wages {1} gold per payday - mood {2}% ov.none = No creatures yet. Build a lair and a mushroom garden and claim a portal. ov.hint = Lv, mood, wage, job, health - click: go there set.voice = Crookfinger's voice +set.track = Music track +val.track_auto = Automatic # ---- campaign editor, block scripts, demo campaign menu.back_editor = Back to the editor diff --git a/lang/es.lang b/lang/es.lang index 1f99aec..18e4d9a 100644 --- a/lang/es.lang +++ b/lang/es.lang @@ -1107,6 +1107,8 @@ ov.summary = {0} criaturas - sueldos {1} de oro por paga - ánimo {2}% ov.none = Aún no hay criaturas. Construye una guarida y un huerto de setas y reclama un portal. ov.hint = Nv, ánimo, sueldo, tarea, salud - clic: ir allí set.voice = Voz de Dedotuerto +set.track = Pista musical +val.track_auto = Automática # ---- campaign editor, block scripts, demo campaign menu.back_editor = Volver al editor diff --git a/lang/fi.lang b/lang/fi.lang index b343303..0d9879e 100644 --- a/lang/fi.lang +++ b/lang/fi.lang @@ -1115,6 +1115,8 @@ ov.summary = {0} olentoa - palkat {1} kultaa/palkkapäivä - mieliala {2}% ov.none = Ei vielä olentoja. Rakenna pesä ja sienitarha ja valtaa portaali. ov.hint = Taso, mieliala, palkka, työ, terveys - klikkaa: mene sinne set.voice = Kierosormen ääni +set.track = Kappale +val.track_auto = Automaattinen # ---- campaign editor, block scripts, demo campaign menu.back_editor = Takaisin editoriin diff --git a/lang/hr.lang b/lang/hr.lang index bd820b9..832412a 100644 --- a/lang/hr.lang +++ b/lang/hr.lang @@ -1115,6 +1115,8 @@ ov.summary = Stvorenja: {0} - plaće {1} zlata po isplati - raspoloženje {2}% ov.none = Još nema stvorenja. Izgradi brlog i vrt gljiva i osvoji portal. ov.hint = Raz., raspoloženje, plaća, posao, zdravlje - klik: idi onamo set.voice = Krivoprstov glas +set.track = Glazbena pjesma +val.track_auto = Automatski # ---- campaign editor, block scripts, demo campaign menu.back_editor = Natrag u uređivač diff --git a/lang/hu.lang b/lang/hu.lang index 8027492..01b14bf 100644 --- a/lang/hu.lang +++ b/lang/hu.lang @@ -1115,6 +1115,8 @@ ov.summary = {0} lény - bér: {1} arany fizetésnaponként - hangulat: {2}% ov.none = Még nincsenek lényeid. Építs odút és gombakertet, és foglalj el egy portált. ov.hint = Szint, hangulat, bér, munka, életerő - kattintás: odamegy set.voice = Görbeujj hangja +set.track = Zeneszám +val.track_auto = Automatikus # ---- campaign editor, block scripts, demo campaign menu.back_editor = Vissza a szerkesztőbe diff --git a/lang/it.lang b/lang/it.lang index bd3686e..df8d5de 100644 --- a/lang/it.lang +++ b/lang/it.lang @@ -1107,6 +1107,8 @@ ov.summary = {0} creature - salari {1} oro per paga - umore {2}% ov.none = Ancora nessuna creatura. Costruisci una tana e una fungaia e conquista un portale. ov.hint = Liv, umore, salario, lavoro, salute - clic: vai lì set.voice = Voce di Ditastorte +set.track = Brano musicale +val.track_auto = Automatico # ---- campaign editor, block scripts, demo campaign menu.back_editor = Torna all'editor diff --git a/lang/nl.lang b/lang/nl.lang index bcb0f5a..2fac40e 100644 --- a/lang/nl.lang +++ b/lang/nl.lang @@ -1115,6 +1115,8 @@ ov.summary = {0} wezens - loon {1} goud per betaaldag - stemming {2}% ov.none = Nog geen wezens. Bouw een hol en een paddenstoeltuin en neem een portaal in. ov.hint = Nv, stemming, loon, taak, leven - klik: ga erheen set.voice = Krommevingers stem +set.track = Muziekstuk +val.track_auto = Automatisch # ---- campaign editor, block scripts, demo campaign menu.back_editor = Terug naar de editor diff --git a/lang/no.lang b/lang/no.lang index e1b690c..4958190 100644 --- a/lang/no.lang +++ b/lang/no.lang @@ -1114,6 +1114,8 @@ ov.summary = {0} skapninger - lønn {1} gull per lønningsdag - humør {2}% ov.none = Ingen skapninger ennå. Bygg et hi og en soppehage, og innta en portal. ov.hint = Nv, humør, lønn, jobb, liv - klikk: gå dit set.voice = Krokfingers stemme +set.track = Musikkspor +val.track_auto = Automatisk # ---- campaign editor, block scripts, demo campaign menu.back_editor = Tilbake til editoren diff --git a/lang/pt.lang b/lang/pt.lang index 1bdb65d..3168e9c 100644 --- a/lang/pt.lang +++ b/lang/pt.lang @@ -1115,6 +1115,8 @@ ov.summary = {0} criaturas - salários {1} de ouro por pagamento - humor {2}% ov.none = Ainda não há criaturas. Constrói um covil e uma horta de cogumelos e reclama um portal. ov.hint = Nv, humor, salário, tarefa, vida - clique: ir até lá set.voice = Voz do Dedotorto +set.track = Faixa musical +val.track_auto = Automática # ---- campaign editor, block scripts, demo campaign menu.back_editor = Voltar ao editor diff --git a/lang/ro.lang b/lang/ro.lang index 0adb254..6a97e1a 100644 --- a/lang/ro.lang +++ b/lang/ro.lang @@ -1115,6 +1115,8 @@ ov.summary = {0} creaturi - simbrii {1} aur pe zi de plată - moral {2}% ov.none = Încă nicio creatură. Construiește un bârlog și o grădină de ciuperci și cucerește un portal. ov.hint = Nv, moral, simbrie, sarcină, viață - clic: du-te acolo set.voice = Vocea lui Degetstrâmb +set.track = Melodie +val.track_auto = Automat # ---- campaign editor, block scripts, demo campaign menu.back_editor = Înapoi la editor diff --git a/lang/ru.lang b/lang/ru.lang index 127c014..1673fea 100644 --- a/lang/ru.lang +++ b/lang/ru.lang @@ -1107,6 +1107,8 @@ ov.summary = Существ: {0} - жалованье {1} золота за вы ov.none = Существ пока нет. Построй логово и грибной сад и захвати портал. ov.hint = Ур., настроение, жалованье, дело, здоровье - клик: перейти set.voice = Голос Кривопала +set.track = Мелодия +val.track_auto = Автоматически # ---- campaign editor, block scripts, demo campaign menu.back_editor = Назад в редактор diff --git a/lang/sk.lang b/lang/sk.lang index b4f057e..fe9f8be 100644 --- a/lang/sk.lang +++ b/lang/sk.lang @@ -1115,6 +1115,8 @@ ov.summary = Tvorov: {0} - mzdy {1} zlata za výplatu - nálada {2} % ov.none = Zatiaľ žiadne tvory. Postav brloh a hubovú záhradu a zaber portál. ov.hint = Úr., nálada, mzda, práca, zdravie - klik: prejsť tam set.voice = Krivoprstov hlas +set.track = Skladba +val.track_auto = Automaticky # ---- campaign editor, block scripts, demo campaign menu.back_editor = Späť do editora diff --git a/lang/sr.lang b/lang/sr.lang index 6b4193f..e08b6b9 100644 --- a/lang/sr.lang +++ b/lang/sr.lang @@ -1115,6 +1115,8 @@ ov.summary = Створења: {0} - плате {1} злата по исплат ov.none = Још нема створења. Изгради брлог и врт печурака и освоји портал. ov.hint = Ниво, расположење, плата, посао, здравље - клик: иди тамо set.voice = Кривопрстов глас +set.track = Музичка нумера +val.track_auto = Аутоматски # ---- campaign editor, block scripts, demo campaign menu.back_editor = Назад у уређивач diff --git a/lang/sv.lang b/lang/sv.lang index 7fecc31..1516286 100644 --- a/lang/sv.lang +++ b/lang/sv.lang @@ -1115,6 +1115,8 @@ ov.summary = {0} varelser - löner {1} guld per lönedag - humör {2}% ov.none = Inga varelser än. Bygg ett näste och en svampträdgård och erövra en portal. ov.hint = Nv, humör, lön, syssla, hälsa - klick: gå dit set.voice = Krokfingers röst +set.track = Musikspår +val.track_auto = Automatiskt # ---- campaign editor, block scripts, demo campaign menu.back_editor = Tillbaka till redigeraren diff --git a/src/audio.rs b/src/audio.rs index d0a4787..497b4e7 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -30,6 +30,36 @@ pub fn tracks_in(c: Category) -> Vec { TRACKS.iter().enumerate().filter(|(_, t)| t.1 == c).map(|(i, _)| i).collect() } +// the calm tracks the music player offers (no battle, victory or defeat music) +pub fn player_tracks() -> Vec { + use Category as C; + TRACKS.iter().enumerate().filter(|(_, t)| matches!(t.1, C::Title | C::Build | C::Surface | C::Torture | C::Temple)).map(|(i, _)| i).collect() +} + +pub fn track_id(t: usize) -> &'static str { + TRACKS.get(t).map_or("", |t| t.0) +} + +pub fn track_by_id(id: &str) -> Option { + if id.is_empty() { + return None; + } + TRACKS.iter().position(|t| t.0 == id) +} + +// the song title stored in the XM module (without the "DungeonCraft - " prefix) +pub fn track_title(t: usize) -> String { + let Some(d) = TRACKS.get(t).map(|t| t.2) else { return String::new() }; + let raw = d.get(17..37).unwrap_or(&[]); + let name: String = raw.iter().take_while(|&&b| b != 0).map(|&b| b as char).collect(); + let name = name.trim().trim_start_matches("DungeonCraft - ").trim().to_string(); + if name.is_empty() { + track_id(t).to_string() + } else { + name + } +} + const MUSIC_GAIN: f32 = 0.45; // our replayer is ~4x louder than libopenmpt; leaves headroom for effects const SFX_GAIN: f32 = 0.55; @@ -218,6 +248,20 @@ pub extern "C" fn dc_audio_render(frames: u32) -> *const f32 { mod tests { use super::*; + #[test] + fn the_music_player_offers_only_calm_tracks_with_titles() { + let l = player_tracks(); + assert!(l.len() > 20); + for t in l { + let id = track_id(t); + assert!(!id.starts_with("battle") && !id.starts_with("victory") && id != "defeat" && !id.starts_with("assault"), "{}", id); + assert!(!track_title(t).is_empty()); + assert_eq!(track_by_id(id), Some(t)); + } + assert_eq!(track_by_id(""), None); + assert_eq!(track_title(track_by_id("build13").unwrap()), "Underground River"); + } + #[test] fn engine_crossfade_and_sfx() { dc_audio_init(48000.0); diff --git a/src/main.rs b/src/main.rs index 13af002..9802667 100644 --- a/src/main.rs +++ b/src/main.rs @@ -481,7 +481,8 @@ impl MusicDirector { t } - fn update(&mut self, g: &Game, title: bool, cam: (f32, f32)) { + // `chosen`: a track picked in the music player; it replaces the calm music + fn update(&mut self, g: &Game, title: bool, cam: (f32, f32), chosen: Option) { use audio::Category as C; let alive = |f: &dyn Fn(&game::Ent) -> bool| g.ents.iter().any(|e| e.alive && f(e)); let near_room = |k: map::Kind| { @@ -535,8 +536,19 @@ impl MusicDirector { C::Build | C::Torture | C::Temple if above => C::Surface, w => w, }; + let calm = matches!(want, C::Build | C::Title | C::Surface | C::Torture | C::Temple); + if let (true, Some(t)) = (calm, chosen) { + if audio::current_track() != Some(t) { + let fade = if self.current.map_or(false, |c| matches!(c, C::Victory | C::Defeat)) { 1.0 } else { 2.5 }; + audio::play_music(t, fade); + } + self.current = Some(want); + return; + } let rotate = matches!(want, C::Build | C::Title | C::Surface) && self.current == Some(want) && audio::music_loops() >= 2; - if self.current != Some(want) || rotate || audio::current_track().is_none() { + // after the chosen track was let go, the director picks again + let foreign = chosen.is_none() && audio::current_track().map_or(false, |t| crate::tracks::TRACKS.get(t).map_or(false, |x| x.1 != want)) && calm && self.current == Some(want); + if self.current != Some(want) || rotate || foreign || audio::current_track().is_none() { let before = self.current; self.current = Some(want); let t = self.pick(want); @@ -2005,7 +2017,7 @@ app.pending_briefing = briefing_of(&campaign, i); | Some(Page::Match { from_title: true }) | Some(Page::Achievements { from_title: true }) ); - music.update(&app.game, on_title, (app.cam.rx, app.cam.rz)); + music.update(&app.game, on_title, (app.cam.rx, app.cam.rz), audio::track_by_id(&app.settings.music_track)); // ---------------- achievements: count the game, unlock, store and share the profile { let ctx = achievements::Context { coop: app.coop, editor_test: app.editor_testing }; diff --git a/src/menu.rs b/src/menu.rs index 855afdb..62e892e 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -67,12 +67,13 @@ pub enum Item { Events, TouchUi, Voice, + Track, } fn tab_items(tab: usize) -> &'static [Item] { use Item::*; match tab { - 0 => &[Language, Volume, MusicVolume, Mute, Voice], + 0 => &[Language, Volume, MusicVolume, Track, Mute, Voice], 1 => &[RotateSpeed, PanSpeed, ZoomSpeed, EdgeScroll, InvertMouse], 2 => &[Outlines, WallDetail, Lights, UiScale, RenderScale], 3 => &[GameSpeed, Hints, Events, TouchUi], @@ -108,6 +109,7 @@ fn item_label(it: Item) -> String { Item::Events => "set.events", Item::TouchUi => "set.touch_ui", Item::Voice => "set.voice", + Item::Track => "set.track", }) } @@ -126,6 +128,7 @@ fn item_value(it: Item, s: &Settings) -> String { Item::MusicVolume => pct(s.music_volume), Item::Mute => onoff(s.muted), Item::Voice => onoff(s.voice), + Item::Track => crate::audio::track_by_id(&s.music_track).map_or_else(|| tr("val.track_auto"), crate::audio::track_title), Item::RotateSpeed => pct(s.rotate_speed), Item::PanSpeed => pct(s.pan_speed), Item::ZoomSpeed => pct(s.zoom_speed), @@ -184,6 +187,13 @@ fn item_change(it: Item, s: &mut Settings, d: i32) { Item::MusicVolume => s.music_volume = step(s.music_volume, d, 0.0, 1.0, 0.1), Item::Mute => s.muted = !s.muted, Item::Voice => s.voice = !s.voice, + Item::Track => { + // automatic, then every calm track + let mut opts: Vec = vec![String::new()]; + opts.extend(crate::audio::player_tracks().into_iter().map(|t| crate::audio::track_id(t).to_string())); + let i = opts.iter().position(|o| *o == s.music_track).unwrap_or(0) as i32; + s.music_track = opts[(i + d).rem_euclid(opts.len() as i32) as usize].clone(); + } Item::RotateSpeed => s.rotate_speed = step(s.rotate_speed, d, 0.25, 2.5, 0.25), Item::PanSpeed => s.pan_speed = step(s.pan_speed, d, 0.25, 2.5, 0.25), Item::ZoomSpeed => s.zoom_speed = step(s.zoom_speed, d, 0.25, 2.5, 0.25), diff --git a/src/settings.rs b/src/settings.rs index 3f45d6f..7d05be4 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -137,6 +137,7 @@ pub struct Settings { pub music_volume: f32, pub muted: bool, pub voice: bool, // Crookfinger's babbling voice + pub music_track: String, // music player: "" = automatic, else the id of a calm track that replaces the calm music // camera pub rotate_speed: f32, pub pan_speed: f32, @@ -177,6 +178,7 @@ impl Default for Settings { music_volume: 0.6, muted: false, voice: true, + music_track: String::new(), rotate_speed: 1.0, pan_speed: 1.25, zoom_speed: 1.0,