diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f3e31439..8fec85559 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ All notable changes to this project will be documented in this file. - [GUI] Don't hide other providing mods for installed, make installed bold (#4163 by: HebaruSan) - [CLI] Set headless mode automatically (#4166 by: HebaruSan) - [Core] Autogenerate spec version for modpacks (#4173 by: HebaruSan) +- [Multiple] Set compat of new instances based on Unity updates and alert user (#4191 by: HebaruSan) ### Bugfixes diff --git a/Core/Configuration/Win32RegistryConfiguration.cs b/Core/Configuration/Win32RegistryConfiguration.cs index a50a2def7..32b7cddd5 100644 --- a/Core/Configuration/Win32RegistryConfiguration.cs +++ b/Core/Configuration/Win32RegistryConfiguration.cs @@ -179,12 +179,12 @@ public void SetAuthToken(string host, string? token) /// /// Not implemented because the Windows registry is deprecated /// - public string[] GlobalInstallFilters { get; set; } = new string[] { }; + public string[] GlobalInstallFilters { get; set; } = Array.Empty(); /// /// Not implemented because the Windows registry is deprecated /// - public string?[] PreferredHosts { get; set; } = new string?[] { }; + public string?[] PreferredHosts { get; set; } = Array.Empty(); /// /// Not implemented because the Windows registry is deprecated diff --git a/Core/GameInstance.cs b/Core/GameInstance.cs index 42a99a16f..715662136 100644 --- a/Core/GameInstance.cs +++ b/Core/GameInstance.cs @@ -164,6 +164,11 @@ private void LoadCompatibleVersions() GameVersion.TryParse(compatibleGameVersions.GameVersionWhenWritten, out GameVersion? mainVer); GameVersionWhenCompatibleVersionsWereStored = mainVer; } + else if (Version() is GameVersion gv) + { + _compatibleVersions = game.DefaultCompatibleVersions(gv) + .ToList(); + } } private string CompatibleGameVersionsFile() diff --git a/Core/Games/IGame.cs b/Core/Games/IGame.cs index 7669ce0ab..1d7488e03 100644 --- a/Core/Games/IGame.cs +++ b/Core/Games/IGame.cs @@ -43,6 +43,7 @@ public interface IGame GameVersion[] EmbeddedGameVersions { get; } GameVersion[] ParseBuildsJson(JToken json); GameVersion? DetectVersion(DirectoryInfo where); + GameVersion[] DefaultCompatibleVersions(GameVersion installedVersion); string CompatibleVersionsFile { get; } string[] InstanceAnchorFiles { get; } diff --git a/Core/Games/KerbalSpaceProgram.cs b/Core/Games/KerbalSpaceProgram.cs index c636086ae..05f433cdd 100644 --- a/Core/Games/KerbalSpaceProgram.cs +++ b/Core/Games/KerbalSpaceProgram.cs @@ -213,14 +213,14 @@ is Stream s ?.Builds ?.Select(b => GameVersion.Parse(b.Value)) .ToArray() - ?? new GameVersion[] { }; + ?? Array.Empty(); public GameVersion[] ParseBuildsJson(JToken json) => json.ToObject() ?.Builds ?.Select(b => GameVersion.Parse(b.Value)) .ToArray() - ?? new GameVersion[] { }; + ?? Array.Empty(); public GameVersion? DetectVersion(DirectoryInfo where) => ServiceLocator.Container @@ -233,6 +233,28 @@ public GameVersion[] ParseBuildsJson(JToken json) ? verFromReadme : null; + public GameVersion[] DefaultCompatibleVersions(GameVersion installedVersion) + // 1.2.9 was a prerelease that broke compatibility with previous 1.2 releases + => installedVersion.Major == 1 && installedVersion.Minor == 2 && installedVersion.Patch == 9 + ? Array.Empty() + : UnityBreakVersions.FirstOrDefault(brk => brk <= installedVersion) + is GameVersion latestUnityBreak + ? MinorVersionsInRange(latestUnityBreak, installedVersion) + : new GameVersion[] + { + new GameVersion(installedVersion.Major, + installedVersion.Minor) + }; + + private static readonly GameVersion[] UnityBreakVersions = + new int[] {8, 4, 2, 1}.Select(minor => new GameVersion(1, minor)) + .ToArray(); + + private static GameVersion[] MinorVersionsInRange(GameVersion min, GameVersion max) + => Enumerable.Range(min.Minor, max.Minor - min.Minor + 1) + .Select(minor => new GameVersion(min.Major, minor)) + .ToArray(); + public string CompatibleVersionsFile => "compatible_ksp_versions.json"; public string[] InstanceAnchorFiles => diff --git a/Core/Games/KerbalSpaceProgram2.cs b/Core/Games/KerbalSpaceProgram2.cs index 3a4a60632..efd4e5578 100644 --- a/Core/Games/KerbalSpaceProgram2.cs +++ b/Core/Games/KerbalSpaceProgram2.cs @@ -161,11 +161,11 @@ public GameVersion[] EmbeddedGameVersions is Stream s ? JsonConvert.DeserializeObject(new StreamReader(s).ReadToEnd()) : null) - ?? new GameVersion[] { }; + ?? Array.Empty(); public GameVersion[] ParseBuildsJson(JToken json) => json.ToObject() - ?? new GameVersion[] { }; + ?? Array.Empty(); public GameVersion DetectVersion(DirectoryInfo where) => VersionFromAssembly(Path.Combine(where.FullName, @@ -202,6 +202,12 @@ public GameVersion DetectVersion(DirectoryInfo where) ? v : null; + public GameVersion[] DefaultCompatibleVersions(GameVersion installedVersion) + // KSP2 didn't last long enough to break compatibility :~( + => Enumerable.Range(1, 2) + .Select(minor => new GameVersion(0, minor)) + .ToArray(); + public string CompatibleVersionsFile => "compatible_game_versions.json"; public string[] InstanceAnchorFiles => diff --git a/GUI/Controls/ModInfoTabs/Versions.cs b/GUI/Controls/ModInfoTabs/Versions.cs index ffec1a78f..5d9c75ebe 100644 --- a/GUI/Controls/ModInfoTabs/Versions.cs +++ b/GUI/Controls/ModInfoTabs/Versions.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.ComponentModel; using System.Collections.Generic; @@ -226,7 +227,7 @@ private ListViewItem[] getItems(GUIMod gmod, List versions) return items; } - return new ListViewItem[] { }; + return Array.Empty(); } [ForbidGUICalls] diff --git a/GUI/Dialogs/CompatibleGameVersionsDialog.Designer.cs b/GUI/Dialogs/CompatibleGameVersionsDialog.Designer.cs index db7790470..8ca619236 100644 --- a/GUI/Dialogs/CompatibleGameVersionsDialog.Designer.cs +++ b/GUI/Dialogs/CompatibleGameVersionsDialog.Designer.cs @@ -30,6 +30,8 @@ private void InitializeComponent() { this.components = new System.ComponentModel.Container(); System.ComponentModel.ComponentResourceManager resources = new SingleAssemblyComponentResourceManager(typeof(CompatibleGameVersionsDialog)); + this.MessageLabel = new System.Windows.Forms.Label(); + this.MainContentsPanel = new System.Windows.Forms.Panel(); this.InstancePathLabel = new System.Windows.Forms.Label(); this.ActualInstancePathLabel = new System.Windows.Forms.Label(); this.GameVersionLabel = new System.Windows.Forms.Label(); @@ -45,8 +47,45 @@ private void InitializeComponent() this.WarningLabel = new System.Windows.Forms.Label(); this.CancelChooseCompatibleVersionsButton = new System.Windows.Forms.Button(); this.SaveButton = new System.Windows.Forms.Button(); + this.MainContentsPanel.SuspendLayout(); this.SuspendLayout(); // + // MessageLabel + // + this.MessageLabel.Dock = System.Windows.Forms.DockStyle.Top; + this.MessageLabel.Font = new System.Drawing.Font(System.Drawing.SystemFonts.DefaultFont.Name, 12, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Pixel); + this.MessageLabel.BackColor = System.Drawing.SystemColors.Window; + this.MessageLabel.ForeColor = System.Drawing.SystemColors.WindowText; + this.MessageLabel.Location = new System.Drawing.Point(0, 0); + this.MessageLabel.Margin = new System.Windows.Forms.Padding(8, 6, 8, 6); + this.MessageLabel.Padding = new System.Windows.Forms.Padding(8, 6, 8, 6); + this.MessageLabel.Name = "MessageLabel"; + this.MessageLabel.Size = new System.Drawing.Size(60, 13); + this.MessageLabel.TabIndex = 0; + this.MessageLabel.Visible = false; + resources.ApplyResources(this.MessageLabel, "MessageLabel"); + // + // MainContentsPanel + // + this.MainContentsPanel.Controls.Add(this.InstancePathLabel); + this.MainContentsPanel.Controls.Add(this.ActualInstancePathLabel); + this.MainContentsPanel.Controls.Add(this.GameVersionLabel); + this.MainContentsPanel.Controls.Add(this.ActualGameVersionLabel); + this.MainContentsPanel.Controls.Add(this.ListHeaderLabel); + this.MainContentsPanel.Controls.Add(this.SelectedVersionsCheckedListBox); + this.MainContentsPanel.Controls.Add(this.ClearSelectionButton); + this.MainContentsPanel.Controls.Add(this.AddVersionLabel); + this.MainContentsPanel.Controls.Add(this.AddVersionToListTextBox); + this.MainContentsPanel.Controls.Add(this.AddVersionToListButton); + this.MainContentsPanel.Controls.Add(this.WildcardExplanationLabel); + this.MainContentsPanel.Controls.Add(this.FutureUpdatesLabel); + this.MainContentsPanel.Controls.Add(this.WarningLabel); + this.MainContentsPanel.Controls.Add(this.CancelChooseCompatibleVersionsButton); + this.MainContentsPanel.Controls.Add(this.SaveButton); + this.MainContentsPanel.Dock = System.Windows.Forms.DockStyle.Fill; + this.MainContentsPanel.Name = "MainContentsPanel"; + this.MainContentsPanel.Size = new System.Drawing.Size(443, 383); + // // InstancePathLabel // this.InstancePathLabel.AutoSize = true; @@ -216,21 +255,8 @@ private void InitializeComponent() this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.CancelButton = this.CancelChooseCompatibleVersionsButton; this.ClientSize = new System.Drawing.Size(443, 383); - this.Controls.Add(this.InstancePathLabel); - this.Controls.Add(this.ActualInstancePathLabel); - this.Controls.Add(this.GameVersionLabel); - this.Controls.Add(this.ActualGameVersionLabel); - this.Controls.Add(this.ListHeaderLabel); - this.Controls.Add(this.SelectedVersionsCheckedListBox); - this.Controls.Add(this.ClearSelectionButton); - this.Controls.Add(this.AddVersionLabel); - this.Controls.Add(this.AddVersionToListTextBox); - this.Controls.Add(this.AddVersionToListButton); - this.Controls.Add(this.WildcardExplanationLabel); - this.Controls.Add(this.FutureUpdatesLabel); - this.Controls.Add(this.WarningLabel); - this.Controls.Add(this.CancelChooseCompatibleVersionsButton); - this.Controls.Add(this.SaveButton); + this.Controls.Add(this.MainContentsPanel); + this.Controls.Add(this.MessageLabel); this.Icon = EmbeddedImages.AppIcon; this.MaximizeBox = false; this.MinimizeBox = false; @@ -240,12 +266,16 @@ private void InitializeComponent() this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; this.Shown += new System.EventHandler(this.CompatibleGameVersionsDialog_Shown); resources.ApplyResources(this, "$this"); + this.MainContentsPanel.ResumeLayout(false); + this.MainContentsPanel.PerformLayout(); this.ResumeLayout(false); this.PerformLayout(); } #endregion + private System.Windows.Forms.Label MessageLabel; + private System.Windows.Forms.Panel MainContentsPanel; private System.Windows.Forms.Label InstancePathLabel; private System.Windows.Forms.Label ActualInstancePathLabel; private System.Windows.Forms.Label GameVersionLabel; diff --git a/GUI/Dialogs/CompatibleGameVersionsDialog.cs b/GUI/Dialogs/CompatibleGameVersionsDialog.cs index 66019ad80..548a7383d 100644 --- a/GUI/Dialogs/CompatibleGameVersionsDialog.cs +++ b/GUI/Dialogs/CompatibleGameVersionsDialog.cs @@ -67,21 +67,51 @@ protected override void OnHelpButtonClicked(CancelEventArgs evt) evt.Cancel = Util.TryOpenWebPage(HelpURLs.CompatibleGameVersions); } + private void ShowMessage(string msg) + { + if (string.IsNullOrEmpty(msg)) + { + MessageLabel.Text = ""; + MessageLabel.Visible = false; + } + else + { + MessageLabel.Text = msg; + MessageLabel.Visible = true; + MessageLabel.Height = Util.LabelStringHeight(CreateGraphics(), + MessageLabel); + Height += MessageLabel.Height; + } + } + + protected override void OnResize(EventArgs e) + { + base.OnResize(e); + if (MessageLabel.Visible) + { + MessageLabel.Height = Util.LabelStringHeight(CreateGraphics(), + MessageLabel); + } + } + private void CompatibleGameVersionsDialog_Shown(object? sender, EventArgs? e) { if (_inst.CompatibleVersionsAreFromDifferentGameVersion) { CancelChooseCompatibleVersionsButton.Visible = false; - ActualGameVersionLabel.Text = string.Format( - Properties.Resources.CompatibleGameVersionsDialogVersionDetails, - _inst.Version(), - _inst.GameVersionWhenCompatibleVersionsWereStored); - ActualGameVersionLabel.ForeColor = Color.Red; - MessageBox.Show(this, - Properties.Resources.CompatibleGameVersionsDialogGameUpdatedDetails, - Properties.Resources.CompatibleGameVersionsDialogGameUpdatedTitle, - MessageBoxButtons.OK, - MessageBoxIcon.Information); + if (_inst.GameVersionWhenCompatibleVersionsWereStored == null) + { + ShowMessage(Properties.Resources.CompatibleGameVersionsDialogDefaultedDetails); + } + else + { + ActualGameVersionLabel.Text = string.Format( + Properties.Resources.CompatibleGameVersionsDialogVersionDetails, + _inst.Version(), + _inst.GameVersionWhenCompatibleVersionsWereStored); + ActualGameVersionLabel.ForeColor = Color.Red; + ShowMessage(Properties.Resources.CompatibleGameVersionsDialogGameUpdatedDetails); + } } } diff --git a/GUI/Properties/Resources.resx b/GUI/Properties/Resources.resx index 7f4a7b5b5..4e225eb1f 100644 --- a/GUI/Properties/Resources.resx +++ b/GUI/Properties/Resources.resx @@ -136,7 +136,9 @@ Create junction points (Windows) or symbolic links (Unix) to stock directories instead of copying them. If you choose this option, DO NOT move or delete the old instance!! <NONE> - Game Version Updated + Default compatibility has been set based on your installed game version. These settings should bring the greatest number of working mods to the most users, but it's still possible that some older mods might misbehave. + +If you want to be cautious, click "Clear selection" to uncheck all the boxes. The game has been updated since you last reviewed your compatible game versions. Please make sure that settings are correct. {0} (previous game version: {1}) Version has invalid format diff --git a/GUI/Util.cs b/GUI/Util.cs index 27103529b..3f0cc74e6 100644 --- a/GUI/Util.cs +++ b/GUI/Util.cs @@ -306,26 +306,26 @@ private static Color AddColors(Color a, Color b) a.G + b.G, a.B + b.B); - public static Bitmap LerpBitmaps(Bitmap a, Bitmap b, float amount) + public static Bitmap LerpBitmaps(Bitmap a, Bitmap b, + float amount) + => amount <= 0 ? a + : amount >= 1 ? b + : MergeBitmaps(a, b, + // Note pixA and pixB are swapped because our blend function + // pretends 'amount' is the first argument's alpha channel, + // so 0 -> third param by itself + (pixA, pixB) => AlphaBlendWith(pixB, amount, pixA)); + + private static Bitmap MergeBitmaps(Bitmap a, Bitmap b, + Func how) { - if (amount <= 0) - { - return a; - } - if (amount >= 1) - { - return b; - } var c = new Bitmap(a); - for (int y = 0; y < c.Height; ++y) + foreach (var y in Enumerable.Range(0, c.Height)) { - for (int x = 0; x < c.Width; ++x) + foreach (var x in Enumerable.Range(0, c.Width)) { - // Note a and b are swapped because our blend function pretends 'amount' - // is the first argument's alpha channel, so 0 -> third param by itself - c.SetPixel(x, y, AlphaBlendWith(b.GetPixel(x, y), - amount, - a.GetPixel(x, y))); + c.SetPixel(x, y, how(a.GetPixel(x, y), + b.GetPixel(x, y))); } } return c; diff --git a/Tests/Core/Games/KerbalSpaceProgram2Tests.cs b/Tests/Core/Games/KerbalSpaceProgram2Tests.cs new file mode 100644 index 000000000..084f6c6c2 --- /dev/null +++ b/Tests/Core/Games/KerbalSpaceProgram2Tests.cs @@ -0,0 +1,35 @@ +using NUnit.Framework; + +using CKAN.Versioning; +using CKAN.Games.KerbalSpaceProgram2; +using System.Linq; + +namespace Tests.Core.Games +{ + [TestFixture] + public class KerbalSpaceProgram2Tests + { + [Test, TestCase("0.1.0.0", + new string[] { "0.1", "0.2" }), + TestCase("0.1.1.0", + new string[] { "0.1", "0.2" }), + TestCase("0.2.0.0", + new string[] { "0.1", "0.2" }), + TestCase("0.2.2.0", + new string[] { "0.1", "0.2" }), + ] + public void DefaultCompatibleVersions_RealVersion_CorrectRange(string installedVersion, + string[] correctCompatVersions) + { + // Arrange + var game = new KerbalSpaceProgram2(); + + // Act + var compat = game.DefaultCompatibleVersions(GameVersion.Parse(installedVersion)); + + // Assert + CollectionAssert.AreEqual(correctCompatVersions.Select(GameVersion.Parse), + compat); + } + } +} diff --git a/Tests/Core/Games/KerbalSpaceProgramTests.cs b/Tests/Core/Games/KerbalSpaceProgramTests.cs new file mode 100644 index 000000000..d047fb265 --- /dev/null +++ b/Tests/Core/Games/KerbalSpaceProgramTests.cs @@ -0,0 +1,44 @@ +using NUnit.Framework; + +using CKAN.Versioning; +using CKAN.Games.KerbalSpaceProgram; +using System.Linq; + +namespace Tests.Core.Games +{ + [TestFixture] + public class KerbalSpaceProgramTests + { + [Test, TestCase("1.12.5", + new string[] { "1.8", "1.9", "1.10", "1.11", "1.12" }), + TestCase("1.12.0", + new string[] { "1.8", "1.9", "1.10", "1.11", "1.12" }), + TestCase("1.10.1", + new string[] { "1.8", "1.9", "1.10" }), + TestCase("1.8.0", + new string[] { "1.8" }), + TestCase("1.7.3", + new string[] { "1.4", "1.5", "1.6", "1.7" }), + TestCase("1.5.1", + new string[] { "1.4", "1.5" }), + TestCase("1.3.1", + new string[] { "1.2", "1.3" }), + TestCase("1.2.9", + new string[] { }), + TestCase("1.0.5", + new string[] { "1.0" })] + public void DefaultCompatibleVersions_RealVersion_CorrectRange(string installedVersion, + string[] correctCompatVersions) + { + // Arrange + var game = new KerbalSpaceProgram(); + + // Act + var compat = game.DefaultCompatibleVersions(GameVersion.Parse(installedVersion)); + + // Assert + CollectionAssert.AreEqual(correctCompatVersions.Select(GameVersion.Parse), + compat); + } + } +}