diff --git a/README.md b/README.md
index 1c36cd7e..9a0905b1 100644
--- a/README.md
+++ b/README.md
@@ -71,22 +71,29 @@ directory containing `src`).
3. If you did everything right so far, you should have a directory like this:
```
- SMAPI-1.0/
+ SMAPI-1.x/
Mono/
Mods/*
+ Mono.Cecil.dll
+ Mono.Cecil.Rocks.dll
Newtonsoft.Json.dll
StardewModdingAPI
StardewModdingAPI.exe
StardewModdingAPI.exe.mdb
StardewModdingAPI-settings.json
+ StardewModdingAPI.AssemblyRewriters.dll
System.Numerics.dll
steam_appid.txt
Windows/
Mods/*
+ Mono.Cecil.dll
+ Mono.Cecil.Rocks.dll
+ Newtonsoft.Json.dll
StardewModdingAPI.exe
StardewModdingAPI.pdb
StardewModdingAPI.xml
StardewModdingAPI-settings.json
+ StardewModdingAPI.AssemblyRewriters.dll
steam_appid.txt
install.exe
readme.txt
diff --git a/release-notes.md b/release-notes.md
index 86169cee..960d66d9 100644
--- a/release-notes.md
+++ b/release-notes.md
@@ -1,5 +1,12 @@
# Release notes
+## 1.3
+See [log](https://github.com/CLxS/SMAPI/compare/1.2...1.3).
+
+For players:
+ * You can now run most mods on any platform (e.g. run Windows mods on Linux/Mac).
+ * Fixed the normal uninstaller not removing files added by the 'SMAPI for developers' installer.
+
## 1.2
See [log](https://github.com/CLxS/SMAPI/compare/1.1.1...1.2).
diff --git a/src/GlobalAssemblyInfo.cs b/src/GlobalAssemblyInfo.cs
index 646d7488..239c5eba 100644
--- a/src/GlobalAssemblyInfo.cs
+++ b/src/GlobalAssemblyInfo.cs
@@ -2,5 +2,5 @@
using System.Runtime.InteropServices;
[assembly: ComVisible(false)]
-[assembly: AssemblyVersion("1.2.0.0")]
-[assembly: AssemblyFileVersion("1.2.0.0")]
\ No newline at end of file
+[assembly: AssemblyVersion("1.3.0.0")]
+[assembly: AssemblyFileVersion("1.3.0.0")]
\ No newline at end of file
diff --git a/src/StardewModdingAPI.AssemblyRewriters/IMethodRewriter.cs b/src/StardewModdingAPI.AssemblyRewriters/IMethodRewriter.cs
new file mode 100644
index 00000000..5cbb7e0d
--- /dev/null
+++ b/src/StardewModdingAPI.AssemblyRewriters/IMethodRewriter.cs
@@ -0,0 +1,21 @@
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+
+namespace StardewModdingAPI.AssemblyRewriters
+{
+ /// Rewrites a method for compatibility.
+ public interface IMethodRewriter
+ {
+ /// Get whether the given method reference can be rewritten.
+ /// The method reference.
+ bool ShouldRewrite(MethodReference methodRef);
+
+ /// Rewrite a method for compatibility.
+ /// The module being rewritten.
+ /// The CIL rewriter.
+ /// The instruction which calls the method.
+ /// The method reference invoked by the .
+ /// Metadata for mapping assemblies to the current platform.
+ void Rewrite(ModuleDefinition module, ILProcessor cil, Instruction callOp, MethodReference methodRef, PlatformAssemblyMap assemblyMap);
+ }
+}
diff --git a/src/StardewModdingAPI.AssemblyRewriters/Platform.cs b/src/StardewModdingAPI.AssemblyRewriters/Platform.cs
new file mode 100644
index 00000000..8888a9a8
--- /dev/null
+++ b/src/StardewModdingAPI.AssemblyRewriters/Platform.cs
@@ -0,0 +1,12 @@
+namespace StardewModdingAPI.AssemblyRewriters
+{
+ /// The game's platform version.
+ public enum Platform
+ {
+ /// The Linux/Mac version of the game.
+ Mono,
+
+ /// The Windows version of the game.
+ Windows
+ }
+}
\ No newline at end of file
diff --git a/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs b/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs
new file mode 100644
index 00000000..f2826080
--- /dev/null
+++ b/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs
@@ -0,0 +1,57 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Mono.Cecil;
+
+namespace StardewModdingAPI.AssemblyRewriters
+{
+ /// Metadata for mapping assemblies to the current .
+ public class PlatformAssemblyMap
+ {
+ /*********
+ ** Accessors
+ *********/
+ /****
+ ** Data
+ ****/
+ /// The target game platform.
+ public readonly Platform TargetPlatform;
+
+ /// The short assembly names to remove as assembly reference, and replace with the . These should be short names (like "Stardew Valley").
+ public readonly string[] RemoveNames;
+
+ /// The assembly filenames to target. Equivalent types should be rewritten to use these assemblies.
+
+ /****
+ ** Metadata
+ ****/
+ /// The assemblies to target. Equivalent types should be rewritten to use these assemblies.
+ public readonly Assembly[] Targets;
+
+ /// An assembly => reference cache.
+ public readonly IDictionary TargetReferences;
+
+ /// An assembly => module cache.
+ public readonly IDictionary TargetModules;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The target game platform.
+ /// The assembly short names to remove (like Stardew Valley).
+ /// The assemblies to target.
+ public PlatformAssemblyMap(Platform targetPlatform, string[] removeAssemblyNames, Assembly[] targetAssemblies)
+ {
+ // save data
+ this.TargetPlatform = targetPlatform;
+ this.RemoveNames = removeAssemblyNames;
+
+ // cache assembly metadata
+ this.Targets = targetAssemblies;
+ this.TargetReferences = this.Targets.ToDictionary(assembly => assembly, assembly => AssemblyNameReference.Parse(assembly.FullName));
+ this.TargetModules = this.Targets.ToDictionary(assembly => assembly, assembly => ModuleDefinition.ReadModule(assembly.Modules.Single().FullyQualifiedName));
+ }
+ }
+}
diff --git a/src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..25fbfef9
--- /dev/null
+++ b/src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs
@@ -0,0 +1,7 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+[assembly: AssemblyTitle("StardewModdingAPI.AssemblyRewriters")]
+[assembly: AssemblyDescription("Contains internal SMAPI code for converting mods between Linux/Mac and Windows. This assembly is used by SMAPI internally and shouldn't be referenced directly by mods.")]
+[assembly: AssemblyProduct("StardewModdingAPI.CrossplatformRewriters")]
+[assembly: Guid("10db0676-9fc1-4771-a2c8-e2519f091e49")]
diff --git a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/BaseMethodRewriter.cs b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/BaseMethodRewriter.cs
new file mode 100644
index 00000000..1af6e6c4
--- /dev/null
+++ b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/BaseMethodRewriter.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+
+namespace StardewModdingAPI.AssemblyRewriters.Rewriters
+{
+ /// Base class for a method rewriter.
+ public abstract class BaseMethodRewriter : IMethodRewriter
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// Get whether the given method reference can be rewritten.
+ /// The method reference.
+ public abstract bool ShouldRewrite(MethodReference methodRef);
+
+ /// Rewrite a method for compatibility.
+ /// The module being rewritten.
+ /// The CIL rewriter.
+ /// The instruction which calls the method.
+ /// The method reference invoked by the .
+ /// Metadata for mapping assemblies to the current platform.
+ public abstract void Rewrite(ModuleDefinition module, ILProcessor cil, Instruction callOp, MethodReference methodRef, PlatformAssemblyMap assemblyMap);
+
+
+ /*********
+ ** Protected methods
+ *********/
+ /// Get whether a method definition matches the signature expected by a method reference.
+ /// The method definition.
+ /// The method reference.
+ protected bool HasMatchingSignature(MethodInfo definition, MethodReference reference)
+ {
+ // same name
+ if (definition.Name != reference.Name)
+ return false;
+
+ // same arguments
+ ParameterInfo[] definitionParameters = definition.GetParameters();
+ ParameterDefinition[] referenceParameters = reference.Parameters.ToArray();
+ if (referenceParameters.Length != definitionParameters.Length)
+ return false;
+ for (int i = 0; i < referenceParameters.Length; i++)
+ {
+ if (!this.IsMatchingType(definitionParameters[i].ParameterType, referenceParameters[i].ParameterType))
+ return false;
+ }
+ return true;
+ }
+
+ /// Get whether a type has a method whose signature matches the one expected by a method reference.
+ /// The type to check.
+ /// The method reference.
+ protected bool HasMatchingSignature(Type type, MethodReference reference)
+ {
+ return type
+ .GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public)
+ .Any(method => this.HasMatchingSignature(method, reference));
+ }
+
+ /// Get whether a type matches a type reference.
+ /// The defined type.
+ /// The type reference.
+ private bool IsMatchingType(Type type, TypeReference reference)
+ {
+ // same namespace & name
+ if (type.Namespace != reference.Namespace || type.Name != reference.Name)
+ return false;
+
+ // same generic parameters
+ if (type.IsGenericType)
+ {
+ if (!reference.IsGenericInstance)
+ return false;
+
+ Type[] defGenerics = type.GetGenericArguments();
+ TypeReference[] refGenerics = ((GenericInstanceType)reference).GenericArguments.ToArray();
+ if (defGenerics.Length != refGenerics.Length)
+ return false;
+ for (int i = 0; i < defGenerics.Length; i++)
+ {
+ if (!this.IsMatchingType(defGenerics[i], refGenerics[i]))
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/SpriteBatchRewriter.cs b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/SpriteBatchRewriter.cs
new file mode 100644
index 00000000..1c0a5cf3
--- /dev/null
+++ b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/SpriteBatchRewriter.cs
@@ -0,0 +1,30 @@
+using Microsoft.Xna.Framework.Graphics;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+using StardewModdingAPI.AssemblyRewriters.Wrappers;
+
+namespace StardewModdingAPI.AssemblyRewriters.Rewriters
+{
+ /// Rewrites references to to fix inconsistent method signatures between MonoGame and XNA.
+ /// MonoGame has one SpriteBatch.Begin method with optional arguments, but XNA has multiple method overloads. Incompatible method references are rewritten to use , which redirects all method signatures to the proper compiled MonoGame/XNA method.
+ public class SpriteBatchRewriter : BaseMethodRewriter
+ {
+ /// Get whether the given method reference can be rewritten.
+ /// The method reference.
+ public override bool ShouldRewrite(MethodReference methodRef)
+ {
+ return methodRef.DeclaringType.FullName == typeof(SpriteBatch).FullName && this.HasMatchingSignature(typeof(CompatibleSpriteBatch), methodRef);
+ }
+
+ /// Rewrite a method for compatibility.
+ /// The module being rewritten.
+ /// The CIL rewriter.
+ /// The instruction which calls the method.
+ /// The method reference invoked by the .
+ /// Metadata for mapping assemblies to the current platform.
+ public override void Rewrite(ModuleDefinition module, ILProcessor cil, Instruction callOp, MethodReference methodRef, PlatformAssemblyMap assemblyMap)
+ {
+ methodRef.DeclaringType = module.Import(typeof(CompatibleSpriteBatch));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj
new file mode 100644
index 00000000..51a49da0
--- /dev/null
+++ b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj
@@ -0,0 +1,94 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {10DB0676-9FC1-4771-A2C8-E2519F091E49}
+ Library
+ Properties
+ StardewModdingAPI.AssemblyRewriters
+ StardewModdingAPI.AssemblyRewriters
+ v4.5
+ 512
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+ true
+ bin\x86\Debug\
+ DEBUG;TRACE;SMAPI_FOR_WINDOWS
+ full
+ x86
+ prompt
+ MinimumRecommendedRules.ruleset
+
+
+ bin\x86\Release\
+ TRACE;SMAPI_FOR_WINDOWS
+ true
+ pdbonly
+ x86
+ prompt
+ MinimumRecommendedRules.ruleset
+
+
+
+
+ ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll
+ True
+
+
+ ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Mdb.dll
+ True
+
+
+ ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll
+ True
+
+
+ ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Rocks.dll
+ True
+
+
+
+
+
+ Properties\GlobalAssemblyInfo.cs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/StardewModdingAPI.AssemblyRewriters/Wrappers/CompatibleSpriteBatch.cs b/src/StardewModdingAPI.AssemblyRewriters/Wrappers/CompatibleSpriteBatch.cs
new file mode 100644
index 00000000..e28d1a68
--- /dev/null
+++ b/src/StardewModdingAPI.AssemblyRewriters/Wrappers/CompatibleSpriteBatch.cs
@@ -0,0 +1,52 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+#pragma warning disable CS0109 // Member does not hide an inherited member; new keyword is not required
+namespace StardewModdingAPI.AssemblyRewriters.Wrappers
+{
+ /// Wraps methods that are incompatible when converting compiled code between MonoGame and XNA.
+ public class CompatibleSpriteBatch : SpriteBatch
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ public CompatibleSpriteBatch(GraphicsDevice graphicsDevice) : base(graphicsDevice) { }
+
+ /****
+ ** MonoGame signatures
+ ****/
+ public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix? matrix)
+ {
+ base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, matrix ?? Matrix.Identity);
+ }
+
+ /****
+ ** XNA signatures
+ ****/
+ public new void Begin()
+ {
+ base.Begin();
+ }
+
+ public new void Begin(SpriteSortMode sortMode, BlendState blendState)
+ {
+ base.Begin(sortMode, blendState);
+ }
+
+ public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState)
+ {
+ base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState);
+ }
+
+ public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect)
+ {
+ base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect);
+ }
+
+ public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix transformMatrix)
+ {
+ base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, transformMatrix);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/StardewModdingAPI.AssemblyRewriters/packages.config b/src/StardewModdingAPI.AssemblyRewriters/packages.config
new file mode 100644
index 00000000..88fbc79d
--- /dev/null
+++ b/src/StardewModdingAPI.AssemblyRewriters/packages.config
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs
index 5be9b14c..1d3802ab 100644
--- a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs
+++ b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs
@@ -27,6 +27,27 @@ namespace StardewModdingApi.Installer
@"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley"
};
+ /// The files to remove when uninstalling SMAPI.
+ private readonly string[] UninstallFiles =
+ {
+ // common
+ "StardewModdingAPI.exe",
+ "StardewModdingAPI-settings.json",
+ "StardewModdingAPI.AssemblyRewriters.dll",
+ "steam_appid.txt",
+
+ // Linux/Mac only
+ "Mono.Cecil.dll",
+ "Mono.Cecil.Rocks.dll",
+ "Newtonsoft.Json.dll",
+ "StardewModdingAPI",
+ "StardewModdingAPI.exe.mdb",
+ "System.Numerics.dll",
+
+ // Windows only
+ "StardewModdingAPI.pdb"
+ };
+
/*********
** Public methods
@@ -47,7 +68,7 @@ namespace StardewModdingApi.Installer
///
/// Uninstall logic:
/// 1. On Linux/Mac: if a backup of the launcher exists, delete the launcher and restore the backup.
- /// 2. Delete all files in the game directory matching a file under package/Windows or package/Mono.
+ /// 2. Delete all files in the game directory matching one of the .
///
public void Run(string[] args)
{
@@ -127,9 +148,9 @@ namespace StardewModdingApi.Installer
// remove SMAPI files
this.PrintDebug("Removing SMAPI files...");
- foreach (FileInfo sourceFile in packageDir.EnumerateFiles())
+ foreach (string filename in this.UninstallFiles)
{
- string targetPath = Path.Combine(installDir.FullName, sourceFile.Name);
+ string targetPath = Path.Combine(installDir.FullName, filename);
if (File.Exists(targetPath))
File.Delete(targetPath);
}
diff --git a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj b/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj
index 6f9ed2eb..0a33cd57 100644
--- a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj
+++ b/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj
@@ -66,18 +66,25 @@
+
+
+
+
+
+
+
diff --git a/src/StardewModdingAPI.sln b/src/StardewModdingAPI.sln
index 31bf5f2f..d97e4645 100644
--- a/src/StardewModdingAPI.sln
+++ b/src/StardewModdingAPI.sln
@@ -23,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Installer
{F1A573B0-F436-472C-AE29-0B91EA6B9F8F} = {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.AssemblyRewriters", "StardewModdingAPI.AssemblyRewriters\StardewModdingAPI.AssemblyRewriters.csproj", "{10DB0676-9FC1-4771-A2C8-E2519F091E49}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -67,6 +69,18 @@ Global
{443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{443DDF81-6AAF-420A-A610-3459F37E5575}.Release|x86.ActiveCfg = Release|Any CPU
{443DDF81-6AAF-420A-A610-3459F37E5575}.Release|x86.Build.0 = Release|Any CPU
+ {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|x86.Build.0 = Debug|Any CPU
+ {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Any CPU.Build.0 = Release|Any CPU
+ {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|x86.ActiveCfg = Release|Any CPU
+ {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs
index 05e410ab..3feb0830 100644
--- a/src/StardewModdingAPI/Constants.cs
+++ b/src/StardewModdingAPI/Constants.cs
@@ -1,7 +1,10 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
+using StardewModdingAPI.AssemblyRewriters;
+using StardewModdingAPI.AssemblyRewriters.Rewriters;
using StardewValley;
namespace StardewModdingAPI
@@ -23,13 +26,13 @@ namespace StardewModdingAPI
** Accessors
*********/
/// SMAPI's current semantic version.
- public static readonly Version Version = new Version(1, 2, 0, null);
+ public static readonly Version Version = new Version(1, 3, 0, null);
/// The minimum supported version of Stardew Valley.
public const string MinimumGameVersion = "1.1";
/// The GitHub repository to check for updates.
- public const string GitHubRepository = "ClxS/SMAPI";
+ public const string GitHubRepository = "Pathoschild/SMAPI";
/// The directory path containing Stardew Valley's app data.
public static string DataPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley");
@@ -63,8 +66,63 @@ namespace StardewModdingAPI
/*********
- ** Private field
+ ** Protected methods
*********/
+ /// Get metadata for mapping assemblies to the current platform.
+ /// The target game platform.
+ internal static PlatformAssemblyMap GetAssemblyMap(Platform targetPlatform)
+ {
+ // get assembly changes needed for platform
+ string[] removeAssemblyReferences;
+ Assembly[] targetAssemblies;
+ switch (targetPlatform)
+ {
+ case Platform.Mono:
+ removeAssemblyReferences = new[]
+ {
+ "Stardew Valley",
+ "Microsoft.Xna.Framework",
+ "Microsoft.Xna.Framework.Game",
+ "Microsoft.Xna.Framework.Graphics"
+ };
+ targetAssemblies = new[]
+ {
+ typeof(StardewValley.Game1).Assembly,
+ typeof(Microsoft.Xna.Framework.Vector2).Assembly
+ };
+ break;
+
+ case Platform.Windows:
+ removeAssemblyReferences = new[]
+ {
+ "StardewValley",
+ "MonoGame.Framework"
+ };
+ targetAssemblies = new[]
+ {
+ typeof(StardewValley.Game1).Assembly,
+ typeof(Microsoft.Xna.Framework.Vector2).Assembly,
+ typeof(Microsoft.Xna.Framework.Game).Assembly,
+ typeof(Microsoft.Xna.Framework.Graphics.SpriteBatch).Assembly
+ };
+ break;
+
+ default:
+ throw new InvalidOperationException($"Unknown target platform '{targetPlatform}'.");
+ }
+
+ return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies);
+ }
+
+ /// Get method rewriters which fix incompatible method calls in mod assemblies.
+ internal static IEnumerable GetMethodRewriters()
+ {
+ return new[]
+ {
+ new SpriteBatchRewriter()
+ };
+ }
+
/// Get the name of a save directory for the current player.
private static string GetSaveFolderName()
{
@@ -72,4 +130,4 @@ namespace StardewModdingAPI
return $"{prefix}_{Game1.uniqueIDForThisGame}";
}
}
-}
\ No newline at end of file
+}
diff --git a/src/StardewModdingAPI/FodyWeavers.xml b/src/StardewModdingAPI/FodyWeavers.xml
deleted file mode 100644
index 0ef30506..00000000
--- a/src/StardewModdingAPI/FodyWeavers.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
- Stardew Valley
- xTile
- Steamworks.NET
- Lidgren.Network
-
-
-
-
\ No newline at end of file
diff --git a/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs b/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs
new file mode 100644
index 00000000..3459488e
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs
@@ -0,0 +1,160 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+using Mono.Cecil.Rocks;
+using StardewModdingAPI.AssemblyRewriters;
+
+namespace StardewModdingAPI.Framework.AssemblyRewriting
+{
+ /// Rewrites type references.
+ internal class AssemblyTypeRewriter
+ {
+ /*********
+ ** Properties
+ *********/
+ /// Metadata for mapping assemblies to the current .
+ private readonly PlatformAssemblyMap AssemblyMap;
+
+ /// A type => assembly lookup for types which should be rewritten.
+ private readonly IDictionary TypeAssemblies;
+
+ /// Encapsulates monitoring and logging.
+ private readonly IMonitor Monitor;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// Metadata for mapping assemblies to the current .
+ /// Encapsulates monitoring and logging.
+ public AssemblyTypeRewriter(PlatformAssemblyMap assemblyMap, IMonitor monitor)
+ {
+ // save config
+ this.AssemblyMap = assemblyMap;
+ this.Monitor = monitor;
+
+ // collect type => assembly lookup
+ this.TypeAssemblies = new Dictionary();
+ foreach (Assembly assembly in assemblyMap.Targets)
+ {
+ ModuleDefinition module = this.AssemblyMap.TargetModules[assembly];
+ foreach (TypeDefinition type in module.GetTypes())
+ {
+ if (!type.IsPublic)
+ continue; // no need to rewrite
+ if (type.Namespace.Contains("<"))
+ continue; // ignore assembly metadata
+ this.TypeAssemblies[type.FullName] = assembly;
+ }
+ }
+ }
+
+ /// Rewrite the types referenced by an assembly.
+ /// The assembly to rewrite.
+ public void RewriteAssembly(AssemblyDefinition assembly)
+ {
+ ModuleDefinition module = assembly.Modules.Single(); // technically an assembly can have multiple modules, but none of the build tools (including MSBuild) support it; simplify by assuming one module
+
+ // remove old assembly references
+ bool shouldRewrite = false;
+ for (int i = 0; i < module.AssemblyReferences.Count; i++)
+ {
+ if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name))
+ {
+ this.Monitor.Log($"removing reference to {module.AssemblyReferences[i]}", LogLevel.Trace);
+ shouldRewrite = true;
+ module.AssemblyReferences.RemoveAt(i);
+ i--;
+ }
+ }
+ if (!shouldRewrite)
+ return;
+
+ // add target assembly references
+ foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values)
+ {
+ this.Monitor.Log($" adding reference to {target}", LogLevel.Trace);
+ module.AssemblyReferences.Add(target);
+ }
+
+ // rewrite type scopes to use target assemblies
+ IEnumerable typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName);
+ string lastTypeLogged = null;
+ foreach (TypeReference type in typeReferences)
+ {
+ this.ChangeTypeScope(type, shouldLog: type.FullName != lastTypeLogged);
+ lastTypeLogged = type.FullName;
+ }
+
+ // rewrite incompatible methods
+ IMethodRewriter[] methodRewriters = Constants.GetMethodRewriters().ToArray();
+ foreach (MethodDefinition method in this.GetMethods(module))
+ {
+ // skip methods with no rewritable method
+ bool hasMethodToRewrite = method.Body.Instructions.Any(op => (op.OpCode == OpCodes.Call || op.OpCode == OpCodes.Callvirt) && methodRewriters.Any(rewriter => rewriter.ShouldRewrite((MethodReference)op.Operand)));
+ if (!hasMethodToRewrite)
+ continue;
+
+ // rewrite method references
+ method.Body.SimplifyMacros();
+ ILProcessor cil = method.Body.GetILProcessor();
+ Instruction[] instructions = cil.Body.Instructions.ToArray();
+ foreach (Instruction op in instructions)
+ {
+ if (op.OpCode == OpCodes.Call || op.OpCode == OpCodes.Callvirt)
+ {
+ IMethodRewriter rewriter = methodRewriters.FirstOrDefault(p => p.ShouldRewrite((MethodReference)op.Operand));
+ if (rewriter != null)
+ {
+ MethodReference methodRef = (MethodReference)op.Operand;
+ this.Monitor.Log($"rewriting method reference {methodRef.DeclaringType.FullName}.{methodRef.Name}", LogLevel.Trace);
+ rewriter.Rewrite(module, cil, op, methodRef, this.AssemblyMap);
+ }
+ }
+ }
+ method.Body.OptimizeMacros();
+ }
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Get the correct reference to use for compatibility with the current platform.
+ /// The type reference to rewrite.
+ /// Whether to log a message.
+ private void ChangeTypeScope(TypeReference type, bool shouldLog)
+ {
+ // check skip conditions
+ if (type == null || type.FullName.StartsWith("System."))
+ return;
+
+ // get assembly
+ Assembly assembly;
+ if (!this.TypeAssemblies.TryGetValue(type.FullName, out assembly))
+ return;
+
+ // replace scope
+ AssemblyNameReference assemblyRef = this.AssemblyMap.TargetReferences[assembly];
+ if (shouldLog)
+ this.Monitor.Log($"redirecting {type.FullName} from {type.Scope.Name} to {assemblyRef.Name}", LogLevel.Trace);
+ type.Scope = assemblyRef;
+ }
+
+ /// Get all methods in a module.
+ /// The module to search.
+ private IEnumerable GetMethods(ModuleDefinition module)
+ {
+ return (
+ from type in module.GetTypes()
+ where type.HasMethods
+ from method in type.Methods
+ where method.HasBody
+ select method
+ );
+ }
+ }
+}
diff --git a/src/StardewModdingAPI/Framework/AssemblyRewriting/CachePaths.cs b/src/StardewModdingAPI/Framework/AssemblyRewriting/CachePaths.cs
new file mode 100644
index 00000000..17c4d188
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/AssemblyRewriting/CachePaths.cs
@@ -0,0 +1,33 @@
+namespace StardewModdingAPI.Framework.AssemblyRewriting
+{
+ /// Contains the paths for an assembly's cached data.
+ internal struct CachePaths
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// The directory path which contains the assembly.
+ public string Directory { get; }
+
+ /// The file path of the assembly file.
+ public string Assembly { get; }
+
+ /// The file path containing the MD5 hash for the assembly.
+ public string Hash { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The directory path which contains the assembly.
+ /// The file path of the assembly file.
+ /// The file path containing the MD5 hash for the assembly.
+ public CachePaths(string directory, string assembly, string hash)
+ {
+ this.Directory = directory;
+ this.Assembly = assembly;
+ this.Hash = hash;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs
new file mode 100644
index 00000000..51018b0b
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/ModAssemblyLoader.cs
@@ -0,0 +1,136 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Security.Cryptography;
+using Mono.Cecil;
+using StardewModdingAPI.AssemblyRewriters;
+using StardewModdingAPI.Framework.AssemblyRewriting;
+
+namespace StardewModdingAPI.Framework
+{
+ /// Preprocesses and loads mod assemblies.
+ internal class ModAssemblyLoader
+ {
+ /*********
+ ** Properties
+ *********/
+ /// The directory in which to cache data.
+ private readonly string CacheDirPath;
+
+ /// Metadata for mapping assemblies to the current .
+ private readonly PlatformAssemblyMap AssemblyMap;
+
+ /// Rewrites assembly types to match the current platform.
+ private readonly AssemblyTypeRewriter AssemblyTypeRewriter;
+
+ /// Encapsulates monitoring and logging.
+ private readonly IMonitor Monitor;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The cache directory.
+ /// The current game platform.
+ /// Encapsulates monitoring and logging.
+ public ModAssemblyLoader(string cacheDirPath, Platform targetPlatform, IMonitor monitor)
+ {
+ this.CacheDirPath = cacheDirPath;
+ this.Monitor = monitor;
+ this.AssemblyMap = Constants.GetAssemblyMap(targetPlatform);
+ this.AssemblyTypeRewriter = new AssemblyTypeRewriter(this.AssemblyMap, monitor);
+ }
+
+ /// Preprocess an assembly and cache the modified version.
+ /// The assembly file path.
+ public void ProcessAssembly(string assemblyPath)
+ {
+ // read assembly data
+ string assemblyFileName = Path.GetFileName(assemblyPath);
+ string assemblyDir = Path.GetDirectoryName(assemblyPath);
+ byte[] assemblyBytes = File.ReadAllBytes(assemblyPath);
+ string hash = $"SMAPI {Constants.Version}|" + string.Join("", MD5.Create().ComputeHash(assemblyBytes).Select(p => p.ToString("X2")));
+
+ // check cache
+ CachePaths cachePaths = this.GetCacheInfo(assemblyPath);
+ bool canUseCache = File.Exists(cachePaths.Assembly) && File.Exists(cachePaths.Hash) && hash == File.ReadAllText(cachePaths.Hash);
+
+ // process assembly if not cached
+ if (!canUseCache)
+ {
+ this.Monitor.Log($"Loading {assemblyFileName} for the first time; preprocessing...", LogLevel.Trace);
+
+ // read assembly definition
+ AssemblyDefinition assembly;
+ using (Stream readStream = new MemoryStream(assemblyBytes))
+ assembly = AssemblyDefinition.ReadAssembly(readStream);
+
+ // rewrite assembly to match platform
+ this.AssemblyTypeRewriter.RewriteAssembly(assembly);
+
+ // write cache
+ using (MemoryStream outStream = new MemoryStream())
+ {
+ // get assembly bytes
+ assembly.Write(outStream);
+ byte[] outBytes = outStream.ToArray();
+
+ // write assembly data
+ Directory.CreateDirectory(cachePaths.Directory);
+ File.WriteAllBytes(cachePaths.Assembly, outBytes);
+ File.WriteAllText(cachePaths.Hash, hash);
+
+ // copy any mdb/pdb files
+ foreach (string path in Directory.GetFiles(assemblyDir, "*.mdb").Concat(Directory.GetFiles(assemblyDir, "*.pdb")))
+ {
+ string filename = Path.GetFileName(path);
+ File.Copy(path, Path.Combine(cachePaths.Directory, filename), overwrite: true);
+ }
+ }
+ }
+ }
+
+ /// Load a preprocessed assembly.
+ /// The assembly file path.
+ public Assembly LoadCachedAssembly(string assemblyPath)
+ {
+ CachePaths cachePaths = this.GetCacheInfo(assemblyPath);
+ if (!File.Exists(cachePaths.Assembly))
+ throw new InvalidOperationException($"The assembly {assemblyPath} doesn't exist in the preprocessed cache.");
+ return Assembly.UnsafeLoadFrom(cachePaths.Assembly); // unsafe load allows DLLs downloaded from the Internet without the user needing to 'unblock' them
+ }
+
+ /// Resolve an assembly from its name.
+ /// The assembly name.
+ ///
+ /// This implementation returns the first loaded assembly which matches the short form of
+ /// the assembly name, to resolve assembly resolution issues when rewriting
+ /// assemblies (especially with Mono). Since this is meant to be called on ,
+ /// the implicit assumption is that loading the exact assembly failed.
+ ///
+ public Assembly ResolveAssembly(string name)
+ {
+ string shortName = name.Split(new[] { ',' }, 2).First(); // get simple name (without version and culture)
+ return AppDomain.CurrentDomain
+ .GetAssemblies()
+ .FirstOrDefault(p => p.GetName().Name == shortName);
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Get the cache details for an assembly.
+ /// The assembly file path.
+ private CachePaths GetCacheInfo(string assemblyPath)
+ {
+ string key = Path.GetFileNameWithoutExtension(assemblyPath);
+ string dirPath = Path.Combine(this.CacheDirPath, new DirectoryInfo(Path.GetDirectoryName(assemblyPath)).Name);
+ string cacheAssemblyPath = Path.Combine(dirPath, $"{key}.dll");
+ string cacheHashPath = Path.Combine(dirPath, $"{key}.hash");
+ return new CachePaths(dirPath, cacheAssemblyPath, cacheHashPath);
+ }
+ }
+}
diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs
index d31a1d39..e648ed64 100644
--- a/src/StardewModdingAPI/Program.cs
+++ b/src/StardewModdingAPI/Program.cs
@@ -9,6 +9,7 @@ using System.Windows.Forms;
#endif
using Microsoft.Xna.Framework.Graphics;
using Newtonsoft.Json;
+using StardewModdingAPI.AssemblyRewriters;
using StardewModdingAPI.Events;
using StardewModdingAPI.Framework;
using StardewModdingAPI.Inheritance;
@@ -23,16 +24,23 @@ namespace StardewModdingAPI
/*********
** Properties
*********/
- /// The full path to the Stardew Valley executable.
+ /// The target game platform.
+ private static readonly Platform TargetPlatform =
#if SMAPI_FOR_WINDOWS
- private static readonly string GameExecutablePath = Path.Combine(Constants.ExecutionPath, "Stardew Valley.exe");
+ Platform.Windows;
#else
- private static readonly string GameExecutablePath = Path.Combine(Constants.ExecutionPath, "StardewValley.exe");
+ Platform.Mono;
#endif
+ /// The full path to the Stardew Valley executable.
+ private static readonly string GameExecutablePath = Path.Combine(Constants.ExecutionPath, Program.TargetPlatform == Platform.Windows ? "Stardew Valley.exe" : "StardewValley.exe");
+
/// The full path to the folder containing mods.
private static readonly string ModPath = Path.Combine(Constants.ExecutionPath, "Mods");
+ /// The full path to the folder containing cached SMAPI data.
+ private static readonly string CachePath = Path.Combine(Program.ModPath, ".cache");
+
/// The log file to which to write messages.
private static readonly LogFileManager LogFile = new LogFileManager(Constants.LogPath);
@@ -126,6 +134,7 @@ namespace StardewModdingAPI
Program.Monitor.Log("Loading SMAPI...");
Console.Title = Constants.ConsoleTitle;
Program.VerifyPath(Program.ModPath);
+ Program.VerifyPath(Program.CachePath);
Program.VerifyPath(Constants.LogDir);
if (!File.Exists(Program.GameExecutablePath))
{
@@ -293,141 +302,175 @@ namespace StardewModdingAPI
private static void LoadMods()
{
Program.Monitor.Log("Loading mods...");
+
+ // get assembly loader
+ ModAssemblyLoader modAssemblyLoader = new ModAssemblyLoader(Program.CachePath, Program.TargetPlatform, Program.Monitor);
+ AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name);
+
+ // load mods
foreach (string directory in Directory.GetDirectories(Program.ModPath))
{
- foreach (string manifestPath in Directory.GetFiles(directory, "manifest.json"))
+ // ignore internal directory
+ if (new DirectoryInfo(directory).Name == ".cache")
+ continue;
+
+ // check for cancellation
+ if (Program.CancellationTokenSource.IsCancellationRequested)
{
- // check for cancellation
- if (Program.CancellationTokenSource.IsCancellationRequested)
+ Program.Monitor.Log("Shutdown requested; interrupting mod loading.", LogLevel.Error);
+ return;
+ }
+
+ // get helper
+ IModHelper helper = new ModHelper(directory);
+
+ // get manifest path
+ string manifestPath = Path.Combine(directory, "manifest.json");
+ string errorPrefix = $"Couldn't load mod for manifest '{manifestPath}'";
+ Manifest manifest;
+ try
+ {
+ // read manifest text
+ string json = File.ReadAllText(manifestPath);
+ if (string.IsNullOrEmpty(json))
{
- Program.Monitor.Log("Shutdown requested; interrupting mod loading.", LogLevel.Error);
- return;
- }
-
- IModHelper helper = new ModHelper(directory);
- string errorPrefix = $"Couldn't load mod for manifest '{manifestPath}'";
-
- // read manifest
- Manifest manifest;
- try
- {
- // read manifest text
- string json = File.ReadAllText(manifestPath);
- if (string.IsNullOrEmpty(json))
- {
- Program.Monitor.Log($"{errorPrefix}: manifest is empty.", LogLevel.Error);
- continue;
- }
-
- // deserialise manifest
- manifest = helper.ReadJsonFile("manifest.json");
- if (manifest == null)
- {
- Program.Monitor.Log($"{errorPrefix}: the manifest file does not exist.", LogLevel.Error);
- continue;
- }
- if (string.IsNullOrEmpty(manifest.EntryDll))
- {
- Program.Monitor.Log($"{errorPrefix}: manifest doesn't specify an entry DLL.", LogLevel.Error);
- continue;
- }
-
- // log deprecated fields
- if (manifest.UsedAuthourField)
- Program.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.Authour)}", "1.0", DeprecationLevel.Notice);
- }
- catch (Exception ex)
- {
- Program.Monitor.Log($"{errorPrefix}: manifest parsing failed.\n{ex.GetLogSummary()}", LogLevel.Error);
+ Program.Monitor.Log($"{errorPrefix}: manifest is empty.", LogLevel.Error);
continue;
}
- // validate version
- if (!string.IsNullOrWhiteSpace(manifest.MinimumApiVersion))
+ // deserialise manifest
+ manifest = helper.ReadJsonFile("manifest.json");
+ if (manifest == null)
{
- try
- {
- Version minVersion = new Version(manifest.MinimumApiVersion);
- if (minVersion.IsNewerThan(Constants.Version))
- {
- Program.Monitor.Log($"{errorPrefix}: this mod requires SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.", LogLevel.Error);
- continue;
- }
- }
- catch (FormatException ex) when (ex.Message.Contains("not a semantic version"))
- {
- Program.Monitor.Log($"{errorPrefix}: the mod specified an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.Version}.", LogLevel.Error);
- continue;
- }
+ Program.Monitor.Log($"{errorPrefix}: the manifest file does not exist.", LogLevel.Error);
+ continue;
+ }
+ if (string.IsNullOrEmpty(manifest.EntryDll))
+ {
+ Program.Monitor.Log($"{errorPrefix}: manifest doesn't specify an entry DLL.", LogLevel.Error);
+ continue;
}
- // create per-save directory
- if (manifest.PerSaveConfigs)
- {
- Program.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Notice);
- try
- {
- string psDir = Path.Combine(directory, "psconfigs");
- Directory.CreateDirectory(psDir);
- if (!Directory.Exists(psDir))
- {
- Program.Monitor.Log($"{errorPrefix}: couldn't create the per-save configuration directory ('psconfigs') requested by this mod. The failure reason is unknown.", LogLevel.Error);
- continue;
- }
- }
- catch (Exception ex)
- {
- Program.Monitor.Log($"{errorPrefix}: couldm't create the per-save configuration directory ('psconfigs') requested by this mod.\n{ex.GetLogSummary()}", LogLevel.Error);
- continue;
- }
- }
+ // log deprecated fields
+ if (manifest.UsedAuthourField)
+ Program.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.Authour)}", "1.0", DeprecationLevel.Notice);
+ }
+ catch (Exception ex)
+ {
+ Program.Monitor.Log($"{errorPrefix}: manifest parsing failed.\n{ex.GetLogSummary()}", LogLevel.Error);
+ continue;
+ }
- // load DLL & hook up mod
+ // validate version
+ if (!string.IsNullOrWhiteSpace(manifest.MinimumApiVersion))
+ {
try
{
- string assemblyPath = Path.Combine(directory, manifest.EntryDll);
- if (!File.Exists(assemblyPath))
+ Version minVersion = new Version(manifest.MinimumApiVersion);
+ if (minVersion.IsNewerThan(Constants.Version))
{
- Program.Monitor.Log($"{errorPrefix}: target DLL '{assemblyPath}' does not exist.", LogLevel.Error);
+ Program.Monitor.Log($"{errorPrefix}: this mod requires SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.", LogLevel.Error);
continue;
}
+ }
+ catch (FormatException ex) when (ex.Message.Contains("not a semantic version"))
+ {
+ Program.Monitor.Log($"{errorPrefix}: the mod specified an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.Version}.", LogLevel.Error);
+ continue;
+ }
+ }
- Assembly modAssembly = Assembly.UnsafeLoadFrom(assemblyPath);
- if (modAssembly.DefinedTypes.Count(x => x.BaseType == typeof(Mod)) > 0)
+ // create per-save directory
+ if (manifest.PerSaveConfigs)
+ {
+ Program.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Notice);
+ try
+ {
+ string psDir = Path.Combine(directory, "psconfigs");
+ Directory.CreateDirectory(psDir);
+ if (!Directory.Exists(psDir))
{
- TypeInfo modEntryType = modAssembly.DefinedTypes.First(x => x.BaseType == typeof(Mod));
- Mod modEntry = (Mod)modAssembly.CreateInstance(modEntryType.ToString());
- if (modEntry != null)
- {
- // track mod
- Program.ModRegistry.Add(manifest, modAssembly);
-
- // hook up mod
- modEntry.Manifest = manifest;
- modEntry.Helper = helper;
- modEntry.Monitor = new Monitor(manifest.Name, Program.LogFile) { ShowTraceInConsole = Program.DeveloperMode };
- modEntry.PathOnDisk = directory;
- Program.Monitor.Log($"Loaded mod: {modEntry.Manifest.Name} by {modEntry.Manifest.Author}, v{modEntry.Manifest.Version} | {modEntry.Manifest.Description}", LogLevel.Info);
- Program.ModsLoaded += 1;
- modEntry.Entry(); // deprecated since 1.0
- modEntry.Entry((ModHelper)modEntry.Helper); // deprecated since 1.1
- modEntry.Entry(modEntry.Helper); // deprecated since 1.1
-
- // raise deprecation warning for old Entry() method
- if (Program.DeprecationManager.IsVirtualMethodImplemented(modEntryType, typeof(Mod), nameof(Mod.Entry), new[] { typeof(object[]) }))
- Program.DeprecationManager.Warn(manifest.Name, $"an old version of {nameof(Mod)}.{nameof(Mod.Entry)}", "1.0", DeprecationLevel.Notice);
- if (Program.DeprecationManager.IsVirtualMethodImplemented(modEntryType, typeof(Mod), nameof(Mod.Entry), new[] { typeof(ModHelper) }))
- Program.DeprecationManager.Warn(manifest.Name, $"an old version of {nameof(Mod)}.{nameof(Mod.Entry)}", "1.1", DeprecationLevel.Notice);
- }
+ Program.Monitor.Log($"{errorPrefix}: couldn't create the per-save configuration directory ('psconfigs') requested by this mod. The failure reason is unknown.", LogLevel.Error);
+ continue;
}
- else
- Program.Monitor.Log($"{errorPrefix}: the mod DLL does not contain an implementation of the 'Mod' class.", LogLevel.Error);
}
catch (Exception ex)
{
- Program.Monitor.Log($"{errorPrefix}: an error occurred while loading the target DLL.\n{ex.GetLogSummary()}", LogLevel.Error);
+ Program.Monitor.Log($"{errorPrefix}: couldn't create the per-save configuration directory ('psconfigs') requested by this mod.\n{ex.GetLogSummary()}", LogLevel.Error);
+ continue;
}
}
+
+ // preprocess mod assemblies
+ {
+ bool succeeded = true;
+ foreach (string assemblyPath in Directory.GetFiles(directory, "*.dll"))
+ {
+ try
+ {
+ modAssemblyLoader.ProcessAssembly(assemblyPath);
+ }
+ catch (Exception ex)
+ {
+ Program.Monitor.Log($"{errorPrefix}: an error occurred while preprocessing '{Path.GetFileName(assemblyPath)}'.\n{ex.GetLogSummary()}", LogLevel.Error);
+ succeeded = false;
+ break;
+ }
+ }
+ if (!succeeded)
+ continue;
+ }
+
+ // load assembly
+ Assembly modAssembly;
+ try
+ {
+ string assemblyPath = Path.Combine(directory, manifest.EntryDll);
+ modAssembly = modAssemblyLoader.LoadCachedAssembly(assemblyPath);
+ if (modAssembly.DefinedTypes.Count(x => x.BaseType == typeof(Mod)) == 0)
+ {
+ Program.Monitor.Log($"{errorPrefix}: the mod DLL does not contain an implementation of the 'Mod' class.", LogLevel.Error);
+ continue;
+ }
+ }
+ catch (Exception ex)
+ {
+ Program.Monitor.Log($"{errorPrefix}: an error occurred while optimising the target DLL.\n{ex.GetLogSummary()}", LogLevel.Error);
+ continue;
+ }
+
+ // hook up mod
+ try
+ {
+ TypeInfo modEntryType = modAssembly.DefinedTypes.First(x => x.BaseType == typeof(Mod));
+ Mod modEntry = (Mod)modAssembly.CreateInstance(modEntryType.ToString());
+ if (modEntry != null)
+ {
+ // track mod
+ Program.ModRegistry.Add(manifest, modAssembly);
+
+ // hook up mod
+ modEntry.Manifest = manifest;
+ modEntry.Helper = helper;
+ modEntry.Monitor = new Monitor(manifest.Name, Program.LogFile) { ShowTraceInConsole = Program.DeveloperMode };
+ modEntry.PathOnDisk = directory;
+ Program.Monitor.Log($"Loaded mod: {modEntry.Manifest.Name} by {modEntry.Manifest.Author}, v{modEntry.Manifest.Version} | {modEntry.Manifest.Description}", LogLevel.Info);
+ Program.ModsLoaded += 1;
+ modEntry.Entry(); // deprecated since 1.0
+ modEntry.Entry((ModHelper)modEntry.Helper); // deprecated since 1.1
+ modEntry.Entry(modEntry.Helper); // deprecated since 1.1
+
+ // raise deprecation warning for old Entry() method
+ if (Program.DeprecationManager.IsVirtualMethodImplemented(modEntryType, typeof(Mod), nameof(Mod.Entry), new[] { typeof(object[]) }))
+ Program.DeprecationManager.Warn(manifest.Name, $"an old version of {nameof(Mod)}.{nameof(Mod.Entry)}", "1.0", DeprecationLevel.Notice);
+ if (Program.DeprecationManager.IsVirtualMethodImplemented(modEntryType, typeof(Mod), nameof(Mod.Entry), new[] { typeof(ModHelper) }))
+ Program.DeprecationManager.Warn(manifest.Name, $"an old version of {nameof(Mod)}.{nameof(Mod.Entry)}", "1.1", DeprecationLevel.Notice);
+ }
+ }
+ catch (Exception ex)
+ {
+ Program.Monitor.Log($"{errorPrefix}: an error occurred while loading the target DLL.\n{ex.GetLogSummary()}", LogLevel.Error);
+ }
}
// print result
diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj
index 0b6a185e..96eb038e 100644
--- a/src/StardewModdingAPI/StardewModdingAPI.csproj
+++ b/src/StardewModdingAPI/StardewModdingAPI.csproj
@@ -79,67 +79,24 @@
icon.ico
-
-
- $(HOME)/GOG Games/Stardew Valley/game
- $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley
-
- $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS
-
- C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley
- C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley
-
-
-
-
- $(DefineConstants);SMAPI_FOR_WINDOWS
-
-
-
- False
-
-
- False
-
-
- False
-
-
- False
-
-
- $(GamePath)\Stardew Valley.exe
- False
-
-
- $(GamePath)\xTile.dll
- False
- False
-
-
-
-
-
- $(DefineConstants);SMAPI_FOR_UNIX
-
-
-
- $(GamePath)\MonoGame.Framework.dll
- False
- False
-
-
- $(GamePath)\StardewValley.exe
- False
-
-
- $(GamePath)\xTile.dll
- False
-
-
-
-
+
+
+ ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll
+ True
+
+
+ ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Mdb.dll
+ True
+
+
+ ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll
+ True
+
+
+ ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Rocks.dll
+ True
+
..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll
True
@@ -197,9 +154,12 @@
+
+
+
@@ -238,7 +198,6 @@
-
Always
@@ -256,19 +215,17 @@
false
+
+
+ {10db0676-9fc1-4771-a2c8-e2519f091e49}
+ StardewModdingAPI.AssemblyRewriters
+
+
-
-
-
-
- This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
-
-
-
@@ -279,10 +236,14 @@
$(GamePath)\StardewModdingAPI.exe
$(GamePath)
-
+
-
-
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/src/StardewModdingAPI/packages.config b/src/StardewModdingAPI/packages.config
index 0aec11c6..e5fa3c3a 100644
--- a/src/StardewModdingAPI/packages.config
+++ b/src/StardewModdingAPI/packages.config
@@ -1,6 +1,5 @@
-
-
+
\ No newline at end of file
diff --git a/src/dependencies.targets b/src/dependencies.targets
new file mode 100644
index 00000000..d5428967
--- /dev/null
+++ b/src/dependencies.targets
@@ -0,0 +1,62 @@
+
+
+
+ $(HOME)/GOG Games/Stardew Valley/game
+ $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley
+
+ $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS
+
+ C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley
+ C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley
+
+
+
+
+ $(DefineConstants);SMAPI_FOR_WINDOWS
+
+
+
+ False
+
+
+ False
+
+
+ False
+
+
+ False
+
+
+ $(GamePath)\Stardew Valley.exe
+ False
+
+
+ $(GamePath)\xTile.dll
+ False
+ False
+
+
+
+
+
+ $(DefineConstants);SMAPI_FOR_UNIX
+
+
+
+ $(GamePath)\MonoGame.Framework.dll
+ False
+ False
+
+
+ $(GamePath)\StardewValley.exe
+ False
+
+
+ $(GamePath)\xTile.dll
+ False
+
+
+
+
+
\ No newline at end of file