summaryrefslogtreecommitdiff
path: root/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs')
-rw-r--r--src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs162
1 files changed, 162 insertions, 0 deletions
diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs
new file mode 100644
index 00000000..9d9054f1
--- /dev/null
+++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs
@@ -0,0 +1,162 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Web.Script.Serialization;
+using StardewModdingAPI.Common;
+
+namespace StardewModdingAPI.ModBuildConfig.Framework
+{
+ /// <summary>Manages the files that are part of a mod package.</summary>
+ internal class ModFileManager
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The name of the manifest file.</summary>
+ private readonly string ManifestFileName = "manifest.json";
+
+ /// <summary>The files that are part of the package.</summary>
+ private readonly IDictionary<string, FileInfo> Files;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="projectDir">The folder containing the project files.</param>
+ /// <param name="targetDir">The folder containing the build output.</param>
+ /// <exception cref="UserErrorException">The mod package isn't valid.</exception>
+ public ModFileManager(string projectDir, string targetDir)
+ {
+ this.Files = new Dictionary<string, FileInfo>(StringComparer.InvariantCultureIgnoreCase);
+
+ // validate paths
+ if (!Directory.Exists(projectDir))
+ throw new UserErrorException("Could not create mod package because the project folder wasn't found.");
+ if (!Directory.Exists(targetDir))
+ throw new UserErrorException("Could not create mod package because no build output was found.");
+
+ // project manifest
+ bool hasProjectManifest = false;
+ {
+ FileInfo manifest = new FileInfo(Path.Combine(projectDir, "manifest.json"));
+ if (manifest.Exists)
+ {
+ this.Files[this.ManifestFileName] = manifest;
+ hasProjectManifest = true;
+ }
+ }
+
+ // project i18n files
+ bool hasProjectTranslations = false;
+ DirectoryInfo translationsFolder = new DirectoryInfo(Path.Combine(projectDir, "i18n"));
+ if (translationsFolder.Exists)
+ {
+ foreach (FileInfo file in translationsFolder.EnumerateFiles())
+ this.Files[Path.Combine("i18n", file.Name)] = file;
+ hasProjectTranslations = true;
+ }
+
+ // build output
+ DirectoryInfo buildFolder = new DirectoryInfo(targetDir);
+ foreach (FileInfo file in buildFolder.EnumerateFiles("*", SearchOption.AllDirectories))
+ {
+ // get relative paths
+ string relativePath = file.FullName.Replace(buildFolder.FullName, "");
+ string relativeDirPath = file.Directory.FullName.Replace(buildFolder.FullName, "");
+
+ // prefer project manifest/i18n files
+ if (hasProjectManifest && relativePath.Equals(this.ManifestFileName, StringComparison.InvariantCultureIgnoreCase))
+ continue;
+ if (hasProjectTranslations && relativeDirPath.Equals("i18n", StringComparison.InvariantCultureIgnoreCase))
+ continue;
+
+ // ignore release zips
+ if (file.Extension.Equals("zip", StringComparison.InvariantCultureIgnoreCase))
+ continue;
+
+ // add file
+ this.Files[relativePath] = file;
+ }
+
+ // check for missing manifest
+ if (!this.Files.ContainsKey(this.ManifestFileName))
+ throw new UserErrorException($"Could not create mod package because no {this.ManifestFileName} was found in the project or build output.");
+
+ // check for missing DLL
+ // ReSharper disable once SimplifyLinqExpression
+ if (!this.Files.Any(p => !p.Key.EndsWith(".dll")))
+ throw new UserErrorException("Could not create mod package because no .dll file was found in the project or build output.");
+ }
+
+ /// <summary>Get the files in the mod package.</summary>
+ public IDictionary<string, FileInfo> GetFiles()
+ {
+ return new Dictionary<string, FileInfo>(this.Files, StringComparer.InvariantCultureIgnoreCase);
+ }
+
+ /// <summary>Get a semantic version from the mod manifest.</summary>
+ /// <exception cref="UserErrorException">The manifest is missing or invalid.</exception>
+ public string GetManifestVersion()
+ {
+ // get manifest file
+ if (!this.Files.TryGetValue(this.ManifestFileName, out FileInfo manifestFile))
+ throw new InvalidOperationException($"The mod does not have a {this.ManifestFileName} file."); // shouldn't happen since we validate in constructor
+
+ // read content
+ string json = File.ReadAllText(manifestFile.FullName);
+ if (string.IsNullOrWhiteSpace(json))
+ throw new UserErrorException("The mod's manifest must not be empty.");
+
+ // parse JSON
+ IDictionary<string, object> data;
+ try
+ {
+ data = this.Parse(json);
+ }
+ catch (Exception ex)
+ {
+ throw new UserErrorException($"The mod's manifest couldn't be parsed. It doesn't seem to be valid JSON.\n{ex}");
+ }
+
+ // get version field
+ object versionObj = data.ContainsKey("Version") ? data["Version"] : null;
+ if (versionObj == null)
+ throw new UserErrorException("The mod's manifest must have a version field.");
+
+ // get version string
+ if (versionObj is IDictionary<string, object> versionFields) // SMAPI 1.x
+ {
+ int major = versionFields.ContainsKey("MajorVersion") ? (int)versionFields["MajorVersion"] : 0;
+ int minor = versionFields.ContainsKey("MinorVersion") ? (int)versionFields["MinorVersion"] : 0;
+ int patch = versionFields.ContainsKey("PatchVersion") ? (int)versionFields["PatchVersion"] : 0;
+ string tag = versionFields.ContainsKey("Build") ? (string)versionFields["Build"] : null;
+ return new SemanticVersionImpl(major, minor, patch, tag).ToString();
+ }
+ return new SemanticVersionImpl(versionObj.ToString()).ToString(); // SMAPI 2.0+
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get a case-insensitive dictionary matching the given JSON.</summary>
+ /// <param name="json">The JSON to parse.</param>
+ private IDictionary<string, object> Parse(string json)
+ {
+ IDictionary<string, object> MakeCaseInsensitive(IDictionary<string, object> dict)
+ {
+ foreach (var field in dict.ToArray())
+ {
+ if (field.Value is IDictionary<string, object> value)
+ dict[field.Key] = MakeCaseInsensitive(value);
+ }
+ return new Dictionary<string, object>(dict, StringComparer.InvariantCultureIgnoreCase);
+ }
+
+ IDictionary<string, object> data = (IDictionary<string, object>)new JavaScriptSerializer().DeserializeObject(json);
+ return MakeCaseInsensitive(data);
+ }
+ }
+}