#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.Traits; using OpenRA.Widgets; namespace OpenRA.Mods.Common.Widgets.Logic { public class MissionBrowserLogic : ChromeLogic { enum PlayingVideo { None, Info, Briefing, GameStart } enum PanelType { MissionInfo, Options } [FluentReference] const string NoVideoTitle = "dialog-no-video.title"; [FluentReference] const string NoVideoPrompt = "dialog-no-video.prompt"; [FluentReference] const string NoVideoCancel = "dialog-no-video.cancel"; [FluentReference] const string CantPlayTitle = "dialog-cant-play-video.title"; [FluentReference] const string CantPlayPrompt = "dialog-cant-play-video.prompt"; [FluentReference] const string CantPlayCancel = "dialog-cant-play-video.cancel"; [FluentReference] const string NotAvailable = "label-not-available"; readonly ModData modData; readonly Action onStart; readonly Widget missionDetail; readonly Widget optionsContainer; readonly Widget checkboxRowTemplate; readonly Widget dropdownRowTemplate; readonly ScrollPanelWidget descriptionPanel; readonly LabelWidget description; readonly SpriteFont descriptionFont; 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; readonly Dictionary missionOptions = new(); PanelType panel = PanelType.MissionInfo; [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) { var titleText = title.GetText(); title.GetText = () => playingVideo != PlayingVideo.None ? selectedMap.Title : titleText; } 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 } }); missionDetail = widget.Get("MISSION_DETAIL"); descriptionPanel = missionDetail.Get("MISSION_DESCRIPTION_PANEL"); descriptionPanel.IsVisible = () => panel == PanelType.MissionInfo; description = descriptionPanel.Get("MISSION_DESCRIPTION"); descriptionFont = Game.Renderer.Fonts[description.Font]; optionsContainer = missionDetail.Get("MISSION_OPTIONS"); optionsContainer.IsVisible = () => panel == PanelType.Options; checkboxRowTemplate = optionsContainer.Get("CHECKBOX_ROW_TEMPLATE"); dropdownRowTemplate = optionsContainer.Get("DROPDOWN_ROW_TEMPLATE"); 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 stringPool = new HashSet(); // Reuse common strings in YAML var yaml = MiniYaml.Merge(modData.Manifest.Missions.Select( m => MiniYaml.FromStream(modData.DefaultFileSystem.Open(m), m, stringPool: stringPool))); 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.PackageName)) }) .Where(x => x.Index != -1) .OrderBy(x => x.Index) .Select(x => x.Preview) .ToList(); if (previews.Count != 0) { 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)) .ToList(); if (loosePreviews.Count != 0) { 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[0]); } // 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); Ui.CloseWindow(); onExit(); }; var tabContainer = widget.Get("MISSION_TABS"); tabContainer.IsVisible = () => true; var optionsTab = tabContainer.Get("OPTIONS_TAB"); optionsTab.IsHighlighted = () => panel == PanelType.Options; optionsTab.IsDisabled = () => false; optionsTab.OnClick = () => panel = PanelType.Options; var missionTab = tabContainer.Get("MISSIONINFO_TAB"); missionTab.IsHighlighted = () => panel == PanelType.MissionInfo; missionTab.IsDisabled = () => false; missionTab.OnClick = () => panel = PanelType.MissionInfo; } 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; var briefingVideo = ""; var briefingVideoVisible = false; var infoVideo = ""; var infoVideoVisible = false; new Thread(() => { 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.GetText = () => briefing; description.Bounds.Height = height; descriptionPanel.Layout.AdjustChildren(); panel = PanelType.MissionInfo; } }); } }).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(); RebuildOptions(); } void RebuildOptions() { if (selectedMap == null || selectedMap.WorldActorInfo == null) return; missionOptions.Clear(); optionsContainer.RemoveChildren(); var allOptions = selectedMap.PlayerActorInfo.TraitInfos() .Concat(selectedMap.WorldActorInfo.TraitInfos()) .SelectMany(t => t.LobbyOptions(selectedMap)) .Where(o => o.IsVisible) .OrderBy(o => o.DisplayOrder).ToArray(); Widget row = null; var checkboxColumns = new Queue(); var dropdownColumns = new Queue(); var yOffset = 0; foreach (var option in allOptions.Where(o => o is LobbyBooleanOption)) { missionOptions[option.Id] = option.DefaultValue; if (checkboxColumns.Count == 0) { row = checkboxRowTemplate.Clone(); row.Bounds.Y = yOffset; yOffset += row.Bounds.Height; foreach (var child in row.Children) if (child is CheckboxWidget childCheckbox) checkboxColumns.Enqueue(childCheckbox); optionsContainer.AddChild(row); } var checkbox = checkboxColumns.Dequeue(); checkbox.GetText = () => option.Name; if (option.Description != null) { var (text, desc) = LobbyUtils.SplitOnFirstToken(option.Description); checkbox.GetTooltipText = () => text; checkbox.GetTooltipDesc = () => desc; } checkbox.IsVisible = () => true; checkbox.IsChecked = () => missionOptions[option.Id] == "True"; checkbox.IsDisabled = () => option.IsLocked; checkbox.OnClick = () => { if (missionOptions[option.Id] == "True") missionOptions[option.Id] = "False"; else missionOptions[option.Id] = "True"; }; } foreach (var option in allOptions.Where(o => o is not LobbyBooleanOption)) { missionOptions[option.Id] = option.DefaultValue; if (dropdownColumns.Count == 0) { row = dropdownRowTemplate.Clone(); row.Bounds.Y = yOffset; yOffset += row.Bounds.Height; foreach (var child in row.Children) if (child is DropDownButtonWidget dropDown) dropdownColumns.Enqueue(dropDown); optionsContainer.AddChild(row); } var dropdown = dropdownColumns.Dequeue(); dropdown.GetText = () => { if (option.Values.TryGetValue(missionOptions[option.Id], out var value)) return value; return FluentProvider.GetMessage(NotAvailable); }; if (option.Description != null) { var (text, desc) = LobbyUtils.SplitOnFirstToken(option.Description); dropdown.GetTooltipText = () => text; dropdown.GetTooltipDesc = () => desc; } dropdown.IsVisible = () => true; dropdown.IsDisabled = () => option.IsLocked; dropdown.OnMouseDown = _ => { ScrollItemWidget SetupItem(KeyValuePair c, ScrollItemWidget template) { bool IsSelected() => missionOptions[option.Id] == c.Key; void OnClick() => missionOptions[option.Id] = c.Key; var item = ScrollItemWidget.Setup(template, IsSelected, OnClick); item.Get("LABEL").GetText = () => c.Value; return item; } dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", option.Values.Count * 30, option.Values, SetupItem); }; var label = row.GetOrNull(dropdown.Id + "_DESC"); if (label != null) { label.GetText = () => option.Name + ":"; label.IsVisible = () => true; } } } 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, onCancel: () => { }, cancelText: NoVideoCancel); } else { StopVideo(player); playingVideo = pv; player.LoadAndPlay(video); if (player.Video == null) { StopVideo(player); ConfirmationDialogs.ButtonPrompt(modData, title: CantPlayTitle, text: CantPlayPrompt, onCancel: () => { }, cancelText: CantPlayCancel); } 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(); foreach (var option in missionOptions) orders.Add(Order.Command($"option {option.Key} {option.Value}")); 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); } } }