#region Copyright & License Information /* * Copyright (c) The OpenRA Developers and Contributors * This file is part of OpenRA, which is free software. It is made * available to you under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. For more * information, see COPYING. */ #endregion using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using OpenRA.Graphics; using OpenRA.Mods.Common.Traits; using OpenRA.Network; using OpenRA.Widgets; namespace OpenRA.Mods.Common.Widgets.Logic { public class MissionBrowserLogic : ChromeLogic { enum PlayingVideo { None, Info, Briefing, GameStart } [TranslationReference] const string NoVideoTitle = "dialog-no-video.title"; [TranslationReference] const string NoVideoPrompt = "dialog-no-video.prompt"; [TranslationReference] const string NoVideoCancel = "dialog-no-video.cancel"; [TranslationReference] const string CantPlayTitle = "dialog-cant-play-video.title"; [TranslationReference] const string CantPlayPrompt = "dialog-cant-play-video.prompt"; [TranslationReference] const string CantPlayCancel = "dialog-cant-play-video.cancel"; readonly ModData modData; readonly Action onStart; readonly ScrollPanelWidget descriptionPanel; readonly LabelWidget description; readonly SpriteFont descriptionFont; readonly DropDownButtonWidget difficultyButton; readonly DropDownButtonWidget gameSpeedButton; readonly ButtonWidget startBriefingVideoButton; readonly ButtonWidget stopBriefingVideoButton; readonly ButtonWidget startInfoVideoButton; readonly ButtonWidget stopInfoVideoButton; readonly VideoPlayerWidget videoPlayer; readonly BackgroundWidget fullscreenVideoPlayer; readonly ScrollPanelWidget missionList; readonly ScrollItemWidget headerTemplate; readonly ScrollItemWidget template; MapPreview selectedMap; PlayingVideo playingVideo; string difficulty; string gameSpeed; [ObjectCreator.UseCtor] public MissionBrowserLogic(Widget widget, ModData modData, World world, Action onStart, Action onExit, string initialMap) { this.modData = modData; this.onStart = onStart; Game.BeforeGameStart += OnGameStart; missionList = widget.Get("MISSION_LIST"); headerTemplate = widget.Get("HEADER"); template = widget.Get("TEMPLATE"); var title = widget.GetOrNull("MISSIONBROWSER_TITLE"); if (title != null) title.GetText = () => playingVideo != PlayingVideo.None ? selectedMap.Title : title.Text; widget.Get("MISSION_INFO").IsVisible = () => selectedMap != null; var previewWidget = widget.Get("MISSION_PREVIEW"); previewWidget.Preview = () => selectedMap; previewWidget.IsVisible = () => playingVideo == PlayingVideo.None; videoPlayer = widget.Get("MISSION_VIDEO"); widget.Get("MISSION_BIN").IsVisible = () => playingVideo != PlayingVideo.None; fullscreenVideoPlayer = Ui.LoadWidget("FULLSCREEN_PLAYER", Ui.Root, new WidgetArgs { { "world", world } }); descriptionPanel = widget.Get("MISSION_DESCRIPTION_PANEL"); description = descriptionPanel.Get("MISSION_DESCRIPTION"); descriptionFont = Game.Renderer.Fonts[description.Font]; difficultyButton = widget.Get("DIFFICULTY_DROPDOWNBUTTON"); gameSpeedButton = widget.GetOrNull("GAMESPEED_DROPDOWNBUTTON"); startBriefingVideoButton = widget.Get("START_BRIEFING_VIDEO_BUTTON"); stopBriefingVideoButton = widget.Get("STOP_BRIEFING_VIDEO_BUTTON"); stopBriefingVideoButton.IsVisible = () => playingVideo == PlayingVideo.Briefing; stopBriefingVideoButton.OnClick = () => StopVideo(videoPlayer); startInfoVideoButton = widget.Get("START_INFO_VIDEO_BUTTON"); stopInfoVideoButton = widget.Get("STOP_INFO_VIDEO_BUTTON"); stopInfoVideoButton.IsVisible = () => playingVideo == PlayingVideo.Info; stopInfoVideoButton.OnClick = () => StopVideo(videoPlayer); var allPreviews = new List(); missionList.RemoveChildren(); // Add a group for each campaign if (modData.Manifest.Missions.Length > 0) { var yaml = MiniYaml.Merge(modData.Manifest.Missions.Select( m => MiniYaml.FromStream(modData.DefaultFileSystem.Open(m), m))); foreach (var kv in yaml) { var missionMapPaths = kv.Value.Nodes.Select(n => n.Key).ToList(); var previews = modData.MapCache .Where(p => p.Class == MapClassification.System && p.Status == MapStatus.Available) .Select(p => new { Preview = p, Index = missionMapPaths.IndexOf(Path.GetFileName(p.Package.Name)) }) .Where(x => x.Index != -1) .OrderBy(x => x.Index) .Select(x => x.Preview); if (previews.Any()) { CreateMissionGroup(kv.Key, previews, onExit); allPreviews.AddRange(previews); } } } // Add an additional group for loose missions var loosePreviews = modData.MapCache .Where(p => p.Status == MapStatus.Available && p.Visibility.HasFlag(MapVisibility.MissionSelector) && !allPreviews.Any(a => a.Uid == p.Uid)); if (loosePreviews.Any()) { CreateMissionGroup("Missions", loosePreviews, onExit); allPreviews.AddRange(loosePreviews); } if (allPreviews.Count > 0) { var uid = modData.MapCache.GetUpdatedMap(initialMap); var map = uid == null ? null : modData.MapCache[uid]; if (map != null && map.Visibility.HasFlag(MapVisibility.MissionSelector)) { SelectMap(map); missionList.ScrollToSelectedItem(); } else SelectMap(allPreviews.First()); } // Preload map preview to reduce jank new Thread(() => { foreach (var p in allPreviews) p.GetMinimap(); }).Start(); var startButton = widget.Get("STARTGAME_BUTTON"); startButton.OnClick = () => StartMissionClicked(onExit); startButton.IsDisabled = () => selectedMap == null; widget.Get("BACK_BUTTON").OnClick = () => { StopVideo(videoPlayer); Game.Disconnect(); Ui.CloseWindow(); onExit(); }; } void OnGameStart() { Ui.CloseWindow(); DiscordService.UpdateStatus(DiscordState.PlayingCampaign); onStart(); } bool disposed; protected override void Dispose(bool disposing) { if (disposing && !disposed) { disposed = true; Game.BeforeGameStart -= OnGameStart; } base.Dispose(disposing); } void CreateMissionGroup(string title, IEnumerable previews, Action onExit) { var header = ScrollItemWidget.Setup(headerTemplate, () => false, () => { }); header.Get("LABEL").GetText = () => title; missionList.AddChild(header); foreach (var preview in previews) { var item = ScrollItemWidget.Setup(template, () => selectedMap != null && selectedMap.Uid == preview.Uid, () => SelectMap(preview), () => StartMissionClicked(onExit)); var label = item.Get("TITLE"); WidgetUtils.TruncateLabelToTooltip(label, preview.Title); missionList.AddChild(item); } } void SelectMap(MapPreview preview) { selectedMap = preview; // Cache the rules on a background thread to avoid jank var difficultyDisabled = true; var difficulties = new Dictionary(); var briefingVideo = ""; var briefingVideoVisible = false; var infoVideo = ""; var infoVideoVisible = false; new Thread(() => { var mapDifficulty = preview.WorldActorInfo.TraitInfos() .FirstOrDefault(sld => sld.ID == "difficulty"); if (mapDifficulty != null) { difficulty = mapDifficulty.Default; difficulties = mapDifficulty.Values; difficultyDisabled = mapDifficulty.Locked; } var missionData = preview.WorldActorInfo.TraitInfoOrDefault(); if (missionData != null) { briefingVideo = missionData.BriefingVideo; briefingVideoVisible = briefingVideo != null; infoVideo = missionData.BackgroundVideo; infoVideoVisible = infoVideo != null; var briefing = WidgetUtils.WrapText(missionData.Briefing?.Replace("\\n", "\n"), description.Bounds.Width, descriptionFont); var height = descriptionFont.Measure(briefing).Y; Game.RunAfterTick(() => { if (preview == selectedMap) { description.Text = briefing; description.Bounds.Height = height; descriptionPanel.Layout.AdjustChildren(); } }); } }).Start(); startBriefingVideoButton.IsVisible = () => briefingVideoVisible && playingVideo != PlayingVideo.Briefing; startBriefingVideoButton.OnClick = () => PlayVideo(videoPlayer, briefingVideo, PlayingVideo.Briefing); startInfoVideoButton.IsVisible = () => infoVideoVisible && playingVideo != PlayingVideo.Info; startInfoVideoButton.OnClick = () => PlayVideo(videoPlayer, infoVideo, PlayingVideo.Info); descriptionPanel.ScrollToTop(); if (difficultyButton != null) { var difficultyName = new CachedTransform(id => id == null || !difficulties.ContainsKey(id) ? "Normal" : difficulties[id]); difficultyButton.IsDisabled = () => difficultyDisabled; difficultyButton.GetText = () => difficultyName.Update(difficulty); difficultyButton.OnMouseDown = _ => { var options = difficulties.Select(kv => new DropDownOption { Title = kv.Value, IsSelected = () => difficulty == kv.Key, OnClick = () => difficulty = kv.Key }); ScrollItemWidget SetupItem(DropDownOption option, ScrollItemWidget template) { var item = ScrollItemWidget.Setup(template, option.IsSelected, option.OnClick); item.Get("LABEL").GetText = () => option.Title; return item; } difficultyButton.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", options.Count() * 30, options, SetupItem); }; } if (gameSpeedButton != null) { var speeds = modData.Manifest.Get().Speeds; gameSpeed = "default"; var speedText = new CachedTransform(s => modData.Translation.GetString(speeds[s].Name)); gameSpeedButton.GetText = () => speedText.Update(gameSpeed); gameSpeedButton.OnMouseDown = _ => { var options = speeds.Select(s => new DropDownOption { Title = modData.Translation.GetString(s.Value.Name), IsSelected = () => gameSpeed == s.Key, OnClick = () => gameSpeed = s.Key }); ScrollItemWidget SetupItem(DropDownOption option, ScrollItemWidget template) { var item = ScrollItemWidget.Setup(template, option.IsSelected, option.OnClick); item.Get("LABEL").GetText = () => option.Title; return item; } gameSpeedButton.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", options.Count() * 30, options, SetupItem); }; } } float cachedSoundVolume; float cachedMusicVolume; void MuteSounds() { cachedSoundVolume = Game.Sound.SoundVolume; cachedMusicVolume = Game.Sound.MusicVolume; Game.Sound.SoundVolume = Game.Sound.MusicVolume = 0; } void UnMuteSounds() { if (cachedSoundVolume > 0) Game.Sound.SoundVolume = cachedSoundVolume; if (cachedMusicVolume > 0) Game.Sound.MusicVolume = cachedMusicVolume; } void PlayVideo(VideoPlayerWidget player, string video, PlayingVideo pv, Action onComplete = null) { if (!modData.DefaultFileSystem.Exists(video)) { ConfirmationDialogs.ButtonPrompt(modData, title: NoVideoTitle, text: NoVideoPrompt, cancelText: NoVideoCancel, onCancel: () => { }); } else { StopVideo(player); playingVideo = pv; player.LoadAndPlay(video); if (player.Video == null) { StopVideo(player); ConfirmationDialogs.ButtonPrompt(modData, title: CantPlayTitle, text: CantPlayPrompt, cancelText: CantPlayCancel, onCancel: () => { }); } else { // video playback runs asynchronously player.PlayThen(() => { StopVideo(player); onComplete?.Invoke(); }); // Mute other distracting sounds MuteSounds(); } } } void StopVideo(VideoPlayerWidget player) { if (playingVideo == PlayingVideo.None) return; UnMuteSounds(); player.Stop(); playingVideo = PlayingVideo.None; } void StartMissionClicked(Action onExit) { StopVideo(videoPlayer); // If selected mission becomes unavailable, exit MissionBrowser to refresh var map = modData.MapCache.GetUpdatedMap(selectedMap.Uid); if (map == null) { Game.Disconnect(); Ui.CloseWindow(); onExit(); return; } selectedMap = modData.MapCache[map]; var orders = new List(); if (difficulty != null) orders.Add(Order.Command($"option difficulty {difficulty}")); orders.Add(Order.Command($"option gamespeed {gameSpeed}")); orders.Add(Order.Command($"state {Session.ClientState.Ready}")); var missionData = selectedMap.WorldActorInfo.TraitInfoOrDefault(); if (missionData != null && missionData.StartVideo != null && modData.DefaultFileSystem.Exists(missionData.StartVideo)) { var fsPlayer = fullscreenVideoPlayer.Get("PLAYER"); fullscreenVideoPlayer.Visible = true; PlayVideo(fsPlayer, missionData.StartVideo, PlayingVideo.GameStart, () => { Game.CreateAndStartLocalServer(selectedMap.Uid, orders); }); } else Game.CreateAndStartLocalServer(selectedMap.Uid, orders); } class DropDownOption { public string Title; public Func IsSelected; public Action OnClick; } } }