diff --git a/OpenRA.Game/GameRules/MusicInfo.cs b/OpenRA.Game/GameRules/MusicInfo.cs index d8568ac898..68ee8c63f7 100644 --- a/OpenRA.Game/GameRules/MusicInfo.cs +++ b/OpenRA.Game/GameRules/MusicInfo.cs @@ -15,8 +15,10 @@ namespace OpenRA.GameRules { public class MusicInfo { - public readonly string Filename = null; - public readonly string Title = null; + public readonly string Filename; + public readonly string Title; + public readonly bool Hidden; + public int Length { get; private set; } // seconds public bool Exists { get; private set; } @@ -25,17 +27,23 @@ namespace OpenRA.GameRules Title = value.Value; var nd = value.ToDictionary(); + if (nd.ContainsKey("Hidden")) + bool.TryParse(nd["Hidden"].Value, out Hidden); + var ext = nd.ContainsKey("Extension") ? nd["Extension"].Value : "aud"; Filename = (nd.ContainsKey("Filename") ? nd["Filename"].Value : key) + "." + ext; + if (!GlobalFileSystem.Exists(Filename)) return; Exists = true; using (var s = GlobalFileSystem.Open(Filename)) + { if (Filename.ToLowerInvariant().EndsWith("wav")) Length = (int)WavLoader.WaveLength(s); else Length = (int)AudLoader.SoundLength(s); + } } public void Reload() @@ -45,10 +53,12 @@ namespace OpenRA.GameRules Exists = true; using (var s = GlobalFileSystem.Open(Filename)) + { if (Filename.ToLowerInvariant().EndsWith("wav")) Length = (int)WavLoader.WaveLength(s); else Length = (int)AudLoader.SoundLength(s); + } } } } diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index f0e91d291e..b31585cea1 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -181,7 +181,6 @@ - diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index 564ab510c2..fbaf2927d0 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -702,6 +702,7 @@ + diff --git a/OpenRA.Mods.Common/Scripting/Global/MediaGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/MediaGlobal.cs index 67121bc392..9672635c20 100644 --- a/OpenRA.Mods.Common/Scripting/Global/MediaGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/MediaGlobal.cs @@ -17,6 +17,7 @@ using OpenRA.FileFormats; using OpenRA.FileSystem; using OpenRA.Graphics; using OpenRA.Mods.Common.Effects; +using OpenRA.Mods.Common.Traits; using OpenRA.Scripting; using OpenRA.Traits; diff --git a/OpenRA.Game/Traits/World/MusicPlaylist.cs b/OpenRA.Mods.Common/Traits/World/MusicPlaylist.cs similarity index 55% rename from OpenRA.Game/Traits/World/MusicPlaylist.cs rename to OpenRA.Mods.Common/Traits/World/MusicPlaylist.cs index 7542d22e2b..af4af52c41 100644 --- a/OpenRA.Game/Traits/World/MusicPlaylist.cs +++ b/OpenRA.Mods.Common/Traits/World/MusicPlaylist.cs @@ -11,8 +11,9 @@ using System; using System.Linq; using OpenRA.GameRules; +using OpenRA.Traits; -namespace OpenRA.Traits +namespace OpenRA.Mods.Common.Traits { [Desc("Trait for music handling. Attach this to the world actor.")] public class MusicPlaylistInfo : ITraitInfo @@ -20,20 +21,15 @@ namespace OpenRA.Traits [Desc("Music to play when the map starts.", "Plays the first song on the playlist when undefined.")] public readonly string StartingMusic = null; - [Desc("Should the starting music loop?")] - public readonly bool LoopStartingMusic = false; - [Desc("Music to play when the game has been won.")] public readonly string VictoryMusic = null; - [Desc("Should the victory music loop?")] - public readonly bool LoopVictoryMusic = false; - [Desc("Music to play when the game has been lost.")] public readonly string DefeatMusic = null; - [Desc("Should the defeat music loop?")] - public readonly bool LoopDefeatMusic = false; + [Desc("This track is played when no other music is playing.", + "It cannot be paused, but can be overridden by selecting a new track.")] + public readonly string BackgroundMusic = null; public object Create(ActorInitializer init) { return new MusicPlaylist(init.World, this); } } @@ -41,44 +37,65 @@ namespace OpenRA.Traits public class MusicPlaylist : INotifyActorDisposing, IGameOver { readonly MusicPlaylistInfo info; + readonly World world; readonly MusicInfo[] random; readonly MusicInfo[] playlist; + public readonly bool IsMusicInstalled; public readonly bool IsMusicAvailable; + public bool CurrentSongIsBackground { get; private set; } MusicInfo currentSong; - bool repeat; + MusicInfo currentBackgroundSong; public MusicPlaylist(World world, MusicPlaylistInfo info) { this.info = info; + this.world = world; - IsMusicAvailable = world.Map.Rules.InstalledMusic.Any(); - - playlist = world.Map.Rules.InstalledMusic.Select(a => a.Value).ToArray(); - - if (!IsMusicAvailable) + IsMusicInstalled = world.Map.Rules.InstalledMusic.Any(); + if (!IsMusicInstalled) return; - random = playlist.Shuffle(Game.CosmeticRandom).ToArray(); + playlist = world.Map.Rules.InstalledMusic + .Where(a => !a.Value.Hidden) + .Select(a => a.Value) + .ToArray(); - if (!string.IsNullOrEmpty(info.StartingMusic) - && world.Map.Rules.Music.ContainsKey(info.StartingMusic) - && world.Map.Rules.Music[info.StartingMusic].Exists) - { + random = playlist.Shuffle(Game.CosmeticRandom).ToArray(); + IsMusicAvailable = playlist.Any(); + + if (SongExists(info.StartingMusic)) currentSong = world.Map.Rules.Music[info.StartingMusic]; - repeat = info.LoopStartingMusic; + else if (SongExists(info.BackgroundMusic)) + { + currentSong = currentBackgroundSong = world.Map.Rules.Music[info.BackgroundMusic]; + CurrentSongIsBackground = true; } else { - currentSong = Game.Settings.Sound.Shuffle ? random.First() : playlist.First(); - repeat = Game.Settings.Sound.Repeat; + // Start playback with a random song, but only if the player has installed more music + var installData = Game.ModData.Manifest.Get(); + if (playlist.Length > installData.ShippedSoundtracks) + currentSong = random.FirstOrDefault(); } Play(); } + bool SongExists(string song) + { + return !string.IsNullOrEmpty(song) + && world.Map.Rules.Music.ContainsKey(song) + && world.Map.Rules.Music[song].Exists; + } + + bool SongExists(MusicInfo song) + { + return song != null && song.Exists; + } + public MusicInfo CurrentSong() { return currentSong; @@ -92,46 +109,34 @@ namespace OpenRA.Traits public void GameOver(World world) { - if (!IsMusicAvailable) - return; - - var playedSong = currentSong; - if (world.LocalPlayer != null && world.LocalPlayer.WinState == WinState.Won) { - if (!string.IsNullOrEmpty(info.VictoryMusic) - && world.Map.Rules.Music.ContainsKey(info.VictoryMusic) - && world.Map.Rules.Music[info.VictoryMusic].Exists) + if (SongExists(info.VictoryMusic)) { - currentSong = world.Map.Rules.Music[info.VictoryMusic]; - repeat = info.LoopVictoryMusic; + currentBackgroundSong = world.Map.Rules.Music[info.VictoryMusic]; + Stop(); } } else { // Most RTS treats observers losing the game, // no need for a special handling involving them here. - if (!string.IsNullOrEmpty(info.DefeatMusic) - && world.Map.Rules.Music.ContainsKey(info.DefeatMusic) - && world.Map.Rules.Music[info.DefeatMusic].Exists) + if (SongExists(info.DefeatMusic)) { - currentSong = world.Map.Rules.Music[info.DefeatMusic]; - repeat = info.LoopDefeatMusic; + currentBackgroundSong = world.Map.Rules.Music[info.DefeatMusic]; + Stop(); } } - - if (playedSong != currentSong) - Play(); } void Play() { - if (currentSong == null || !IsMusicAvailable) + if (!SongExists(currentSong)) return; Sound.PlayMusicThen(currentSong, () => { - if (!repeat) + if (!CurrentSongIsBackground && !Game.Settings.Sound.Repeat) currentSong = GetNextSong(); Play(); @@ -140,27 +145,22 @@ namespace OpenRA.Traits public void Play(MusicInfo music) { - if (music == null || !IsMusicAvailable) + if (music == null) return; currentSong = music; - repeat = Game.Settings.Sound.Repeat; + CurrentSongIsBackground = false; - Sound.PlayMusicThen(music, () => - { - if (!repeat) - currentSong = GetNextSong(); - - Play(); - }); + Play(); } public void Play(MusicInfo music, Action onComplete) { - if (music == null || !IsMusicAvailable) + if (music == null) return; currentSong = music; + CurrentSongIsBackground = false; Sound.PlayMusicThen(music, onComplete); } @@ -181,22 +181,34 @@ namespace OpenRA.Traits var songs = Game.Settings.Sound.Shuffle ? random : playlist; - return reverse ? songs.SkipWhile(m => m != currentSong) - .Skip(1).FirstOrDefault() ?? songs.FirstOrDefault() : - songs.Reverse().SkipWhile(m => m != currentSong) - .Skip(1).FirstOrDefault() ?? songs.Reverse().FirstOrDefault(); + var next = reverse ? songs.Reverse().SkipWhile(m => m != currentSong) + .Skip(1).FirstOrDefault() ?? songs.Reverse().FirstOrDefault() : + songs.SkipWhile(m => m != currentSong) + .Skip(1).FirstOrDefault() ?? songs.FirstOrDefault(); + + if (SongExists(next)) + return next; + + return null; } public void Stop() { currentSong = null; Sound.StopMusic(); + + if (currentBackgroundSong != null) + { + currentSong = currentBackgroundSong; + CurrentSongIsBackground = true; + Play(); + } } public void Disposing(Actor self) { if (currentSong != null) - Stop(); + Sound.StopMusic(); } } } diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs index 824f5e23c9..0052a8a805 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs @@ -560,7 +560,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic musicButton.OnClick = () => Ui.OpenWindow("MUSIC_PANEL", new WidgetArgs { { "onExit", DoNothing }, - { "world", orderManager.World } + { "world", worldRenderer.World } }); var settingsButton = lobby.GetOrNull("SETTINGS_BUTTON"); diff --git a/OpenRA.Mods.Common/Widgets/Logic/MusicPlayerLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MusicPlayerLogic.cs index 2830512b0e..52e42b04de 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/MusicPlayerLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/MusicPlayerLogic.cs @@ -11,7 +11,7 @@ using System; using System.Linq; using OpenRA.GameRules; -using OpenRA.Traits; +using OpenRA.Mods.Common.Traits; using OpenRA.Widgets; namespace OpenRA.Mods.Common.Widgets.Logic @@ -35,8 +35,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic BuildMusicTable(); - Func noMusic = () => !musicPlaylist.IsMusicAvailable; - panel.Get("NO_MUSIC_LABEL").IsVisible = noMusic; + Func noMusic = () => !musicPlaylist.IsMusicAvailable || musicPlaylist.CurrentSongIsBackground || currentSong == null; + panel.Get("NO_MUSIC_LABEL").IsVisible = () => !musicPlaylist.IsMusicAvailable; var playButton = panel.Get("BUTTON_PLAY"); playButton.OnClick = Play; @@ -63,14 +63,25 @@ namespace OpenRA.Mods.Common.Widgets.Logic var shuffleCheckbox = panel.Get("SHUFFLE"); shuffleCheckbox.IsChecked = () => Game.Settings.Sound.Shuffle; shuffleCheckbox.OnClick = () => Game.Settings.Sound.Shuffle ^= true; + shuffleCheckbox.IsDisabled = () => musicPlaylist.CurrentSongIsBackground; var repeatCheckbox = panel.Get("REPEAT"); repeatCheckbox.IsChecked = () => Game.Settings.Sound.Repeat; repeatCheckbox.OnClick = () => Game.Settings.Sound.Repeat ^= true; + repeatCheckbox.IsDisabled = () => musicPlaylist.CurrentSongIsBackground; - panel.Get("TIME_LABEL").GetText = () => (currentSong == null) ? "" : - "{0:D2}:{1:D2} / {2:D2}:{3:D2}".F((int)Sound.MusicSeekPosition / 60, (int)Sound.MusicSeekPosition % 60, - currentSong.Length / 60, currentSong.Length % 60); + panel.Get("TIME_LABEL").GetText = () => + { + if (currentSong == null || musicPlaylist.CurrentSongIsBackground) + return ""; + + var minutes = (int)Sound.MusicSeekPosition / 60; + var seconds = (int)Sound.MusicSeekPosition % 60; + var totalMinutes = currentSong.Length / 60; + var totalSeconds = currentSong.Length % 60; + + return "{0:D2}:{1:D2} / {2:D2}:{3:D2}".F(minutes, seconds, totalMinutes, totalSeconds); + }; var musicSlider = panel.Get("MUSIC_SLIDER"); musicSlider.OnChange += x => Sound.MusicVolume = x; @@ -94,7 +105,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic { songWatcher.OnTick = () => { - if (Sound.CurrentMusic == null || currentSong == Sound.CurrentMusic) + if (musicPlaylist.CurrentSongIsBackground && currentSong != null) + currentSong = null; + + if (Sound.CurrentMusic == null || currentSong == Sound.CurrentMusic || musicPlaylist.CurrentSongIsBackground) return; currentSong = Sound.CurrentMusic; @@ -111,23 +125,18 @@ namespace OpenRA.Mods.Common.Widgets.Logic var music = musicPlaylist.AvailablePlaylist(); currentSong = musicPlaylist.CurrentSong(); - if (currentSong == null && music.Any()) - currentSong = musicPlaylist.GetNextSong(); musicList.RemoveChildren(); foreach (var s in music) { var song = s; - if (currentSong == null) - currentSong = song; - var item = ScrollItemWidget.Setup(song.Filename, itemTemplate, () => currentSong == song, () => { currentSong = song; Play(); }, () => { }); item.Get("TITLE").GetText = () => song.Title; item.Get("LENGTH").GetText = () => SongLengthLabel(song); musicList.AddChild(item); } - if (currentSong != null) + if (currentSong != null && !musicPlaylist.CurrentSongIsBackground) musicList.ScrollToItem(currentSong.Filename); } diff --git a/mods/cnc/audio/music.yaml b/mods/cnc/audio/music.yaml index 555f2c9731..363ef9c569 100644 --- a/mods/cnc/audio/music.yaml +++ b/mods/cnc/audio/music.yaml @@ -24,6 +24,7 @@ fwp: Fight Win Prevail fist226m: Iron Fist warfare: Warfare (Full Stop) win1: Great Shot! + Hidden: true win2: Great Shot! (Voiced) Filename: win2 Extension: var @@ -41,6 +42,7 @@ justdoit2: Just Do it Up (Voiced) Filename: justdoit Extension: var map1: Map Theme + Hidden: true march: March to Doom nomercy: No Mercy nomercy2: No Mercy (Voiced) @@ -58,5 +60,9 @@ target: Target (Mechanical Man) j1: Untamed Land valkyrie: Ride of the Valkyries voic226m: Voice Rhythm +outtakes: Outtakes + Hidden: true nod_map1: Nod Map Theme -nod_win1: Nod Win Theme \ No newline at end of file + Hidden: true +nod_win1: Nod Win Theme + Hidden: true diff --git a/mods/cnc/maps/shellmap/map.yaml b/mods/cnc/maps/shellmap/map.yaml index 2c1f73a826..acf5eb080e 100644 --- a/mods/cnc/maps/shellmap/map.yaml +++ b/mods/cnc/maps/shellmap/map.yaml @@ -995,8 +995,7 @@ Rules: LuaScript: Scripts: shellmap.lua MusicPlaylist: - StartingMusic: map1 - LoopStartingMusic: True + BackgroundMusic: map1 LST: Mobile: Speed: 42 diff --git a/mods/d2k/audio/music.yaml b/mods/d2k/audio/music.yaml index ae048b4609..4e811574c1 100644 --- a/mods/d2k/audio/music.yaml +++ b/mods/d2k/audio/music.yaml @@ -17,6 +17,7 @@ LANDSAND: Land of Sand Extension: AUD OPTIONS: Options Extension: AUD + Hidden: true PLOTTING: Plotting Extension: AUD RISEHARK: Rise of Harkonnen @@ -25,6 +26,7 @@ ROBOTIX: Robotix Extension: AUD SCORE: Score Extension: AUD + Hidden: true SOLDAPPR: The Soldiers Approach Extension: AUD SPICESCT: Spice Scouting diff --git a/mods/d2k/maps/shellmap/map.yaml b/mods/d2k/maps/shellmap/map.yaml index 31b324a808..ab67831c2a 100644 --- a/mods/d2k/maps/shellmap/map.yaml +++ b/mods/d2k/maps/shellmap/map.yaml @@ -124,8 +124,7 @@ Rules: Minimum: 1 Maximum: 1 MusicPlaylist: - StartingMusic: options - LoopStartingMusic: True + BackgroundMusic: options rockettower: Power: Amount: 100 diff --git a/mods/ra/audio/music.yaml b/mods/ra/audio/music.yaml index 638ecf0cdc..adab542155 100644 --- a/mods/ra/audio/music.yaml +++ b/mods/ra/audio/music.yaml @@ -1,9 +1,12 @@ intro: Intro + Hidden: true map: Map + Hidden: true score: Militant Force + Hidden: true await_r: Await bigf226m: Bigfoot -credits: End Credits +credits: Reload Fire crus226m: Crush dense_r: Dense fac1226m: Face to the Enemy 1 diff --git a/mods/ra/maps/desert-shellmap/map.yaml b/mods/ra/maps/desert-shellmap/map.yaml index 664d90db32..799075da85 100644 --- a/mods/ra/maps/desert-shellmap/map.yaml +++ b/mods/ra/maps/desert-shellmap/map.yaml @@ -1255,6 +1255,8 @@ Rules: -CrateSpawner: -SpawnMPUnits: -MPStartLocations: + MusicPlaylist: + BackgroundMusic: intro ResourceType@ore: ValuePerUnit: 0 LuaScript: diff --git a/mods/ts/audio/music.yaml b/mods/ts/audio/music.yaml index b40169be56..a255032ac2 100644 --- a/mods/ts/audio/music.yaml +++ b/mods/ts/audio/music.yaml @@ -1,5 +1,6 @@ #Tiberian Sun intro: Intro + Hidden: true approach: Approach defense: The Defense duskhour: Dusk Hour @@ -14,12 +15,14 @@ nodcrush: Nod Crush pharotek: Pharotek redsky: Red Sky score: Score + Hidden: true scout: Scouting storm: Storm timebomb: Timebomb valves1b: Valves whatlurk: What Lurks maps: Map Selection + Hidden: true #Firestorm dmachine: Deploy Machines elusive: Elusive diff --git a/mods/ts/maps/blank-shellmap/map.yaml b/mods/ts/maps/blank-shellmap/map.yaml index a7bd4c73b0..c25582f018 100644 --- a/mods/ts/maps/blank-shellmap/map.yaml +++ b/mods/ts/maps/blank-shellmap/map.yaml @@ -41,8 +41,7 @@ Rules: -SpawnMPUnits: -MPStartLocations: MusicPlaylist: - StartingMusic: intro - LoopStartingMusic: True + BackgroundMusic: intro Sequences: