summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/SMAPI.Installer/InteractiveInstaller.cs2
-rw-r--r--src/SMAPI.Internal/ExceptionExtensions.cs27
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs3
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs3
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/manifest.json4
-rw-r--r--src/SMAPI.Mods.ErrorHandler/manifest.json4
-rw-r--r--src/SMAPI.Mods.SaveBackup/manifest.json4
-rw-r--r--src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs6
-rw-r--r--src/SMAPI.Toolkit/SemanticVersionComparer.cs32
-rw-r--r--src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs7
-rw-r--r--src/SMAPI.Web/Controllers/ModsApiController.cs6
-rw-r--r--src/SMAPI.Web/Framework/ModSiteManager.cs73
-rw-r--r--src/SMAPI.Web/SMAPI.Web.csproj2
-rw-r--r--src/SMAPI.Web/Views/Mods/Index.cshtml2
-rw-r--r--src/SMAPI/Constants.cs29
-rw-r--r--src/SMAPI/Framework/Logging/LogManager.cs7
-rw-r--r--src/SMAPI/Framework/ModHelpers/DataHelper.cs6
-rw-r--r--src/SMAPI/Framework/ModHelpers/TranslationHelper.cs6
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs16
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyLoader.cs38
-rw-r--r--src/SMAPI/Framework/ModLoading/Symbols/SymbolReader.cs72
-rw-r--r--src/SMAPI/Framework/ModLoading/Symbols/SymbolReaderProvider.cs54
-rw-r--r--src/SMAPI/Framework/ModLoading/Symbols/SymbolWriterProvider.cs40
-rw-r--r--src/SMAPI/Framework/SCore.cs15
-rw-r--r--src/SMAPI/Framework/Translator.cs65
-rw-r--r--src/SMAPI/IDataHelper.cs6
-rw-r--r--src/SMAPI/ITranslationHelper.cs7
27 files changed, 445 insertions, 91 deletions
diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs
index ab07c864..b91d0dd3 100644
--- a/src/SMAPI.Installer/InteractiveInstaller.cs
+++ b/src/SMAPI.Installer/InteractiveInstaller.cs
@@ -272,7 +272,6 @@ namespace StardewModdingApi.Installer
DirectoryInfo bundleDir = new DirectoryInfo(this.BundlePath);
paths = new InstallerPaths(bundleDir, installDir, context.ExecutableName);
}
- Console.Clear();
/*********
@@ -309,6 +308,7 @@ namespace StardewModdingApi.Installer
return;
}
}
+ Console.Clear();
/*********
diff --git a/src/SMAPI.Internal/ExceptionExtensions.cs b/src/SMAPI.Internal/ExceptionExtensions.cs
index 5f1ee1fa..d8189048 100644
--- a/src/SMAPI.Internal/ExceptionExtensions.cs
+++ b/src/SMAPI.Internal/ExceptionExtensions.cs
@@ -13,19 +13,26 @@ namespace StardewModdingAPI.Internal
/// <param name="exception">The error to summarize.</param>
public static string GetLogSummary(this Exception exception)
{
- switch (exception)
+ try
{
- case TypeLoadException ex:
- return $"Failed loading type '{ex.TypeName}': {exception}";
+ switch (exception)
+ {
+ case TypeLoadException ex:
+ return $"Failed loading type '{ex.TypeName}': {exception}";
- case ReflectionTypeLoadException ex:
- string summary = ex.ToString();
- foreach (Exception childEx in ex.LoaderExceptions ?? new Exception[0])
- summary += $"\n\n{childEx?.GetLogSummary()}";
- return summary;
+ case ReflectionTypeLoadException ex:
+ string summary = ex.ToString();
+ foreach (Exception childEx in ex.LoaderExceptions ?? new Exception[0])
+ summary += $"\n\n{childEx?.GetLogSummary()}";
+ return summary;
- default:
- return exception.ToString();
+ default:
+ return exception?.ToString() ?? $"<null exception>\n{Environment.StackTrace}";
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException($"Failed handling {exception?.GetType().FullName} (original message: {exception?.Message})", ex);
}
}
diff --git a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs
index a9b981bd..1efc1616 100644
--- a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs
+++ b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs
@@ -174,6 +174,9 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
/// <param name="context">The analysis context.</param>
public override void Initialize(AnalysisContext context)
{
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
+ context.EnableConcurrentExecution();
+
context.RegisterSyntaxNodeAction(
this.AnalyzeMemberAccess,
SyntaxKind.SimpleMemberAccessExpression,
diff --git a/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs
index d071f0c1..722d5227 100644
--- a/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs
+++ b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs
@@ -56,6 +56,9 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
/// <param name="context">The analysis context.</param>
public override void Initialize(AnalysisContext context)
{
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
+ context.EnableConcurrentExecution();
+
context.RegisterSyntaxNodeAction(
this.AnalyzeObsoleteFields,
SyntaxKind.SimpleMemberAccessExpression,
diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json
index 6b49cd5f..e04d3497 100644
--- a/src/SMAPI.Mods.ConsoleCommands/manifest.json
+++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Console Commands",
"Author": "SMAPI",
- "Version": "3.12.2",
+ "Version": "3.12.3",
"Description": "Adds SMAPI console commands that let you manipulate the game.",
"UniqueID": "SMAPI.ConsoleCommands",
"EntryDll": "ConsoleCommands.dll",
- "MinimumApiVersion": "3.12.2"
+ "MinimumApiVersion": "3.12.3"
}
diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json
index 7759d1d5..54758a36 100644
--- a/src/SMAPI.Mods.ErrorHandler/manifest.json
+++ b/src/SMAPI.Mods.ErrorHandler/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Error Handler",
"Author": "SMAPI",
- "Version": "3.12.2",
+ "Version": "3.12.3",
"Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.",
"UniqueID": "SMAPI.ErrorHandler",
"EntryDll": "ErrorHandler.dll",
- "MinimumApiVersion": "3.12.2"
+ "MinimumApiVersion": "3.12.3"
}
diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json
index 1e5b8b97..f23f0958 100644
--- a/src/SMAPI.Mods.SaveBackup/manifest.json
+++ b/src/SMAPI.Mods.SaveBackup/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Save Backup",
"Author": "SMAPI",
- "Version": "3.12.2",
+ "Version": "3.12.3",
"Description": "Automatically backs up all your saves once per day into its folder.",
"UniqueID": "SMAPI.SaveBackup",
"EntryDll": "SaveBackup.dll",
- "MinimumApiVersion": "3.12.2"
+ "MinimumApiVersion": "3.12.3"
}
diff --git a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs
index be0c18ce..2636aae0 100644
--- a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs
+++ b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs
@@ -90,10 +90,10 @@ namespace StardewModdingAPI.Toolkit.Framework
}
/// <summary>Get whether an executable is 64-bit.</summary>
- /// <param name="executablePath">The absolute path to the executable file.</param>
- public static bool Is64BitAssembly(string executablePath)
+ /// <param name="path">The absolute path to the assembly file.</param>
+ public static bool Is64BitAssembly(string path)
{
- return AssemblyName.GetAssemblyName(executablePath).ProcessorArchitecture != ProcessorArchitecture.X86;
+ return AssemblyName.GetAssemblyName(path).ProcessorArchitecture != ProcessorArchitecture.X86;
}
diff --git a/src/SMAPI.Toolkit/SemanticVersionComparer.cs b/src/SMAPI.Toolkit/SemanticVersionComparer.cs
new file mode 100644
index 00000000..9f6b57a2
--- /dev/null
+++ b/src/SMAPI.Toolkit/SemanticVersionComparer.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+
+namespace StardewModdingAPI.Toolkit
+{
+ /// <summary>A comparer for semantic versions based on the <see cref="SemanticVersion.CompareTo(ISemanticVersion)"/> field.</summary>
+ public class SemanticVersionComparer : IComparer<ISemanticVersion>
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>A singleton instance of the comparer.</summary>
+ public static SemanticVersionComparer Instance { get; } = new SemanticVersionComparer();
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <inheritdoc />
+ public int Compare(ISemanticVersion x, ISemanticVersion y)
+ {
+ if (object.ReferenceEquals(x, y))
+ return 0;
+
+ if (x is null)
+ return -1;
+ if (y is null)
+ return 1;
+
+ return x.CompareTo(y);
+ }
+ }
+}
diff --git a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs
index 62bd13cd..6de79a85 100644
--- a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs
+++ b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs
@@ -46,5 +46,12 @@ namespace StardewModdingAPI.Toolkit.Utilities
{
return LowLevelEnvironmentUtility.GetExecutableName(platform.ToString());
}
+
+ /// <summary>Get whether an executable is 64-bit.</summary>
+ /// <param name="path">The absolute path to the assembly file.</param>
+ public static bool Is64BitAssembly(string path)
+ {
+ return LowLevelEnvironmentUtility.Is64BitAssembly(path);
+ }
}
}
diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs
index 1956bf29..c6e9a713 100644
--- a/src/SMAPI.Web/Controllers/ModsApiController.cs
+++ b/src/SMAPI.Web/Controllers/ModsApiController.cs
@@ -123,6 +123,10 @@ namespace StardewModdingAPI.Web.Controllers
ModOverrideConfig overrides = this.Config.Value.ModOverrides.FirstOrDefault(p => p.ID.Equals(search.ID?.Trim(), StringComparison.OrdinalIgnoreCase));
bool allowNonStandardVersions = overrides?.AllowNonStandardVersions ?? false;
+ // SMAPI versions with a '-beta' tag indicate major changes that may need beta mod versions.
+ // This doesn't apply to normal prerelease versions which have an '-alpha' tag.
+ bool isSmapiBeta = apiVersion.IsPrerelease() && apiVersion.PrereleaseTag.StartsWith("beta");
+
// get latest versions
ModEntryModel result = new ModEntryModel { ID = search.ID };
IList<string> errors = new List<string>();
@@ -198,7 +202,7 @@ namespace StardewModdingAPI.Web.Controllers
List<ModEntryVersionModel> updates = new List<ModEntryVersionModel>();
if (this.IsRecommendedUpdate(installedVersion, main?.Version, useBetaChannel: true))
updates.Add(main);
- if (this.IsRecommendedUpdate(installedVersion, optional?.Version, useBetaChannel: installedVersion.IsPrerelease() || search.IsBroken))
+ if (this.IsRecommendedUpdate(installedVersion, optional?.Version, useBetaChannel: isSmapiBeta || installedVersion.IsPrerelease() || search.IsBroken))
updates.Add(optional);
if (this.IsRecommendedUpdate(installedVersion, unofficial?.Version, useBetaChannel: true))
updates.Add(unofficial);
diff --git a/src/SMAPI.Web/Framework/ModSiteManager.cs b/src/SMAPI.Web/Framework/ModSiteManager.cs
index 68b4c6ac..8f21d2f5 100644
--- a/src/SMAPI.Web/Framework/ModSiteManager.cs
+++ b/src/SMAPI.Web/Framework/ModSiteManager.cs
@@ -110,41 +110,70 @@ namespace StardewModdingAPI.Web.Framework
main = null;
preview = null;
- ISemanticVersion ParseVersion(string raw)
+ // parse all versions from the mod page
+ IEnumerable<(string name, string description, ISemanticVersion version)> GetAllVersions()
{
- raw = this.NormalizeVersion(raw);
- return this.GetMappedVersion(raw, mapRemoteVersions, allowNonStandardVersions);
+ if (mod != null)
+ {
+ ISemanticVersion ParseAndMapVersion(string raw)
+ {
+ raw = this.NormalizeVersion(raw);
+ return this.GetMappedVersion(raw, mapRemoteVersions, allowNonStandardVersions);
+ }
+
+ // get mod version
+ ISemanticVersion modVersion = ParseAndMapVersion(mod.Version);
+ if (modVersion != null)
+ yield return (name: null, description: null, version: ParseAndMapVersion(mod.Version));
+
+ // get file versions
+ foreach (IModDownload download in mod.Downloads)
+ {
+ ISemanticVersion cur = ParseAndMapVersion(download.Version);
+ if (cur != null)
+ yield return (download.Name, download.Description, cur);
+ }
+ }
}
+ var versions = GetAllVersions()
+ .OrderByDescending(p => p.version, SemanticVersionComparer.Instance)
+ .ToArray();
- if (mod != null)
+ // get main + preview versions
+ void TryGetVersions(out ISemanticVersion mainVersion, out ISemanticVersion previewVersion, Func<(string name, string description, ISemanticVersion version), bool> filter = null)
{
- // get mod version
- if (subkey == null)
- main = ParseVersion(mod.Version);
+ mainVersion = null;
+ previewVersion = null;
- // get file versions
- foreach (IModDownload download in mod.Downloads)
+ // get latest main + preview version
+ foreach (var entry in versions)
{
- // check for subkey if specified
- if (subkey != null && download.Name?.Contains(subkey, StringComparison.OrdinalIgnoreCase) != true && download.Description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) != true)
+ if (filter?.Invoke(entry) == false)
continue;
- // parse version
- ISemanticVersion cur = ParseVersion(download.Version);
- if (cur == null)
- continue;
+ if (entry.version.IsPrerelease())
+ previewVersion ??= entry.version;
+ else
+ mainVersion ??= entry.version;
- // track highest versions
- if (main == null || cur.IsNewerThan(main))
- main = cur;
- if (cur.IsPrerelease() && (preview == null || cur.IsNewerThan(preview)))
- preview = cur;
+ if (mainVersion != null)
+ break; // any other values will be older
}
- if (preview != null && !preview.IsNewerThan(main))
- preview = null;
+ // normalize values
+ if (previewVersion is not null)
+ {
+ mainVersion ??= previewVersion; // if every version is prerelease, latest one is the main version
+ if (!previewVersion.IsNewerThan(mainVersion))
+ previewVersion = null;
+ }
}
+ if (subkey is not null)
+ TryGetVersions(out main, out preview, entry => entry.name?.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true || entry.description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true);
+ if (main is null)
+ TryGetVersions(out main, out preview);
+
return main != null;
}
diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj
index e9d209ec..4c8465a6 100644
--- a/src/SMAPI.Web/SMAPI.Web.csproj
+++ b/src/SMAPI.Web/SMAPI.Web.csproj
@@ -2,7 +2,7 @@
<PropertyGroup>
<AssemblyName>SMAPI.Web</AssemblyName>
<RootNamespace>StardewModdingAPI.Web</RootNamespace>
- <TargetFramework>netcoreapp3.1</TargetFramework>
+ <TargetFramework>net5.0</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>
diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml
index fa77c220..7dcd0718 100644
--- a/src/SMAPI.Web/Views/Mods/Index.cshtml
+++ b/src/SMAPI.Web/Views/Mods/Index.cshtml
@@ -9,7 +9,7 @@
TimeSpan staleAge = DateTimeOffset.UtcNow - Model.LastUpdated;
bool hasBeta = Model.BetaVersion != null;
- string betaLabel = "SDV @Model.BetaVersion only";
+ string betaLabel = $"SDV {Model.BetaVersion} only";
}
@section Head {
<link rel="stylesheet" href="~/Content/css/mods.css?r=20200218" />
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index 6cbdeb8e..cce267ba 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
+using Mono.Cecil;
using StardewModdingAPI.Enums;
using StardewModdingAPI.Framework;
using StardewModdingAPI.Framework.ModLoading;
@@ -61,7 +62,7 @@ namespace StardewModdingAPI
internal static int? LogScreenId { get; set; }
/// <summary>SMAPI's current raw semantic version.</summary>
- internal static string RawApiVersion = "3.12.2";
+ internal static string RawApiVersion = "3.12.3";
}
/// <summary>Contains SMAPI's constants and assumptions.</summary>
@@ -229,6 +230,32 @@ namespace StardewModdingAPI
}
}
+ /// <summary>Configure the Mono.Cecil assembly resolver.</summary>
+ /// <param name="resolver">The assembly resolver.</param>
+ internal static void ConfigureAssemblyResolver(AssemblyDefinitionResolver resolver)
+ {
+ // add search paths
+ resolver.AddSearchDirectory(Constants.ExecutionPath);
+ resolver.AddSearchDirectory(Constants.InternalFilesPath);
+
+ // add SMAPI explicitly
+ // Normally this would be handled automatically by the search paths, but for some reason there's a specific
+ // case involving unofficial 64-bit Stardew Valley when launched through Steam (for some players only)
+ // where Mono.Cecil can't resolve references to SMAPI.
+ resolver.Add(AssemblyDefinition.ReadAssembly(typeof(SGame).Assembly.Location));
+
+ // make sure game assembly names can be resolved
+ // The game assembly can have one of three names depending how the mod was compiled:
+ // - 'StardewValley': assembly name on Linux/macOS;
+ // - 'Stardew Valley': assembly name on Windows;
+ // - 'Netcode': an assembly that's separate on Windows only.
+ resolver.Add(AssemblyDefinition.ReadAssembly(typeof(Game1).Assembly.Location), "StardewValley", "Stardew Valley"
+#if !SMAPI_FOR_WINDOWS
+ , "Netcode"
+#endif
+ );
+ }
+
/// <summary>Get metadata for mapping assemblies to the current platform.</summary>
/// <param name="targetPlatform">The target game platform.</param>
/// <param name="framework">The game framework running the game.</param>
diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs
index a3d4f23d..c6faa90d 100644
--- a/src/SMAPI/Framework/Logging/LogManager.cs
+++ b/src/SMAPI/Framework/Logging/LogManager.cs
@@ -109,9 +109,12 @@ namespace StardewModdingAPI.Framework.Logging
output.OnMessageIntercepted += message => this.HandleConsoleMessage(this.MonitorForGame, message);
Console.SetOut(output);
- // enable Unicode handling
+ // enable Unicode handling on Windows
+ // (the terminal defaults to UTF-8 on Linux/macOS)
+#if SMAPI_FOR_WINDOWS
Console.InputEncoding = Encoding.Unicode;
Console.OutputEncoding = Encoding.Unicode;
+#endif
}
/// <summary>Get a monitor instance derived from SMAPI's current settings.</summary>
@@ -162,8 +165,6 @@ namespace StardewModdingAPI.Framework.Logging
// keep console thread alive while the game is running
while (continueWhile())
Thread.Sleep(1000 / 10);
- if (inputThread.ThreadState == ThreadState.Running)
- inputThread.Abort();
}
/// <summary>Show a 'press any key to exit' message, and exit when they press a key.</summary>
diff --git a/src/SMAPI/Framework/ModHelpers/DataHelper.cs b/src/SMAPI/Framework/ModHelpers/DataHelper.cs
index 0fe3209f..4cbfd73f 100644
--- a/src/SMAPI/Framework/ModHelpers/DataHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/DataHelper.cs
@@ -58,7 +58,11 @@ namespace StardewModdingAPI.Framework.ModHelpers
throw new InvalidOperationException($"You must call {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteJsonFile)} with a relative path (without directory climbing).");
path = Path.Combine(this.ModFolderPath, PathUtilities.NormalizePath(path));
- this.JsonHelper.WriteJsonFile(path, data);
+
+ if (data != null)
+ this.JsonHelper.WriteJsonFile(path, data);
+ else
+ File.Delete(path);
}
/****
diff --git a/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs b/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs
index a88ca9c9..869664fe 100644
--- a/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs
@@ -55,6 +55,12 @@ namespace StardewModdingAPI.Framework.ModHelpers
return this.Translator.Get(key, tokens);
}
+ /// <inheritdoc />
+ public IDictionary<string, Translation> GetInAllLocales(string key, bool withFallback = false)
+ {
+ return this.Translator.GetInAllLocales(key, withFallback);
+ }
+
/// <summary>Set the translations to use.</summary>
/// <param name="translations">The translations to use.</param>
internal TranslationHelper SetTranslations(IDictionary<string, IDictionary<string, string>> translations)
diff --git a/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs
index aefb0126..b3415609 100644
--- a/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs
@@ -21,11 +21,17 @@ namespace StardewModdingAPI.Framework.ModLoading
public void Add(params AssemblyDefinition[] assemblies)
{
foreach (AssemblyDefinition assembly in assemblies)
- {
- this.RegisterAssembly(assembly);
- this.Lookup[assembly.Name.Name] = assembly;
- this.Lookup[assembly.Name.FullName] = assembly;
- }
+ this.Add(assembly, assembly.Name.Name, assembly.Name.FullName);
+ }
+
+ /// <summary>Add known assemblies to the resolver.</summary>
+ /// <param name="assembly">The assembly to add.</param>
+ /// <param name="names">The assembly names for which it should be returned.</param>
+ public void Add(AssemblyDefinition assembly, params string[] names)
+ {
+ this.RegisterAssembly(assembly);
+ foreach (string name in names)
+ this.Lookup[name] = assembly;
}
/// <summary>Resolve an assembly reference.</summary>
diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
index 2b71038a..e1ad9d37 100644
--- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
+++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
@@ -7,6 +7,7 @@ using Mono.Cecil;
using Mono.Cecil.Cil;
using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Framework.ModLoading.Framework;
+using StardewModdingAPI.Framework.ModLoading.Symbols;
using StardewModdingAPI.Metadata;
using StardewModdingAPI.Toolkit.Framework.ModData;
using StardewModdingAPI.Toolkit.Utilities;
@@ -34,6 +35,12 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>A minimal assembly definition resolver which resolves references to known loaded assemblies.</summary>
private readonly AssemblyDefinitionResolver AssemblyDefinitionResolver;
+ /// <summary>Provides assembly symbol readers for Mono.Cecil.</summary>
+ private readonly SymbolReaderProvider SymbolReaderProvider = new SymbolReaderProvider();
+
+ /// <summary>Provides assembly symbol writers for Mono.Cecil.</summary>
+ private readonly SymbolWriterProvider SymbolWriterProvider = new SymbolWriterProvider();
+
/// <summary>The objects to dispose as part of this instance.</summary>
private readonly HashSet<IDisposable> Disposables = new HashSet<IDisposable>();
@@ -59,9 +66,7 @@ namespace StardewModdingAPI.Framework.ModLoading
// init resolver
this.AssemblyDefinitionResolver = this.TrackForDisposal(new AssemblyDefinitionResolver());
- this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.ExecutionPath);
- this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.InternalFilesPath);
- this.AssemblyDefinitionResolver.Add(AssemblyDefinition.ReadAssembly(typeof(SGame).Assembly.Location)); // for some reason Mono.Cecil can't resolve SMAPI in very specific cases involving unofficial 64-bit Stardew Valley when launched through Steam (for some players only)
+ Constants.ConfigureAssemblyResolver(this.AssemblyDefinitionResolver);
// generate type => assembly lookup for types which should be rewritten
this.TypeAssemblies = new Dictionary<string, Assembly>();
@@ -136,20 +141,12 @@ namespace StardewModdingAPI.Framework.ModLoading
if (!oneAssembly)
this.Monitor.Log($" Loading {assembly.File.Name} (rewritten)...", LogLevel.Trace);
- // load PDB file if present
- byte[] symbols;
- {
- string symbolsPath = Path.Combine(Path.GetDirectoryName(assemblyPath), Path.GetFileNameWithoutExtension(assemblyPath)) + ".pdb";
- symbols = File.Exists(symbolsPath)
- ? File.ReadAllBytes(symbolsPath)
- : null;
- }
-
// load assembly
- using MemoryStream outStream = new MemoryStream();
- assembly.Definition.Write(outStream);
- byte[] bytes = outStream.ToArray();
- lastAssembly = Assembly.Load(bytes, symbols);
+ using MemoryStream outAssemblyStream = new MemoryStream();
+ using MemoryStream outSymbolStream = new MemoryStream();
+ assembly.Definition.Write(outAssemblyStream, new WriterParameters { WriteSymbols = true, SymbolStream = outSymbolStream, SymbolWriterProvider = this.SymbolWriterProvider });
+ byte[] bytes = outAssemblyStream.ToArray();
+ lastAssembly = Assembly.Load(bytes, outSymbolStream.ToArray());
}
else
{
@@ -236,10 +233,15 @@ namespace StardewModdingAPI.Framework.ModLoading
if (!file.Exists)
yield break; // not a local assembly
- // read assembly
+ // read assembly and symbols
byte[] assemblyBytes = File.ReadAllBytes(file.FullName);
Stream readStream = this.TrackForDisposal(new MemoryStream(assemblyBytes));
- AssemblyDefinition assembly = this.TrackForDisposal(AssemblyDefinition.ReadAssembly(readStream, new ReaderParameters(ReadingMode.Immediate) { AssemblyResolver = assemblyResolver, InMemory = true }));
+ {
+ FileInfo symbolsFile = new FileInfo(Path.Combine(Path.GetDirectoryName(file.FullName)!, Path.GetFileNameWithoutExtension(file.FullName)) + ".pdb");
+ if (symbolsFile.Exists)
+ this.SymbolReaderProvider.TryAddSymbolData(file.Name, () => this.TrackForDisposal(symbolsFile.OpenRead()));
+ }
+ AssemblyDefinition assembly = this.TrackForDisposal(AssemblyDefinition.ReadAssembly(readStream, new ReaderParameters(ReadingMode.Immediate) { AssemblyResolver = assemblyResolver, InMemory = true, ReadSymbols = true, SymbolReaderProvider = this.SymbolReaderProvider }));
// skip if already visited
if (visitedAssemblyNames.Contains(assembly.Name.Name))
diff --git a/src/SMAPI/Framework/ModLoading/Symbols/SymbolReader.cs b/src/SMAPI/Framework/ModLoading/Symbols/SymbolReader.cs
new file mode 100644
index 00000000..2171895d
--- /dev/null
+++ b/src/SMAPI/Framework/ModLoading/Symbols/SymbolReader.cs
@@ -0,0 +1,72 @@
+using System.IO;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+using Mono.Cecil.Pdb;
+
+namespace StardewModdingAPI.Framework.ModLoading.Symbols
+{
+ /// <summary>Reads symbol data for an assembly.</summary>
+ internal class SymbolReader : ISymbolReader
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The module for which to read symbols.</summary>
+ private readonly ModuleDefinition Module;
+
+ /// <summary>The symbol file stream.</summary>
+ private readonly Stream Stream;
+
+ /// <summary>The underlying symbol reader.</summary>
+ private ISymbolReader Reader;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="module">The module for which to read symbols.</param>
+ /// <param name="stream">The symbol file stream.</param>
+ public SymbolReader(ModuleDefinition module, Stream stream)
+ {
+ this.Module = module;
+ this.Stream = stream;
+ this.Reader = new NativePdbReaderProvider().GetSymbolReader(module, stream);
+ }
+
+ /// <summary>Get the symbol writer provider for the assembly.</summary>
+ public ISymbolWriterProvider GetWriterProvider()
+ {
+ return new PortablePdbWriterProvider();
+ }
+
+ /// <summary>Process a debug header in the symbol file.</summary>
+ /// <param name="header">The debug header.</param>
+ public bool ProcessDebugHeader(ImageDebugHeader header)
+ {
+ try
+ {
+ return this.Reader.ProcessDebugHeader(header);
+ }
+ catch
+ {
+ this.Reader.Dispose();
+ this.Reader = new PortablePdbReaderProvider().GetSymbolReader(this.Module, this.Stream);
+ return this.Reader.ProcessDebugHeader(header);
+ }
+ }
+
+ /// <summary>Read the method debug information for a method in the assembly.</summary>
+ /// <param name="method">The method definition.</param>
+ public MethodDebugInformation Read(MethodDefinition method)
+ {
+ return this.Reader.Read(method);
+ }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ this.Reader.Dispose();
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/ModLoading/Symbols/SymbolReaderProvider.cs b/src/SMAPI/Framework/ModLoading/Symbols/SymbolReaderProvider.cs
new file mode 100644
index 00000000..44074337
--- /dev/null
+++ b/src/SMAPI/Framework/ModLoading/Symbols/SymbolReaderProvider.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+
+namespace StardewModdingAPI.Framework.ModLoading.Symbols
+{
+ /// <summary>Provides assembly symbol readers for Mono.Cecil.</summary>
+ internal class SymbolReaderProvider : ISymbolReaderProvider
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The underlying symbol reader provider.</summary>
+ private readonly ISymbolReaderProvider BaseProvider = new DefaultSymbolReaderProvider(throwIfNoSymbol: false);
+
+ /// <summary>The symbol data loaded by absolute assembly path.</summary>
+ private readonly Dictionary<string, Stream> SymbolsByAssemblyPath = new Dictionary<string, Stream>(StringComparer.OrdinalIgnoreCase);
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Add the symbol file for a given assembly name, if it's not already registered.</summary>
+ /// <param name="fileName">The assembly file name.</param>
+ /// <param name="getSymbolStream">Get the raw file stream for the symbols.</param>
+ public void TryAddSymbolData(string fileName, Func<Stream> getSymbolStream)
+ {
+ if (!this.SymbolsByAssemblyPath.ContainsKey(fileName))
+ this.SymbolsByAssemblyPath.Add(fileName, getSymbolStream());
+ }
+
+ /// <summary>Get a symbol reader for a given module and assembly name.</summary>
+ /// <param name="module">The loaded assembly module.</param>
+ /// <param name="fileName">The assembly file name.</param>
+ public ISymbolReader GetSymbolReader(ModuleDefinition module, string fileName)
+ {
+ return this.SymbolsByAssemblyPath.TryGetValue(module.Name, out Stream symbolData)
+ ? new SymbolReader(module, symbolData)
+ : this.BaseProvider.GetSymbolReader(module, fileName);
+ }
+
+ /// <summary>Get a symbol reader for a given module and symbol stream.</summary>
+ /// <param name="module">The loaded assembly module.</param>
+ /// <param name="symbolStream">The loaded symbol file stream.</param>
+ public ISymbolReader GetSymbolReader(ModuleDefinition module, Stream symbolStream)
+ {
+ return this.SymbolsByAssemblyPath.TryGetValue(module.Name, out Stream symbolData)
+ ? new SymbolReader(module, symbolData)
+ : this.BaseProvider.GetSymbolReader(module, symbolStream);
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/ModLoading/Symbols/SymbolWriterProvider.cs b/src/SMAPI/Framework/ModLoading/Symbols/SymbolWriterProvider.cs
new file mode 100644
index 00000000..8f7e05d1
--- /dev/null
+++ b/src/SMAPI/Framework/ModLoading/Symbols/SymbolWriterProvider.cs
@@ -0,0 +1,40 @@
+using System.IO;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+
+namespace StardewModdingAPI.Framework.ModLoading.Symbols
+{
+ /// <summary>Provides assembly symbol writers for Mono.Cecil.</summary>
+ internal class SymbolWriterProvider : ISymbolWriterProvider
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The default symbol writer provider.</summary>
+ private readonly ISymbolWriterProvider DefaultProvider = new DefaultSymbolWriterProvider();
+
+ /// <summary>The symbol writer provider for the portable PDB format.</summary>
+ private readonly ISymbolWriterProvider PortablePdbProvider = new PortablePdbWriterProvider();
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Get a symbol writer for a given module and assembly path.</summary>
+ /// <param name="module">The loaded assembly module.</param>
+ /// <param name="fileName">The assembly name.</param>
+ public ISymbolWriter GetSymbolWriter(ModuleDefinition module, string fileName)
+ {
+ return this.DefaultProvider.GetSymbolWriter(module, fileName);
+ }
+
+ /// <summary>Get a symbol writer for a given module and symbol stream.</summary>
+ /// <param name="module">The loaded assembly module.</param>
+ /// <param name="symbolStream">The loaded symbol file stream.</param>
+ public ISymbolWriter GetSymbolWriter(ModuleDefinition module, Stream symbolStream)
+ {
+ // Not implemented in default native pdb writer, so fallback to portable
+ return this.PortablePdbProvider.GetSymbolWriter(module, symbolStream);
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index 5fb4aa03..dff0fc55 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -1704,12 +1704,21 @@ namespace StardewModdingAPI.Framework
// load as mod
else
{
+ // get mod info
IManifest manifest = mod.Manifest;
+ string assemblyPath = Path.Combine(mod.DirectoryPath, manifest.EntryDll);
+
+ // assert 64-bit
+#if SMAPI_FOR_WINDOWS_64BIT_HACK
+ if (!EnvironmentUtility.Is64BitAssembly(assemblyPath))
+ {
+ errorReasonPhrase = "it needs to be updated for 64-bit mode.";
+ failReason = ModFailReason.LoadFailed;
+ return false;
+ }
+#endif
// load mod
- string assemblyPath = manifest?.EntryDll != null
- ? Path.Combine(mod.DirectoryPath, manifest.EntryDll)
- : null;
Assembly modAssembly;
try
{
diff --git a/src/SMAPI/Framework/Translator.cs b/src/SMAPI/Framework/Translator.cs
index 11ec983b..4492b17f 100644
--- a/src/SMAPI/Framework/Translator.cs
+++ b/src/SMAPI/Framework/Translator.cs
@@ -46,18 +46,10 @@ namespace StardewModdingAPI.Framework
this.LocaleEnum = localeEnum;
this.ForLocale = new Dictionary<string, Translation>(StringComparer.OrdinalIgnoreCase);
- foreach (string next in this.GetRelevantLocales(this.Locale))
+ foreach (string key in this.GetAllKeysRaw())
{
- // skip if locale not defined
- if (!this.All.TryGetValue(next, out IDictionary<string, string> translations))
- continue;
-
- // add missing translations
- foreach (var pair in translations)
- {
- if (!this.ForLocale.ContainsKey(pair.Key))
- this.ForLocale.Add(pair.Key, new Translation(this.Locale, pair.Key, pair.Value));
- }
+ string text = this.GetRaw(key, locale, withFallback: true);
+ this.ForLocale.Add(key, new Translation(this.Locale, key, text));
}
}
@@ -83,6 +75,25 @@ namespace StardewModdingAPI.Framework
return this.Get(key).Tokens(tokens);
}
+ /// <summary>Get a translation in every locale for which it's defined.</summary>
+ /// <param name="key">The translation key.</param>
+ /// <param name="withFallback">Whether to add duplicate translations for locale fallback. For example, if a translation is defined in <c>default.json</c> but not <c>fr.json</c>, setting this to true will add a <c>fr</c> entry which duplicates the default text.</param>
+ public IDictionary<string, Translation> GetInAllLocales(string key, bool withFallback)
+ {
+ IDictionary<string, Translation> translations = new Dictionary<string, Translation>();
+
+ foreach (var localeSet in this.All)
+ {
+ string locale = localeSet.Key;
+ string text = this.GetRaw(key, locale, withFallback);
+
+ if (text != null)
+ translations[locale] = new Translation(locale, key, text);
+ }
+
+ return translations;
+ }
+
/// <summary>Set the translations to use.</summary>
/// <param name="translations">The translations to use.</param>
internal Translator SetTranslations(IDictionary<string, IDictionary<string, string>> translations)
@@ -102,6 +113,38 @@ namespace StardewModdingAPI.Framework
/*********
** Private methods
*********/
+ /// <summary>Get all translation keys in the underlying translation data, ignoring the <see cref="ForLocale"/> cache.</summary>
+ private IEnumerable<string> GetAllKeysRaw()
+ {
+ return new HashSet<string>(
+ this.All.SelectMany(p => p.Value.Keys),
+ StringComparer.OrdinalIgnoreCase
+ );
+ }
+
+ /// <summary>Get a translation from the underlying translation data, ignoring the <see cref="ForLocale"/> cache.</summary>
+ /// <param name="key">The translation key.</param>
+ /// <param name="locale">The locale to get.</param>
+ /// <param name="withFallback">Whether to add duplicate translations for locale fallback. For example, if a translation is defined in <c>default.json</c> but not <c>fr.json</c>, setting this to true will add a <c>fr</c> entry which duplicates the default text.</param>
+ private string GetRaw(string key, string locale, bool withFallback)
+ {
+ foreach (string next in this.GetRelevantLocales(locale))
+ {
+ string translation = null;
+ bool hasTranslation =
+ this.All.TryGetValue(next, out IDictionary<string, string> translations)
+ && translations.TryGetValue(key, out translation);
+
+ if (hasTranslation)
+ return translation;
+
+ if (!withFallback)
+ break;
+ }
+
+ return null;
+ }
+
/// <summary>Get the locales which can provide translations for the given locale, in precedence order.</summary>
/// <param name="locale">The locale for which to find valid locales.</param>
private IEnumerable<string> GetRelevantLocales(string locale)
diff --git a/src/SMAPI/IDataHelper.cs b/src/SMAPI/IDataHelper.cs
index 252030bd..901266d7 100644
--- a/src/SMAPI/IDataHelper.cs
+++ b/src/SMAPI/IDataHelper.cs
@@ -21,7 +21,7 @@ namespace StardewModdingAPI
/// <summary>Save data to a JSON file in the mod's folder.</summary>
/// <typeparam name="TModel">The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types.</typeparam>
/// <param name="path">The file path relative to the mod folder.</param>
- /// <param name="data">The arbitrary data to save.</param>
+ /// <param name="data">The arbitrary data to save, or <c>null</c> to delete the file.</param>
/// <exception cref="InvalidOperationException">The <paramref name="path"/> is not relative or contains directory climbing (../).</exception>
void WriteJsonFile<TModel>(string path, TModel data) where TModel : class;
@@ -38,7 +38,7 @@ namespace StardewModdingAPI
/// <summary>Save arbitrary data to the current save slot. This is only possible if a save has been loaded, and the data will be lost if the player exits without saving the current day.</summary>
/// <typeparam name="TModel">The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types.</typeparam>
/// <param name="key">The unique key identifying the data.</param>
- /// <param name="data">The arbitrary data to save.</param>
+ /// <param name="data">The arbitrary data to save, or <c>null</c> to remove the entry.</param>
/// <exception cref="InvalidOperationException">The player hasn't loaded a save file yet or isn't the main player.</exception>
void WriteSaveData<TModel>(string key, TModel data) where TModel : class;
@@ -55,7 +55,7 @@ namespace StardewModdingAPI
/// <summary>Save arbitrary data to the local computer, synchronised by GOG/Steam if applicable.</summary>
/// <typeparam name="TModel">The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types.</typeparam>
/// <param name="key">The unique key identifying the data.</param>
- /// <param name="data">The arbitrary data to save.</param>
+ /// <param name="data">The arbitrary data to save, or <c>null</c> to delete the file.</param>
void WriteGlobalData<TModel>(string key, TModel data) where TModel : class;
}
}
diff --git a/src/SMAPI/ITranslationHelper.cs b/src/SMAPI/ITranslationHelper.cs
index c4b72444..b30d9b14 100644
--- a/src/SMAPI/ITranslationHelper.cs
+++ b/src/SMAPI/ITranslationHelper.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using StardewValley;
namespace StardewModdingAPI
@@ -30,5 +30,10 @@ namespace StardewModdingAPI
/// <param name="key">The translation key.</param>
/// <param name="tokens">An object containing token key/value pairs. This can be an anonymous object (like <c>new { value = 42, name = "Cranberries" }</c>), a dictionary, or a class instance.</param>
Translation Get(string key, object tokens);
+
+ /// <summary>Get a translation in every locale for which it's defined.</summary>
+ /// <param name="key">The translation key.</param>
+ /// <param name="withFallback">Whether to add duplicate translations for locale fallback. For example, if a translation is defined in <c>default.json</c> but not <c>fr.json</c>, setting this to true will add a <c>fr</c> entry which duplicates the default text.</param>
+ IDictionary<string, Translation> GetInAllLocales(string key, bool withFallback = false);
}
}