diff --git a/Core/GameInstance.cs b/Core/GameInstance.cs index 2fd5b6fe7e..ab6b7ad5df 100644 --- a/Core/GameInstance.cs +++ b/Core/GameInstance.cs @@ -325,44 +325,48 @@ public GameVersionCriteria VersionCriteria() /// public bool Scan() { - var manager = RegistryManager.Instance(this); - using (TransactionScope tx = CkanTransaction.CreateTransactionScope()) + if (Directory.Exists(game.PrimaryModDirectory(this))) { - var oldDlls = new HashSet(manager.registry.InstalledDlls); - manager.registry.ClearDlls(); - - // TODO: It would be great to optimise this to skip .git directories and the like. - // Yes, I keep my GameData in git. - - // Alas, EnumerateFiles is *case-sensitive* in its pattern, which causes - // DLL files to be missed under Linux; we have to pick .dll, .DLL, or scanning - // GameData *twice*. - // - // The least evil is to walk it once, and filter it ourselves. - IEnumerable files = Directory - .EnumerateFiles(game.PrimaryModDirectory(this), "*", SearchOption.AllDirectories) - .Where(file => file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase)) - .Select(CKANPathUtils.NormalizePath) - .Where(absPath => !game.StockFolders.Any(f => - ToRelativeGameDir(absPath).StartsWith($"{f}/"))); - - foreach (string dll in files) + var manager = RegistryManager.Instance(this); + using (TransactionScope tx = CkanTransaction.CreateTransactionScope()) { - manager.registry.RegisterDll(this, dll); + var oldDlls = new HashSet(manager.registry.InstalledDlls); + manager.registry.ClearDlls(); + + // TODO: It would be great to optimise this to skip .git directories and the like. + // Yes, I keep my GameData in git. + + // Alas, EnumerateFiles is *case-sensitive* in its pattern, which causes + // DLL files to be missed under Linux; we have to pick .dll, .DLL, or scanning + // GameData *twice*. + // + // The least evil is to walk it once, and filter it ourselves. + IEnumerable files = Directory + .EnumerateFiles(game.PrimaryModDirectory(this), "*", SearchOption.AllDirectories) + .Where(file => file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase)) + .Select(CKANPathUtils.NormalizePath) + .Where(absPath => !game.StockFolders.Any(f => + ToRelativeGameDir(absPath).StartsWith($"{f}/"))); + + foreach (string dll in files) + { + manager.registry.RegisterDll(this, dll); + } + var newDlls = new HashSet(manager.registry.InstalledDlls); + bool dllChanged = !oldDlls.SetEquals(newDlls); + bool dlcChanged = manager.ScanDlc(); + + if (dllChanged || dlcChanged) + { + manager.Save(false); + } + + tx.Complete(); + + return dllChanged || dlcChanged; } - var newDlls = new HashSet(manager.registry.InstalledDlls); - bool dllChanged = !oldDlls.SetEquals(newDlls); - bool dlcChanged = manager.ScanDlc(); - - if (dllChanged || dlcChanged) - { - manager.Save(false); - } - - tx.Complete(); - - return dllChanged || dlcChanged; } + return false; } #endregion diff --git a/Core/Games/IGame.cs b/Core/Games/IGame.cs index 5a2775ab7f..5cfb4e9027 100644 --- a/Core/Games/IGame.cs +++ b/Core/Games/IGame.cs @@ -19,7 +19,9 @@ public interface IGame // What do we contain? string PrimaryModDirectoryRelative { get; } string PrimaryModDirectory(GameInstance inst); - string[] StockFolders { get; } + string[] StockFolders { get; } + string[] ReservedPaths { get; } + string[] CreateableDirs { get; } bool IsReservedDirectory(GameInstance inst, string path); bool AllowInstallationIn(string name, out string path); void RebuildSubdirectories(GameInstance inst); diff --git a/Core/Games/KerbalSpaceProgram.cs b/Core/Games/KerbalSpaceProgram.cs index c4d1c7867c..4baf4bcb42 100644 --- a/Core/Games/KerbalSpaceProgram.cs +++ b/Core/Games/KerbalSpaceProgram.cs @@ -107,6 +107,16 @@ public string PrimaryModDirectory(GameInstance inst) "GameData/SquadExpansion" }; + public string[] ReservedPaths => new string[] + { + "GameData", "Ships", "Missions" + }; + + public string[] CreateableDirs => new string[] + { + "GameData", "Tutorial", "Scenarios", "Missions", "Ships/Script" + }; + /// /// Checks the path against a list of reserved game directories /// diff --git a/Core/DLC/BreakingGroundDlcDetector.cs b/Core/Games/KerbalSpaceProgram/DLC/BreakingGroundDlcDetector.cs similarity index 100% rename from Core/DLC/BreakingGroundDlcDetector.cs rename to Core/Games/KerbalSpaceProgram/DLC/BreakingGroundDlcDetector.cs diff --git a/Core/DLC/IDlcDetector.cs b/Core/Games/KerbalSpaceProgram/DLC/IDlcDetector.cs similarity index 100% rename from Core/DLC/IDlcDetector.cs rename to Core/Games/KerbalSpaceProgram/DLC/IDlcDetector.cs diff --git a/Core/DLC/MakingHistoryDlcDetector.cs b/Core/Games/KerbalSpaceProgram/DLC/MakingHistoryDlcDetector.cs similarity index 100% rename from Core/DLC/MakingHistoryDlcDetector.cs rename to Core/Games/KerbalSpaceProgram/DLC/MakingHistoryDlcDetector.cs diff --git a/Core/DLC/StandardDlcDetectorBase.cs b/Core/Games/KerbalSpaceProgram/DLC/StandardDlcDetectorBase.cs similarity index 100% rename from Core/DLC/StandardDlcDetectorBase.cs rename to Core/Games/KerbalSpaceProgram/DLC/StandardDlcDetectorBase.cs diff --git a/Core/GameVersionProviders/IGameVersionProvider.cs b/Core/Games/KerbalSpaceProgram/GameVersionProviders/IGameVersionProvider.cs similarity index 100% rename from Core/GameVersionProviders/IGameVersionProvider.cs rename to Core/Games/KerbalSpaceProgram/GameVersionProviders/IGameVersionProvider.cs diff --git a/Core/GameVersionProviders/IKspBuildMap.cs b/Core/Games/KerbalSpaceProgram/GameVersionProviders/IKspBuildMap.cs similarity index 100% rename from Core/GameVersionProviders/IKspBuildMap.cs rename to Core/Games/KerbalSpaceProgram/GameVersionProviders/IKspBuildMap.cs diff --git a/Core/GameVersionProviders/KspBuildIdVersionProvider.cs b/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildIdVersionProvider.cs similarity index 100% rename from Core/GameVersionProviders/KspBuildIdVersionProvider.cs rename to Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildIdVersionProvider.cs diff --git a/Core/GameVersionProviders/KspBuildMap.cs b/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildMap.cs similarity index 100% rename from Core/GameVersionProviders/KspBuildMap.cs rename to Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildMap.cs diff --git a/Core/GameVersionProviders/KspReadmeVersionProvider.cs b/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspReadmeVersionProvider.cs similarity index 100% rename from Core/GameVersionProviders/KspReadmeVersionProvider.cs rename to Core/Games/KerbalSpaceProgram/GameVersionProviders/KspReadmeVersionProvider.cs diff --git a/Core/GameVersionProviders/KspVersionSource.cs b/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspVersionSource.cs similarity index 100% rename from Core/GameVersionProviders/KspVersionSource.cs rename to Core/Games/KerbalSpaceProgram/GameVersionProviders/KspVersionSource.cs diff --git a/Core/Net/Repo.cs b/Core/Net/Repo.cs index ce7ad091ff..7ff6a67760 100644 --- a/Core/Net/Repo.cs +++ b/Core/Net/Repo.cs @@ -65,7 +65,8 @@ public static RepoUpdateResult UpdateAllRepositories(RegistryManager registry_ma // Merge all the lists allAvail.AddRange(avail); repository.Value.last_server_etag = newETag; - user.RaiseMessage("Updated {0}", repository.Value.name); + user.RaiseMessage("Updated {0} ({1} modules)", + repository.Value.name, avail.Count); } ++index; } @@ -98,6 +99,7 @@ public static RepoUpdateResult UpdateAllRepositories(RegistryManager registry_ma else { // Return failure + user.RaiseMessage("No modules found!"); return RepoUpdateResult.Failed; } } @@ -381,19 +383,9 @@ private static List UpdateRegistryFromTarGz(string path, out SortedD // Create a handle for the tar stream. using (TarInputStream tarStream = new TarInputStream(gzipStream)) { - // Walk the archive, looking for .ckan files. - const string filter = @"\.ckan$"; - - while (true) + TarEntry entry; + while ((entry = tarStream.GetNextEntry()) != null) { - TarEntry entry = tarStream.GetNextEntry(); - - // Check for EOF. - if (entry == null) - { - break; - } - string filename = entry.Name; if (filename.EndsWith("download_counts.json")) @@ -401,24 +393,24 @@ private static List UpdateRegistryFromTarGz(string path, out SortedD downloadCounts = JsonConvert.DeserializeObject>( tarStreamString(tarStream, entry) ); - continue; } - else if (!Regex.IsMatch(filename, filter)) + else if (filename.EndsWith(".ckan")) { - // Skip things we don't want. - log.DebugFormat("Skipping archive entry {0}", filename); - continue; - } - - log.DebugFormat("Reading CKAN data from {0}", filename); + log.DebugFormat("Reading CKAN data from {0}", filename); - // Read each file into a buffer. - string metadata_json = tarStreamString(tarStream, entry); + // Read each file into a buffer. + string metadata_json = tarStreamString(tarStream, entry); - CkanModule module = ProcessRegistryMetadataFromJSON(metadata_json, filename); - if (module != null) + CkanModule module = ProcessRegistryMetadataFromJSON(metadata_json, filename); + if (module != null) + { + modules.Add(module); + } + } + else { - modules.Add(module); + // Skip things we don't want. + log.DebugFormat("Skipping archive entry {0}", filename); } } } @@ -460,35 +452,35 @@ private static List UpdateRegistryFromZip(string path) List modules = new List(); using (var zipfile = new ZipFile(path)) { - // Walk the archive, looking for .ckan files. - const string filter = @"\.ckan$"; - foreach (ZipEntry entry in zipfile) { string filename = entry.Name; - // Skip things we don't want. - if (! Regex.IsMatch(filename, filter)) + if (filename.EndsWith(".ckan")) { - log.DebugFormat("Skipping archive entry {0}", filename); - continue; - } + log.DebugFormat("Reading CKAN data from {0}", filename); - log.DebugFormat("Reading CKAN data from {0}", filename); + // Read each file into a string. + string metadata_json; + using (var stream = new StreamReader(zipfile.GetInputStream(entry))) + { + metadata_json = stream.ReadToEnd(); + stream.Close(); + } - // Read each file into a string. - string metadata_json; - using (var stream = new StreamReader(zipfile.GetInputStream(entry))) - { - metadata_json = stream.ReadToEnd(); - stream.Close(); + CkanModule module = ProcessRegistryMetadataFromJSON(metadata_json, filename); + if (module != null) + { + modules.Add(module); + } } - - CkanModule module = ProcessRegistryMetadataFromJSON(metadata_json, filename); - if (module != null) + else { - modules.Add(module); + // Skip things we don't want. + log.DebugFormat("Skipping archive entry {0}", filename); + continue; } + } zipfile.Close(); diff --git a/Core/Registry/Registry.cs b/Core/Registry/Registry.cs index a5aad943d1..697beb688f 100644 --- a/Core/Registry/Registry.cs +++ b/Core/Registry/Registry.cs @@ -881,18 +881,18 @@ public void RegisterDlc(string identifier, UnmanagedModuleVersion version) if (dlcModule == null) { // Don't have the real thing, make a fake one - dlcModule = new CkanModule() - { - spec_version = new ModuleVersion("v1.28"), - identifier = identifier, - name = identifier, - @abstract = "An official expansion pack for KSP", - author = new List() { "SQUAD" }, - version = version, - kind = "dlc", - license = new List() { new License("restricted") }, - }; - dlcModule.CalculateSearchables(); + dlcModule = new CkanModule( + new ModuleVersion("v1.28"), + identifier, + identifier, + "An official expansion pack for KSP", + null, + new List() { "SQUAD" }, + new List() { new License("restricted") }, + version, + null, + "dlc" + ); } installed_modules.Add( identifier, diff --git a/Core/Registry/RegistryManager.cs b/Core/Registry/RegistryManager.cs index d4211d99e3..ff1ef44753 100644 --- a/Core/Registry/RegistryManager.cs +++ b/Core/Registry/RegistryManager.cs @@ -314,7 +314,6 @@ private void LoadOrCreate() log.ErrorFormat("Uncaught exception loading registry: {0}", ex.ToString()); throw; } - AscertainDefaultRepo(); } @@ -322,6 +321,7 @@ private void Create() { log.InfoFormat("Creating new CKAN registry at {0}", path); registry = Registry.Empty(); + AscertainDefaultRepo(); Save(); } @@ -435,17 +435,20 @@ public CkanModule GenerateModpack(bool recommends = false, bool with_versions = { string kspInstanceName = ksp.Name; string name = $"installed-{kspInstanceName}"; - var module = new CkanModule() - { + var module = new CkanModule( // v1.18 to allow Unlicense - spec_version = new ModuleVersion("v1.18"), - identifier = Identifier.Sanitize(name), - name = name, - author = new List() { System.Environment.UserName }, - @abstract = $"A list of modules installed on the {kspInstanceName} KSP instance", - kind = "metapackage", - version = new ModuleVersion(DateTime.UtcNow.ToString("yyyy.MM.dd.hh.mm.ss")), - license = new List() { new License("unknown") }, + new ModuleVersion("v1.18"), + Identifier.Sanitize(name), + name, + $"A list of modules installed on the {kspInstanceName} KSP instance", + null, + new List() { System.Environment.UserName }, + new List() { new License("unknown") }, + new ModuleVersion(DateTime.UtcNow.ToString("yyyy.MM.dd.hh.mm.ss")), + null, + "metapackage" + ) + { download_content_type = "application/zip", release_date = DateTime.Now, }; diff --git a/Core/Types/CkanModule.cs b/Core/Types/CkanModule.cs index 584e40d4d0..0e23b32bc9 100644 --- a/Core/Types/CkanModule.cs +++ b/Core/Types/CkanModule.cs @@ -278,13 +278,54 @@ internal CkanModule() _comparator = ServiceLocator.Container.Resolve(); } + /// + /// Initialize a CkanModule + /// + /// The version of the spec obeyed by this module + /// This module's machine-readable identifier + /// This module's user-visible display name + /// Short description of this module + /// Long description of this module + /// Authors of this module + /// Licenses of this module + /// Version number of this release + /// Where to download this module + /// package, metapackage, or dlc + /// Object used for checking compatibility of this module + public CkanModule( + ModuleVersion spec_version, + string identifier, + string name, + string @abstract, + string description, + List author, + List license, + ModuleVersion version, + Uri download, + string kind = null, + IGameComparator comparator = null + ) + { + this.spec_version = spec_version; + this.identifier = identifier; + this.name = name; + this.@abstract = @abstract; + this.description = description; + this.author = author; + this.license = license; + this.version = version; + this.download = download; + this.kind = kind; + this._comparator = comparator ?? ServiceLocator.Container.Resolve(); + CheckHealth(); + CalculateSearchables(); + } + /// /// Inflates a CKAN object from a JSON string. /// public CkanModule(string json, IGameComparator comparator) { - _comparator = comparator; - try { // Use the json string to populate our object @@ -294,21 +335,22 @@ public CkanModule(string json, IGameComparator comparator) { throw new BadMetadataKraken(null, string.Format("JSON deserialization error: {0}", ex.Message), ex); } + _comparator = comparator; + CheckHealth(); + CalculateSearchables(); + } - // NOTE: Many of these tests may be better in our Deserialisation handler. + /// + /// Throw an exception if there's anything wrong with this module + /// + private void CheckHealth() + { if (!IsSpecSupported()) { - throw new UnsupportedKraken( - String.Format( - "{0} requires CKAN {1}, we can't read it.", - this, - spec_version - ) - ); + throw new UnsupportedKraken($"{this} requires CKAN {spec_version}, we can't read it."); } // Check everything in the spec is defined. - // TODO: This *can* and *should* be done with JSON attributes! foreach (string field in required_fields[kind ?? "package"]) { object value = null; @@ -324,20 +366,15 @@ public CkanModule(string json, IGameComparator comparator) if (value == null) { - string error = String.Format("{0} missing required field {1}", identifier, field); - - throw new BadMetadataKraken(null, error); + throw new BadMetadataKraken(null, $"{identifier} missing required field {field}"); } } - - // Calculate the Searchables. - CalculateSearchables(); } /// /// Calculate the mod properties used for searching via Regex. /// - public void CalculateSearchables() + private void CalculateSearchables() { SearchableIdentifier = identifier == null ? string.Empty : CkanModule.nonAlphaNums.Replace(identifier, ""); SearchableName = name == null ? string.Empty : CkanModule.nonAlphaNums.Replace(name, ""); @@ -498,8 +535,7 @@ internal static bool UniConflicts(CkanModule mod1, CkanModule mod2) /// public bool IsCompatibleKSP(GameVersionCriteria version) { - log.DebugFormat("Testing if {0} is compatible with KSP {1}", this, version.ToString()); - + log.DebugFormat("Testing if {0} is compatible with game versions {1}", this, version.ToString()); return _comparator.Compatible(version, this); } @@ -606,12 +642,12 @@ bool IEquatable.Equals(CkanModule other) /// /// Returns true if we support at least spec_version of the CKAN spec. /// - internal static bool IsSpecSupported(ModuleVersion spec_vesion) + internal static bool IsSpecSupported(ModuleVersion spec_version) { // This could be a read-only state variable; do we have those in C#? ModuleVersion release = new ModuleVersion(Meta.GetVersion(VersionFormat.Short)); - return release == null || release.IsGreaterThan(spec_vesion); + return release == null || release.IsGreaterThan(spec_version); } /// diff --git a/Core/Types/ModuleInstallDescriptor.cs b/Core/Types/ModuleInstallDescriptor.cs index 8b24e31a63..6fbe0cd14a 100644 --- a/Core/Types/ModuleInstallDescriptor.cs +++ b/Core/Types/ModuleInstallDescriptor.cs @@ -11,6 +11,8 @@ using ICSharpCode.SharpZipLib.Zip; using Newtonsoft.Json; +using CKAN.Games; + [assembly: InternalsVisibleTo("CKAN.Tests")] namespace CKAN @@ -67,11 +69,6 @@ public class ModuleInstallDescriptor : ICloneable, IEquatable FindInstallableFiles(ZipFile zipfile, GameInstance // Get the full name of the file. // Update our file info with the install location file_info.destination = TransformOutputName( - entry.Name, installDir, @as); + ksp.game, entry.Name, installDir, @as); file_info.makedir = AllowDirectoryCreation( + ksp.game, ksp?.ToRelativeGameDir(file_info.destination) ?? file_info.destination); } @@ -430,13 +428,9 @@ public List FindInstallableFiles(ZipFile zipfile, GameInstance return files; } - private static string[] CreateableDirs = { - "GameData", "Tutorial", "Scenarios", "Missions", "Ships/Script" - }; - - private bool AllowDirectoryCreation(string relativePath) + private bool AllowDirectoryCreation(IGame game, string relativePath) { - return CreateableDirs.Any(dir => + return game.CreateableDirs.Any(dir => relativePath == dir || relativePath.StartsWith($"{dir}/")); } @@ -449,7 +443,7 @@ private bool AllowDirectoryCreation(string relativePath) /// The name of the file to transform /// The installation dir where the file should end up with /// The output name - internal string TransformOutputName(string outputName, string installDir, string @as) + internal string TransformOutputName(IGame game, string outputName, string installDir, string @as) { string leadingPathToRemove = Path .GetDirectoryName(ShortestMatchingPrefix(outputName)) @@ -484,7 +478,7 @@ internal string TransformOutputName(string outputName, string installDir, string } else { - var reservedPrefix = ReservedPaths.FirstOrDefault(prefix => + var reservedPrefix = game.ReservedPaths.FirstOrDefault(prefix => outputName.StartsWith(prefix + "/", StringComparison.InvariantCultureIgnoreCase)); if (reservedPrefix != null) { diff --git a/Core/Versioning/GameVersion.cs b/Core/Versioning/GameVersion.cs index fd98a94a74..0d4a7b99df 100644 --- a/Core/Versioning/GameVersion.cs +++ b/Core/Versioning/GameVersion.cs @@ -198,16 +198,16 @@ public GameVersion(int major, int minor, int patch) public GameVersion(int major, int minor, int patch, int build) { if (major < 0) - throw new ArgumentOutOfRangeException("major"); + throw new ArgumentOutOfRangeException("major", major, $"{major}"); if (minor < 0) - throw new ArgumentOutOfRangeException("minor"); + throw new ArgumentOutOfRangeException("minor", minor, $"{minor}"); if (patch < 0) - throw new ArgumentOutOfRangeException("patch"); + throw new ArgumentOutOfRangeException("patch", patch, $"{patch}"); if (build < 0) - throw new ArgumentOutOfRangeException("build"); + throw new ArgumentOutOfRangeException("build", build, $"{build}"); _major = major; _minor = minor; @@ -415,20 +415,36 @@ public static bool TryParse(string input, out GameVersion result) var buildGroup = match.Groups["build"]; if (majorGroup.Success) + { if (!int.TryParse(majorGroup.Value, out major)) return false; + if (major < 0 || major == Int32.MaxValue) + major = Undefined; + } if (minorGroup.Success) + { if (!int.TryParse(minorGroup.Value, out minor)) return false; + if (minor < 0 || minor == Int32.MaxValue) + minor = Undefined; + } if (patchGroup.Success) + { if (!int.TryParse(patchGroup.Value, out patch)) return false; + if (patch < 0 || patch == Int32.MaxValue) + patch = Undefined; + } if (buildGroup.Success) + { if (!int.TryParse(buildGroup.Value, out build)) return false; + if (build < 0 || build == Int32.MaxValue) + build = Undefined; + } if (minor == Undefined) result = new GameVersion(major); diff --git a/GUI/Main/MainWait.cs b/GUI/Main/MainWait.cs index bddfaf3334..4a4e84ad22 100644 --- a/GUI/Main/MainWait.cs +++ b/GUI/Main/MainWait.cs @@ -62,7 +62,9 @@ public void SetProgress(int progress) Wait.ProgressIndeterminate = false; Util.Invoke(statusStrip1, () => { - StatusProgress.Value = progress; + StatusProgress.Value = + Math.Max(StatusProgress.Minimum, + Math.Min(StatusProgress.Maximum, progress)); StatusProgress.Style = ProgressBarStyle.Continuous; }); } diff --git a/Tests/Core/Types/ModuleInstallDescriptorTests.cs b/Tests/Core/Types/ModuleInstallDescriptorTests.cs index aeafc1cd65..2b65c24b09 100644 --- a/Tests/Core/Types/ModuleInstallDescriptorTests.cs +++ b/Tests/Core/Types/ModuleInstallDescriptorTests.cs @@ -1,10 +1,11 @@ using System.Collections.Generic; -using CKAN; using NUnit.Framework; using Tests.Data; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using ICSharpCode.SharpZipLib.Zip; +using CKAN; +using CKAN.Games; namespace Tests.Core.Types { @@ -65,7 +66,7 @@ public void TransformOutputName(string file, string outputName, string installDi $"{{\"file\": \"{file}\"}}"); // Act - var result = stanza.TransformOutputName(outputName, installDir, @as); + var result = stanza.TransformOutputName(new KerbalSpaceProgram(), outputName, installDir, @as); // Assert Assert.That(result, Is.EqualTo(expected)); @@ -81,7 +82,7 @@ public void TransformOutputNameThrowsOnInvalidParameters(string file, string out $"{{\"file\": \"{file}\"}}"); // Act - TestDelegate act = () => stanza.TransformOutputName(outputName, installDir, @as); + TestDelegate act = () => stanza.TransformOutputName(new KerbalSpaceProgram(), outputName, installDir, @as); // Assert Assert.That(act, Throws.Exception);