Skip to content

Commit 8bbbf08

Browse files
committed
Merge branch 'mod-package-token' into develop
2 parents 5877f64 + 1eaf637 commit 8bbbf08

10 files changed

+447
-274
lines changed

docs/technical/mod-package-release-notes.md

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
← [mod build config](./mod-build-config.md)
22

33
## Release notes
4-
# 4.3.2
4+
## 4.4.0
5+
Released 13 April 2025 for SMAPI 4.1.0 or later.
6+
7+
* Added `%ProjectVersion%` [token you can use in `manifest.json` files](mod-package.md#manifest-tokens).
8+
* Revamped [package documentation](mod-package.md).
9+
_This makes it easier to see the features at a glance, and collects the info for each feature into one place._
10+
11+
## 4.3.2
512
Released 09 November 2024 for SMAPI 4.1.0 or later.
613

714
* Fixed `IgnoreModFilePaths` and `IgnoreModFilePatterns` not working correctly in recent versions.

docs/technical/mod-package.md

+214-154
Large diffs are not rendered by default.

src/SMAPI.ModBuildConfig/DeployModTask.cs

+23-50
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,7 @@
77
using System.Text.RegularExpressions;
88
using Microsoft.Build.Framework;
99
using Microsoft.Build.Utilities;
10-
using Newtonsoft.Json;
1110
using StardewModdingAPI.ModBuildConfig.Framework;
12-
using StardewModdingAPI.Toolkit.Framework;
13-
using StardewModdingAPI.Toolkit.Serialization;
14-
using StardewModdingAPI.Toolkit.Serialization.Models;
1511
using StardewModdingAPI.Toolkit.Utilities;
1612

1713
namespace StardewModdingAPI.ModBuildConfig;
@@ -34,6 +30,10 @@ public class DeployModTask : Task
3430
[Required]
3531
public string ModZipPath { get; set; }
3632

33+
/// <summary>The version number for the project assembly.</summary>
34+
[Required]
35+
public string ProjectVersion { get; set; }
36+
3737
/// <summary>The folder containing the project files.</summary>
3838
[Required]
3939
public string ProjectDir { get; set; }
@@ -87,33 +87,12 @@ public override bool Execute()
8787
if (!this.EnableModDeploy && !this.EnableModZip)
8888
return true;
8989

90-
// validate the manifest file
91-
IManifest manifest;
90+
// read & validate manifest
91+
string manifestPath = Path.Combine(this.ProjectDir, BundleFile.ManifestFileName);
92+
if (!ManifestHelper.TryLoadManifest(manifestPath, this.ProjectVersion, out IManifest manifest, out string overrideManifestJson, out string error))
9293
{
93-
try
94-
{
95-
string manifestPath = Path.Combine(this.ProjectDir, "manifest.json");
96-
if (!new JsonHelper().ReadJsonFileIfExists(manifestPath, out Manifest rawManifest))
97-
{
98-
this.Log.LogError("[mod build package] The mod's manifest.json file doesn't exist.");
99-
return false;
100-
}
101-
manifest = rawManifest;
102-
}
103-
catch (JsonReaderException ex)
104-
{
105-
// log the inner exception, otherwise the message will be generic
106-
Exception exToShow = ex.InnerException ?? ex;
107-
this.Log.LogError($"[mod build package] The mod's manifest.json file isn't valid JSON: {exToShow.Message}");
108-
return false;
109-
}
110-
111-
// validate manifest fields
112-
if (!ManifestValidator.TryValidateFields(manifest, out string error))
113-
{
114-
this.Log.LogError($"[mod build package] The mod's manifest.json file is invalid: {error}");
115-
return false;
116-
}
94+
this.Log.LogError($"[mod build package] The mod's {BundleFile.ManifestFileName} is invalid: {error}");
95+
return false;
11796
}
11897

11998
// deploy files
@@ -128,7 +107,7 @@ public override bool Execute()
128107

129108
var modPackages = new Dictionary<string, IModFileManager>
130109
{
131-
[this.ModFolderName] = new MainModFileManager(this.ProjectDir, this.TargetDir, ignoreFilePaths, ignoreFilePatterns, bundleAssemblyTypes, this.ModDllName, validateRequiredModFiles: this.EnableModDeploy || this.EnableModZip)
110+
[this.ModFolderName] = new MainModFileManager(this.ProjectDir, this.TargetDir, ignoreFilePaths, ignoreFilePatterns, bundleAssemblyTypes, this.ModDllName, overrideManifestJson, validateRequiredModFiles: this.EnableModDeploy || this.EnableModZip)
132111
};
133112

134113
if (this.ContentPacks != null)
@@ -158,7 +137,7 @@ public override bool Execute()
158137
this.Log.LogMessage(MessageImportance.High, $"[mod build package] Bundling content pack: {folderName} v{version} at {contentPath}.");
159138
modPackages.Add(
160139
folderName,
161-
new ContentPackFileManager(this.ProjectDir, contentPath, version, ignoreFilePaths, ignoreFilePatterns, validateManifest)
140+
new ContentPackFileManager(this.ProjectDir, contentPath, this.ProjectVersion, version, ignoreFilePaths, ignoreFilePatterns, validateManifest)
162141
);
163142
}
164143
}
@@ -299,21 +278,17 @@ private IEnumerable<string> GetCustomIgnoreFilePaths(string pattern)
299278
/// <param name="outputPath">The folder path to create with the mod files.</param>
300279
private void CreateModFolder(IDictionary<string, IModFileManager> modPackages, string outputPath)
301280
{
302-
foreach (var mod in modPackages)
281+
foreach (var pair in modPackages)
303282
{
304-
string relativePath = modPackages.Count == 1
305-
? outputPath
306-
: Path.Combine(outputPath, this.EscapeInvalidFilenameCharacters(mod.Key));
307-
308-
foreach (var file in mod.Value.GetFiles())
309-
{
310-
string fromPath = file.Value.FullName;
311-
string toPath = Path.Combine(relativePath, file.Key);
283+
string folderName = pair.Key;
284+
IModFileManager fileManager = pair.Value;
312285

313-
Directory.CreateDirectory(Path.GetDirectoryName(toPath)!);
286+
string folderPath = modPackages.Count == 1
287+
? outputPath
288+
: Path.Combine(outputPath, this.EscapeInvalidFilenameCharacters(folderName));
314289

315-
File.Copy(fromPath, toPath, overwrite: true);
316-
}
290+
foreach (BundleFile from in fileManager.GetFiles())
291+
from.CopyToFolder(folderPath);
317292
}
318293
}
319294

@@ -330,21 +305,19 @@ private void CreateReleaseZip(IDictionary<string, IModFileManager> modPackages,
330305
foreach (var mod in modPackages)
331306
{
332307
string modFolder = this.EscapeInvalidFilenameCharacters(mod.Key);
333-
foreach (var file in mod.Value.GetFiles())
308+
foreach (BundleFile from in mod.Value.GetFiles())
334309
{
335-
string relativePath = file.Key;
310+
string relativePath = from.RelativePath;
311+
336312
if (relativePath.Contains('\\'))
337313
relativePath = string.Join("/", PathUtilities.GetSegments(relativePath)); // zip files use forward slashes regardless of OS
338314

339-
FileInfo fileInfo = file.Value;
340-
341315
string archivePath = modPackages.Count == 1
342316
? $"{modFolder}/{relativePath}"
343317
: $"{this.ModFolderName}/{modFolder}/{relativePath}";
344318

345-
using Stream fileStream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read);
346319
using Stream fileStreamInZip = archive.CreateEntry(archivePath).Open();
347-
fileStream.CopyTo(fileStreamInZip);
320+
from.CopyToStream(fileStreamInZip);
348321
}
349322
}
350323
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using System;
2+
using System.IO;
3+
using System.Text;
4+
5+
namespace StardewModdingAPI.ModBuildConfig.Framework;
6+
7+
/// <summary>A file that should be deployed or zipped as part of a mod.</summary>
8+
internal class BundleFile
9+
{
10+
/*********
11+
** Accessors
12+
*********/
13+
/// <summary>The name of the manifest file.</summary>
14+
public const string ManifestFileName = "manifest.json";
15+
16+
/// <summary>The file's relative path within the mod.</summary>
17+
public string RelativePath { get; }
18+
19+
/// <summary>The file to copy from.</summary>
20+
public FileInfo File { get; }
21+
22+
/// <summary>If set, deploy this content instead of copying the original file.</summary>
23+
public string OverrideContent { get; }
24+
25+
/*********
26+
** Public methods
27+
*********/
28+
/// <summary>Construct an instance.</summary>
29+
/// <param name="relativePath">The file's relative path within the mod.</param>
30+
/// <param name="file">The file to copy from.</param>
31+
/// <param name="overrideContent">If set, deploy this content instead of copying the original file.</param>
32+
public BundleFile(string relativePath, FileInfo file, string overrideContent = null)
33+
{
34+
this.RelativePath = relativePath;
35+
this.File = file;
36+
this.OverrideContent = overrideContent;
37+
}
38+
39+
/// <summary>Get whether this entry is for the mod's <samp>manifest.json</samp> file.</summary>
40+
public bool IsModManifest()
41+
{
42+
return BundleFile.IsModManifest(this.RelativePath);
43+
}
44+
45+
/// <summary>Copy the file into a destination folder.</summary>
46+
/// <param name="folderPath">The folder path to use as the base for the <see cref="RelativePath"/>.</param>
47+
public void CopyToFolder(string folderPath)
48+
{
49+
string toPath = Path.Combine(folderPath, this.RelativePath);
50+
51+
Directory.CreateDirectory(Path.GetDirectoryName(toPath)!);
52+
53+
if (this.OverrideContent != null)
54+
System.IO.File.WriteAllText(toPath, this.OverrideContent, Encoding.UTF8);
55+
else
56+
this.File.CopyTo(toPath, overwrite: true);
57+
}
58+
59+
/// <summary>Copy the file's contents into a stream.</summary>
60+
/// <param name="stream">The stream into which to write the file's contents.</param>
61+
public void CopyToStream(Stream stream)
62+
{
63+
using Stream fromStream = this.OverrideContent != null
64+
? new MemoryStream(Encoding.UTF8.GetBytes(this.OverrideContent))
65+
: this.File.OpenRead();
66+
67+
fromStream.CopyTo(stream);
68+
}
69+
70+
/// <summary>Get whether a relative path is for the mod's <samp>manifest.json</samp> file.</summary>
71+
/// <param name="relativePath">The relative path within the mod folder within the mod's folder.</param>
72+
public static bool IsModManifest(string relativePath)
73+
{
74+
return string.Equals(relativePath, BundleFile.ManifestFileName, StringComparison.OrdinalIgnoreCase);
75+
}
76+
}

src/SMAPI.ModBuildConfig/Framework/ContentPackFileManager.cs

+21-39
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,7 @@
33
using System.IO;
44
using System.Linq;
55
using System.Text.RegularExpressions;
6-
using Newtonsoft.Json;
76
using StardewModdingAPI.Toolkit;
8-
using StardewModdingAPI.Toolkit.Framework;
9-
using StardewModdingAPI.Toolkit.Serialization;
10-
using StardewModdingAPI.Toolkit.Serialization.Models;
117
using StardewModdingAPI.Toolkit.Utilities;
128

139
namespace StardewModdingAPI.ModBuildConfig.Framework;
@@ -18,11 +14,8 @@ internal class ContentPackFileManager : IModFileManager
1814
/*********
1915
** Fields
2016
*********/
21-
/// <summary>The name of the manifest file.</summary>
22-
private readonly string ManifestFileName = "manifest.json";
23-
2417
/// <summary>The files that are part of the package.</summary>
25-
private readonly Dictionary<string, FileInfo> Files = new(StringComparer.OrdinalIgnoreCase);
18+
private readonly List<BundleFile> Files = [];
2619

2720

2821
/*********
@@ -31,58 +24,47 @@ internal class ContentPackFileManager : IModFileManager
3124
/// <summary>Construct an instance.</summary>
3225
/// <param name="projectDir">The folder containing the project files.</param>
3326
/// <param name="contentPackDir">The absolute or relative path to the content pack folder.</param>
27+
/// <param name="projectVersion">The version number for the project assembly.</param>
3428
/// <param name="version">The mod version.</param>
3529
/// <param name="ignoreFilePaths">The custom relative file paths provided by the user to ignore.</param>
3630
/// <param name="ignoreFilePatterns">Custom regex patterns matching files to ignore when deploying or zipping the mod.</param>
3731
/// <param name="validateManifest">Whether to validate that the content pack's manifest is valid.</param>
3832
/// <exception cref="UserErrorException">The mod package isn't valid.</exception>
39-
public ContentPackFileManager(string projectDir, string contentPackDir, string version, string[] ignoreFilePaths, Regex[] ignoreFilePatterns, bool validateManifest)
33+
public ContentPackFileManager(string projectDir, string contentPackDir, string projectVersion, string version, string[] ignoreFilePaths, Regex[] ignoreFilePatterns, bool validateManifest)
4034
{
4135
// get folders
4236
DirectoryInfo projectDirInfo = new(Path.Combine(projectDir, contentPackDir));
4337
if (!projectDirInfo.Exists)
4438
throw GetError($"that folder doesn't exist at {projectDirInfo.FullName}");
4539

40+
// load manifest
41+
string manifestPath = Path.Combine(contentPackDir, BundleFile.ManifestFileName);
42+
if (!ManifestHelper.TryLoadManifest(manifestPath, projectVersion, out IManifest manifest, out string overrideManifestJson, out string error))
43+
throw GetError($"its {BundleFile.ManifestFileName} file is invalid: {error}");
44+
4645
// collect files
47-
foreach (FileInfo entry in projectDirInfo.GetFiles("*", SearchOption.AllDirectories))
46+
foreach (FileInfo file in projectDirInfo.GetFiles("*", SearchOption.AllDirectories))
4847
{
49-
string relativePath = PathUtilities.GetRelativePath(projectDirInfo.FullName, entry.FullName);
50-
FileInfo file = entry;
48+
string relativePath = PathUtilities.GetRelativePath(projectDirInfo.FullName, file.FullName);
49+
50+
if (this.ShouldIgnore(file, relativePath, ignoreFilePaths, ignoreFilePatterns))
51+
continue;
5152

52-
if (!this.ShouldIgnore(file, relativePath, ignoreFilePaths, ignoreFilePatterns))
53-
this.Files[relativePath] = file;
53+
this.Files.Add(BundleFile.IsModManifest(relativePath)
54+
? new BundleFile(relativePath, file, overrideManifestJson)
55+
: new BundleFile(relativePath, file)
56+
);
5457
}
5558

56-
// validate manifest
59+
// validate manifest version
5760
if (validateManifest)
5861
{
59-
// get manifest file
60-
if (!this.Files.TryGetValue(this.ManifestFileName, out FileInfo manifestFile))
61-
throw GetError($"it has no {this.ManifestFileName} file");
62-
63-
// parse file
64-
Manifest manifest;
65-
try
66-
{
67-
new JsonHelper().ReadJsonFileIfExists(manifestFile.FullName, out Manifest rawManifest);
68-
manifest = rawManifest;
69-
}
70-
catch (JsonReaderException ex)
71-
{
72-
throw GetError($"its {this.ManifestFileName} file isn't valid JSON: {ex.InnerException?.Message ?? ex.Message}");
73-
}
74-
75-
// validate manifest fields
76-
if (!ManifestValidator.TryValidateFields(manifest, out string error))
77-
throw GetError($"its {this.ManifestFileName} file is invalid: {error}");
78-
79-
// validate version
8062
if (version == null)
8163
throw GetError("no Version value was provided");
8264
if (!SemanticVersion.TryParse(version, out ISemanticVersion requiredVersion))
8365
throw GetError($"the provided Version value '{version}' isn't a valid semantic version");
8466
if (manifest.Version.CompareTo(requiredVersion) != 0)
85-
throw GetError($"its {this.ManifestFileName} has version '{manifest.Version}' instead of the required '{requiredVersion}'");
67+
throw GetError($"its {BundleFile.ManifestFileName} has version '{manifest.Version}' instead of the required '{requiredVersion}'");
8668
}
8769

8870
UserErrorException GetError(string reasonPhrase)
@@ -92,9 +74,9 @@ UserErrorException GetError(string reasonPhrase)
9274
}
9375

9476
///<inheritdoc/>
95-
public IDictionary<string, FileInfo> GetFiles()
77+
public IEnumerable<BundleFile> GetFiles()
9678
{
97-
return new Dictionary<string, FileInfo>(this.Files, StringComparer.OrdinalIgnoreCase);
79+
return this.Files;
9880
}
9981

10082
/// <summary>Get whether a content file should be ignored.</summary>
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
using System.Collections.Generic;
2-
using System.IO;
32

43
namespace StardewModdingAPI.ModBuildConfig.Framework;
54

65
/// <summary>Manages the files that are part of a mod in the release package.</summary>
7-
public interface IModFileManager
6+
internal interface IModFileManager
87
{
98
/// <summary>Get the files in the mod package.</summary>
10-
public IDictionary<string, FileInfo> GetFiles();
9+
public IEnumerable<BundleFile> GetFiles();
1110
}

0 commit comments

Comments
 (0)