#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 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 selectedPackages; Mode visible = Mode.Progress; [ObjectCreator.UseCtor] public InstallFromSourceLogic( Widget widget, ModData modData, ModContent content, Dictionary 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("TITLE"); primaryButton = panel.Get("PRIMARY_BUTTON"); secondaryButton = panel.Get("SECONDARY_BUTTON"); // Progress view progressContainer = panel.Get("PROGRESS"); progressContainer.IsVisible = () => visible == Mode.Progress; progressBar = panel.Get("PROGRESS_BAR"); progressLabel = panel.Get("PROGRESS_MESSAGE"); progressLabel.IsVisible = () => visible == Mode.Progress; // Message view messageContainer = panel.Get("MESSAGE"); messageContainer.IsVisible = () => visible == Mode.Message; messageLabel = messageContainer.Get("MESSAGE_MESSAGE"); // List view listContainer = panel.Get("LIST"); listContainer.IsVisible = () => visible == Mode.List; listPanel = listContainer.Get("LIST_PANEL"); listHeaderTemplate = listPanel.Get("LIST_HEADER_TEMPLATE"); labelListTemplate = listPanel.Get("LABEL_LIST_TEMPLATE"); checkboxListTemplate = listPanel.Get("CHECKBOX_LIST_TEMPLATE"); listPanel.RemoveChildren(); listLabel = listContainer.Get("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($"{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(); var digitalInstalls = new HashSet(); foreach (var source in missingSources) { var sourceResolver = source.ObjectCreator.CreateObject($"{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>(); 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(); 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($"{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 getMessage) { visible = Mode.Progress; titleLabel.GetText = () => title; progressBar.IsIndeterminate = () => true; var font = Game.Renderer.Fonts[progressLabel.Font]; var status = new CachedTransform(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("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("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> 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("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); } } }