diff --git a/CKAN.schema b/CKAN.schema index defe003f0f..08103e1ebc 100644 --- a/CKAN.schema +++ b/CKAN.schema @@ -99,15 +99,15 @@ "enum" : [ "stable", "testing", "development" ] }, "ksp_version" : { - "description" : "Optional target KSP version", + "description" : "Optional target game version", "$ref" : "#/definitions/ksp_version" }, "ksp_version_min" : { - "description" : "Optional minimum KSP version", + "description" : "Optional minimum game version", "$ref" : "#/definitions/ksp_version" }, "ksp_version_max" : { - "description" : "Optional maximum KSP version", + "description" : "Optional maximum game version", "$ref" : "#/definitions/ksp_version" }, "ksp_version_strict" : { @@ -123,7 +123,7 @@ } }, "localizations" : { - "description" : "A list of KSP locale strings for which this mod includes localizations", + "description" : "A list of locale strings for which this mod includes localizations", "type" : "array", "items" : { "type" : "string" }, "uniqueItems" : true @@ -361,9 +361,9 @@ "type" : "string" }, "ksp_version" : { - "description" : "A version of KSP", + "description" : "A game version", "type" : "string", - "pattern" : "^(any|[0-9]+\\.[0-9]+(\\.[0-9]+)?)$" + "pattern" : "^(any|[0-9]+(\\.[0-9]+(\\.[0-9]+)?)?)$" }, "relationship" : { "description" : "A relationship to a list of mods", @@ -439,6 +439,11 @@ "description" : "Spec v1.25 Missions path", "type" : "string", "pattern" : "^Missions" + }, + { + "description" : "KSP2 BepInEx plugins folder", + "type" : "string", + "pattern" : "BepInEx/plugins" } ] }, diff --git a/Core/CKAN-core.csproj b/Core/CKAN-core.csproj index 3993d3ada0..a8508e4077 100644 --- a/Core/CKAN-core.csproj +++ b/Core/CKAN-core.csproj @@ -65,7 +65,8 @@ - + + CKAN.Core.CKAN.schema diff --git a/Core/GameInstanceManager.cs b/Core/GameInstanceManager.cs index 44dd27decc..7f775ce3be 100644 --- a/Core/GameInstanceManager.cs +++ b/Core/GameInstanceManager.cs @@ -22,7 +22,8 @@ public class GameInstanceManager : IDisposable { private static IGame[] knownGames = new IGame[] { - new KerbalSpaceProgram() + new KerbalSpaceProgram(), + new KerbalSpaceProgram2(), }; /// @@ -646,5 +647,8 @@ public IGame DetermineGame(DirectoryInfo path, IUser user) } } + public static IGame GameByShortName(string shortName) + => knownGames.FirstOrDefault(g => g.ShortName == shortName); + } } diff --git a/Core/Games/IGame.cs b/Core/Games/IGame.cs index 5cfb4e9027..3dfdf5f6b9 100644 --- a/Core/Games/IGame.cs +++ b/Core/Games/IGame.cs @@ -29,6 +29,7 @@ public interface IGame string[] AdjustCommandLine(string[] args, GameVersion installedVersion); // Which versions exist and which is present? + void RefreshVersions(); List KnownVersions { get; } GameVersion DetectVersion(DirectoryInfo where); string CompatibleVersionsFile { get; } diff --git a/Core/Games/KerbalSpaceProgram.cs b/Core/Games/KerbalSpaceProgram.cs index 4baf4bcb42..f19aadbf21 100644 --- a/Core/Games/KerbalSpaceProgram.cs +++ b/Core/Games/KerbalSpaceProgram.cs @@ -170,6 +170,11 @@ public string[] AdjustCommandLine(string[] args, GameVersion installedVersion) return args; } + public void RefreshVersions() + { + ServiceLocator.Container.Resolve().Refresh(); + } + public List KnownVersions => ServiceLocator.Container.Resolve().KnownVersions; diff --git a/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildMap.cs b/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildMap.cs index c6a5291bbc..6da7278ad1 100644 --- a/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildMap.cs +++ b/Core/Games/KerbalSpaceProgram/GameVersionProviders/KspBuildMap.cs @@ -35,7 +35,7 @@ public sealed class KspBuildMap : IKspBuildMap new Uri("https://raw.githubusercontent.com/KSP-CKAN/CKAN-meta/master/builds.json"); private static readonly string cachedBuildMapPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "CKAN", "builds.json"); + "CKAN", "builds-ksp.json"); private static readonly ILog Log = LogManager.GetLogger(typeof(KspBuildMap)); @@ -154,7 +154,7 @@ private bool TrySetEmbeddedBuildMap() { Log.Debug("Getting embedded build map"); var resourceStream = Assembly.GetExecutingAssembly() - .GetManifestResourceStream("CKAN.builds.json"); + .GetManifestResourceStream("CKAN.builds-ksp.json"); if (resourceStream != null) { diff --git a/Core/Games/KerbalSpaceProgram2.cs b/Core/Games/KerbalSpaceProgram2.cs new file mode 100644 index 0000000000..0ce0247931 --- /dev/null +++ b/Core/Games/KerbalSpaceProgram2.cs @@ -0,0 +1,228 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.IO; +using System.Collections.Generic; +using System.Reflection; + +using Autofac; +using log4net; +using Newtonsoft.Json; + +using CKAN.Versioning; + +namespace CKAN.Games +{ + public class KerbalSpaceProgram2 : IGame + { + public string ShortName => "KSP2"; + + public bool GameInFolder(DirectoryInfo where) + => where.EnumerateFiles().Any(f => f.Name == "KSP2_x64.exe") + && where.EnumerateDirectories().Any(d => d.Name == "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() + { + if (Platform.IsMac) + { + string installPath = Path.Combine( + // This is "/Applications" in Mono on Mac + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), + "Kerbal Space Program 2" + ); + return Directory.Exists(installPath) ? installPath : null; + } + return null; + } + + public string PrimaryModDirectoryRelative => "BepInEx/plugins"; + + public string PrimaryModDirectory(GameInstance inst) + => CKANPathUtils.NormalizePath( + Path.Combine(inst.GameDir(), PrimaryModDirectoryRelative)); + + public string[] StockFolders => new string[] + { + "KSP2_x64_Data", + "MonoBleedingEdge", + "PDLauncher", + }; + + public string[] ReservedPaths => new string[] + { + }; + + public string[] CreateableDirs => new string[] + { + "BepInEx", + "BepInEx/plugins", + }; + + /// + /// Checks the path against a list of reserved game directories + /// + /// + /// + public bool IsReservedDirectory(GameInstance inst, string path) + => path == inst.GameDir() || path == inst.CkanDir() + || path == PrimaryModDirectory(inst); + + public bool AllowInstallationIn(string name, out string path) + => allowedFolders.TryGetValue(name, out path); + + public void RebuildSubdirectories(GameInstance inst) + { + } + + public string DefaultCommandLine => + Platform.IsUnix ? "./KSP2.x86_64 -single-instance" + : Platform.IsMac ? "./KSP2.app/Contents/MacOS/KSP" + : "KSP2_x64.exe -single-instance"; + + public string[] AdjustCommandLine(string[] args, GameVersion installedVersion) + => args; + + private static readonly Uri BuildMapUri = + new Uri("https://raw.githubusercontent.com/KSP-CKAN/KSP2-CKAN-meta/main/builds.json"); + private static readonly string cachedBuildMapPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "CKAN", "builds-ksp2.json"); + + private List versions = JsonConvert.DeserializeObject>( + File.Exists(cachedBuildMapPath) + ? File.ReadAllText(cachedBuildMapPath) + : new StreamReader(Assembly.GetExecutingAssembly() + .GetManifestResourceStream("CKAN.builds-ksp2.json")) + .ReadToEnd()); + + public void RefreshVersions() + { + try + { + var json = Net.DownloadText(BuildMapUri); + versions = JsonConvert.DeserializeObject>(json); + // Save to disk if download and parse succeeds + new FileInfo(cachedBuildMapPath).Directory.Create(); + File.WriteAllText(cachedBuildMapPath, json); + } + catch (Exception e) + { + log.WarnFormat("Could not retrieve latest build map from: {0}", BuildMapUri); + log.Debug(e); + } + } + + public List KnownVersions => versions; + + public GameVersion DetectVersion(DirectoryInfo where) + => GameVersion.Parse( + FileVersionInfo.GetVersionInfo( + Path.Combine(where.FullName, "KSP2_x64.exe")).ProductVersion); + + public string CompatibleVersionsFile => "compatible_game_versions.json"; + + public string[] BuildIDFiles => new string[] + { + "KSP2_x64.exe", + }; + + public Uri DefaultRepositoryURL => new Uri("https://github.com/KSP-CKAN/KSP2-CKAN-meta/archive/main.tar.gz"); + + public Uri RepositoryListURL => new Uri("https://raw.githubusercontent.com/KSP-CKAN/KSP2-CKAN-meta/main/repositories.json"); + + private readonly Dictionary allowedFolders = new Dictionary + { + { "BepInEx", "BepInEx" }, + { "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"); + + if (Directory.Exists(installPath)) + { + return installPath; + } + return null; + } + + private static readonly ILog log = LogManager.GetLogger(typeof(KerbalSpaceProgram2)); + } +} diff --git a/Core/Net/Repo.cs b/Core/Net/Repo.cs index af98a5e0bc..301ad0e559 100644 --- a/Core/Net/Repo.cs +++ b/Core/Net/Repo.cs @@ -43,7 +43,7 @@ public static RepoUpdateResult UpdateAllRepositories(RegistryManager registry_ma // Get latest copy of the game versions data (remote build map) user.RaiseMessage(Properties.Resources.NetRepoUpdatingBuildMap); - ServiceLocator.Container.Resolve().Refresh(); + ksp.game.RefreshVersions(); // Check if the ETags have changed, quit if not user.RaiseProgress(Properties.Resources.NetRepoCheckingForUpdates, 0); diff --git a/Core/builds.json b/Core/builds-ksp.json similarity index 100% rename from Core/builds.json rename to Core/builds-ksp.json diff --git a/Core/builds-ksp2.json b/Core/builds-ksp2.json new file mode 100644 index 0000000000..d68cf229ef --- /dev/null +++ b/Core/builds-ksp2.json @@ -0,0 +1,3 @@ +[ + "0.1.0.0" +] diff --git a/Dockerfile.netkan b/Dockerfile.netkan index f2715ce594..259298be75 100644 --- a/Dockerfile.netkan +++ b/Dockerfile.netkan @@ -5,6 +5,6 @@ RUN useradd -ms /bin/bash netkan USER netkan WORKDIR /home/netkan ADD netkan.exe . -ENTRYPOINT /usr/bin/mono netkan.exe --queues $QUEUES \ +ENTRYPOINT /usr/bin/mono netkan.exe --game $GAME --queues $QUEUES \ --net-useragent 'Mozilla/5.0 (compatible; Netkanbot/1.0; CKAN; +https://github.com/KSP-CKAN/NetKAN-Infra)' \ --github-token $GH_Token --gitlab-token "$GL_Token" --cachedir ckan_cache -v diff --git a/Netkan/CKAN-netkan.csproj b/Netkan/CKAN-netkan.csproj index 7d65873867..b0e8c02e48 100644 --- a/Netkan/CKAN-netkan.csproj +++ b/Netkan/CKAN-netkan.csproj @@ -108,6 +108,7 @@ + @@ -128,6 +129,7 @@ + @@ -180,4 +182,4 @@ - \ No newline at end of file + diff --git a/Netkan/CmdLineOptions.cs b/Netkan/CmdLineOptions.cs index 8c6eeafd1b..e412274dda 100644 --- a/Netkan/CmdLineOptions.cs +++ b/Netkan/CmdLineOptions.cs @@ -55,6 +55,9 @@ internal class CmdLineOptions [Option("version", HelpText = "Display the netkan version number and exit")] public bool Version { get; set; } + [Option("game", DefaultValue = "KSP", HelpText = "Short name of the game for which to inflate mods")] + public string Game { get; set; } + [ValueOption(0)] public string File { get; set; } } diff --git a/Netkan/Processors/Inflator.cs b/Netkan/Processors/Inflator.cs index 3d0baf3bb6..f0345c3076 100644 --- a/Netkan/Processors/Inflator.cs +++ b/Netkan/Processors/Inflator.cs @@ -4,18 +4,20 @@ using System.Linq; using Autofac; using log4net; + using CKAN.Configuration; using CKAN.Versioning; using CKAN.NetKAN.Model; using CKAN.NetKAN.Services; using CKAN.NetKAN.Transformers; using CKAN.NetKAN.Validators; +using CKAN.Games; namespace CKAN.NetKAN.Processors { public class Inflator { - public Inflator(string cacheDir, bool overwriteCache, string githubToken, string gitlabToken, bool prerelease) + public Inflator(string cacheDir, bool overwriteCache, string githubToken, string gitlabToken, bool prerelease, IGame game) { log.Debug("Initializing inflator"); cache = FindCache( @@ -24,11 +26,11 @@ public Inflator(string cacheDir, bool overwriteCache, string githubToken, string cacheDir ); - IModuleService moduleService = new ModuleService(); + IModuleService moduleService = new ModuleService(game); IFileService fileService = new FileService(cache); http = new CachingHttpService(cache, overwriteCache); - ckanValidator = new CkanValidator(http, moduleService); - transformer = new NetkanTransformer(http, fileService, moduleService, githubToken, gitlabToken, prerelease, netkanValidator); + ckanValidator = new CkanValidator(http, moduleService, game); + transformer = new NetkanTransformer(http, fileService, moduleService, githubToken, gitlabToken, prerelease, game, netkanValidator); } internal IEnumerable Inflate(string filename, Metadata netkan, TransformOptions opts) @@ -40,18 +42,18 @@ internal IEnumerable Inflate(string filename, Metadata netkan, Transfo http.ClearRequestedURLs(); netkanValidator.ValidateNetkan(netkan, filename); - log.Info("Input successfully passed pre-validation"); + log.Debug("Input successfully passed pre-validation"); IEnumerable ckans = transformer .Transform(netkan, opts) .ToList(); - log.Info("Finished transformation"); + log.Debug("Finished transformation"); foreach (Metadata ckan in ckans) { ckanValidator.ValidateCkan(ckan, netkan); } - log.Info("Output successfully passed post-validation"); + log.Debug("Output successfully passed post-validation"); return ckans; } catch (Exception) diff --git a/Netkan/Processors/QueueHandler.cs b/Netkan/Processors/QueueHandler.cs index ee8c842528..335482d819 100644 --- a/Netkan/Processors/QueueHandler.cs +++ b/Netkan/Processors/QueueHandler.cs @@ -13,22 +13,25 @@ using log4net.Repository.Hierarchy; using Newtonsoft.Json; using Newtonsoft.Json.Linq; + using CKAN.Versioning; using CKAN.NetKAN.Transformers; using CKAN.NetKAN.Model; using CKAN.NetKAN.Extensions; +using CKAN.Games; namespace CKAN.NetKAN.Processors { public class QueueHandler { - public QueueHandler(string inputQueueName, string outputQueueName, string cacheDir, bool overwriteCache, string githubToken, string gitlabToken, bool prerelease) + public QueueHandler(string inputQueueName, string outputQueueName, string cacheDir, bool overwriteCache, string githubToken, string gitlabToken, bool prerelease, IGame game) { warningAppender = GetQueueLogAppender(); (LogManager.GetRepository() as Hierarchy)?.Root.AddAppender(warningAppender); + this.game = game; log.Debug("Initializing SQS queue handler"); - inflator = new Inflator(cacheDir, overwriteCache, githubToken, gitlabToken, prerelease); + inflator = new Inflator(cacheDir, overwriteCache, githubToken, gitlabToken, prerelease, game); inputQueueURL = getQueueUrl(inputQueueName); outputQueueURL = getQueueUrl(outputQueueName); @@ -211,6 +214,14 @@ private SendMessageBatchRequestEntry inflationMessage(Metadata ckan, Metadata ne DataType = "String", StringValue = DateTime.UtcNow.ToString("s", CultureInfo.InvariantCulture) } + }, + { + "GameId", + new MessageAttributeValue() + { + DataType = "String", + StringValue = game.ShortName + } } }; if (ckan != null) @@ -298,8 +309,9 @@ private DeleteMessageBatchRequestEntry Delete(Message msg) }; } - private Inflator inflator; - private AmazonSQSClient client = new AmazonSQSClient(); + private readonly IGame game; + private readonly Inflator inflator; + private readonly AmazonSQSClient client = new AmazonSQSClient(); private readonly string inputQueueURL; private readonly string outputQueueURL; diff --git a/Netkan/Program.cs b/Netkan/Program.cs index 84378fcb4d..124bdd77c5 100644 --- a/Netkan/Program.cs +++ b/Netkan/Program.cs @@ -16,6 +16,7 @@ using CKAN.NetKAN.Processors; using CKAN.NetKAN.Transformers; using CKAN.NetKAN.Extensions; +using CKAN.Games; namespace CKAN.NetKAN { @@ -47,6 +48,8 @@ public static int Main(string[] args) return ExitOk; } + var game = GameInstanceManager.GameByShortName(Options.Game); + if (!string.IsNullOrEmpty(Options.ValidateCkan)) { var ckan = new Metadata(JObject.Parse(File.ReadAllText(Options.ValidateCkan))); @@ -55,7 +58,8 @@ public static int Main(string[] args) Options.OverwriteCache, Options.GitHubToken, Options.GitLabToken, - Options.PreRelease + Options.PreRelease, + game ); inf.ValidateCkan(ckan); Console.WriteLine(QueueHandler.serializeCkan( @@ -74,7 +78,8 @@ public static int Main(string[] args) Options.OverwriteCache, Options.GitHubToken, Options.GitLabToken, - Options.PreRelease + Options.PreRelease, + game ); qh.Process(); return ExitOk; @@ -92,7 +97,8 @@ public static int Main(string[] args) Options.OverwriteCache, Options.GitHubToken, Options.GitLabToken, - Options.PreRelease + Options.PreRelease, + game ); var ckans = inf.Inflate( Options.File, diff --git a/Netkan/Services/IModuleService.cs b/Netkan/Services/IModuleService.cs index ed3ce3000b..76fe89dee7 100644 --- a/Netkan/Services/IModuleService.cs +++ b/Netkan/Services/IModuleService.cs @@ -5,6 +5,7 @@ using Newtonsoft.Json.Linq; using CKAN.NetKAN.Sources.Avc; +using CKAN.NetKAN.Sources.SpaceWarp; namespace CKAN.NetKAN.Services { @@ -19,6 +20,8 @@ internal interface IModuleService IEnumerable GetPlugins(CkanModule module, ZipFile zip, GameInstance inst); IEnumerable GetCrafts(CkanModule module, ZipFile zip, GameInstance inst); + SpaceWarpInfo GetSpaceWarpInfo(CkanModule module, ZipFile zip, GameInstance inst, string internalFilePath = null); + IEnumerable FileSources(CkanModule module, ZipFile zip, GameInstance inst); IEnumerable FileDestinations(CkanModule module, string filePath); } diff --git a/Netkan/Services/ModuleService.cs b/Netkan/Services/ModuleService.cs index 4e091c2e08..77d93be5b8 100644 --- a/Netkan/Services/ModuleService.cs +++ b/Netkan/Services/ModuleService.cs @@ -12,12 +12,20 @@ using CKAN.Versioning; using CKAN.Extensions; using CKAN.NetKAN.Sources.Avc; +using CKAN.NetKAN.Sources.SpaceWarp; using CKAN.Games; namespace CKAN.NetKAN.Services { internal sealed class ModuleService : IModuleService { + public ModuleService(IGame game) + { + this.game = game; + } + + private IGame game; + private static readonly ILog Log = LogManager.GetLogger(typeof(ModuleService)); public AvcVersion GetInternalAvc(CkanModule module, string zipFilePath, string internalFilePath = null) @@ -66,7 +74,7 @@ public bool HasInstallableFiles(CkanModule module, string filePath) try { ModuleInstaller.FindInstallableFiles(module, filePath, - new GameInstance(new KerbalSpaceProgram(), "/", "dummy", new NullUser())); + new GameInstance(game, "/", "dummy", new NullUser())); } catch (BadMetadataKraken) { @@ -97,7 +105,7 @@ public IEnumerable FileSources(CkanModule module, ZipFile zip, GameIns public IEnumerable FileDestinations(CkanModule module, string filePath) { - var inst = new GameInstance(new KerbalSpaceProgram(), "/", "dummy", null, false); + var inst = new GameInstance(game, "/", "dummy", null, false); return ModuleInstaller .FindInstallableFiles(module, filePath, inst) .Where(f => !f.source.IsDirectory) @@ -230,7 +238,7 @@ public Tuple FindInternalAvc(CkanModule module, ZipFile zipfile, const string versionExt = ".version"; // Get all our version files - var ksp = new GameInstance(new KerbalSpaceProgram(), "/", "dummy", new NullUser()); + var ksp = new GameInstance(game, "/", "dummy", new NullUser()); var files = ModuleInstaller.FindInstallableFiles(module, zipfile, ksp) .Select(x => x.source) .Where(source => source.Name.EndsWith(versionExt, @@ -315,5 +323,18 @@ public static AvcVersion GetInternalAvc(ZipFile zipfile, ZipEntry avcEntry) } } } + + private const string SpaceWarpInfoFilename = "swinfo.json"; + + public SpaceWarpInfo GetSpaceWarpInfo(CkanModule module, ZipFile zip, GameInstance inst, string internalFilePath = null) + => (string.IsNullOrWhiteSpace(internalFilePath) + ? GetFilesBySuffix(module, zip, SpaceWarpInfoFilename, inst) + : ModuleInstaller.FindInstallableFiles(module, zip, inst) + .Where(instF => instF.source.Name == internalFilePath)) + .Select(instF => instF.source) + .Select(entry => + JsonConvert.DeserializeObject( + new StreamReader(zip.GetInputStream(entry)).ReadToEnd())) + .FirstOrDefault(); } } diff --git a/Netkan/Sources/SpaceWarp/SpaceWarpInfo.cs b/Netkan/Sources/SpaceWarp/SpaceWarpInfo.cs new file mode 100644 index 0000000000..7e76913063 --- /dev/null +++ b/Netkan/Sources/SpaceWarp/SpaceWarpInfo.cs @@ -0,0 +1,50 @@ +using System; + +using Newtonsoft.Json; + +// https://github.com/SpaceWarpDev/SpaceWarp/blob/main/example_mod_info.json + +namespace CKAN.NetKAN.Sources.SpaceWarp +{ + public class SpaceWarpInfo + { + public string mod_id; + public string name; + public string author; + public string description; + public string source; + public string version; + public Dependency[] dependencies; + public VersionMinMax ksp2_version; + } + + public class Dependency + { + public string id; + public VersionMinMax version; + } + + public class VersionMinMax + { + [JsonConverter(typeof(SpaceWarpGameVersionConverter))] + public string min; + [JsonConverter(typeof(SpaceWarpGameVersionConverter))] + public string max; + } + + public class SpaceWarpGameVersionConverter : JsonConverter + { + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + => (reader.Value as string)?.Replace("*", "any"); + + public override bool CanConvert(Type objectType) + { + throw new NotImplementedException(); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } +} diff --git a/Netkan/Transformers/DownloadAttributeTransformer.cs b/Netkan/Transformers/DownloadAttributeTransformer.cs index ab44b91758..956109ef9e 100644 --- a/Netkan/Transformers/DownloadAttributeTransformer.cs +++ b/Netkan/Transformers/DownloadAttributeTransformer.cs @@ -32,7 +32,7 @@ public IEnumerable Transform(Metadata metadata, TransformOptions opts) { var json = metadata.Json(); - Log.InfoFormat("Executing Download attribute transformation with {0}", metadata.Kref); + Log.Debug("Executing Download attribute transformation"); Log.DebugFormat("Input metadata:{0}{1}", Environment.NewLine, json); string file = _http.DownloadModule(metadata); diff --git a/Netkan/Transformers/GeneratedByTransformer.cs b/Netkan/Transformers/GeneratedByTransformer.cs index db396b4860..15d95818dc 100644 --- a/Netkan/Transformers/GeneratedByTransformer.cs +++ b/Netkan/Transformers/GeneratedByTransformer.cs @@ -19,7 +19,7 @@ public IEnumerable Transform(Metadata metadata, TransformOptions opts) { var json = metadata.Json(); - Log.InfoFormat("Executing generated by transformation with {0}", metadata.Kref); + Log.Debug("Executing generated by transformation"); Log.DebugFormat("Input metadata:{0}{1}", Environment.NewLine, json); json["x_generated_by"] = "netkan"; // TODO: We should write the specific version here too diff --git a/Netkan/Transformers/InstallSizeTransformer.cs b/Netkan/Transformers/InstallSizeTransformer.cs index 2bec4cb65e..ea2bcfceb6 100644 --- a/Netkan/Transformers/InstallSizeTransformer.cs +++ b/Netkan/Transformers/InstallSizeTransformer.cs @@ -13,10 +13,11 @@ internal sealed class InstallSizeTransformer : ITransformer { public string Name { get { return "install_size"; } } - public InstallSizeTransformer(IHttpService http, IModuleService moduleService) + public InstallSizeTransformer(IHttpService http, IModuleService moduleService, IGame game) { _http = http; _moduleService = moduleService; + _game = game; } public IEnumerable Transform(Metadata metadata, TransformOptions opts) @@ -26,7 +27,7 @@ public IEnumerable Transform(Metadata metadata, TransformOptions opts) var json = metadata.Json(); CkanModule mod = CkanModule.FromJson(json.ToString()); ZipFile zip = new ZipFile(_http.DownloadModule(metadata)); - GameInstance inst = new GameInstance(new KerbalSpaceProgram(), "/", "dummy", new NullUser()); + GameInstance inst = new GameInstance(_game, "/", "dummy", new NullUser()); json["install_size"] = _moduleService.FileSources(mod, zip, inst) .Select(ze => ze.Size) .Sum(); @@ -40,5 +41,6 @@ public IEnumerable Transform(Metadata metadata, TransformOptions opts) private readonly IHttpService _http; private readonly IModuleService _moduleService; + private readonly IGame _game; } } diff --git a/Netkan/Transformers/InternalCkanTransformer.cs b/Netkan/Transformers/InternalCkanTransformer.cs index c66c479874..5d30d06054 100644 --- a/Netkan/Transformers/InternalCkanTransformer.cs +++ b/Netkan/Transformers/InternalCkanTransformer.cs @@ -19,13 +19,15 @@ internal sealed class InternalCkanTransformer : ITransformer private readonly IHttpService _http; private readonly IModuleService _moduleService; + private readonly IGame _game; public string Name { get { return "internal_ckan"; } } - public InternalCkanTransformer(IHttpService http, IModuleService moduleService) + public InternalCkanTransformer(IHttpService http, IModuleService moduleService, IGame game) { _http = http; _moduleService = moduleService; + _game = game; } public IEnumerable Transform(Metadata metadata, TransformOptions opts) @@ -39,7 +41,7 @@ public IEnumerable Transform(Metadata metadata, TransformOptions opts) var moduleJson = metadata.Json(); moduleJson.SafeAdd("version", "1"); CkanModule mod = CkanModule.FromJson(moduleJson.ToString()); - GameInstance inst = new GameInstance(new KerbalSpaceProgram(), "/", "dummy", new NullUser()); + GameInstance inst = new GameInstance(_game, "/", "dummy", new NullUser()); var internalJson = _moduleService.GetInternalCkan(mod, _http.DownloadModule(metadata), inst); diff --git a/Netkan/Transformers/LocalizationsTransformer.cs b/Netkan/Transformers/LocalizationsTransformer.cs index 731d897dee..88b87089f0 100644 --- a/Netkan/Transformers/LocalizationsTransformer.cs +++ b/Netkan/Transformers/LocalizationsTransformer.cs @@ -21,10 +21,11 @@ internal sealed class LocalizationsTransformer : ITransformer /// /// HTTP service /// Module service - public LocalizationsTransformer(IHttpService http, IModuleService moduleService) + public LocalizationsTransformer(IHttpService http, IModuleService moduleService, IGame game) { _http = http; _moduleService = moduleService; + _game = game; } /// @@ -52,7 +53,7 @@ public IEnumerable Transform(Metadata metadata, TransformOptions opts) { CkanModule mod = CkanModule.FromJson(json.ToString()); ZipFile zip = new ZipFile(_http.DownloadModule(metadata)); - GameInstance inst = new GameInstance(new KerbalSpaceProgram(), "/", "dummy", new NullUser()); + GameInstance inst = new GameInstance(_game, "/", "dummy", new NullUser()); log.Debug("Extracting locales"); // Extract the locale names from the ZIP's cfg files @@ -86,6 +87,7 @@ public IEnumerable Transform(Metadata metadata, TransformOptions opts) private readonly IHttpService _http; private readonly IModuleService _moduleService; + private readonly IGame _game; private static readonly ILog log = LogManager.GetLogger(typeof(LocalizationsTransformer)); diff --git a/Netkan/Transformers/NetkanTransformer.cs b/Netkan/Transformers/NetkanTransformer.cs index bc81d8f081..1c64100e95 100644 --- a/Netkan/Transformers/NetkanTransformer.cs +++ b/Netkan/Transformers/NetkanTransformer.cs @@ -9,6 +9,7 @@ using CKAN.NetKAN.Sources.Gitlab; using CKAN.NetKAN.Sources.Jenkins; using CKAN.NetKAN.Sources.Spacedock; +using CKAN.Games; namespace CKAN.NetKAN.Transformers { @@ -29,6 +30,7 @@ public NetkanTransformer( string githubToken, string gitlabToken, bool prerelease, + IGame game, IValidator validator ) { @@ -37,7 +39,7 @@ IValidator validator var glApi = new GitlabApi(http, gitlabToken); _transformers = InjectVersionedOverrideTransformers(new List { - new StagingTransformer(), + new StagingTransformer(game), new MetaNetkanTransformer(http, ghApi), new SpacedockTransformer(new SpacedockApi(http), ghApi), new CurseTransformer(new CurseApi(http)), @@ -46,9 +48,10 @@ IValidator validator new HttpTransformer(), new JenkinsTransformer(new JenkinsApi(http)), new AvcKrefTransformer(http, ghApi), - new InternalCkanTransformer(http, moduleService), + new InternalCkanTransformer(http, moduleService, game), + new SpaceWarpInfoTransformer(http, moduleService, game), new AvcTransformer(http, moduleService, ghApi), - new LocalizationsTransformer(http, moduleService), + new LocalizationsTransformer(http, moduleService, game), new VersionEditTransformer(), new ForcedVTransformer(), new EpochTransformer(), @@ -57,7 +60,7 @@ IValidator validator // specify a before or after property. new VersionedOverrideTransformer(before: new string[] { null }, after: new string[] { null }), new DownloadAttributeTransformer(http, fileService), - new InstallSizeTransformer(http, moduleService), + new InstallSizeTransformer(http, moduleService, game), new GeneratedByTransformer(), new OptimusPrimeTransformer(), new StripNetkanMetadataTransformer(), diff --git a/Netkan/Transformers/PropertySortTransformer.cs b/Netkan/Transformers/PropertySortTransformer.cs index 1839515bb7..314f55f24d 100644 --- a/Netkan/Transformers/PropertySortTransformer.cs +++ b/Netkan/Transformers/PropertySortTransformer.cs @@ -73,7 +73,7 @@ public IEnumerable Transform(Metadata metadata, TransformOptions opts) var json = metadata.Json(); var sortedJson = new JObject(); - Log.InfoFormat("Executing property sort transformation"); + Log.Debug("Executing property sort transformation"); Log.DebugFormat("Input metadata:{0}{1}", Environment.NewLine, json); var sortedPropertyNames = json diff --git a/Netkan/Transformers/SpaceWarpInfoTransformer.cs b/Netkan/Transformers/SpaceWarpInfoTransformer.cs new file mode 100644 index 0000000000..6d6a26450d --- /dev/null +++ b/Netkan/Transformers/SpaceWarpInfoTransformer.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; + +using ICSharpCode.SharpZipLib.Zip; +using log4net; + +using CKAN.NetKAN.Model; +using CKAN.NetKAN.Sources.SpaceWarp; +using CKAN.NetKAN.Services; +using CKAN.NetKAN.Extensions; +using CKAN.Versioning; +using CKAN.Games; + +namespace CKAN.NetKAN.Transformers +{ + internal sealed class SpaceWarpInfoTransformer : ITransformer + { + public SpaceWarpInfoTransformer(IHttpService httpSvc, IModuleService modSvc, IGame game) + { + this.httpSvc = httpSvc; + this.modSvc = modSvc; + this.game = game; + } + + public string Name => "space_warp_info"; + + public IEnumerable Transform(Metadata metadata, TransformOptions opts) + { + if (metadata.Vref != null && metadata.Vref.Source == "space-warp") + { + var moduleJson = metadata.Json(); + moduleJson.SafeAdd("version", "1"); + CkanModule mod = CkanModule.FromJson(moduleJson.ToString()); + GameInstance inst = new GameInstance(game, "/", "dummy", new NullUser()); + ZipFile zip = new ZipFile(httpSvc.DownloadModule(metadata)); + SpaceWarpInfo swinfo = modSvc.GetSpaceWarpInfo(mod, zip, inst, metadata.Vref.Id); + if (swinfo != null) + { + log.Info("Found swinfo.json file"); + var json = metadata.Json(); + json.SafeAdd("name", swinfo.name); + json.SafeAdd("author", swinfo.author); + json.SafeAdd("abstract", swinfo.description); + json.SafeAdd("version", swinfo.version); + GameVersion minVer = null, maxVer = null; + if (GameVersion.TryParse(swinfo.ksp2_version.min, out minVer) + || GameVersion.TryParse(swinfo.ksp2_version.max, out maxVer)) + { + log.InfoFormat("Found compatibility: {0}–{1}", minVer, maxVer); + ModuleService.ApplyVersions(json, null, minVer, maxVer); + } + log.DebugFormat("Transformed metadata:{0}{1}", + Environment.NewLine, json); + yield return new Metadata(json); + yield break; + } + } + yield return metadata; + } + + private readonly IHttpService httpSvc; + private readonly IModuleService modSvc; + private readonly IGame game; + + private static readonly ILog log = LogManager.GetLogger(typeof(SpaceWarpInfoTransformer)); + } +} diff --git a/Netkan/Transformers/SpacedockTransformer.cs b/Netkan/Transformers/SpacedockTransformer.cs index d863770925..1ee9bcd387 100644 --- a/Netkan/Transformers/SpacedockTransformer.cs +++ b/Netkan/Transformers/SpacedockTransformer.cs @@ -76,7 +76,7 @@ private Metadata TransformOne(Metadata metadata, JObject json, SpacedockMod sdMo if (json["ksp_version_min"] == null && json["ksp_version_max"] == null && json["ksp_version"] == null) { Log.DebugFormat("Writing ksp_version from SpaceDock: {0}", latestVersion.KSP_version); - json["ksp_version"] = latestVersion.KSP_version.ToString(); + json["ksp_version"] = latestVersion.KSP_version.WithoutBuild.ToString(); } json.SafeAdd("name", sdMod.name); diff --git a/Netkan/Transformers/StagingTransformer.cs b/Netkan/Transformers/StagingTransformer.cs index be4835fef2..ec17214216 100644 --- a/Netkan/Transformers/StagingTransformer.cs +++ b/Netkan/Transformers/StagingTransformer.cs @@ -13,9 +13,10 @@ namespace CKAN.NetKAN.Transformers { internal sealed class StagingTransformer : ITransformer { - public StagingTransformer() + public StagingTransformer(IGame game) { - currentRelease = new KerbalSpaceProgram().KnownVersions.Max().ToVersionRange(); + this.game = game; + currentRelease = game.KnownVersions.Max().ToVersionRange(); } public string Name { get { return "staging"; } } @@ -41,7 +42,6 @@ private bool VersionsNeedManualReview(Metadata metadata, out string reason) var maxVer = maxStr == null ? GameVersion.Any : GameVersion.Parse((string)maxStr); if (currentRelease.IntersectWith(new GameVersionRange(minVer, maxVer)) == null) { - var game = new KerbalSpaceProgram(); reason = $"Hard-coded game versions not compatible with current release: {GameVersionRange.VersionSpan(game, minVer, maxVer)}\r\nPlease check that they match the forum thread."; return true; } @@ -53,7 +53,9 @@ private bool VersionsNeedManualReview(Metadata metadata, out string reason) } } - private static GameVersionRange currentRelease; + private readonly GameVersionRange currentRelease; + private readonly IGame game; + private static readonly ILog Log = LogManager.GetLogger(typeof(StagingTransformer)); } } diff --git a/Netkan/Transformers/StripNetkanMetadataTransformer.cs b/Netkan/Transformers/StripNetkanMetadataTransformer.cs index 8d6f8a383f..2f36a0c22d 100644 --- a/Netkan/Transformers/StripNetkanMetadataTransformer.cs +++ b/Netkan/Transformers/StripNetkanMetadataTransformer.cs @@ -20,7 +20,7 @@ public IEnumerable Transform(Metadata metadata, TransformOptions opts) { var json = metadata.Json(); - Log.InfoFormat("Executing strip Netkan metadata transformation with {0}", metadata.Kref); + Log.Debug("Executing strip Netkan metadata transformation"); Log.DebugFormat("Input metadata:{0}{1}", Environment.NewLine, json); Strip(json); diff --git a/Netkan/Validators/CkanValidator.cs b/Netkan/Validators/CkanValidator.cs index 53018e8f2e..9b6fb9e19a 100644 --- a/Netkan/Validators/CkanValidator.cs +++ b/Netkan/Validators/CkanValidator.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; + using CKAN.NetKAN.Model; using CKAN.NetKAN.Services; +using CKAN.Games; namespace CKAN.NetKAN.Validators { @@ -8,7 +10,7 @@ internal sealed class CkanValidator : IValidator { private readonly List _validators; - public CkanValidator(IHttpService downloader, IModuleService moduleService) + public CkanValidator(IHttpService downloader, IModuleService moduleService, IGame game) { this.downloader = downloader; this.moduleService = moduleService; @@ -16,14 +18,14 @@ public CkanValidator(IHttpService downloader, IModuleService moduleService) { new IsCkanModuleValidator(), new TagsValidator(), - new InstallsFilesValidator(downloader, moduleService), - new MatchesKnownGameVersionsValidator(), + new InstallsFilesValidator(downloader, moduleService, game), + new MatchesKnownGameVersionsValidator(game), new ObeysCKANSchemaValidator(), new KindValidator(), - new HarmonyValidator(downloader, moduleService), - new ModuleManagerDependsValidator(downloader, moduleService), - new PluginsValidator(downloader, moduleService), - new CraftsInShipsValidator(downloader, moduleService), + new HarmonyValidator(downloader, moduleService, game), + new ModuleManagerDependsValidator(downloader, moduleService, game), + new PluginsValidator(downloader, moduleService, game), + new CraftsInShipsValidator(downloader, moduleService, game), }; } diff --git a/Netkan/Validators/CraftsInShipsValidator.cs b/Netkan/Validators/CraftsInShipsValidator.cs index 7f3fd9ebe2..9f09fde265 100644 --- a/Netkan/Validators/CraftsInShipsValidator.cs +++ b/Netkan/Validators/CraftsInShipsValidator.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq; using ICSharpCode.SharpZipLib.Zip; using log4net; + using CKAN.NetKAN.Services; using CKAN.NetKAN.Model; using CKAN.Games; @@ -12,15 +13,16 @@ namespace CKAN.NetKAN.Validators { internal sealed class CraftsInShipsValidator : IValidator { - public CraftsInShipsValidator(IHttpService http, IModuleService moduleService) + public CraftsInShipsValidator(IHttpService http, IModuleService moduleService, IGame game) { _http = http; _moduleService = moduleService; + _game = game; } public void Validate(Metadata metadata) { - Log.Info("Validating that craft files are installed into Ships"); + Log.Debug("Validating that craft files are installed into Ships"); JObject json = metadata.Json(); CkanModule mod = CkanModule.FromJson(json.ToString()); @@ -30,7 +32,7 @@ public void Validate(Metadata metadata) if (!string.IsNullOrEmpty(package)) { var zip = new ZipFile(package); - var inst = new GameInstance(new KerbalSpaceProgram(), "/", "dummy", null, false); + var inst = new GameInstance(_game, "/", "dummy", null, false); var badCrafts = _moduleService.GetCrafts(mod, zip, inst) .Where(f => !AllowedCraftPath(inst.ToRelativeGameDir(f.destination))) .ToList(); @@ -55,6 +57,7 @@ private bool AllowedCraftPath(string path) private readonly IHttpService _http; private readonly IModuleService _moduleService; + private readonly IGame _game; private static readonly ILog Log = LogManager.GetLogger(typeof(CraftsInShipsValidator)); } diff --git a/Netkan/Validators/HarmonyValidator.cs b/Netkan/Validators/HarmonyValidator.cs index 1eb5950cfe..13cfa89fc0 100644 --- a/Netkan/Validators/HarmonyValidator.cs +++ b/Netkan/Validators/HarmonyValidator.cs @@ -13,10 +13,11 @@ namespace CKAN.NetKAN.Validators { internal sealed class HarmonyValidator : IValidator { - public HarmonyValidator(IHttpService http, IModuleService moduleService) + public HarmonyValidator(IHttpService http, IModuleService moduleService, IGame game) { _http = http; _moduleService = moduleService; + _game = game; } public void Validate(Metadata metadata) @@ -25,14 +26,14 @@ public void Validate(Metadata metadata) CkanModule mod = CkanModule.FromJson(json.ToString()); // The Harmony2 module is allowed to install a Harmony DLL; // anybody else must have "provides":["Harmony1"] to do so - if (!mod.IsDLC && mod.identifier != "Harmony2") + if (_game.ShortName == "KSP" && !mod.IsDLC && mod.identifier != "Harmony2") { // Need to peek at the mod's files var package = _http.DownloadModule(metadata); if (!string.IsNullOrEmpty(package)) { ZipFile zip = new ZipFile(package); - GameInstance inst = new GameInstance(new KerbalSpaceProgram(), "/", "dummy", new NullUser()); + GameInstance inst = new GameInstance(_game, "/", "dummy", new NullUser()); var harmonyDLLs = _moduleService.GetPlugins(mod, zip, inst) .Select(instF => instF.source.Name) @@ -55,6 +56,7 @@ public void Validate(Metadata metadata) private readonly IHttpService _http; private readonly IModuleService _moduleService; + private readonly IGame _game; private static readonly ILog Log = LogManager.GetLogger(typeof(HarmonyValidator)); } diff --git a/Netkan/Validators/HasIdentifierValidator.cs b/Netkan/Validators/HasIdentifierValidator.cs index 64bbb8a7ad..ac5091828e 100644 --- a/Netkan/Validators/HasIdentifierValidator.cs +++ b/Netkan/Validators/HasIdentifierValidator.cs @@ -9,7 +9,7 @@ internal sealed class HasIdentifierValidator : IValidator public void Validate(Metadata metadata) { - Log.Info("Validating that metadata contains an identifier"); + Log.Debug("Validating that metadata contains an identifier"); if (string.IsNullOrWhiteSpace(metadata.Identifier)) { diff --git a/Netkan/Validators/InstallsFilesValidator.cs b/Netkan/Validators/InstallsFilesValidator.cs index bb86fef45f..68448b4070 100644 --- a/Netkan/Validators/InstallsFilesValidator.cs +++ b/Netkan/Validators/InstallsFilesValidator.cs @@ -12,11 +12,13 @@ internal sealed class InstallsFilesValidator : IValidator { private readonly IHttpService _http; private readonly IModuleService _moduleService; + private readonly IGame _game; - public InstallsFilesValidator(IHttpService http, IModuleService moduleService) + public InstallsFilesValidator(IHttpService http, IModuleService moduleService, IGame game) { _http = http; _moduleService = moduleService; + _game = game; } public void Validate(Metadata metadata) @@ -31,7 +33,7 @@ public void Validate(Metadata metadata) { throw new Kraken(string.Format( "Module contains no files matching: {0}", - mod.DescribeInstallStanzas(new KerbalSpaceProgram()) + mod.DescribeInstallStanzas(_game) )); } diff --git a/Netkan/Validators/KrefValidator.cs b/Netkan/Validators/KrefValidator.cs index 9e6bb85b10..2f989bdad8 100644 --- a/Netkan/Validators/KrefValidator.cs +++ b/Netkan/Validators/KrefValidator.cs @@ -9,7 +9,7 @@ public KrefValidator() { } public void Validate(Metadata metadata) { - Log.Info("Validating that metadata contains valid or null $kref"); + Log.Debug("Validating that metadata contains valid or null $kref"); switch (metadata.Kref?.Source) { diff --git a/Netkan/Validators/MatchesKnownGameVersionsValidator.cs b/Netkan/Validators/MatchesKnownGameVersionsValidator.cs index cc309198d4..44d419dc44 100644 --- a/Netkan/Validators/MatchesKnownGameVersionsValidator.cs +++ b/Netkan/Validators/MatchesKnownGameVersionsValidator.cs @@ -1,17 +1,19 @@ using System.Collections.Generic; using Autofac; + using CKAN.GameVersionProviders; using CKAN.Versioning; -using CKAN.Games; using CKAN.NetKAN.Model; +using CKAN.Games; namespace CKAN.NetKAN.Validators { internal sealed class MatchesKnownGameVersionsValidator : IValidator { - public MatchesKnownGameVersionsValidator() + public MatchesKnownGameVersionsValidator(IGame game) { - knownVersions = new KerbalSpaceProgram().KnownVersions; + this.game = game; + knownVersions = game.KnownVersions; } public void Validate(Metadata metadata) @@ -21,11 +23,11 @@ public void Validate(Metadata metadata) { GameVersion minKsp = null, maxKsp = null; Registry.GetMinMaxVersions(new List() {mod}, out _, out _, out minKsp, out maxKsp); - var game = new KerbalSpaceProgram(); throw new Kraken($"{metadata.Identifier} doesn't match any valid game version: {GameVersionRange.VersionSpan(game, minKsp, maxKsp)}"); } } private List knownVersions; + private IGame game; } } diff --git a/Netkan/Validators/ModuleManagerDependsValidator.cs b/Netkan/Validators/ModuleManagerDependsValidator.cs index 58d1e9cbda..f30dbdc254 100644 --- a/Netkan/Validators/ModuleManagerDependsValidator.cs +++ b/Netkan/Validators/ModuleManagerDependsValidator.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq; using ICSharpCode.SharpZipLib.Zip; using log4net; + using CKAN.NetKAN.Services; using CKAN.NetKAN.Model; using CKAN.Extensions; @@ -13,15 +14,16 @@ namespace CKAN.NetKAN.Validators { internal sealed class ModuleManagerDependsValidator : IValidator { - public ModuleManagerDependsValidator(IHttpService http, IModuleService moduleService) + public ModuleManagerDependsValidator(IHttpService http, IModuleService moduleService, IGame game) { _http = http; _moduleService = moduleService; + _game = game; } public void Validate(Metadata metadata) { - Log.Info("Validating that metadata dependencies are consistent with cfg file syntax"); + Log.Debug("Validating that metadata dependencies are consistent with cfg file syntax"); JObject json = metadata.Json(); CkanModule mod = CkanModule.FromJson(json.ToString()); @@ -31,7 +33,7 @@ public void Validate(Metadata metadata) if (!string.IsNullOrEmpty(package)) { ZipFile zip = new ZipFile(package); - GameInstance inst = new GameInstance(new KerbalSpaceProgram(), "/", "dummy", new NullUser()); + GameInstance inst = new GameInstance(_game, "/", "dummy", new NullUser()); var mmConfigs = _moduleService.GetConfigFiles(mod, zip, inst) .Where(cfg => moduleManagerRegex.IsMatch( new StreamReader(zip.GetInputStream(cfg.source)).ReadToEnd())) @@ -63,6 +65,7 @@ public void Validate(Metadata metadata) private readonly IHttpService _http; private readonly IModuleService _moduleService; + private readonly IGame _game; private static readonly ILog Log = LogManager.GetLogger(typeof(ModuleManagerDependsValidator)); } diff --git a/Netkan/Validators/PluginsValidator.cs b/Netkan/Validators/PluginsValidator.cs index c4e25f3dc1..bfb9e0a38e 100644 --- a/Netkan/Validators/PluginsValidator.cs +++ b/Netkan/Validators/PluginsValidator.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq; using ICSharpCode.SharpZipLib.Zip; using log4net; + using CKAN.Extensions; using CKAN.NetKAN.Services; using CKAN.NetKAN.Model; @@ -13,15 +14,16 @@ namespace CKAN.NetKAN.Validators { internal sealed class PluginsValidator : IValidator { - public PluginsValidator(IHttpService http, IModuleService moduleService) + public PluginsValidator(IHttpService http, IModuleService moduleService, IGame game) { _http = http; _moduleService = moduleService; + _game = game; } public void Validate(Metadata metadata) { - Log.Info("Validating that metadata is appropriate for DLLs"); + Log.Debug("Validating that metadata is appropriate for DLLs"); JObject json = metadata.Json(); CkanModule mod = CkanModule.FromJson(json.ToString()); @@ -31,7 +33,7 @@ public void Validate(Metadata metadata) if (!string.IsNullOrEmpty(package)) { ZipFile zip = new ZipFile(package); - GameInstance inst = new GameInstance(new KerbalSpaceProgram(), "/", "dummy", new NullUser()); + GameInstance inst = new GameInstance(_game, "/", "dummy", new NullUser()); var plugins = _moduleService.GetPlugins(mod, zip, inst).ToList(); bool hasPlugin = plugins.Any(); @@ -76,6 +78,7 @@ public void Validate(Metadata metadata) private readonly IHttpService _http; private readonly IModuleService _moduleService; + private readonly IGame _game; private static readonly ILog Log = LogManager.GetLogger(typeof(PluginsValidator)); } diff --git a/Netkan/Validators/TagsValidator.cs b/Netkan/Validators/TagsValidator.cs index bb1786fc23..aecc5859ca 100644 --- a/Netkan/Validators/TagsValidator.cs +++ b/Netkan/Validators/TagsValidator.cs @@ -11,7 +11,7 @@ public TagsValidator() { } public void Validate(Metadata metadata) { - Log.Info("Validating that metadata has tags"); + Log.Debug("Validating that metadata has tags"); JObject json = metadata.Json(); JArray tags = !json.ContainsKey("tags") ? null : (JArray)json["tags"]; diff --git a/Netkan/Validators/VrefValidator.cs b/Netkan/Validators/VrefValidator.cs index 6fd44b592c..14be8ce46e 100644 --- a/Netkan/Validators/VrefValidator.cs +++ b/Netkan/Validators/VrefValidator.cs @@ -16,7 +16,7 @@ public VrefValidator(IHttpService http, IModuleService moduleService) public void Validate(Metadata metadata) { - Log.Info("Validating that metadata vref is consistent with download contents"); + Log.Debug("Validating that metadata vref is consistent with download contents"); JObject json = metadata.Json(); var noVersion = metadata.Version == null; @@ -35,7 +35,7 @@ public void Validate(Metadata metadata) var zipFilePath = _http.DownloadModule(metadata); if (!string.IsNullOrEmpty(zipFilePath)) { - bool hasVref = (metadata.Vref != null); + bool hasAvcVref = (metadata.Vref?.Source == "ksp-avc"); string path = null; bool installable = false; @@ -69,11 +69,11 @@ public void Validate(Metadata metadata) bool hasVersionFile = (path != null); - if (hasVref && !hasVersionFile) + if (hasAvcVref && !hasVersionFile) { Log.Warn("$vref present, version file missing"); } - else if (!hasVref && hasVersionFile && installable) + else if (!hasAvcVref && hasVersionFile && installable) { Log.WarnFormat("$vref absent, version file present: {0}", path); } diff --git a/Spec.md b/Spec.md index 4e15854afc..0e6dbfb012 100644 --- a/Spec.md +++ b/Spec.md @@ -293,8 +293,9 @@ three source directives: In addition a destination directive *must* be provided: - `install_to`: The target location where the matched file or directory should be installed. - - Valid values for this entry are `GameData`, `Missions`(**v1.25**), `Ships`, `Ships/SPH`(**v1.12**), `Ships/VAB`(**v1.12**), `Ships/@thumbs/VAB`(**v1.16**), `Ships/@thumbs/SPH`(**v1.16**), `Ships/Script`(**v1.29**), `Tutorial`, `Scenarios` (**v1.14**) + - Valid values for this entry for KSP1 mods are `GameData`, `Missions`(**v1.25**), `Ships`, `Ships/SPH`(**v1.12**), `Ships/VAB`(**v1.12**), `Ships/@thumbs/VAB`(**v1.16**), `Ships/@thumbs/SPH`(**v1.16**), `Ships/Script`(**v1.29**), `Tutorial`, `Scenarios` (**v1.14**), and `GameRoot` (which should be used sparingly, if at all). + - Valid values for this entry for KSP2 mods are `GameRoot`, `BepInEx/plugins` (**v1.32**), and `SpaceWarp/Mods` (**v1.32**) - A path to a given subfolder location can be specified *only* under `GameData` (**v1.2**); for example: `GameData/MyMod/Plugins`. The client *must* check this path and abort the install if any attempts to traverse up directories are found (eg: `GameData/../Example`). @@ -985,6 +986,22 @@ If (and only if) no mod version number has been identified (eg a `#/ckan/http/:u - `version` +###### `#/ckan/space-warp[[/path]/swinfo.json]` + +If present, a `$vref` symbol of `#/ckan/space-warp` states that version +information should be retrieved from an embedded SpaceWarp `swinfo.json` file in the +file downloaded by the `download` field. + +When used, the following fields are auto-generated: + +- `ksp_version` +- `ksp_version_min` +- `ksp_version_max` +- `name` +- `author` +- `abstract` +- `version` + ##### `x_netkan_epoch` The `x_netkan_epoch` field is used to specify a minimum `epoch` number manually in the `version` field. Its value should be diff --git a/Tests/NetKAN/Services/ModuleServiceTests.cs b/Tests/NetKAN/Services/ModuleServiceTests.cs index 0264b49f35..fbea93058e 100644 --- a/Tests/NetKAN/Services/ModuleServiceTests.cs +++ b/Tests/NetKAN/Services/ModuleServiceTests.cs @@ -21,7 +21,7 @@ public void HasInstallableFilesReturnsFalseWhenNoInstallableFiles() var json = JObject.Parse(TestData.DogeCoinFlag_101()); json["install"][0]["file"] = "DOES_NOT_EXIST"; - var sut = new ModuleService(); + var sut = new ModuleService(new KerbalSpaceProgram()); // Act var result = sut.HasInstallableFiles(CkanModule.FromJson(json.ToString()), zip); @@ -39,7 +39,7 @@ public void HasInstallableFilesReturnsTrueWhenInstallableFiles() var zip = TestData.DogeCoinFlagZip(); var json = JObject.Parse(TestData.DogeCoinFlag_101()); - var sut = new ModuleService(); + var sut = new ModuleService(new KerbalSpaceProgram()); // Act var result = sut.HasInstallableFiles(CkanModule.FromJson(json.ToString()), zip); @@ -54,7 +54,7 @@ public void HasInstallableFilesReturnsTrueWhenInstallableFiles() public void GetsInternalCkanCorrectly() { // Arrange - var sut = new ModuleService(); + var sut = new ModuleService(new KerbalSpaceProgram()); CkanModule mod = CkanModule.FromJson(TestData.DogeCoinFlag_101()); GameInstance inst = new GameInstance(new KerbalSpaceProgram(), "/", "dummy", new NullUser()); @@ -80,7 +80,7 @@ public void GetsInternalAvcCorrectly() json["version"] = "1.0.0"; json["download"] = "https://awesomemod.example/AwesomeMod.zip"; - var sut = new ModuleService(); + var sut = new ModuleService(new KerbalSpaceProgram()); // Act var result = sut.GetInternalAvc(CkanModule.FromJson(json.ToString()), TestData.DogeCoinFlagAvcZip()); diff --git a/Tests/NetKAN/Transformers/InstallSizeTransformerTests.cs b/Tests/NetKAN/Transformers/InstallSizeTransformerTests.cs index 8896b322aa..a41fe4d686 100644 --- a/Tests/NetKAN/Transformers/InstallSizeTransformerTests.cs +++ b/Tests/NetKAN/Transformers/InstallSizeTransformerTests.cs @@ -7,6 +7,7 @@ using CKAN.NetKAN.Model; using CKAN.NetKAN.Services; using CKAN.NetKAN.Transformers; +using CKAN.Games; using Tests.Data; namespace Tests.NetKAN.Transformers @@ -24,9 +25,9 @@ public void Transform_NormalModule_CorrectInstallSize() mHttp.Setup(i => i.DownloadModule(It.IsAny())) .Returns(TestData.DogeCoinFlagZip()); - var modSvc = new ModuleService(); + var modSvc = new ModuleService(new KerbalSpaceProgram()); - ITransformer sut = new InstallSizeTransformer(mHttp.Object, modSvc); + ITransformer sut = new InstallSizeTransformer(mHttp.Object, modSvc, new KerbalSpaceProgram()); // Act var result = sut.Transform(new Metadata(json), opts).First(); diff --git a/Tests/NetKAN/Transformers/InternalCkanTransformerTests.cs b/Tests/NetKAN/Transformers/InternalCkanTransformerTests.cs index b0b68d283a..ce117bdb67 100644 --- a/Tests/NetKAN/Transformers/InternalCkanTransformerTests.cs +++ b/Tests/NetKAN/Transformers/InternalCkanTransformerTests.cs @@ -10,6 +10,7 @@ using CKAN.NetKAN.Model; using CKAN.NetKAN.Services; using CKAN.NetKAN.Transformers; +using CKAN.Games; namespace Tests.NetKAN.Transformers { @@ -39,7 +40,7 @@ public void AddsMissingProperties() It.IsAny())) .Returns(internalCkan); - var sut = new InternalCkanTransformer(mHttp.Object, mModuleService.Object); + var sut = new InternalCkanTransformer(mHttp.Object, mModuleService.Object, new KerbalSpaceProgram()); var json = new JObject(); json["spec_version"] = 1; @@ -78,7 +79,7 @@ public void DoesNotOverrideExistingProperties() It.IsAny())) .Returns(internalCkan); - var sut = new InternalCkanTransformer(mHttp.Object, mModuleService.Object); + var sut = new InternalCkanTransformer(mHttp.Object, mModuleService.Object, new KerbalSpaceProgram()); var json = new JObject(); json["spec_version"] = 1; @@ -120,7 +121,7 @@ public void HigherOfTwoSpecVersionsIsChosen( It.IsAny())) .Returns(internalCkan); - var sut = new InternalCkanTransformer(mHttp.Object, mModuleService.Object); + var sut = new InternalCkanTransformer(mHttp.Object, mModuleService.Object, new KerbalSpaceProgram()); var json = new JObject(); json["spec_version"] = specVersion; diff --git a/Tests/NetKAN/Validators/CkanValidatorTests.cs b/Tests/NetKAN/Validators/CkanValidatorTests.cs index 5a27a64565..2c1ef3aadb 100644 --- a/Tests/NetKAN/Validators/CkanValidatorTests.cs +++ b/Tests/NetKAN/Validators/CkanValidatorTests.cs @@ -1,10 +1,12 @@ -using CKAN; +using Moq; +using Newtonsoft.Json.Linq; +using NUnit.Framework; + +using CKAN; using CKAN.NetKAN.Model; using CKAN.NetKAN.Services; using CKAN.NetKAN.Validators; -using Moq; -using Newtonsoft.Json.Linq; -using NUnit.Framework; +using CKAN.Games; namespace Tests.NetKAN.Validators { @@ -37,7 +39,7 @@ public void DoesNotThrowOnValidCkan() mModuleService.Setup(i => i.HasInstallableFiles(It.IsAny(), It.IsAny())) .Returns(true); - var sut = new CkanValidator(mHttp.Object, mModuleService.Object); + var sut = new CkanValidator(mHttp.Object, mModuleService.Object, new KerbalSpaceProgram()); var json = (JObject)ValidCkan.DeepClone(); // Act @@ -62,7 +64,7 @@ public void DoesThrowWhenMissingProperty(string propertyName) mModuleService.Setup(i => i.HasInstallableFiles(It.IsAny(), It.IsAny())) .Returns(true); - var sut = new CkanValidator(mHttp.Object, mModuleService.Object); + var sut = new CkanValidator(mHttp.Object, mModuleService.Object, new KerbalSpaceProgram()); var json = (JObject)ValidCkan.DeepClone(); json.Remove(propertyName); @@ -85,7 +87,7 @@ public void DoesThrowWhenIdentifiersDoNotMatch() mModuleService.Setup(i => i.HasInstallableFiles(It.IsAny(), It.IsAny())) .Returns(true); - var sut = new CkanValidator(mHttp.Object, mModuleService.Object); + var sut = new CkanValidator(mHttp.Object, mModuleService.Object, new KerbalSpaceProgram()); var json = new JObject(); json["spec_version"] = 1; json["identifier"] = "AmazingMod"; @@ -113,7 +115,7 @@ public void DoesThrowWhenNoInstallableFiles() netkan["spec_version"] = 1; netkan["identifier"] = "AwesomeMod"; - var sut = new CkanValidator(mHttp.Object, mModuleService.Object); + var sut = new CkanValidator(mHttp.Object, mModuleService.Object, new KerbalSpaceProgram()); var json = (JObject)ValidCkan.DeepClone(); // Act diff --git a/Tests/NetKAN/Validators/InstallsFilesValidatorTests.cs b/Tests/NetKAN/Validators/InstallsFilesValidatorTests.cs index 320f190d79..93a4680400 100644 --- a/Tests/NetKAN/Validators/InstallsFilesValidatorTests.cs +++ b/Tests/NetKAN/Validators/InstallsFilesValidatorTests.cs @@ -1,10 +1,12 @@ -using CKAN; +using Moq; +using Newtonsoft.Json.Linq; +using NUnit.Framework; + +using CKAN; using CKAN.NetKAN.Model; using CKAN.NetKAN.Services; using CKAN.NetKAN.Validators; -using Moq; -using Newtonsoft.Json.Linq; -using NUnit.Framework; +using CKAN.Games; namespace Tests.NetKAN.Validators { @@ -27,7 +29,7 @@ public void DoesNotThrowWhenInstallableFiles() json["version"] = "1.0.0"; json["download"] = "https://www.awesome-mod.example/AwesomeMod.zip"; - var sut = new InstallsFilesValidator(mHttp.Object, mModuleService.Object); + var sut = new InstallsFilesValidator(mHttp.Object, mModuleService.Object, new KerbalSpaceProgram()); // Act TestDelegate act = () => sut.Validate(new Metadata(json)); @@ -54,7 +56,7 @@ public void DoesThrowWhenNoInstallableFiles() json["version"] = "1.0.0"; json["download"] = "https://www.awesome-mod.example/AwesomeMod.zip"; - var sut = new InstallsFilesValidator(mHttp.Object, mModuleService.Object); + var sut = new InstallsFilesValidator(mHttp.Object, mModuleService.Object, new KerbalSpaceProgram()); // Act TestDelegate act = () => sut.Validate(new Metadata(json)); diff --git a/Tests/NetKAN/Validators/MatchesKnownGameVersionsValidatorTests.cs b/Tests/NetKAN/Validators/MatchesKnownGameVersionsValidatorTests.cs index fecd7f4c5c..8dac393ceb 100644 --- a/Tests/NetKAN/Validators/MatchesKnownGameVersionsValidatorTests.cs +++ b/Tests/NetKAN/Validators/MatchesKnownGameVersionsValidatorTests.cs @@ -1,7 +1,9 @@ using Newtonsoft.Json.Linq; using NUnit.Framework; + using CKAN.NetKAN.Model; using CKAN.NetKAN.Validators; +using CKAN.Games; namespace Tests.NetKAN.Validators { @@ -55,7 +57,7 @@ private void TryVersion(string ksp_version, string ksp_version_min, string ksp_v } // Act - var val = new MatchesKnownGameVersionsValidator(); + var val = new MatchesKnownGameVersionsValidator(new KerbalSpaceProgram()); val.Validate(new Metadata(json)); } }