The Text element of these widgets was changed from display text to a translation key as part of adding translation support. Functions interested in the display text need to invoke GetText instead. Lots of functions have not been updated, resulting in symptoms such as measuring the font size of the translation key rather than the display text and resizing a widget to the wrong size. Update all callers to use GetText when getting or setting display text. This ensure their existing functionality that was intended to work in terms of the display text and not the translation key works as expected.
521 lines
15 KiB
C#
521 lines
15 KiB
C#
#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 }
|
|
|
|
[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 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<string, string> 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<ScrollPanelWidget>("MISSION_LIST");
|
|
|
|
headerTemplate = widget.Get<ScrollItemWidget>("HEADER");
|
|
template = widget.Get<ScrollItemWidget>("TEMPLATE");
|
|
|
|
var title = widget.GetOrNull<LabelWidget>("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<MapPreviewWidget>("MISSION_PREVIEW");
|
|
previewWidget.Preview = () => selectedMap;
|
|
previewWidget.IsVisible = () => playingVideo == PlayingVideo.None;
|
|
|
|
videoPlayer = widget.Get<VideoPlayerWidget>("MISSION_VIDEO");
|
|
widget.Get("MISSION_BIN").IsVisible = () => playingVideo != PlayingVideo.None;
|
|
fullscreenVideoPlayer = Ui.LoadWidget<BackgroundWidget>("FULLSCREEN_PLAYER", Ui.Root, new WidgetArgs { { "world", world } });
|
|
|
|
missionDetail = widget.Get("MISSION_DETAIL");
|
|
|
|
descriptionPanel = missionDetail.Get<ScrollPanelWidget>("MISSION_DESCRIPTION_PANEL");
|
|
descriptionPanel.IsVisible = () => panel == PanelType.MissionInfo;
|
|
|
|
description = descriptionPanel.Get<LabelWidget>("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<ButtonWidget>("START_BRIEFING_VIDEO_BUTTON");
|
|
stopBriefingVideoButton = widget.Get<ButtonWidget>("STOP_BRIEFING_VIDEO_BUTTON");
|
|
stopBriefingVideoButton.IsVisible = () => playingVideo == PlayingVideo.Briefing;
|
|
stopBriefingVideoButton.OnClick = () => StopVideo(videoPlayer);
|
|
|
|
startInfoVideoButton = widget.Get<ButtonWidget>("START_INFO_VIDEO_BUTTON");
|
|
stopInfoVideoButton = widget.Get<ButtonWidget>("STOP_INFO_VIDEO_BUTTON");
|
|
stopInfoVideoButton.IsVisible = () => playingVideo == PlayingVideo.Info;
|
|
stopInfoVideoButton.OnClick = () => StopVideo(videoPlayer);
|
|
|
|
var allPreviews = new List<MapPreview>();
|
|
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)
|
|
.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<ButtonWidget>("STARTGAME_BUTTON");
|
|
startButton.OnClick = () => StartMissionClicked(onExit);
|
|
startButton.IsDisabled = () => selectedMap == null;
|
|
|
|
widget.Get<ButtonWidget>("BACK_BUTTON").OnClick = () =>
|
|
{
|
|
StopVideo(videoPlayer);
|
|
Game.Disconnect();
|
|
Ui.CloseWindow();
|
|
onExit();
|
|
};
|
|
|
|
var tabContainer = widget.Get("MISSION_TABS");
|
|
tabContainer.IsVisible = () => true;
|
|
|
|
var optionsTab = tabContainer.Get<ButtonWidget>("OPTIONS_TAB");
|
|
optionsTab.IsHighlighted = () => panel == PanelType.Options;
|
|
optionsTab.IsDisabled = () => false;
|
|
optionsTab.OnClick = () => panel = PanelType.Options;
|
|
|
|
var missionTab = tabContainer.Get<ButtonWidget>("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<MapPreview> previews, Action onExit)
|
|
{
|
|
var header = ScrollItemWidget.Setup(headerTemplate, () => false, () => { });
|
|
header.Get<LabelWidget>("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<LabelWithTooltipWidget>("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;
|
|
|
|
panel = PanelType.MissionInfo;
|
|
|
|
new Thread(() =>
|
|
{
|
|
var missionData = preview.WorldActorInfo.TraitInfoOrDefault<MissionDataInfo>();
|
|
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();
|
|
}
|
|
});
|
|
}
|
|
}).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<ILobbyOptions>()
|
|
.Concat(selectedMap.WorldActorInfo.TraitInfos<ILobbyOptions>())
|
|
.SelectMany(t => t.LobbyOptions(selectedMap))
|
|
.Where(o => o.IsVisible)
|
|
.OrderBy(o => o.DisplayOrder).ToArray();
|
|
|
|
Widget row = null;
|
|
var checkboxColumns = new Queue<CheckboxWidget>();
|
|
var dropdownColumns = new Queue<DropDownButtonWidget>();
|
|
|
|
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 = () => option.Values[missionOptions[option.Id]];
|
|
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<string, string> 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<LabelWidget>("LABEL").GetText = () => c.Value;
|
|
return item;
|
|
}
|
|
|
|
dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", option.Values.Count * 30, option.Values, SetupItem);
|
|
};
|
|
|
|
var label = row.GetOrNull<LabelWidget>(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<Order>();
|
|
|
|
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<MissionDataInfo>();
|
|
if (missionData != null && missionData.StartVideo != null && modData.DefaultFileSystem.Exists(missionData.StartVideo))
|
|
{
|
|
var fsPlayer = fullscreenVideoPlayer.Get<VideoPlayerWidget>("PLAYER");
|
|
fullscreenVideoPlayer.Visible = true;
|
|
PlayVideo(fsPlayer, missionData.StartVideo, PlayingVideo.GameStart,
|
|
() => Game.CreateAndStartLocalServer(selectedMap.Uid, orders));
|
|
}
|
|
else
|
|
Game.CreateAndStartLocalServer(selectedMap.Uid, orders);
|
|
}
|
|
}
|
|
}
|