Mod metadata, load screens and mod content is all now sourced from ftl files, allowing these items to be translated. Translations are now initialized as part of ModData creation, as currently they are made available too late for the usage we need here. The "modcontent" mod learns a new parameter for "Content.TranslationFile" - this allows a mod to provide the path of a translation file to the mod which it can load. This allows mods such as ra, cnc, d2k, ts to own the translations for their ModContent, yet still make them accessible to the modcontent mod. CheckFluentReference learns to validate all these new fields to ensure translations have been set.
440 lines
14 KiB
C#
440 lines
14 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.Tasks;
|
|
using OpenRA.Mods.Common.Installer;
|
|
using OpenRA.Primitives;
|
|
using OpenRA.Widgets;
|
|
|
|
namespace OpenRA.Mods.Common.Widgets.Logic
|
|
{
|
|
public class InstallFromSourceLogic : ChromeLogic
|
|
{
|
|
[FluentReference]
|
|
const string DetectingSources = "label-detecting-sources";
|
|
|
|
[FluentReference]
|
|
const string CheckingSources = "label-checking-sources";
|
|
|
|
[FluentReference("title")]
|
|
const string SearchingSourceFor = "label-searching-source-for";
|
|
|
|
[FluentReference]
|
|
const string ContentPackageInstallation = "label-content-package-installation";
|
|
|
|
[FluentReference]
|
|
const string GameSources = "label-game-sources";
|
|
|
|
[FluentReference]
|
|
const string DigitalInstalls = "label-digital-installs";
|
|
|
|
[FluentReference]
|
|
const string GameContentNotFound = "label-game-content-not-found";
|
|
|
|
[FluentReference]
|
|
const string AlternativeContentSources = "label-alternative-content-sources";
|
|
|
|
[FluentReference]
|
|
const string InstallingContent = "label-installing-content";
|
|
|
|
[FluentReference("filename")]
|
|
public const string CopyingFilename = "label-copying-filename";
|
|
|
|
[FluentReference("filename", "progress")]
|
|
public const string CopyingFilenameProgress = "label-copying-filename-progress";
|
|
|
|
[FluentReference]
|
|
const string InstallationFailed = "label-installation-failed";
|
|
|
|
[FluentReference]
|
|
const string CheckInstallLog = "label-check-install-log";
|
|
|
|
[FluentReference("filename")]
|
|
public const string Extracting = "label-extracting-filename";
|
|
|
|
[FluentReference("filename", "progress")]
|
|
public const string ExtractingProgress = "label-extracting-filename-progress";
|
|
|
|
[FluentReference]
|
|
public const string Continue = "button-continue";
|
|
|
|
[FluentReference]
|
|
const string Cancel = "button-cancel";
|
|
|
|
[FluentReference]
|
|
const string Retry = "button-retry";
|
|
|
|
[FluentReference]
|
|
const string Back = "button-back";
|
|
|
|
// Hide percentage indicators for files smaller than 25 MB
|
|
public const int ShowPercentageThreshold = 26214400;
|
|
|
|
enum Mode { Progress, Message, List }
|
|
|
|
readonly ModData modData;
|
|
readonly ModContent content;
|
|
readonly Dictionary<string, ModContent.ModSource> sources;
|
|
readonly FluentBundle externalFluentBundle;
|
|
|
|
readonly Widget panel;
|
|
readonly LabelWidget titleLabel;
|
|
readonly ButtonWidget primaryButton;
|
|
readonly ButtonWidget secondaryButton;
|
|
|
|
// Progress panel
|
|
readonly Widget progressContainer;
|
|
readonly ProgressBarWidget progressBar;
|
|
readonly LabelWidget progressLabel;
|
|
|
|
// Message panel
|
|
readonly Widget messageContainer;
|
|
readonly LabelWidget messageLabel;
|
|
|
|
// List Panel
|
|
readonly Widget listContainer;
|
|
readonly ScrollPanelWidget listPanel;
|
|
readonly Widget listHeaderTemplate;
|
|
readonly LabelWidget labelListTemplate;
|
|
readonly ContainerWidget checkboxListTemplate;
|
|
readonly LabelWidget listLabel;
|
|
|
|
ModContent.ModPackage[] availablePackages;
|
|
IDictionary<string, bool> selectedPackages;
|
|
|
|
Mode visible = Mode.Progress;
|
|
|
|
[ObjectCreator.UseCtor]
|
|
public InstallFromSourceLogic(
|
|
Widget widget, ModData modData, ModContent content, Dictionary<string, ModContent.ModSource> sources, FluentBundle externalFluentBundle)
|
|
{
|
|
this.modData = modData;
|
|
this.content = content;
|
|
this.sources = sources;
|
|
this.externalFluentBundle = externalFluentBundle;
|
|
|
|
Log.AddChannel("install", "install.log");
|
|
|
|
panel = widget.Get("SOURCE_INSTALL_PANEL");
|
|
|
|
titleLabel = panel.Get<LabelWidget>("TITLE");
|
|
|
|
primaryButton = panel.Get<ButtonWidget>("PRIMARY_BUTTON");
|
|
secondaryButton = panel.Get<ButtonWidget>("SECONDARY_BUTTON");
|
|
|
|
// Progress view
|
|
progressContainer = panel.Get("PROGRESS");
|
|
progressContainer.IsVisible = () => visible == Mode.Progress;
|
|
progressBar = panel.Get<ProgressBarWidget>("PROGRESS_BAR");
|
|
progressLabel = panel.Get<LabelWidget>("PROGRESS_MESSAGE");
|
|
progressLabel.IsVisible = () => visible == Mode.Progress;
|
|
|
|
// Message view
|
|
messageContainer = panel.Get("MESSAGE");
|
|
messageContainer.IsVisible = () => visible == Mode.Message;
|
|
messageLabel = messageContainer.Get<LabelWidget>("MESSAGE_MESSAGE");
|
|
|
|
// List view
|
|
listContainer = panel.Get("LIST");
|
|
listContainer.IsVisible = () => visible == Mode.List;
|
|
|
|
listPanel = listContainer.Get<ScrollPanelWidget>("LIST_PANEL");
|
|
listHeaderTemplate = listPanel.Get("LIST_HEADER_TEMPLATE");
|
|
labelListTemplate = listPanel.Get<LabelWidget>("LABEL_LIST_TEMPLATE");
|
|
checkboxListTemplate = listPanel.Get<ContainerWidget>("CHECKBOX_LIST_TEMPLATE");
|
|
listPanel.RemoveChildren();
|
|
|
|
listLabel = listContainer.Get<LabelWidget>("LIST_MESSAGE");
|
|
|
|
DetectContentSources();
|
|
}
|
|
|
|
void DetectContentSources()
|
|
{
|
|
var message = FluentProvider.GetString(DetectingSources);
|
|
ShowProgressbar(FluentProvider.GetString(CheckingSources), () => message);
|
|
ShowBackRetry(DetectContentSources);
|
|
|
|
new Task(() =>
|
|
{
|
|
foreach (var kv in sources)
|
|
{
|
|
message = FluentProvider.GetString(SearchingSourceFor, "title", kv.Value.Title);
|
|
|
|
var sourceResolver = kv.Value.ObjectCreator.CreateObject<ISourceResolver>($"{kv.Value.Type.Value}SourceResolver");
|
|
|
|
var path = sourceResolver.FindSourcePath(kv.Value);
|
|
if (path != null)
|
|
{
|
|
Log.Write("install", $"Using installer `{kv.Key}: {kv.Value.Title}` of type `{kv.Value.Type.Value}`:");
|
|
|
|
availablePackages = content.Packages.Values
|
|
.Where(p => p.Sources.Contains(kv.Key) && !p.IsInstalled())
|
|
.ToArray();
|
|
|
|
selectedPackages = availablePackages.ToDictionary(x => x.Identifier, y => y.Required);
|
|
|
|
// Ignore source if content is already installed
|
|
if (availablePackages.Length != 0)
|
|
{
|
|
Game.RunAfterTick(() =>
|
|
{
|
|
ShowList(kv.Value, FluentProvider.GetString(ContentPackageInstallation));
|
|
ShowContinueCancel(() => InstallFromSource(path, kv.Value));
|
|
});
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
var missingSources = content.Packages.Values
|
|
.Where(p => !p.IsInstalled())
|
|
.SelectMany(p => p.Sources)
|
|
.Select(d => sources[d]);
|
|
|
|
var gameSources = new HashSet<string>();
|
|
var digitalInstalls = new HashSet<string>();
|
|
|
|
foreach (var source in missingSources)
|
|
{
|
|
var sourceResolver = source.ObjectCreator.CreateObject<ISourceResolver>($"{source.Type.Value}SourceResolver");
|
|
|
|
var availability = sourceResolver.GetAvailability();
|
|
|
|
if (availability == Availability.GameSource)
|
|
gameSources.Add(source.Title);
|
|
else if (availability == Availability.DigitalInstall)
|
|
digitalInstalls.Add(source.Title);
|
|
}
|
|
|
|
var options = new Dictionary<string, IEnumerable<string>>();
|
|
|
|
if (gameSources.Count != 0)
|
|
options.Add(FluentProvider.GetString(GameSources), gameSources);
|
|
|
|
if (digitalInstalls.Count != 0)
|
|
options.Add(FluentProvider.GetString(DigitalInstalls), digitalInstalls);
|
|
|
|
Game.RunAfterTick(() =>
|
|
{
|
|
ShowList(FluentProvider.GetString(GameContentNotFound), FluentProvider.GetString(AlternativeContentSources), options);
|
|
ShowBackRetry(DetectContentSources);
|
|
});
|
|
}).Start();
|
|
}
|
|
|
|
void InstallFromSource(string path, ModContent.ModSource modSource)
|
|
{
|
|
var message = "";
|
|
ShowProgressbar(FluentProvider.GetString(InstallingContent), () => message);
|
|
ShowDisabledCancel();
|
|
|
|
new Task(() =>
|
|
{
|
|
var extracted = new List<string>();
|
|
|
|
try
|
|
{
|
|
void RunSourceActions(MiniYamlNode contentPackageYaml)
|
|
{
|
|
var sourceActionListYaml = contentPackageYaml.Value.NodeWithKeyOrDefault("Actions");
|
|
if (sourceActionListYaml == null)
|
|
return;
|
|
|
|
foreach (var sourceActionNode in sourceActionListYaml.Value.Nodes)
|
|
{
|
|
var key = sourceActionNode.Key;
|
|
var split = key.IndexOf('@');
|
|
if (split != -1)
|
|
key = key[..split];
|
|
var sourceAction = modSource.ObjectCreator.CreateObject<ISourceAction>($"{key}SourceAction");
|
|
sourceAction.RunActionOnSource(sourceActionNode.Value, path, modData, extracted, m => message = m);
|
|
}
|
|
}
|
|
|
|
var beforeInstall = modSource.Install.FirstOrDefault(x => x.Key == "BeforeInstall");
|
|
if (beforeInstall != null)
|
|
RunSourceActions(beforeInstall);
|
|
|
|
foreach (var packageInstallationNode in modSource.Install.Where(
|
|
x => x.Key == "ContentPackage" || x.Key.StartsWith("ContentPackage@", StringComparison.Ordinal)))
|
|
{
|
|
var packageName = packageInstallationNode.Value.NodeWithKeyOrDefault("Name")?.Value.Value;
|
|
if (!string.IsNullOrEmpty(packageName) && selectedPackages.TryGetValue(packageName, out var required) && required)
|
|
RunSourceActions(packageInstallationNode);
|
|
}
|
|
|
|
var afterInstall = modSource.Install.FirstOrDefault(x => x.Key == "AfterInstall");
|
|
if (afterInstall != null)
|
|
RunSourceActions(afterInstall);
|
|
|
|
Game.RunAfterTick(Ui.CloseWindow);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Write("install", e.ToString());
|
|
|
|
foreach (var f in extracted)
|
|
{
|
|
Log.Write("install", "Deleting " + f);
|
|
File.Delete(f);
|
|
}
|
|
|
|
Game.RunAfterTick(() =>
|
|
{
|
|
ShowMessage(FluentProvider.GetString(InstallationFailed), FluentProvider.GetString(CheckInstallLog));
|
|
ShowBackRetry(() => InstallFromSource(path, modSource));
|
|
});
|
|
}
|
|
}).Start();
|
|
}
|
|
|
|
void ShowMessage(string title, string message)
|
|
{
|
|
visible = Mode.Message;
|
|
titleLabel.GetText = () => title;
|
|
messageLabel.GetText = () => message;
|
|
|
|
primaryButton.Bounds.Y += messageContainer.Bounds.Height - panel.Bounds.Height;
|
|
secondaryButton.Bounds.Y += messageContainer.Bounds.Height - panel.Bounds.Height;
|
|
panel.Bounds.Y -= (messageContainer.Bounds.Height - panel.Bounds.Height) / 2;
|
|
panel.Bounds.Height = messageContainer.Bounds.Height;
|
|
}
|
|
|
|
void ShowProgressbar(string title, Func<string> getMessage)
|
|
{
|
|
visible = Mode.Progress;
|
|
titleLabel.GetText = () => title;
|
|
progressBar.IsIndeterminate = () => true;
|
|
|
|
var font = Game.Renderer.Fonts[progressLabel.Font];
|
|
var status = new CachedTransform<string, string>(s => WidgetUtils.TruncateText(s, progressLabel.Bounds.Width, font));
|
|
progressLabel.GetText = () => status.Update(getMessage());
|
|
|
|
primaryButton.Bounds.Y += progressContainer.Bounds.Height - panel.Bounds.Height;
|
|
secondaryButton.Bounds.Y += progressContainer.Bounds.Height - panel.Bounds.Height;
|
|
panel.Bounds.Y -= (progressContainer.Bounds.Height - panel.Bounds.Height) / 2;
|
|
panel.Bounds.Height = progressContainer.Bounds.Height;
|
|
}
|
|
|
|
void ShowList(ModContent.ModSource source, string message)
|
|
{
|
|
visible = Mode.List;
|
|
var titleText = source.Title;
|
|
titleLabel.GetText = () => titleText;
|
|
listLabel.GetText = () => message;
|
|
|
|
listPanel.RemoveChildren();
|
|
foreach (var package in availablePackages)
|
|
{
|
|
var containerWidget = (ContainerWidget)checkboxListTemplate.Clone();
|
|
var checkboxWidget = containerWidget.Get<CheckboxWidget>("PACKAGE_CHECKBOX");
|
|
var title = externalFluentBundle.GetString(package.Title);
|
|
checkboxWidget.GetText = () => title;
|
|
checkboxWidget.IsDisabled = () => package.Required;
|
|
checkboxWidget.IsChecked = () => selectedPackages[package.Identifier];
|
|
checkboxWidget.OnClick = () => selectedPackages[package.Identifier] = !selectedPackages[package.Identifier];
|
|
|
|
var contentPackageNode = source.Install.FirstOrDefault(x =>
|
|
x.Value.NodeWithKeyOrDefault("Name")?.Value.Value == package.Identifier);
|
|
|
|
var tooltipText = contentPackageNode?.Value.NodeWithKeyOrDefault(nameof(ModContent.ModSource.TooltipText))?.Value.Value;
|
|
var tooltipIcon = containerWidget.Get<ImageWidget>("PACKAGE_INFO");
|
|
tooltipIcon.IsVisible = () => !string.IsNullOrWhiteSpace(tooltipText);
|
|
tooltipIcon.GetTooltipText = () => tooltipText;
|
|
|
|
listPanel.AddChild(containerWidget);
|
|
}
|
|
|
|
primaryButton.Bounds.Y += listContainer.Bounds.Height - panel.Bounds.Height;
|
|
secondaryButton.Bounds.Y += listContainer.Bounds.Height - panel.Bounds.Height;
|
|
panel.Bounds.Y -= (listContainer.Bounds.Height - panel.Bounds.Height) / 2;
|
|
panel.Bounds.Height = listContainer.Bounds.Height;
|
|
}
|
|
|
|
void ShowList(string title, string message, Dictionary<string, IEnumerable<string>> groups)
|
|
{
|
|
visible = Mode.List;
|
|
titleLabel.GetText = () => title;
|
|
listLabel.GetText = () => message;
|
|
|
|
listPanel.RemoveChildren();
|
|
|
|
foreach (var kv in groups)
|
|
{
|
|
if (kv.Value.Any())
|
|
{
|
|
var groupTitle = kv.Key;
|
|
var headerWidget = listHeaderTemplate.Clone();
|
|
var headerTitleWidget = headerWidget.Get<LabelWidget>("LABEL");
|
|
headerTitleWidget.GetText = () => groupTitle;
|
|
listPanel.AddChild(headerWidget);
|
|
}
|
|
|
|
foreach (var i in kv.Value)
|
|
{
|
|
var item = i;
|
|
var labelWidget = (LabelWidget)labelListTemplate.Clone();
|
|
labelWidget.GetText = () => item;
|
|
listPanel.AddChild(labelWidget);
|
|
}
|
|
}
|
|
|
|
primaryButton.Bounds.Y += listContainer.Bounds.Height - panel.Bounds.Height;
|
|
secondaryButton.Bounds.Y += listContainer.Bounds.Height - panel.Bounds.Height;
|
|
panel.Bounds.Y -= (listContainer.Bounds.Height - panel.Bounds.Height) / 2;
|
|
panel.Bounds.Height = listContainer.Bounds.Height;
|
|
}
|
|
|
|
void ShowContinueCancel(Action continueAction)
|
|
{
|
|
primaryButton.OnClick = continueAction;
|
|
var primaryButtonText = FluentProvider.GetString(Continue);
|
|
primaryButton.GetText = () => primaryButtonText;
|
|
primaryButton.Visible = true;
|
|
|
|
secondaryButton.OnClick = Ui.CloseWindow;
|
|
var secondaryButtonText = FluentProvider.GetString(Cancel);
|
|
secondaryButton.GetText = () => secondaryButtonText;
|
|
secondaryButton.Visible = true;
|
|
secondaryButton.Disabled = false;
|
|
Game.RunAfterTick(Ui.ResetTooltips);
|
|
}
|
|
|
|
void ShowBackRetry(Action retryAction)
|
|
{
|
|
primaryButton.OnClick = retryAction;
|
|
var primaryButtonText = FluentProvider.GetString(Retry);
|
|
primaryButton.GetText = () => primaryButtonText;
|
|
primaryButton.Visible = true;
|
|
|
|
secondaryButton.OnClick = Ui.CloseWindow;
|
|
var secondaryButtonText = FluentProvider.GetString(Back);
|
|
secondaryButton.GetText = () => secondaryButtonText;
|
|
secondaryButton.Visible = true;
|
|
secondaryButton.Disabled = false;
|
|
Game.RunAfterTick(Ui.ResetTooltips);
|
|
}
|
|
|
|
void ShowDisabledCancel()
|
|
{
|
|
primaryButton.Visible = false;
|
|
secondaryButton.Disabled = true;
|
|
Game.RunAfterTick(Ui.ResetTooltips);
|
|
}
|
|
}
|
|
}
|