From 29088c0804bb61ffe68d7680a51e36a41cc28ea7 Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Sun, 28 Jan 2024 23:16:01 -0600 Subject: [PATCH] Alternate game command lines and Steam refactor --- Cmdline/Action/GameInstance.cs | 2 +- Cmdline/ConsoleUser.cs | 16 +- Core/CKAN-core.csproj | 1 + Core/CKANPathUtils.cs | 43 ----- .../JsonToGamesDictionaryConverter.cs | 4 +- Core/Extensions/EnumerableExtensions.cs | 11 ++ Core/GameInstance.cs | 39 +---- Core/GameInstanceManager.cs | 101 ++++++------ Core/Games/IGame.cs | 9 +- Core/Games/KerbalSpaceProgram.cs | 102 ++---------- Core/Games/KerbalSpaceProgram2.cs | 102 ++---------- Core/Games/KnownGames.cs | 31 ++++ Core/SteamLibrary.cs | 149 ++++++++++++++++++ GUI/Controls/EditModSearchDetails.Designer.cs | 12 +- GUI/Controls/HintTextBox.Designer.cs | 2 +- GUI/Controls/LeftRightRowPanel.cs | 10 ++ GUI/Controls/ManageMods.Designer.cs | 44 ++++-- GUI/Controls/ManageMods.cs | 52 +++++- GUI/Controls/ManageMods.resx | 3 +- GUI/Controls/ModInfo.Designer.cs | 6 +- GUI/Controls/PlayTime.Designer.cs | 2 +- .../GameCommandLineOptionsDialog.Designer.cs | 146 ++++++++++++----- GUI/Dialogs/GameCommandLineOptionsDialog.cs | 104 +++++++++++- GUI/Dialogs/GameCommandLineOptionsDialog.resx | 10 +- .../ManageGameInstancesDialog.Designer.cs | 10 ++ GUI/Dialogs/ManageGameInstancesDialog.cs | 17 +- GUI/Dialogs/ManageGameInstancesDialog.resx | 2 + GUI/Localization/de-DE/ManageMods.de-DE.resx | 2 +- GUI/Localization/fr-FR/ManageMods.fr-FR.resx | 2 +- GUI/Localization/it-IT/ManageMods.it-IT.resx | 2 +- GUI/Localization/ja-JP/ManageMods.ja-JP.resx | 2 +- GUI/Localization/ko-KR/ManageMods.ko-KR.resx | 2 +- GUI/Localization/nl-NL/ManageMods.nl-NL.resx | 2 +- GUI/Localization/pl-PL/ManageMods.pl-PL.resx | 2 +- GUI/Localization/pt-BR/ManageMods.pt-BR.resx | 2 +- GUI/Localization/ru-RU/ManageMods.ru-RU.resx | 2 +- GUI/Localization/zh-CN/ManageMods.zh-CN.resx | 2 +- GUI/Main/Main.Designer.cs | 40 ++--- GUI/Main/Main.cs | 31 ++-- GUI/Main/Main.resx | 2 +- GUI/Model/GUIConfiguration.cs | 42 +++-- GUI/Properties/Resources.resx | 2 + Netkan/Processors/Inflator.cs | 5 +- Netkan/Program.cs | 3 +- Tests/GUI/GUIConfiguration.cs | 6 +- build.cake | 33 +++- 46 files changed, 747 insertions(+), 467 deletions(-) create mode 100644 Core/Games/KnownGames.cs create mode 100644 Core/SteamLibrary.cs diff --git a/Cmdline/Action/GameInstance.cs b/Cmdline/Action/GameInstance.cs index 849d954703..2690379f85 100644 --- a/Cmdline/Action/GameInstance.cs +++ b/Cmdline/Action/GameInstance.cs @@ -558,7 +558,7 @@ int badArgument() string path = options.path; GameVersion version; bool setDefault = options.setDefault; - IGame game = GameInstanceManager.GameByShortName(options.gameId); + IGame game = KnownGames.GameByShortName(options.gameId); if (game == null) { User.RaiseMessage(Properties.Resources.InstanceFakeBadGame, options.gameId); diff --git a/Cmdline/ConsoleUser.cs b/Cmdline/ConsoleUser.cs index 2df7feed7d..47bf56d6a9 100644 --- a/Cmdline/ConsoleUser.cs +++ b/Cmdline/ConsoleUser.cs @@ -254,11 +254,15 @@ public void RaiseError(string message, params object[] args) /// Progress in percent public void RaiseProgress(string message, int percent) { - // The percent looks weird on non-download messages. - // The leading newline makes sure we don't end up with a mess from previous - // download messages. - GoToStartOfLine(); - Console.Write("{0}", message); + if (message != lastProgressMessage) + { + // The percent looks weird on non-download messages. + // The leading newline makes sure we don't end up with a mess from previous + // download messages. + GoToStartOfLine(); + Console.Write("{0}", message); + lastProgressMessage = message; + } // This message leaves the cursor at the end of a line of text atStartOfLine = false; @@ -286,6 +290,8 @@ public void RaiseProgress(int percent, long bytesPerSecond, long bytesLeft) /// private int previousPercent = -1; + private string lastProgressMessage = null; + /// /// Writes a message to the console /// diff --git a/Core/CKAN-core.csproj b/Core/CKAN-core.csproj index 4d73dad1d0..ac3d1cd5dd 100644 --- a/Core/CKAN-core.csproj +++ b/Core/CKAN-core.csproj @@ -27,6 +27,7 @@ + diff --git a/Core/CKANPathUtils.cs b/Core/CKANPathUtils.cs index 1d18a32c68..b0bce16138 100644 --- a/Core/CKANPathUtils.cs +++ b/Core/CKANPathUtils.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Linq; using System.Text.RegularExpressions; using log4net; @@ -20,48 +19,6 @@ public static class CKANPathUtils private static readonly ILog log = LogManager.GetLogger(typeof(CKANPathUtils)); - /// - /// Finds Steam on the current machine. - /// - /// The path to Steam, or null if not found - public static string SteamPath() - { - foreach (var steam in SteamPaths.Where(p => !string.IsNullOrEmpty(p))) - { - log.DebugFormat("Looking for Steam in {0}", steam); - if (Directory.Exists(steam)) - { - log.InfoFormat("Found Steam at {0}", steam); - return steam; - } - } - log.Info("Steam not found on this system."); - return null; - } - - private const string steamRegKey = @"HKEY_CURRENT_USER\Software\Valve\Steam"; - private const string steamRegValue = @"SteamPath"; - - private static string[] SteamPaths - => Platform.IsWindows ? new string[] - { - // First check the registry - (string)Microsoft.Win32.Registry.GetValue(steamRegKey, steamRegValue, null), - } - : Platform.IsUnix ? new string[] - { - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), - ".local", "share", "Steam"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), - ".steam", "steam"), - } - : Platform.IsMac ? new string[] - { - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), - "Library", "Application Support", "Steam"), - } - : Array.Empty(); - /// /// Normalizes the path by replacing all \ with / and removing any trailing slash. /// diff --git a/Core/Converters/JsonToGamesDictionaryConverter.cs b/Core/Converters/JsonToGamesDictionaryConverter.cs index 4f8b3ccbd6..0b57f9ca6b 100644 --- a/Core/Converters/JsonToGamesDictionaryConverter.cs +++ b/Core/Converters/JsonToGamesDictionaryConverter.cs @@ -4,6 +4,8 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using CKAN.Games; + namespace CKAN { /// @@ -59,7 +61,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist var obj = (IDictionary)Activator.CreateInstance(objectType); if (!IsTokenEmpty(token)) { - foreach (var gameName in GameInstanceManager.AllGameShortNames()) + foreach (var gameName in KnownGames.AllGameShortNames()) { // Make a new copy of the value for each game obj.Add(gameName, token.ToObject(valueType)); diff --git a/Core/Extensions/EnumerableExtensions.cs b/Core/Extensions/EnumerableExtensions.cs index a6d4aef63e..ca57a2aedf 100644 --- a/Core/Extensions/EnumerableExtensions.cs +++ b/Core/Extensions/EnumerableExtensions.cs @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using System.Linq; using System.Threading; +using System.Text.RegularExpressions; namespace CKAN.Extensions { @@ -216,6 +217,16 @@ public static void Deconstruct(this KeyValuePair kvp, out T1 key val = kvp.Value; } + /// + /// Try matching a regex against a series of strings and return the Match objects + /// + /// Sequence of strings to scan + /// Pattern to match + /// Sequence of Match objects + public static IEnumerable WithMatches(this IEnumerable source, Regex pattern) + => source.Select(val => pattern.TryMatch(val, out Match match) ? match : null) + .Where(m => m != null); + } /// diff --git a/Core/GameInstance.cs b/Core/GameInstance.cs index 9cea2f3622..1189a68013 100644 --- a/Core/GameInstance.cs +++ b/Core/GameInstance.cs @@ -33,6 +33,7 @@ public class GameInstance : IEquatable public TimeLog playTime; public string Name { get; set; } + /// /// Returns a file system safe version of the instance name that can be used within file names. /// @@ -240,44 +241,6 @@ public static string PortableDir(IGame game) return null; } - /// - /// Attempts to automatically find a KSP install on this system. - /// Returns the path to the install on success. - /// Throws a DirectoryNotFoundException on failure. - /// - public static string FindGameDir(IGame game) - { - // See if we can find KSP as part of a Steam install. - string gameSteamPath = game.SteamPath(); - if (gameSteamPath != null) - { - if (game.GameInFolder(new DirectoryInfo(gameSteamPath))) - { - return gameSteamPath; - } - - log.DebugFormat("Have Steam, but {0} is not at \"{1}\".", - game.ShortName, gameSteamPath); - } - - // See if we can find a non-Steam Mac KSP install - string kspMacPath = game.MacPath(); - if (kspMacPath != null) - { - if (game.GameInFolder(new DirectoryInfo(kspMacPath))) - { - log.InfoFormat("Found a {0} install at {1}", - game.ShortName, kspMacPath); - return kspMacPath; - } - log.DebugFormat("Default Mac {0} folder exists at \"{1}\", but {0} is not installed there.", - game.ShortName, kspMacPath); - } - - // Oh noes! We can't find KSP! - throw new DirectoryNotFoundException(); - } - /// /// Detects the version of a game in a given directory. /// diff --git a/Core/GameInstanceManager.cs b/Core/GameInstanceManager.cs index 6e800ff35b..a6733ef540 100644 --- a/Core/GameInstanceManager.cs +++ b/Core/GameInstanceManager.cs @@ -23,12 +23,6 @@ namespace CKAN /// public class GameInstanceManager : IDisposable { - private static readonly IGame[] knownGames = new IGame[] - { - new KerbalSpaceProgram(), - new KerbalSpaceProgram2(), - }; - /// /// An IUser object for user interaction. /// It is initialized during the startup with a ConsoleUser, @@ -40,11 +34,13 @@ public class GameInstanceManager : IDisposable public NetModuleCache Cache { get; private set; } + public readonly SteamLibrary SteamLibrary = new SteamLibrary(); + private static readonly ILog log = LogManager.GetLogger(typeof (GameInstanceManager)); private readonly SortedList instances = new SortedList(); - public string[] AllInstanceAnchorFiles => knownGames + public string[] AllInstanceAnchorFiles => KnownGames.knownGames .SelectMany(g => g.InstanceAnchorFiles) .Distinct() .ToArray(); @@ -98,7 +94,7 @@ public GameInstance GetPreferredInstance() // Actual worker for GetPreferredInstance() internal GameInstance _GetPreferredInstance() { - foreach (IGame game in knownGames) + foreach (IGame game in KnownGames.knownGames) { // TODO: Check which ones match, prompt user if >1 @@ -135,7 +131,7 @@ internal GameInstance _GetPreferredInstance() // If we know of no instances, try to find one. // Otherwise, we know of too many instances! // We don't know which one to pick, so we return null. - return !instances.Any() ? FindAndRegisterDefaultInstance() : null; + return !instances.Any() ? FindAndRegisterDefaultInstances() : null; } /// @@ -145,35 +141,58 @@ internal GameInstance _GetPreferredInstance() /// /// Returns the resulting game instance if found. /// - public GameInstance FindAndRegisterDefaultInstance() + public GameInstance FindAndRegisterDefaultInstances() { if (instances.Any()) { throw new KSPManagerKraken("Attempted to scan for defaults with instances"); } - GameInstance val = null; - foreach (IGame game in knownGames) + var found = FindDefaultInstances(); + foreach (var inst in found) { - try + log.DebugFormat("Registering {0} at {1}...", + inst.Name, inst.GameDir()); + AddInstance(inst); + } + return found.FirstOrDefault(); + } + + public GameInstance[] FindDefaultInstances() + { + var found = KnownGames.knownGames.SelectMany(g => + SteamLibrary.Games + .Select(sg => new { name = sg.Name, dir = sg.GameDir }) + .Append(new + { + name = string.Format(Properties.Resources.GameInstanceManagerAuto, + g.ShortName), + dir = g.MacPath(), + }) + .Where(obj => obj.dir != null && g.GameInFolder(obj.dir)) + .Select(obj => new GameInstance(g, obj.dir.FullName, obj.name, User))) + .Where(inst => inst.Valid) + .ToArray(); + foreach (var group in found.GroupBy(inst => inst.Name)) + { + if (group.Count() > 1) { - string gamedir = GameInstance.FindGameDir(game); - GameInstance foundInst = new GameInstance( - game, gamedir, string.Format(Properties.Resources.GameInstanceManagerAuto, game.ShortName), User); - if (foundInst.Valid) + // Make sure the names are unique + int index = 0; + foreach (var inst in group) { - var inst = AddInstance(foundInst); - val = val ?? inst; + // Find an unused name + string name; + do + { + ++index; + name = $"{group.Key} ({++index})"; + } + while (found.Any(other => other.Name == name)); + inst.Name = name; } } - catch (DirectoryNotFoundException) - { - // Thrown if no folder found for a game - } - catch (NotKSPDirKraken) - { - } } - return val; + return found; } /// @@ -429,7 +448,7 @@ public void SetCurrentInstance(string name) public void SetCurrentInstanceByPath(string path) { - var matchingGames = knownGames + var matchingGames = KnownGames.knownGames .Where(g => g.GameInFolder(new DirectoryInfo(path))) .ToList(); switch (matchingGames.Count) @@ -458,7 +477,7 @@ public void SetCurrentInstanceByPath(string path) public GameInstance InstanceAt(string path) { - var matchingGames = knownGames + var matchingGames = KnownGames.knownGames .Where(g => g.GameInFolder(new DirectoryInfo(path))) .ToList(); switch (matchingGames.Count) @@ -514,8 +533,8 @@ private void LoadInstances() var gameName = instance.Item3; try { - var game = knownGames.FirstOrDefault(g => g.ShortName == gameName) - ?? knownGames[0]; + var game = KnownGames.knownGames.FirstOrDefault(g => g.ShortName == gameName) + ?? KnownGames.knownGames[0]; log.DebugFormat("Loading {0} from {1}", name, path); // Add unconditionally, sort out invalid instances downstream instances.Add(name, new GameInstance(game, path, name, User)); @@ -614,7 +633,7 @@ public void Dispose() } public static bool IsGameInstanceDir(DirectoryInfo path) - => knownGames.Any(g => g.GameInFolder(path)); + => KnownGames.knownGames.Any(g => g.GameInFolder(path)); /// /// Tries to determine the game that is installed at the given path @@ -625,7 +644,7 @@ public static bool IsGameInstanceDir(DirectoryInfo path) /// Thrown when no games found public IGame DetermineGame(DirectoryInfo path, IUser user) { - var matchingGames = knownGames.Where(g => g.GameInFolder(path)).ToList(); + var matchingGames = KnownGames.knownGames.Where(g => g.GameInFolder(path)).ToList(); switch (matchingGames.Count) { case 0: @@ -642,21 +661,5 @@ public IGame DetermineGame(DirectoryInfo path, IUser user) return selection >= 0 ? matchingGames[selection] : null; } } - - /// - /// Return a game object based on its short name - /// - /// The short name to find - /// A game object or null if none found - public static IGame GameByShortName(string shortName) - => knownGames.FirstOrDefault(g => g.ShortName == shortName); - - /// - /// Return the short names of all known games - /// - /// Sequence of short name strings - public static IEnumerable AllGameShortNames() - => knownGames.Select(g => g.ShortName); - } } diff --git a/Core/Games/IGame.cs b/Core/Games/IGame.cs index 5af71c556d..58ff2f6a91 100644 --- a/Core/Games/IGame.cs +++ b/Core/Games/IGame.cs @@ -13,13 +13,12 @@ public interface IGame { // Identification, used for display and saved/loaded in settings JSON // Must be unique! - string ShortName { get; } + string ShortName { get; } DateTime FirstReleaseDate { get; } // Where are we? - bool GameInFolder(DirectoryInfo where); - string SteamPath(); - string MacPath(); + bool GameInFolder(DirectoryInfo where); + DirectoryInfo MacPath(); // What do we contain? string PrimaryModDirectoryRelative { get; } @@ -32,7 +31,7 @@ public interface IGame bool IsReservedDirectory(GameInstance inst, string path); bool AllowInstallationIn(string name, out string path); void RebuildSubdirectories(string absGameRoot); - string DefaultCommandLine(string path); + string[] DefaultCommandLines(SteamLibrary steamLib, DirectoryInfo path); string[] AdjustCommandLine(string[] args, GameVersion installedVersion); IDlcDetector[] DlcDetectors { get; } diff --git a/Core/Games/KerbalSpaceProgram.cs b/Core/Games/KerbalSpaceProgram.cs index beaf56edd0..a91f980886 100644 --- a/Core/Games/KerbalSpaceProgram.cs +++ b/Core/Games/KerbalSpaceProgram.cs @@ -25,68 +25,13 @@ public bool GameInFolder(DirectoryInfo where) => InstanceAnchorFiles.Any(f => File.Exists(Path.Combine(where.FullName, f))) && Directory.Exists(Path.Combine(where.FullName, "GameData")); - /// - /// Finds the Steam KSP path. Returns null if the folder cannot be located. - /// - /// The KSP path. - public string SteamPath() - { - // Attempt to get the Steam path. - string steamPath = CKANPathUtils.SteamPath(); - - if (steamPath == null) - { - return null; - } - - // Default steam library - string installPath = KSPDirectory(steamPath); - if (installPath != null) - { - return installPath; - } - - // Attempt to find through config file - string configPath = Path.Combine(steamPath, "config", "config.vdf"); - if (File.Exists(configPath)) - { - log.InfoFormat("Found Steam config file at {0}", configPath); - StreamReader reader = new StreamReader(configPath); - string line; - while ((line = reader.ReadLine()) != null) - { - // Found Steam library - if (line.Contains("BaseInstallFolder")) - { - // This assumes config file is valid, we just skip it if it looks funny. - string[] split_line = line.Split('"'); - - if (split_line.Length > 3) - { - log.DebugFormat("Found a Steam Libary Location at {0}", split_line[3]); - - installPath = KSPDirectory(split_line[3]); - if (installPath != null) - { - log.InfoFormat("Found a KSP install at {0}", installPath); - return installPath; - } - } - } - } - } - - // Could not locate the folder. - return null; - } - /// /// Get the default non-Steam path to KSP on macOS /// /// /// "/Applications/Kerbal Space Program" if it exists and we're on a Mac, else null /// - public string MacPath() + public DirectoryInfo MacPath() { if (Platform.IsMac) { @@ -95,7 +40,8 @@ public string MacPath() Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Kerbal Space Program" ); - return Directory.Exists(installPath) ? installPath : null; + return Directory.Exists(installPath) ? new DirectoryInfo(installPath) + : null; } return null; } @@ -175,14 +121,18 @@ public void RebuildSubdirectories(string absGameRoot) } } - public string DefaultCommandLine(string path) - => Platform.IsMac - ? "./KSP.app/Contents/MacOS/KSP" - : string.Format(Platform.IsUnix ? "./{0} -single-instance" - : "{0} -single-instance", - InstanceAnchorFiles.FirstOrDefault(f => - File.Exists(Path.Combine(path, f))) - ?? InstanceAnchorFiles.First()); + public string[] DefaultCommandLines(SteamLibrary steamLib, DirectoryInfo path) + => Enumerable.Repeat(Platform.IsMac + ? "./KSP.app/Contents/MacOS/KSP" + : string.Format(Platform.IsUnix ? "./{0} -single-instance" + : "{0} -single-instance", + InstanceAnchorFiles.FirstOrDefault(f => + File.Exists(Path.Combine(path.FullName, f))) + ?? InstanceAnchorFiles.First()), + 1) + .Concat(steamLib.GameAppURLs(path) + .Select(url => url.ToString())) + .ToArray(); public string[] AdjustCommandLine(string[] args, GameVersion installedVersion) { @@ -325,28 +275,6 @@ private static string Scenarios(GameInstance inst) { "Ships/Script", "Ships/Script" } }; - /// - /// Finds the KSP path under a Steam Library. Returns null if the folder cannot be located. - /// - /// Steam Library Path - /// The KSP path. - private static string KSPDirectory(string steamPath) - { - // There are several possibilities for the path under Linux. - // Try with the uppercase version. - string installPath = Path.Combine(steamPath, "SteamApps", "common", "Kerbal Space Program"); - - if (Directory.Exists(installPath)) - { - return installPath; - } - - // Try with the lowercase version. - installPath = Path.Combine(steamPath, "steamapps", "common", "Kerbal Space Program"); - - return Directory.Exists(installPath) ? installPath : null; - } - /// /// If the installed game version is in the given range, /// return the given array without the given parameter, diff --git a/Core/Games/KerbalSpaceProgram2.cs b/Core/Games/KerbalSpaceProgram2.cs index c85c535318..46c3ff14fd 100644 --- a/Core/Games/KerbalSpaceProgram2.cs +++ b/Core/Games/KerbalSpaceProgram2.cs @@ -23,68 +23,13 @@ public bool GameInFolder(DirectoryInfo where) => InstanceAnchorFiles.Any(f => File.Exists(Path.Combine(where.FullName, f))) && Directory.Exists(Path.Combine(where.FullName, "KSP2_x64_Data")); - /// - /// Finds the Steam KSP path. Returns null if the folder cannot be located. - /// - /// The KSP path. - public string SteamPath() - { - // Attempt to get the Steam path. - string steamPath = CKANPathUtils.SteamPath(); - - if (steamPath == null) - { - return null; - } - - // Default steam library - string installPath = GameDirectory(steamPath); - if (installPath != null) - { - return installPath; - } - - // Attempt to find through config file - string configPath = Path.Combine(steamPath, "config", "config.vdf"); - if (File.Exists(configPath)) - { - log.InfoFormat("Found Steam config file at {0}", configPath); - StreamReader reader = new StreamReader(configPath); - string line; - while ((line = reader.ReadLine()) != null) - { - // Found Steam library - if (line.Contains("BaseInstallFolder")) - { - // This assumes config file is valid, we just skip it if it looks funny. - string[] split_line = line.Split('"'); - - if (split_line.Length > 3) - { - log.DebugFormat("Found a Steam Libary Location at {0}", split_line[3]); - - installPath = GameDirectory(split_line[3]); - if (installPath != null) - { - log.InfoFormat("Found a KSP install at {0}", installPath); - return installPath; - } - } - } - } - } - - // Could not locate the folder. - return null; - } - /// /// Get the default non-Steam path to KSP on macOS /// /// /// "/Applications/Kerbal Space Program" if it exists and we're on a Mac, else null /// - public string MacPath() + public DirectoryInfo MacPath() { if (Platform.IsMac) { @@ -93,7 +38,8 @@ public string MacPath() Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Kerbal Space Program 2" ); - return Directory.Exists(installPath) ? installPath : null; + return Directory.Exists(installPath) ? new DirectoryInfo(installPath) + : null; } return null; } @@ -146,14 +92,18 @@ public void RebuildSubdirectories(string absGameRoot) } } - public string DefaultCommandLine(string path) - => Platform.IsMac - ? "./KSP2.app/Contents/MacOS/KSP2" - : string.Format(Platform.IsUnix ? "./{0} -single-instance" - : "{0} -single-instance", - InstanceAnchorFiles.FirstOrDefault(f => - File.Exists(Path.Combine(path, f))) - ?? InstanceAnchorFiles.First()); + public string[] DefaultCommandLines(SteamLibrary steamLib, DirectoryInfo path) + => Enumerable.Repeat(Platform.IsMac + ? "./KSP2.app/Contents/MacOS/KSP2" + : string.Format(Platform.IsUnix ? "./{0} -single-instance" + : "{0} -single-instance", + InstanceAnchorFiles.FirstOrDefault(f => + File.Exists(Path.Combine(path.FullName, f))) + ?? InstanceAnchorFiles.First()), + 1) + .Concat(steamLib.GameAppURLs(path) + .Select(url => url.ToString())) + .ToArray(); public string[] AdjustCommandLine(string[] args, GameVersion installedVersion) => args; @@ -244,28 +194,6 @@ private GameVersion VersionFromFile(string path) { "BepInEx/plugins", "BepInEx/plugins" }, }; - /// - /// Finds the KSP path under a Steam Library. Returns null if the folder cannot be located. - /// - /// Steam Library Path - /// The KSP path. - private static string GameDirectory(string steamPath) - { - // There are several possibilities for the path under Linux. - // Try with the uppercase version. - string installPath = Path.Combine(steamPath, "SteamApps", "common", "Kerbal Space Program 2"); - - if (Directory.Exists(installPath)) - { - return installPath; - } - - // Try with the lowercase version. - installPath = Path.Combine(steamPath, "steamapps", "common", "Kerbal Space Program 2"); - - return Directory.Exists(installPath) ? installPath : null; - } - private static readonly ILog log = LogManager.GetLogger(typeof(KerbalSpaceProgram2)); } } diff --git a/Core/Games/KnownGames.cs b/Core/Games/KnownGames.cs new file mode 100644 index 0000000000..dc8f1009b6 --- /dev/null +++ b/Core/Games/KnownGames.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; + + +namespace CKAN.Games +{ + public static class KnownGames + { + public static readonly IGame[] knownGames = new IGame[] + { + new KerbalSpaceProgram.KerbalSpaceProgram(), + new KerbalSpaceProgram2.KerbalSpaceProgram2(), + }; + + /// + /// Return a game object based on its short name + /// + /// The short name to find + /// A game object or null if none found + public static IGame GameByShortName(string shortName) + => knownGames.FirstOrDefault(g => g.ShortName == shortName); + + /// + /// Return the short names of all known games + /// + /// Sequence of short name strings + public static IEnumerable AllGameShortNames() + => knownGames.Select(g => g.ShortName); + + } +} diff --git a/Core/SteamLibrary.cs b/Core/SteamLibrary.cs new file mode 100644 index 0000000000..137acca7aa --- /dev/null +++ b/Core/SteamLibrary.cs @@ -0,0 +1,149 @@ +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; + +using log4net; +using ValveKeyValue; + +namespace CKAN +{ + public class SteamLibrary + { + public SteamLibrary() + { + var libraryPath = SteamPaths.Where(p => !string.IsNullOrEmpty(p)) + .FirstOrDefault(Directory.Exists); + if (libraryPath == null) + { + log.Info("Steam not found"); + Games = Array.Empty(); + } + else + { + log.InfoFormat("Found Steam at {0}", libraryPath); + var txtParser = KVSerializer.Create(KVSerializationFormat.KeyValues1Text); + var appPaths = txtParser.Deserialize>( + File.OpenRead(Path.Combine(libraryPath, + "config", + "libraryfolders.vdf"))) + .Select(lf => appRelPaths.Select(p => Path.Combine(lf.Path, p)) + .FirstOrDefault(Directory.Exists)) + .Where(p => p != null) + .ToArray(); + var steamGames = appPaths.SelectMany(p => LibraryPathGames(txtParser, p)); + var binParser = KVSerializer.Create(KVSerializationFormat.KeyValues1Binary); + var nonSteamGames = Directory.EnumerateDirectories(Path.Combine(libraryPath, "userdata")) + .Select(dirName => Path.Combine(dirName, "config")) + .Where(Directory.Exists) + .Select(dirName => Path.Combine(dirName, "shortcuts.vdf")) + .Where(File.Exists) + .SelectMany(p => ShortcutsFileGames(binParser, p)); + Games = steamGames.Concat(nonSteamGames) + .ToArray(); + log.DebugFormat("Games: {0}", + string.Join(", ", Games.Select(g => $"{g.LaunchUrl} ({g.GameDir})"))); + } + } + + public IEnumerable GameAppURLs(DirectoryInfo gameDir) + => Games.Where(g => gameDir.FullName.Equals(g.GameDir.FullName, compareOpt)) + .Select(g => g.LaunchUrl); + + public readonly GameBase[] Games; + + private static IEnumerable LibraryPathGames(KVSerializer acfParser, + string appPath) + => Directory.EnumerateFiles(appPath, "*.acf") + .Select(acfFile => acfParser.Deserialize(File.OpenRead(acfFile)) + .NormalizeDir(Path.Combine(appPath, "common"))); + + private static IEnumerable ShortcutsFileGames(KVSerializer vdfParser, + string path) + => vdfParser.Deserialize>(File.OpenRead(path)) + .Select(nsg => nsg.NormalizeDir(path)); + + private const string registryKey = @"HKEY_CURRENT_USER\Software\Valve\Steam"; + private const string registryValue = @"SteamPath"; + private static string[] SteamPaths + => Platform.IsWindows ? new string[] + { + // First check the registry + (string)Microsoft.Win32.Registry.GetValue(registryKey, registryValue, null), + } + : Platform.IsUnix ? new string[] + { + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), + ".local", "share", "Steam"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), + ".steam", "steam"), + } + : Platform.IsMac ? new string[] + { + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), + "Library", "Application Support", "Steam"), + } + : Array.Empty(); + + private static readonly string[] appRelPaths = new string[] { "SteamApps", "steamapps" }; + + private static readonly StringComparison compareOpt + = Platform.IsWindows ? StringComparison.InvariantCultureIgnoreCase + : StringComparison.InvariantCulture; + + private static readonly ILog log = LogManager.GetLogger(typeof(SteamLibrary)); + } + + public class LibraryFolder + { + [KVProperty("path")] public string Path { get; set; } + } + + public abstract class GameBase + { + public abstract string Name { get; set; } + + [KVIgnore] public DirectoryInfo GameDir { get; set; } + [KVIgnore] public abstract Uri LaunchUrl { get; } + + public abstract GameBase NormalizeDir(string appPath); + } + + public class SteamGame : GameBase + { + [KVProperty("appid")] public ulong AppId { get; set; } + [KVProperty("name")] public override string Name { get; set; } + [KVProperty("installdir")] public string InstallDir { get; set; } + + [KVIgnore] + public override Uri LaunchUrl => new Uri($"steam://rungameid/{AppId}"); + + public override GameBase NormalizeDir(string commonPath) + { + GameDir = new DirectoryInfo(CKANPathUtils.NormalizePath(Path.Combine(commonPath, InstallDir))); + return this; + } + } + + public class NonSteamGame : GameBase + { + [KVProperty("appid")] + public int AppId { get; set; } + [KVProperty("AppName")] + public override string Name { get; set; } + public string Exe { get; set; } + public string StartDir { get; set; } + + [KVIgnore] + private ulong UrlId => (unchecked((ulong)AppId) << 32) | 0x02000000; + + [KVIgnore] + public override Uri LaunchUrl => new Uri($"steam://rungameid/{UrlId}"); + + public override GameBase NormalizeDir(string appPath) + { + GameDir = new DirectoryInfo(CKANPathUtils.NormalizePath(StartDir.Trim('"'))); + return this; + } + } +} diff --git a/GUI/Controls/EditModSearchDetails.Designer.cs b/GUI/Controls/EditModSearchDetails.Designer.cs index b96b377c69..baa13f6216 100644 --- a/GUI/Controls/EditModSearchDetails.Designer.cs +++ b/GUI/Controls/EditModSearchDetails.Designer.cs @@ -344,7 +344,7 @@ private void InitializeComponent() // CompatibleToggle // this.CompatibleToggle.Location = new System.Drawing.Point(130, 293); - this.CompatibleToggle.Changed += TriStateChanged; + this.CompatibleToggle.Changed += this.TriStateChanged; // // InstalledLabel // @@ -360,7 +360,7 @@ private void InitializeComponent() // InstalledToggle // this.InstalledToggle.Location = new System.Drawing.Point(130, 319); - this.InstalledToggle.Changed += TriStateChanged; + this.InstalledToggle.Changed += this.TriStateChanged; // // CachedLabel // @@ -376,7 +376,7 @@ private void InitializeComponent() // CachedToggle // this.CachedToggle.Location = new System.Drawing.Point(130, 345); - this.CachedToggle.Changed += TriStateChanged; + this.CachedToggle.Changed += this.TriStateChanged; // // NewlyCompatibleLabel // @@ -392,7 +392,7 @@ private void InitializeComponent() // NewlyCompatibleToggle // this.NewlyCompatibleToggle.Location = new System.Drawing.Point(130, 371); - this.NewlyCompatibleToggle.Changed += TriStateChanged; + this.NewlyCompatibleToggle.Changed += this.TriStateChanged; // // UpgradeableLabel // @@ -408,7 +408,7 @@ private void InitializeComponent() // UpgradeableToggle // this.UpgradeableToggle.Location = new System.Drawing.Point(130, 397); - this.UpgradeableToggle.Changed += TriStateChanged; + this.UpgradeableToggle.Changed += this.TriStateChanged; // // ReplaceableLabel // @@ -424,7 +424,7 @@ private void InitializeComponent() // ReplaceableToggle // this.ReplaceableToggle.Location = new System.Drawing.Point(130, 423); - this.ReplaceableToggle.Changed += TriStateChanged; + this.ReplaceableToggle.Changed += this.TriStateChanged; // // EditModSearchDetails // diff --git a/GUI/Controls/HintTextBox.Designer.cs b/GUI/Controls/HintTextBox.Designer.cs index 8a203e107e..a3916db255 100644 --- a/GUI/Controls/HintTextBox.Designer.cs +++ b/GUI/Controls/HintTextBox.Designer.cs @@ -39,7 +39,7 @@ private void InitializeComponent() this.ClearIcon.Image = global::CKAN.GUI.EmbeddedImages.textClear; this.ClearIcon.SizeMode = System.Windows.Forms.PictureBoxSizeMode.CenterImage; this.ClearIcon.Size = new System.Drawing.Size(18, 18); - this.ClearIcon.Click += HintClearIcon_Click; + this.ClearIcon.Click += this.HintClearIcon_Click; // // HintTextBox // diff --git a/GUI/Controls/LeftRightRowPanel.cs b/GUI/Controls/LeftRightRowPanel.cs index 12bdfd5596..297f24086c 100644 --- a/GUI/Controls/LeftRightRowPanel.cs +++ b/GUI/Controls/LeftRightRowPanel.cs @@ -1,4 +1,5 @@ using System.Windows.Forms; +using System.Drawing; namespace CKAN.GUI { @@ -31,6 +32,15 @@ public LeftRightRowPanel() FlowDirection = FlowDirection.RightToLeft, }; + // Let the outer control handle horizontal padding + LeftPanel.Margin = new Padding(0, LeftPanel.Margin.Top, + 0, LeftPanel.Margin.Bottom); + RightPanel.Margin = new Padding(0, RightPanel.Margin.Top, + 0, RightPanel.Margin.Bottom); + + // Don't overwrite graphics drawn on parent + BackColor = LeftPanel.BackColor = RightPanel.BackColor = Color.Transparent; + AutoSize = true; AutoSizeMode = AutoSizeMode.GrowAndShrink; // Throw exceptions if the table gets bigger than a 2x1 layout diff --git a/GUI/Controls/ManageMods.Designer.cs b/GUI/Controls/ManageMods.Designer.cs index dcb5089a9b..3caf5e9f57 100644 --- a/GUI/Controls/ManageMods.Designer.cs +++ b/GUI/Controls/ManageMods.Designer.cs @@ -32,7 +32,9 @@ private void InitializeComponent() System.ComponentModel.ComponentResourceManager resources = new SingleAssemblyComponentResourceManager(typeof(ManageMods)); this.ToolTip = new System.Windows.Forms.ToolTip(); this.menuStrip2 = new System.Windows.Forms.MenuStrip(); - this.launchGameToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.LaunchGameToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.CommandLinesToolStripSeparator = new System.Windows.Forms.ToolStripSeparator(); + this.EditCommandLinesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.RefreshToolButton = new System.Windows.Forms.ToolStripMenuItem(); this.UpdateAllToolButton = new System.Windows.Forms.ToolStripMenuItem(); this.ApplyToolButton = new System.Windows.Forms.ToolStripMenuItem(); @@ -101,7 +103,7 @@ private void InitializeComponent() this.menuStrip2.Dock = System.Windows.Forms.DockStyle.Top; this.menuStrip2.ImageScalingSize = new System.Drawing.Size(24, 24); this.menuStrip2.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.launchGameToolStripMenuItem, + this.LaunchGameToolStripMenuItem, this.RefreshToolButton, this.UpdateAllToolButton, this.ApplyToolButton, @@ -117,15 +119,23 @@ private void InitializeComponent() this.menuStrip2.TabIndex = 4; this.menuStrip2.Text = "menuStrip2"; // - // launchGameToolStripMenuItem + // LaunchGameToolStripMenuItem // - this.launchGameToolStripMenuItem.Image = global::CKAN.GUI.EmbeddedImages.ksp; - this.launchGameToolStripMenuItem.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None; - this.launchGameToolStripMenuItem.Name = "launchGameToolStripMenuItem"; - this.launchGameToolStripMenuItem.Size = new System.Drawing.Size(146, 56); - this.launchGameToolStripMenuItem.Overflow = System.Windows.Forms.ToolStripItemOverflow.AsNeeded; - this.launchGameToolStripMenuItem.Click += new System.EventHandler(this.launchGameToolStripMenuItem_Click); - resources.ApplyResources(this.launchGameToolStripMenuItem, "launchGameToolStripMenuItem"); + this.LaunchGameToolStripMenuItem.MouseHover += new System.EventHandler(LaunchGameToolStripMenuItem_MouseHover); + this.LaunchGameToolStripMenuItem.Image = global::CKAN.GUI.EmbeddedImages.ksp; + this.LaunchGameToolStripMenuItem.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None; + this.LaunchGameToolStripMenuItem.Name = "LaunchGameToolStripMenuItem"; + this.LaunchGameToolStripMenuItem.Size = new System.Drawing.Size(146, 56); + this.LaunchGameToolStripMenuItem.Overflow = System.Windows.Forms.ToolStripItemOverflow.AsNeeded; + this.LaunchGameToolStripMenuItem.Click += new System.EventHandler(this.LaunchGameToolStripMenuItem_Click); + resources.ApplyResources(this.LaunchGameToolStripMenuItem, "LaunchGameToolStripMenuItem"); + // + // EditCommandLinesToolStripMenuItem + // + this.EditCommandLinesToolStripMenuItem.Name = "EditCommandLinesToolStripMenuItem"; + this.EditCommandLinesToolStripMenuItem.Size = new System.Drawing.Size(179, 22); + this.EditCommandLinesToolStripMenuItem.Click += new System.EventHandler(this.EditCommandLinesToolStripMenuItem_Click); + resources.ApplyResources(this.EditCommandLinesToolStripMenuItem, "EditCommandLinesToolStripMenuItem"); // // RefreshToolButton // @@ -299,9 +309,9 @@ private void InitializeComponent() // EditModSearches // this.EditModSearches.Dock = System.Windows.Forms.DockStyle.Top; - this.EditModSearches.ApplySearches += EditModSearches_ApplySearches; - this.EditModSearches.SurrenderFocus += EditModSearches_SurrenderFocus; - this.EditModSearches.ShowError += EditModSearches_ShowError; + this.EditModSearches.ApplySearches += this.EditModSearches_ApplySearches; + this.EditModSearches.SurrenderFocus += this.EditModSearches_SurrenderFocus; + this.EditModSearches.ShowError += this.EditModSearches_ShowError; // // ModGrid // @@ -550,8 +560,8 @@ private void InitializeComponent() this.hiddenTagsLabelsLinkList.Location = new System.Drawing.Point(0, 0); this.hiddenTagsLabelsLinkList.Name = "hiddenTagsLabelsLinkList"; this.hiddenTagsLabelsLinkList.Size = new System.Drawing.Size(500, 20); - this.hiddenTagsLabelsLinkList.TagClicked += hiddenTagsLabelsLinkList_TagClicked; - this.hiddenTagsLabelsLinkList.LabelClicked += hiddenTagsLabelsLinkList_LabelClicked; + this.hiddenTagsLabelsLinkList.TagClicked += this.hiddenTagsLabelsLinkList_TagClicked; + this.hiddenTagsLabelsLinkList.LabelClicked += this.hiddenTagsLabelsLinkList_LabelClicked; resources.ApplyResources(this.hiddenTagsLabelsLinkList, "hiddenTagsLabelsLinkList"); // // ManageMods @@ -581,7 +591,9 @@ private void InitializeComponent() private System.Windows.Forms.ToolTip ToolTip; private System.Windows.Forms.MenuStrip menuStrip2; private System.Windows.Forms.CheckBox InstallAllCheckbox; - private System.Windows.Forms.ToolStripMenuItem launchGameToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem LaunchGameToolStripMenuItem; + private System.Windows.Forms.ToolStripSeparator CommandLinesToolStripSeparator; + private System.Windows.Forms.ToolStripMenuItem EditCommandLinesToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem RefreshToolButton; private System.Windows.Forms.ToolStripMenuItem UpdateAllToolButton; private System.Windows.Forms.ToolStripMenuItem ApplyToolButton; diff --git a/GUI/Controls/ManageMods.cs b/GUI/Controls/ManageMods.cs index 89d8405fc8..e9588dc354 100644 --- a/GUI/Controls/ManageMods.cs +++ b/GUI/Controls/ManageMods.cs @@ -40,7 +40,6 @@ public ManageMods() mainModList = new ModList(); mainModList.ModFiltersUpdated += UpdateFilters; FilterToolButton.MouseHover += (sender, args) => FilterToolButton.ShowDropDown(); - launchGameToolStripMenuItem.MouseHover += (sender, args) => launchGameToolStripMenuItem.ShowDropDown(); ApplyToolButton.MouseHover += (sender, args) => ApplyToolButton.ShowDropDown(); ApplyToolButton.Enabled = false; @@ -60,6 +59,7 @@ public ManageMods() FilterToolButton.DropDown.Renderer = new FlatToolStripRenderer(); FilterTagsToolButton.DropDown.Renderer = new FlatToolStripRenderer(); FilterLabelsToolButton.DropDown.Renderer = new FlatToolStripRenderer(); + LaunchGameToolStripMenuItem.DropDown.Renderer = new FlatToolStripRenderer(); ModListContextMenuStrip.Renderer = new FlatToolStripRenderer(); ModListHeaderContextMenuStrip.Renderer = new FlatToolStripRenderer(); LabelsContextMenuStrip.Renderer = new FlatToolStripRenderer(); @@ -80,6 +80,8 @@ public ManageMods() public event Action RaiseMessage; public event Action RaiseError; public event Action ClearStatusBar; + public event Action LaunchGame; + public event Action EditCommandLines; public readonly ModList mainModList; private List SortColumns @@ -549,9 +551,42 @@ private void _MarkModForUpdate(string identifier, bool value) mod.SetUpgradeChecked(row, UpdateCol, value); } - private void launchGameToolStripMenuItem_Click(object sender, EventArgs e) + private void LaunchGameToolStripMenuItem_MouseHover(object sender, EventArgs e) { - Main.Instance.LaunchGame(); + var cmdLines = Main.Instance.configuration.CommandLines; + LaunchGameToolStripMenuItem.Tag = + LaunchGameToolStripMenuItem.ToolTipText = cmdLines.First(); + LaunchGameToolStripMenuItem.DropDownItems.Clear(); + LaunchGameToolStripMenuItem.DropDownItems.AddRange( + cmdLines.Select(cmdLine => (ToolStripItem) + new ToolStripMenuItem(cmdLine, null, + LaunchGameToolStripMenuItem_Click) + { + Tag = cmdLine, + ShortcutKeyDisplayString = CmdLineHelp(cmdLine), + }) + .Append(CommandLinesToolStripSeparator) + .Append(EditCommandLinesToolStripMenuItem) + .ToArray()); + LaunchGameToolStripMenuItem.ShowDropDown(); + } + + private string CmdLineHelp(string cmdLine) + => Main.Instance.Manager.SteamLibrary.Games.Length > 0 + ? cmdLine.StartsWith("steam://", StringComparison.InvariantCultureIgnoreCase) + ? Properties.Resources.ManageModsSteamPlayTimeYesTooltip + : Properties.Resources.ManageModsSteamPlayTimeNoTooltip + : ""; + + private void LaunchGameToolStripMenuItem_Click(object sender, EventArgs e) + { + var menuItem = sender as ToolStripMenuItem; + LaunchGame?.Invoke(menuItem?.Tag as string); + } + + private void EditCommandLinesToolStripMenuItem_Click(object sender, EventArgs e) + { + EditCommandLines?.Invoke(); } private void NavBackwardToolButton_Click(object sender, EventArgs e) @@ -1156,15 +1191,15 @@ private void _UpdateFilters() GUIMod selected_mod = null; if (ModGrid.CurrentRow != null) { - selected_mod = (GUIMod) ModGrid.CurrentRow.Tag; + selected_mod = (GUIMod)ModGrid.CurrentRow.Tag; } - var registry = RegistryManager.Instance(Main.Instance.CurrentInstance, repoData).registry; + var inst = Main.Instance.CurrentInstance; + var registry = RegistryManager.Instance(inst, repoData).registry; ModGrid.Rows.Clear(); - var instName = Main.Instance.CurrentInstance.Name; - var instGame = Main.Instance.CurrentInstance.game; rows.AsParallel().ForAll(row => - row.Visible = mainModList.IsVisible((GUIMod)row.Tag, instName, instGame, registry)); + row.Visible = mainModList.IsVisible((GUIMod)row.Tag, + inst.Name, inst.game, registry)); ApplyHeaderGlyphs(); ModGrid.Rows.AddRange(Sort(rows.Where(row => row.Visible)).ToArray()); @@ -1740,6 +1775,7 @@ public void InstanceUpdated() { Conflicts = null; ChangeSet = null; + ModGrid.CurrentCell = null; } [ForbidGUICalls] diff --git a/GUI/Controls/ManageMods.resx b/GUI/Controls/ManageMods.resx index f3e99c918b..d630c3bd40 100644 --- a/GUI/Controls/ManageMods.resx +++ b/GUI/Controls/ManageMods.resx @@ -117,7 +117,8 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Play + Play + Edit command lines... Refresh Update all Apply diff --git a/GUI/Controls/ModInfo.Designer.cs b/GUI/Controls/ModInfo.Designer.cs index e5a2d115ad..423855f4e9 100644 --- a/GUI/Controls/ModInfo.Designer.cs +++ b/GUI/Controls/ModInfo.Designer.cs @@ -89,8 +89,8 @@ private void InitializeComponent() this.tagsLabelsLinkList.Location = new System.Drawing.Point(0, 0); this.tagsLabelsLinkList.Name = "tagsLabelsLinkList"; this.tagsLabelsLinkList.Size = new System.Drawing.Size(500, 20); - this.tagsLabelsLinkList.TagClicked += tagsLabelsLinkList_TagClicked; - this.tagsLabelsLinkList.LabelClicked += tagsLabelsLinkList_LabelClicked; + this.tagsLabelsLinkList.TagClicked += this.tagsLabelsLinkList_TagClicked; + this.tagsLabelsLinkList.LabelClicked += this.tagsLabelsLinkList_LabelClicked; // // MetadataModuleAbstractLabel // @@ -153,7 +153,7 @@ private void InitializeComponent() this.Metadata.Margin = new System.Windows.Forms.Padding(2); this.Metadata.Name = "Metadata"; this.Metadata.TabIndex = 6; - this.Metadata.OnChangeFilter += Metadata_OnChangeFilter; + this.Metadata.OnChangeFilter += this.Metadata_OnChangeFilter; // // RelationshipTabPage // diff --git a/GUI/Controls/PlayTime.Designer.cs b/GUI/Controls/PlayTime.Designer.cs index 47015dd913..95d998462e 100755 --- a/GUI/Controls/PlayTime.Designer.cs +++ b/GUI/Controls/PlayTime.Designer.cs @@ -80,7 +80,7 @@ private void InitializeComponent() this.PlayTimeGrid.Size = new System.Drawing.Size(1536, 837); this.PlayTimeGrid.StandardTab = false; this.PlayTimeGrid.TabIndex = 1; - this.PlayTimeGrid.CellValueChanged += PlayTimeGrid_CellValueChanged; + this.PlayTimeGrid.CellValueChanged += this.PlayTimeGrid_CellValueChanged; // // NameColumn // diff --git a/GUI/Dialogs/GameCommandLineOptionsDialog.Designer.cs b/GUI/Dialogs/GameCommandLineOptionsDialog.Designer.cs index b818ed746f..05da3c7440 100644 --- a/GUI/Dialogs/GameCommandLineOptionsDialog.Designer.cs +++ b/GUI/Dialogs/GameCommandLineOptionsDialog.Designer.cs @@ -30,67 +30,138 @@ private void InitializeComponent() { this.components = new System.ComponentModel.Container(); System.ComponentModel.ComponentResourceManager resources = new SingleAssemblyComponentResourceManager(typeof(GameCommandLineOptionsDialog)); - this.AdditionalArguments = new System.Windows.Forms.TextBox(); - this.label1 = new System.Windows.Forms.Label(); + this.CmdLineGrid = new System.Windows.Forms.DataGridView(); + this.CmdLineColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.BottomButtonPanel = new CKAN.GUI.LeftRightRowPanel(); + this.ResetToDefaultsButton = new System.Windows.Forms.Button(); + this.AddButton = new System.Windows.Forms.Button(); this.AcceptChangesButton = new System.Windows.Forms.Button(); this.CancelChangesButton = new System.Windows.Forms.Button(); this.SuspendLayout(); // - // AdditionalArguments + // CmdLineGrid // - this.AdditionalArguments.Location = new System.Drawing.Point(15, 25); - this.AdditionalArguments.Name = "AdditionalArguments"; - this.AdditionalArguments.Size = new System.Drawing.Size(457, 20); - this.AdditionalArguments.AutoCompleteMode = System.Windows.Forms.AutoCompleteMode.SuggestAppend; - this.AdditionalArguments.AutoCompleteSource = System.Windows.Forms.AutoCompleteSource.CustomSource; - this.AdditionalArguments.TabIndex = 1; + this.CmdLineGrid.AutoGenerateColumns = false; + this.CmdLineGrid.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D; + this.CmdLineGrid.Dock = System.Windows.Forms.DockStyle.Fill; + this.CmdLineGrid.EditMode = System.Windows.Forms.DataGridViewEditMode.EditOnKeystrokeOrF2; + this.CmdLineGrid.AllowUserToAddRows = true; + this.CmdLineGrid.AllowUserToDeleteRows = true; + this.CmdLineGrid.AllowUserToResizeRows = false; + this.CmdLineGrid.BackgroundColor = System.Drawing.SystemColors.Window; + this.CmdLineGrid.EnableHeadersVisualStyles = false; + this.CmdLineGrid.ColumnHeadersDefaultCellStyle.Padding = new System.Windows.Forms.Padding(1, 3, 1, 3); + this.CmdLineGrid.ColumnHeadersDefaultCellStyle.BackColor = System.Drawing.SystemColors.Control; + this.CmdLineGrid.ColumnHeadersDefaultCellStyle.ForeColor = System.Drawing.SystemColors.ControlText; + this.CmdLineGrid.ColumnHeadersDefaultCellStyle.SelectionBackColor = System.Drawing.SystemColors.Control; + this.CmdLineGrid.DefaultCellStyle.ForeColor = System.Drawing.SystemColors.WindowText; + this.CmdLineGrid.CellBorderStyle = System.Windows.Forms.DataGridViewCellBorderStyle.SingleHorizontal; + this.CmdLineGrid.ColumnHeadersBorderStyle = System.Windows.Forms.DataGridViewHeaderBorderStyle.Raised; + this.CmdLineGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; + this.CmdLineGrid.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { + this.CmdLineColumn}); + this.CmdLineGrid.Location = new System.Drawing.Point(12, 9); + this.CmdLineGrid.MultiSelect = false; + this.CmdLineGrid.Name = "CmdLineGrid"; + this.CmdLineGrid.RowHeadersVisible = false; + this.CmdLineGrid.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect; + this.CmdLineGrid.Size = new System.Drawing.Size(457, 20); + this.CmdLineGrid.StandardTab = false; + this.CmdLineGrid.TabIndex = 1; + this.CmdLineGrid.EditingControlShowing += this.CmdLineGrid_EditingControlShowing; + this.CmdLineGrid.UserDeletingRow += this.CmdLineGrid_UserDeletingRow; // - // label1 + // CmdLineColumn // - this.label1.AutoSize = true; - this.label1.Location = new System.Drawing.Point(12, 9); - this.label1.Name = "label1"; - this.label1.Size = new System.Drawing.Size(60, 13); - this.label1.TabIndex = 2; - resources.ApplyResources(this.label1, "label1"); + this.CmdLineColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; + this.CmdLineColumn.Name = "CmdLineColumn"; + this.CmdLineColumn.DataPropertyName = "CmdLine"; + this.CmdLineColumn.ReadOnly = false; + this.CmdLineColumn.ValueType = typeof(string); + this.CmdLineColumn.Width = 250; + resources.ApplyResources(this.CmdLineColumn, "CmdLineColumn"); // - // AcceptChangesButton + // BottomButtonPanel // - this.AcceptChangesButton.DialogResult = System.Windows.Forms.DialogResult.OK; - this.AcceptChangesButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.AcceptChangesButton.Location = new System.Drawing.Point(397, 51); - this.AcceptChangesButton.Name = "AcceptChangesButton"; - this.AcceptChangesButton.Size = new System.Drawing.Size(75, 23); - this.AcceptChangesButton.TabIndex = 3; - this.AcceptChangesButton.UseVisualStyleBackColor = true; - resources.ApplyResources(this.AcceptChangesButton, "AcceptChangesButton"); + this.BottomButtonPanel.LeftControls.Add(this.ResetToDefaultsButton); + this.BottomButtonPanel.LeftControls.Add(this.AddButton); + this.BottomButtonPanel.RightControls.Add(this.AcceptChangesButton); + this.BottomButtonPanel.RightControls.Add(this.CancelChangesButton); + this.BottomButtonPanel.Dock = System.Windows.Forms.DockStyle.Bottom; + this.BottomButtonPanel.Name = "BottomButtonPanel"; + // + // ResetToDefaultsButton + // + this.ResetToDefaultsButton.AutoSize = true; + this.ResetToDefaultsButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly; + this.ResetToDefaultsButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.ResetToDefaultsButton.Location = new System.Drawing.Point(316, 51); + this.ResetToDefaultsButton.Margin = new System.Windows.Forms.Padding(0, 4, 8, 4); + this.ResetToDefaultsButton.Name = "ResetToDefaultsButton"; + this.ResetToDefaultsButton.Size = new System.Drawing.Size(75, 23); + this.ResetToDefaultsButton.TabIndex = 2; + this.ResetToDefaultsButton.UseVisualStyleBackColor = true; + this.ResetToDefaultsButton.Click += this.ResetToDefaultsButton_Click; + resources.ApplyResources(this.ResetToDefaultsButton, "ResetToDefaultsButton"); + // + // AddButton + // + this.AddButton.AutoSize = true; + this.AddButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly; + this.AddButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.AddButton.Location = new System.Drawing.Point(316, 51); + this.AddButton.Margin = new System.Windows.Forms.Padding(0, 4, 8, 4); + this.AddButton.Name = "AddButton"; + this.AddButton.Size = new System.Drawing.Size(75, 23); + this.AddButton.TabIndex = 2; + this.AddButton.UseVisualStyleBackColor = true; + this.AddButton.Visible = false; + this.AddButton.Click += this.AddButton_Click; + resources.ApplyResources(this.AddButton, "AddButton"); // // CancelChangesButton // + this.CancelChangesButton.AutoSize = true; + this.CancelChangesButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly; this.CancelChangesButton.DialogResult = System.Windows.Forms.DialogResult.Cancel; this.CancelChangesButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; this.CancelChangesButton.Location = new System.Drawing.Point(316, 51); + this.CancelChangesButton.Margin = new System.Windows.Forms.Padding(8, 4, 0, 4); this.CancelChangesButton.Name = "CancelChangesButton"; this.CancelChangesButton.Size = new System.Drawing.Size(75, 23); - this.CancelChangesButton.TabIndex = 4; + this.CancelChangesButton.TabIndex = 2; this.CancelChangesButton.UseVisualStyleBackColor = true; resources.ApplyResources(this.CancelChangesButton, "CancelChangesButton"); // + // AcceptChangesButton + // + this.AcceptChangesButton.AutoSize = true; + this.AcceptChangesButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly; + this.AcceptChangesButton.DialogResult = System.Windows.Forms.DialogResult.OK; + this.AcceptChangesButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.AcceptChangesButton.Location = new System.Drawing.Point(397, 51); + this.AcceptChangesButton.Margin = new System.Windows.Forms.Padding(8, 4, 0, 4); + this.AcceptChangesButton.Name = "AcceptChangesButton"; + this.AcceptChangesButton.Size = new System.Drawing.Size(75, 23); + this.AcceptChangesButton.TabIndex = 3; + this.AcceptChangesButton.UseVisualStyleBackColor = true; + this.AcceptChangesButton.Click += this.AcceptChangesButton_Click; + resources.ApplyResources(this.AcceptChangesButton, "AcceptChangesButton"); + // // GameCommandLineOptionsDialog // - this.AcceptButton = this.AcceptChangesButton; this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.CancelButton = this.CancelChangesButton; - this.ClientSize = new System.Drawing.Size(481, 112); + this.ClientSize = new System.Drawing.Size(320, 180); this.ControlBox = false; - this.Controls.Add(this.CancelChangesButton); - this.Controls.Add(this.AcceptChangesButton); - this.Controls.Add(this.label1); - this.Controls.Add(this.AdditionalArguments); - this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.Controls.Add(this.CmdLineGrid); + this.Controls.Add(this.BottomButtonPanel); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.SizableToolWindow; this.Icon = EmbeddedImages.AppIcon; + this.MinimumSize = new System.Drawing.Size(320, 180); this.Name = "GameCommandLineOptionsDialog"; + this.Padding = new System.Windows.Forms.Padding(8, 8, 8, 0); + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; resources.ApplyResources(this, "$this"); this.ResumeLayout(false); this.PerformLayout(); @@ -99,9 +170,12 @@ private void InitializeComponent() #endregion - private System.Windows.Forms.Label label1; + private System.Windows.Forms.DataGridView CmdLineGrid; + private System.Windows.Forms.DataGridViewTextBoxColumn CmdLineColumn; + private CKAN.GUI.LeftRightRowPanel BottomButtonPanel; + private System.Windows.Forms.Button ResetToDefaultsButton; + private System.Windows.Forms.Button AddButton; private System.Windows.Forms.Button AcceptChangesButton; private System.Windows.Forms.Button CancelChangesButton; - public System.Windows.Forms.TextBox AdditionalArguments; } } diff --git a/GUI/Dialogs/GameCommandLineOptionsDialog.cs b/GUI/Dialogs/GameCommandLineOptionsDialog.cs index 0773dc807a..4c8d58c877 100644 --- a/GUI/Dialogs/GameCommandLineOptionsDialog.cs +++ b/GUI/Dialogs/GameCommandLineOptionsDialog.cs @@ -1,26 +1,116 @@ +using System; +using System.ComponentModel; +using System.Collections.Generic; +using System.Linq; using System.Windows.Forms; namespace CKAN.GUI { - public partial class GameCommandLineOptionsDialog : Form { public GameCommandLineOptionsDialog() { InitializeComponent(); + if (Platform.IsMono) + { + // Mono's DataGridView has showstopper bugs with AllowUserToAddRows, + // so use an Add button instead + CmdLineGrid.AllowUserToAddRows = false; + AddButton.Visible = true; + } + } + + public DialogResult ShowGameCommandLineOptionsDialog(IWin32Window parent, + List cmdLines, + string[] defaults) + { + rows = cmdLines.Select(cmdLine => new CmdLineRow(cmdLine)) + .ToList(); + CmdLineGrid.DataSource = new BindingList(rows) + { + AllowEdit = true, + AllowRemove = true, + }; + this.defaults = defaults; + return ShowDialog(parent); + } + + protected override void OnShown(EventArgs e) + { + base.OnShown(e); + // Edit the top cell immediately for convenience + CmdLineGrid.BeginEdit(false); + } + + public List Results => rows.Select(row => row.CmdLine) + .Where(str => !string.IsNullOrEmpty(str)) + .ToList(); + + private void CmdLineGrid_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e) + { + // Don't auto-select the text when the user clicks on it + if (e.Control is DataGridViewTextBoxEditingControl tbec) + { + BeginInvoke(new Action(() => + { + tbec.SelectionLength = 0; + })); + } + } + + private void CmdLineGrid_UserDeletingRow(object sender, DataGridViewRowCancelEventArgs e) + { + // You can't delete the last row + if (rows.Count == 1) + { + e.Cancel = true; + } + } - StartPosition = FormStartPosition.CenterScreen; + private void ResetToDefaultsButton_Click(object sender, EventArgs e) + { + rows = defaults.Select(cmdLine => new CmdLineRow(cmdLine)) + .ToList(); + CmdLineGrid.DataSource = new BindingList(rows) + { + AllowEdit = true, + AllowRemove = true, + }; } - public DialogResult ShowGameCommandLineOptionsDialog(string arguments) + private void AddButton_Click(object sender, EventArgs e) { - AdditionalArguments.Text = arguments; - return ShowDialog(); + (CmdLineGrid.DataSource as BindingList)?.AddNew(); + CmdLineGrid.CurrentCell = CmdLineGrid.Rows[CmdLineGrid.RowCount - 1].Cells[0]; + CmdLineGrid.BeginEdit(false); } - public string GetResult() + private void AcceptChangesButton_Click(object sender, EventArgs e) { - return AdditionalArguments.Text; + if (Results.Count < 1) + { + // Don't accept an empty grid (shouldn't happen because of row deletion limit above) + DialogResult = DialogResult.None; + } } + + private string[] defaults; + private List rows; + } + + public class CmdLineRow + { + // Called when the user clicks on an empty row + public CmdLineRow() + { + CmdLine = ""; + } + + public CmdLineRow(string cmdLine) + { + CmdLine = cmdLine; + } + + public string CmdLine { get; set; } } } diff --git a/GUI/Dialogs/GameCommandLineOptionsDialog.resx b/GUI/Dialogs/GameCommandLineOptionsDialog.resx index 34c8811b97..4892216cea 100644 --- a/GUI/Dialogs/GameCommandLineOptionsDialog.resx +++ b/GUI/Dialogs/GameCommandLineOptionsDialog.resx @@ -117,8 +117,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Arguments: - OK - Cancel - Command-line arguments + Command lines + &Reset + Add + &Accept + &Cancel + Game command lines diff --git a/GUI/Dialogs/ManageGameInstancesDialog.Designer.cs b/GUI/Dialogs/ManageGameInstancesDialog.Designer.cs index fc0e493bc1..98d4efcebb 100644 --- a/GUI/Dialogs/ManageGameInstancesDialog.Designer.cs +++ b/GUI/Dialogs/ManageGameInstancesDialog.Designer.cs @@ -46,6 +46,7 @@ private void InitializeComponent() this.InstanceListContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(this.components); this.openDirectoryMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.AddToCKANMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.ImportFromSteamMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.CloneGameInstanceMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.RenameButton = new System.Windows.Forms.Button(); this.SetAsDefaultCheckbox = new System.Windows.Forms.CheckBox(); @@ -149,6 +150,7 @@ private void InitializeComponent() // this.AddNewMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { this.AddToCKANMenuItem, + this.ImportFromSteamMenuItem, this.CloneGameInstanceMenuItem}); this.AddNewMenu.Name = "AddNewMenu"; this.AddNewMenu.Size = new System.Drawing.Size(222, 48); @@ -160,6 +162,13 @@ private void InitializeComponent() this.AddToCKANMenuItem.Click += new System.EventHandler(this.AddToCKANMenuItem_Click); resources.ApplyResources(this.AddToCKANMenuItem, "AddToCKANMenuItem"); // + // ImportFromSteamMenuItem + // + this.ImportFromSteamMenuItem.Name = "ImportFromSteamMenuItem"; + this.ImportFromSteamMenuItem.Size = new System.Drawing.Size(216, 22); + this.ImportFromSteamMenuItem.Click += new System.EventHandler(this.ImportFromSteamMenuItem_Click); + resources.ApplyResources(this.ImportFromSteamMenuItem, "ImportFromSteamMenuItem"); + // // CloneGameInstanceMenuItem // this.CloneGameInstanceMenuItem.Name = "CloneGameInstanceMenuItem"; @@ -246,6 +255,7 @@ private void InitializeComponent() private System.Windows.Forms.ContextMenuStrip InstanceListContextMenuStrip; private System.Windows.Forms.ToolStripMenuItem openDirectoryMenuItem; private System.Windows.Forms.ToolStripMenuItem AddToCKANMenuItem; + private System.Windows.Forms.ToolStripMenuItem ImportFromSteamMenuItem; private System.Windows.Forms.ToolStripMenuItem CloneGameInstanceMenuItem; private System.Windows.Forms.Button RenameButton; private System.Windows.Forms.CheckBox SetAsDefaultCheckbox; diff --git a/GUI/Dialogs/ManageGameInstancesDialog.cs b/GUI/Dialogs/ManageGameInstancesDialog.cs index 1349c800ad..06dcd6fbe4 100644 --- a/GUI/Dialogs/ManageGameInstancesDialog.cs +++ b/GUI/Dialogs/ManageGameInstancesDialog.cs @@ -61,7 +61,7 @@ public ManageGameInstancesDialog(bool centerScreen, IUser user) if (!_manager.Instances.Any()) { - _manager.FindAndRegisterDefaultInstance(); + _manager.FindAndRegisterDefaultInstances(); } // Set the renderer for the AddNewMenu @@ -195,6 +195,20 @@ private void AddToCKANMenuItem_Click(object sender, EventArgs e) } } + private void ImportFromSteamMenuItem_Click(object sender, EventArgs e) + { + var currentDirs = _manager.Instances.Values + .Select(inst => inst.GameDir()) + .ToHashSet(); + var toAdd = _manager.FindDefaultInstances() + .Where(inst => !currentDirs.Contains(inst.GameDir())); + foreach (var inst in toAdd) + { + _manager.AddInstance(inst); + } + UpdateInstancesList(); + } + private void CloneGameInstanceMenuItem_Click(object sender, EventArgs e) { var old_instance = Main.Instance.CurrentInstance; @@ -343,6 +357,7 @@ private void UpdateButtonState() { RenameButton.Enabled = SelectButton.Enabled = SetAsDefaultCheckbox.Enabled = CloneGameInstanceMenuItem.Enabled = HasSelections; ForgetButton.Enabled = HasSelections && (string)GameInstancesListView.SelectedItems[0].Tag != _manager.CurrentInstance?.Name; + ImportFromSteamMenuItem.Enabled = _manager.SteamLibrary.Games.Length > 0; } } } diff --git a/GUI/Dialogs/ManageGameInstancesDialog.resx b/GUI/Dialogs/ManageGameInstancesDialog.resx index dce76c9b0c..e69459a1f9 100644 --- a/GUI/Dialogs/ManageGameInstancesDialog.resx +++ b/GUI/Dialogs/ManageGameInstancesDialog.resx @@ -115,6 +115,8 @@ Select New game instance Add instance to CKAN + Import from Steam + Scan your Steam library for recognized game directories and add them as instances Clone instance Rename Set as default diff --git a/GUI/Localization/de-DE/ManageMods.de-DE.resx b/GUI/Localization/de-DE/ManageMods.de-DE.resx index 295ded3591..81f3764207 100644 --- a/GUI/Localization/de-DE/ManageMods.de-DE.resx +++ b/GUI/Localization/de-DE/ManageMods.de-DE.resx @@ -117,7 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Spiel starten + Spiel starten Aktualisieren Verfügbare Updates auswählen Annehmen diff --git a/GUI/Localization/fr-FR/ManageMods.fr-FR.resx b/GUI/Localization/fr-FR/ManageMods.fr-FR.resx index 181ec29574..51145f25f5 100644 --- a/GUI/Localization/fr-FR/ManageMods.fr-FR.resx +++ b/GUI/Localization/fr-FR/ManageMods.fr-FR.resx @@ -117,7 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + Lancer le Jeu diff --git a/GUI/Localization/it-IT/ManageMods.it-IT.resx b/GUI/Localization/it-IT/ManageMods.it-IT.resx index 851ea2fa86..f08cd1b96e 100644 --- a/GUI/Localization/it-IT/ManageMods.it-IT.resx +++ b/GUI/Localization/it-IT/ManageMods.it-IT.resx @@ -117,7 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + Avvia Gioco diff --git a/GUI/Localization/ja-JP/ManageMods.ja-JP.resx b/GUI/Localization/ja-JP/ManageMods.ja-JP.resx index b21f2e8de9..d2783b6840 100644 --- a/GUI/Localization/ja-JP/ManageMods.ja-JP.resx +++ b/GUI/Localization/ja-JP/ManageMods.ja-JP.resx @@ -117,7 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + ゲームを起動 diff --git a/GUI/Localization/ko-KR/ManageMods.ko-KR.resx b/GUI/Localization/ko-KR/ManageMods.ko-KR.resx index 651c39dcba..ee77e07035 100644 --- a/GUI/Localization/ko-KR/ManageMods.ko-KR.resx +++ b/GUI/Localization/ko-KR/ManageMods.ko-KR.resx @@ -117,7 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + 게임 실행 diff --git a/GUI/Localization/nl-NL/ManageMods.nl-NL.resx b/GUI/Localization/nl-NL/ManageMods.nl-NL.resx index f0cf79c998..d549f3bd5b 100644 --- a/GUI/Localization/nl-NL/ManageMods.nl-NL.resx +++ b/GUI/Localization/nl-NL/ManageMods.nl-NL.resx @@ -117,7 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + Spel Starten diff --git a/GUI/Localization/pl-PL/ManageMods.pl-PL.resx b/GUI/Localization/pl-PL/ManageMods.pl-PL.resx index d426fcfd54..4826171059 100644 --- a/GUI/Localization/pl-PL/ManageMods.pl-PL.resx +++ b/GUI/Localization/pl-PL/ManageMods.pl-PL.resx @@ -117,7 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + Uruchom grę diff --git a/GUI/Localization/pt-BR/ManageMods.pt-BR.resx b/GUI/Localization/pt-BR/ManageMods.pt-BR.resx index 3f16ae3841..d7aa0d2ea0 100644 --- a/GUI/Localization/pt-BR/ManageMods.pt-BR.resx +++ b/GUI/Localization/pt-BR/ManageMods.pt-BR.resx @@ -117,7 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + Abrir Jogo diff --git a/GUI/Localization/ru-RU/ManageMods.ru-RU.resx b/GUI/Localization/ru-RU/ManageMods.ru-RU.resx index 30ba17c97b..7a77fc2c69 100644 --- a/GUI/Localization/ru-RU/ManageMods.ru-RU.resx +++ b/GUI/Localization/ru-RU/ManageMods.ru-RU.resx @@ -117,7 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + Запустить игру diff --git a/GUI/Localization/zh-CN/ManageMods.zh-CN.resx b/GUI/Localization/zh-CN/ManageMods.zh-CN.resx index fb10c1f52d..3d1b8f8082 100644 --- a/GUI/Localization/zh-CN/ManageMods.zh-CN.resx +++ b/GUI/Localization/zh-CN/ManageMods.zh-CN.resx @@ -117,7 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - 启动游戏 + 启动游戏 刷新 添加可用更新 应用更改 diff --git a/GUI/Main/Main.Designer.cs b/GUI/Main/Main.Designer.cs index c813a2a20f..8bf05c4aa8 100644 --- a/GUI/Main/Main.Designer.cs +++ b/GUI/Main/Main.Designer.cs @@ -400,7 +400,7 @@ private void InitializeComponent() this.ModInfo.Name = "ModInfo"; this.ModInfo.Size = new System.Drawing.Size(360, 836); this.ModInfo.TabIndex = 34; - this.ModInfo.OnDownloadClick += ModInfo_OnDownloadClick; + this.ModInfo.OnDownloadClick += this.ModInfo_OnDownloadClick; // // StatusLabel // @@ -475,14 +475,16 @@ private void InitializeComponent() this.ManageMods.Name = "ManageMods"; this.ManageMods.Size = new System.Drawing.Size(500, 500); this.ManageMods.TabIndex = 4; - this.ManageMods.OnSelectedModuleChanged += ManageMods_OnSelectedModuleChanged; - this.ManageMods.OnChangeSetChanged += ManageMods_OnChangeSetChanged; - this.ManageMods.OnRegistryChanged += ManageMods_OnRegistryChanged; - this.ManageMods.LabelsAfterUpdate += ManageMods_LabelsAfterUpdate; - this.ManageMods.StartChangeSet += ManageMods_StartChangeSet; - this.ManageMods.RaiseMessage += ManageMods_RaiseMessage; - this.ManageMods.RaiseError += ManageMods_RaiseError; - this.ManageMods.ClearStatusBar += ManageMods_ClearStatusBar; + this.ManageMods.OnSelectedModuleChanged += this.ManageMods_OnSelectedModuleChanged; + this.ManageMods.OnChangeSetChanged += this.ManageMods_OnChangeSetChanged; + this.ManageMods.OnRegistryChanged += this.ManageMods_OnRegistryChanged; + this.ManageMods.LabelsAfterUpdate += this.ManageMods_LabelsAfterUpdate; + this.ManageMods.StartChangeSet += this.ManageMods_StartChangeSet; + this.ManageMods.RaiseMessage += this.ManageMods_RaiseMessage; + this.ManageMods.RaiseError += this.ManageMods_RaiseError; + this.ManageMods.ClearStatusBar += this.ManageMods_ClearStatusBar; + this.ManageMods.LaunchGame += this.LaunchGame; + this.ManageMods.EditCommandLines += this.EditCommandLines; // // ChangesetTabPage // @@ -505,9 +507,9 @@ private void InitializeComponent() this.Changeset.Name = "Changeset"; this.Changeset.Size = new System.Drawing.Size(500, 500); this.Changeset.TabIndex = 32; - this.Changeset.OnSelectedItemsChanged += Changeset_OnSelectedItemsChanged; - this.Changeset.OnConfirmChanges += Changeset_OnConfirmChanges; - this.Changeset.OnCancelChanges += Changeset_OnCancelChanges; + this.Changeset.OnSelectedItemsChanged += this.Changeset_OnSelectedItemsChanged; + this.Changeset.OnConfirmChanges += this.Changeset_OnConfirmChanges; + this.Changeset.OnCancelChanges += this.Changeset_OnCancelChanges; // // WaitTabPage // @@ -530,8 +532,8 @@ private void InitializeComponent() this.Wait.Name = "Wait"; this.Wait.Size = new System.Drawing.Size(500, 500); this.Wait.TabIndex = 32; - this.Wait.OnRetry += Wait_OnRetry; - this.Wait.OnOk += Wait_OnOk; + this.Wait.OnRetry += this.Wait_OnRetry; + this.Wait.OnOk += this.Wait_OnOk; // // ChooseRecommendedModsTabPage // @@ -578,7 +580,7 @@ private void InitializeComponent() this.PlayTime.Name = "PlayTime"; this.PlayTime.Size = new System.Drawing.Size(500, 500); this.PlayTime.TabIndex = 32; - this.PlayTime.Done += PlayTime_Done; + this.PlayTime.Done += this.PlayTime_Done; // // UnmanagedFilesTabPage // @@ -601,7 +603,7 @@ private void InitializeComponent() this.UnmanagedFiles.Name = "UnmanagedFiles"; this.UnmanagedFiles.Size = new System.Drawing.Size(500, 500); this.UnmanagedFiles.TabIndex = 32; - this.UnmanagedFiles.Done += UnmanagedFiles_Done; + this.UnmanagedFiles.Done += this.UnmanagedFiles_Done; // // InstallationHistoryTabPage // @@ -624,9 +626,9 @@ private void InitializeComponent() this.InstallationHistory.Name = "InstallationHistory"; this.InstallationHistory.Size = new System.Drawing.Size(500, 500); this.InstallationHistory.TabIndex = 32; - this.InstallationHistory.OnSelectedModuleChanged += InstallationHistory_OnSelectedModuleChanged; - this.InstallationHistory.Install += InstallationHistory_Install; - this.InstallationHistory.Done += InstallationHistory_Done; + this.InstallationHistory.OnSelectedModuleChanged += this.InstallationHistory_OnSelectedModuleChanged; + this.InstallationHistory.Install += this.InstallationHistory_Install; + this.InstallationHistory.Done += this.InstallationHistory_Done; // // ChooseProvidedModsTabPage // diff --git a/GUI/Main/Main.cs b/GUI/Main/Main.cs index 6a796a7804..b46430682f 100644 --- a/GUI/Main/Main.cs +++ b/GUI/Main/Main.cs @@ -172,6 +172,7 @@ protected override void OnLoad(EventArgs e) // We need a config object to get the window geometry, but we don't need the registry lock yet configuration = GUIConfigForInstance( + Manager.SteamLibrary, // Find the most recently used instance if no default instance CurrentInstance ?? InstanceWithNewestGUIConfig(Manager.Instances.Values)); @@ -203,10 +204,13 @@ protected override void OnLoad(EventArgs e) private static string GUIConfigPath(GameInstance inst) => Path.Combine(inst.CkanDir(), GUIConfigFilename); - private static GUIConfiguration GUIConfigForInstance(GameInstance inst) + private static GUIConfiguration GUIConfigForInstance(SteamLibrary steamLib, GameInstance inst) => inst == null ? new GUIConfiguration() - : GUIConfiguration.LoadOrCreateConfiguration(GUIConfigPath(inst), - inst.game); + : GUIConfiguration.LoadOrCreateConfiguration( + GUIConfigPath(inst), + inst.game.DefaultCommandLines(steamLib, + new DirectoryInfo(inst.GameDir())) + .ToList()); private static GameInstance InstanceWithNewestGUIConfig(IEnumerable instances) => instances.Where(inst => inst.Valid) @@ -409,7 +413,7 @@ private void CurrentInstanceUpdated() } configuration?.Save(); - configuration = GUIConfigForInstance(CurrentInstance); + configuration = GUIConfigForInstance(Manager.SteamLibrary, CurrentInstance); AutoUpdatePrompts(ServiceLocator.Container .Resolve(), @@ -427,6 +431,7 @@ private void CurrentInstanceUpdated() CurrentInstance.game.RebuildSubdirectories(CurrentInstance.GameDir()); + ManageMods.InstanceUpdated(); bool repoUpdateNeeded = configuration.RefreshOnStartup; if (!autoUpdating) { @@ -441,7 +446,6 @@ private void CurrentInstanceUpdated() RefreshModList(registry.Repositories.Count > 0); } } - ManageMods.InstanceUpdated(); } /// @@ -607,11 +611,18 @@ private void aboutToolStripMenuItem_Click(object sender, EventArgs e) } private void GameCommandlineToolStripMenuItem_Click(object sender, EventArgs e) + { + EditCommandLines(); + } + + private void EditCommandLines() { var dialog = new GameCommandLineOptionsDialog(); - if (dialog.ShowGameCommandLineOptionsDialog(configuration.CommandLineArguments) == DialogResult.OK) + var defaults = CurrentInstance.game.DefaultCommandLines(Manager.SteamLibrary, + new DirectoryInfo(CurrentInstance.GameDir())); + if (dialog.ShowGameCommandLineOptionsDialog(this, configuration.CommandLines, defaults) == DialogResult.OK) { - configuration.CommandLineArguments = dialog.GetResult(); + configuration.CommandLines = dialog.Results; } } @@ -921,12 +932,12 @@ private void openGameDirectoryToolStripMenuItem_Click(object sender, EventArgs e private void openGameToolStripMenuItem_Click(object sender, EventArgs e) { - LaunchGame(); + LaunchGame(configuration.CommandLines.First()); } - public void LaunchGame() + private void LaunchGame(string command) { - var split = configuration.CommandLineArguments.Split(' '); + var split = command.Split(' '); if (split.Length == 0) { return; diff --git a/GUI/Main/Main.resx b/GUI/Main/Main.resx index aa1f3d90c7..2aa48a35df 100644 --- a/GUI/Main/Main.resx +++ b/GUI/Main/Main.resx @@ -134,7 +134,7 @@ CKAN &plugins Preferred &hosts Installation &filters - &Game command-line + &Game command lines &Compatible game versions &Help &User guide diff --git a/GUI/Model/GUIConfiguration.cs b/GUI/Model/GUIConfiguration.cs index 1fd31c4233..0910132568 100644 --- a/GUI/Model/GUIConfiguration.cs +++ b/GUI/Model/GUIConfiguration.cs @@ -1,19 +1,21 @@ using System; using System.Xml; using System.Collections.Generic; +using System.Linq; using System.Drawing; using System.IO; using System.Xml.Serialization; -using CKAN.Games; - namespace CKAN.GUI { [XmlRoot("Configuration")] public class GUIConfiguration { - public string CommandLineArguments = ""; - public bool AutoCloseWaitDialog = false; + public string CommandLineArguments = null; + + [XmlArray, XmlArrayItem(ElementName = "CommandLine")] + public List CommandLines = new List(); + public bool URLHandlerNoNag = false; public bool CheckForUpdatesOnLaunch = false; @@ -25,7 +27,8 @@ public class GUIConfiguration public bool HideEpochs = true; public bool HideV = false; - public bool RefreshOnStartup = true; // Defaults to true, so everyone is forced to refresh on first start + // Defaults to true, so everyone is forced to refresh on first start + public bool RefreshOnStartup = true; public bool RefreshOnStartupNoNag = false; public bool RefreshPaused = false; @@ -107,25 +110,27 @@ public void Save() } } - public static GUIConfiguration LoadOrCreateConfiguration(string path, IGame game) + public static GUIConfiguration LoadOrCreateConfiguration(string path, + List defaultCommandLines) { if (!File.Exists(path) || new FileInfo(path).Length == 0) { var configuration = new GUIConfiguration { - path = path, - CommandLineArguments = game.DefaultCommandLine(path), + path = path, + CommandLines = defaultCommandLines, }; SaveConfiguration(configuration); } - return LoadConfiguration(path); + return LoadConfiguration(path, defaultCommandLines); } - private static GUIConfiguration LoadConfiguration(string path) + private static GUIConfiguration LoadConfiguration(string path, + List defaultCommandLines) { - var serializer = new XmlSerializer(typeof (GUIConfiguration)); + var serializer = new XmlSerializer(typeof(GUIConfiguration)); GUIConfiguration configuration; using (var stream = new StreamReader(path)) @@ -163,7 +168,7 @@ private static GUIConfiguration LoadConfiguration(string path) } configuration.path = path; - if (DeserializationFixes(configuration)) + if (DeserializationFixes(configuration, defaultCommandLines)) { SaveConfiguration(configuration); } @@ -175,7 +180,8 @@ private static GUIConfiguration LoadConfiguration(string path) /// /// The current configuration to apply the fixes on /// A bool indicating whether something changed and the configuration should be saved to disk - private static bool DeserializationFixes(GUIConfiguration configuration) + private static bool DeserializationFixes(GUIConfiguration configuration, + List defaultCommandLines) { bool needsSave = false; @@ -187,6 +193,16 @@ private static bool DeserializationFixes(GUIConfiguration configuration) needsSave = FixColumnName(configuration.SortColumns, "SizeCol", "DownloadSize") || needsSave; needsSave = FixColumnName(configuration.HiddenColumnNames, "SizeCol", "DownloadSize") || needsSave; + if (!string.IsNullOrEmpty(configuration.CommandLineArguments)) + { + configuration.CommandLines.AddRange( + Enumerable.Repeat(configuration.CommandLineArguments, 1) + .Concat(defaultCommandLines) + .Distinct()); + configuration.CommandLineArguments = null; + needsSave = true; + } + return needsSave; } diff --git a/GUI/Properties/Resources.resx b/GUI/Properties/Resources.resx index e8fde203ac..869ca7a985 100644 --- a/GUI/Properties/Resources.resx +++ b/GUI/Properties/Resources.resx @@ -432,6 +432,8 @@ Do you want to allow CKAN to do this? If you click no you won't see this message replaceable Expand or collapse the detailed search fields (Ctrl-Shift-F) Combine a new search with your current searches + (will count play time in Steam) + (will NOT count play time in Steam) Uncheck to uninstall all mods, check to clear change set Hidden labels: Hidden tags: diff --git a/Netkan/Processors/Inflator.cs b/Netkan/Processors/Inflator.cs index 6991e612da..87aa6288a4 100644 --- a/Netkan/Processors/Inflator.cs +++ b/Netkan/Processors/Inflator.cs @@ -20,7 +20,6 @@ public Inflator(string cacheDir, bool overwriteCache, string githubToken, string { log.Debug("Initializing inflator"); cache = FindCache( - new GameInstanceManager(new ConsoleUser(false)), ServiceLocator.Container.Resolve(), cacheDir); @@ -80,7 +79,7 @@ internal void ValidateCkan(Metadata ckan) ckanValidator.Validate(ckan); } - private static NetFileCache FindCache(GameInstanceManager kspManager, IConfiguration cfg, string cacheDir) + private static NetFileCache FindCache(IConfiguration cfg, string cacheDir) { if (cacheDir != null) { @@ -92,7 +91,7 @@ private static NetFileCache FindCache(GameInstanceManager kspManager, IConfigura { log.InfoFormat("Using main CKAN meta-cache at {0}", cfg.DownloadCacheDir); // Create a new file cache in the same location so NetKAN can download pure URLs not sourced from CkanModules - return new NetFileCache(kspManager, cfg.DownloadCacheDir); + return new NetFileCache(null, cfg.DownloadCacheDir); } catch { diff --git a/Netkan/Program.cs b/Netkan/Program.cs index 84144c1ab1..c63b32434c 100644 --- a/Netkan/Program.cs +++ b/Netkan/Program.cs @@ -11,6 +11,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using CKAN.Games; using CKAN.Versioning; using CKAN.NetKAN.Model; using CKAN.NetKAN.Processors; @@ -48,7 +49,7 @@ public static int Main(string[] args) return ExitOk; } - var game = GameInstanceManager.GameByShortName(Options.Game); + var game = KnownGames.GameByShortName(Options.Game); if (!string.IsNullOrEmpty(Options.ValidateCkan)) { diff --git a/Tests/GUI/GUIConfiguration.cs b/Tests/GUI/GUIConfiguration.cs index c5a11c77d4..9c474c0d8d 100644 --- a/Tests/GUI/GUIConfiguration.cs +++ b/Tests/GUI/GUIConfiguration.cs @@ -1,10 +1,10 @@ using System.IO; +using System.Collections.Generic; using NUnit.Framework; using CKAN; using CKAN.GUI; -using CKAN.Games.KerbalSpaceProgram; using Tests.Data; @@ -37,7 +37,7 @@ public void LoadOrCreateConfiguration_MalformedXMLFile_ThrowsKraken() stream.Write("This is not a valid XML file."); } - Assert.Throws(() => GUIConfiguration.LoadOrCreateConfiguration(tempFile, new KerbalSpaceProgram())); + Assert.Throws(() => GUIConfiguration.LoadOrCreateConfiguration(tempFile, new List())); } [Test] @@ -50,7 +50,7 @@ public void LoadOrCreateConfiguration_CorrectConfigurationFile_Loaded() stream.Write(TestData.ConfigurationFile()); } - var result = GUIConfiguration.LoadOrCreateConfiguration(tempFile, new KerbalSpaceProgram()); + var result = GUIConfiguration.LoadOrCreateConfiguration(tempFile, new List()); Assert.IsNotNull(result); } diff --git a/build.cake b/build.cake index 33ea649416..350ac4e99b 100644 --- a/build.cake +++ b/build.cake @@ -16,6 +16,8 @@ var solution = Argument("solution", "CKAN.sln"); var rootDirectory = Context.Environment.WorkingDirectory; var buildDirectory = rootDirectory.Combine("_build"); +var nugetDirectory = buildDirectory.Combine("lib") + .Combine("nuget"); var outDirectory = buildDirectory.Combine("out"); var repackDirectory = buildDirectory.Combine("repack"); var ckanFile = repackDirectory.Combine(configuration) @@ -188,8 +190,6 @@ Task("Restore") .Description("Intermediate - Download dependencies") .Does(() => { - var nugetDirectory = buildDirectory.Combine("lib") - .Combine("nuget"); if (IsRunningOnWindows()) { DotNetRestore(solution, new DotNetRestoreSettings @@ -305,7 +305,8 @@ Task("Repack-Ckan") assemblyPaths, new ILRepackSettings { - Libs = new List { cmdLineBinDirectory.FullPath }, + Libs = new List { cmdLineBinDirectory, + netstandardRefDirectory() }, TargetPlatform = TargetPlatformVersion.v4, Parallel = true, Verbose = false, @@ -329,7 +330,7 @@ Task("Repack-Ckan") autoupdateBinDirectory)), new ILRepackSettings { - Libs = new List { autoupdateBinDirectory.FullPath }, + Libs = new List { autoupdateBinDirectory }, TargetPlatform = TargetPlatformVersion.v4, Parallel = true, Verbose = false, @@ -352,15 +353,16 @@ Task("Repack-Netkan") .Combine(buildNetFramework); var netkanLogFile = repackDirectory.Combine(configuration) .CombineWithFilePath($"netkan.log"); + var assemblyPaths = GetFiles(string.Format("{0}/*.dll", netkanBinDirectory)); ReportRepacking(netkanFile, netkanLogFile); ILRepack( netkanFile, netkanBinDirectory.CombineWithFilePath("CKAN-NetKAN.exe"), - GetFiles(string.Format("{0}/*.dll", - netkanBinDirectory)), + assemblyPaths, new ILRepackSettings { - Libs = new List { netkanBinDirectory.FullPath }, + Libs = new List { netkanBinDirectory, + netstandardRefDirectory() }, TargetPlatform = TargetPlatformVersion.v4, Parallel = true, Verbose = false, @@ -520,6 +522,23 @@ Teardown(context => RunTarget(target); +private DirectoryPath netstandardRefDirectory() +{ + // We need to tell ILRepack where to find netstandard.dll (on Linux), + // which doesn't get copied to the output folder + var netstandardDirectory = nugetDirectory.Combine("netstandard.library"); + var netstandardVersion = System.IO.Directory + .EnumerateDirectories(netstandardDirectory.ToString()) + .Select(p => System.IO.Path.GetFileName(p)) + .OrderBy(p => ParseSemVer(p)) + .Last(); + return netstandardDirectory.Combine(netstandardVersion) + .Combine("build") + .Combine("netstandard2.0") + .Combine("ref"); +} + + private Semver.SemVersion GetVersion() { var pattern = new Regex(@"^\s*##\s+v(?\S+)\s?.*$");