diff --git a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs
index 1e490448..b01d8b21 100644
--- a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs
+++ b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs
@@ -6,18 +6,17 @@ using System.Linq;
using System.Management;
#endif
using System.Runtime.InteropServices;
+using StardewModdingAPI.Toolkit.Utilities;
-namespace StardewModdingAPI.Toolkit.Utilities
+namespace StardewModdingAPI.Toolkit.Framework
{
- /// Provides methods for fetching environment information.
- public static class EnvironmentUtility
+ /// Provides low-level methods for fetching environment information.
+ /// This is used by the SMAPI core before the toolkit DLL is available; most code should use instead.
+ internal static class LowLevelEnvironmentUtility
{
/*********
** Fields
*********/
- /// The cached platform.
- private static Platform? CachedPlatform;
-
/// Get the OS name from the system uname command.
/// The buffer to fill with the resulting string.
[DllImport("libc")]
@@ -28,16 +27,32 @@ namespace StardewModdingAPI.Toolkit.Utilities
** Public methods
*********/
/// Detect the current OS.
- public static Platform DetectPlatform()
+ public static string DetectPlatform()
{
- return EnvironmentUtility.CachedPlatform ??= EnvironmentUtility.DetectPlatformImpl();
+ switch (Environment.OSVersion.Platform)
+ {
+ case PlatformID.MacOSX:
+ return nameof(Platform.Mac);
+
+ case PlatformID.Unix when LowLevelEnvironmentUtility.IsRunningAndroid():
+ return nameof(Platform.Android);
+
+ case PlatformID.Unix when LowLevelEnvironmentUtility.IsRunningMac():
+ return nameof(Platform.Mac);
+
+ case PlatformID.Unix:
+ return nameof(Platform.Linux);
+
+ default:
+ return nameof(Platform.Windows);
+ }
}
/// Get the human-readable OS name and version.
/// The current platform.
[SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")]
- public static string GetFriendlyPlatformName(Platform platform)
+ public static string GetFriendlyPlatformName(string platform)
{
#if SMAPI_FOR_WINDOWS
try
@@ -54,11 +69,11 @@ namespace StardewModdingAPI.Toolkit.Utilities
string name = Environment.OSVersion.ToString();
switch (platform)
{
- case Platform.Android:
+ case nameof(Platform.Android):
name = $"Android {name}";
break;
- case Platform.Mac:
+ case nameof(Platform.Mac):
name = $"MacOS {name}";
break;
}
@@ -67,46 +82,24 @@ namespace StardewModdingAPI.Toolkit.Utilities
/// Get the name of the Stardew Valley executable.
/// The current platform.
- public static string GetExecutableName(Platform platform)
+ public static string GetExecutableName(string platform)
{
- return platform == Platform.Windows
+ return platform == nameof(Platform.Windows)
? "Stardew Valley.exe"
: "StardewValley.exe";
}
/// Get whether the platform uses Mono.
/// The current platform.
- public static bool IsMono(this Platform platform)
+ public static bool IsMono(string platform)
{
- return platform == Platform.Linux || platform == Platform.Mac;
+ return platform == nameof(Platform.Linux) || platform == nameof(Platform.Mac);
}
/*********
** Private methods
*********/
- /// Detect the current OS.
- private static Platform DetectPlatformImpl()
- {
- switch (Environment.OSVersion.Platform)
- {
- case PlatformID.MacOSX:
- return Platform.Mac;
-
- case PlatformID.Unix when EnvironmentUtility.IsRunningAndroid():
- return Platform.Android;
-
- case PlatformID.Unix when EnvironmentUtility.IsRunningMac():
- return Platform.Mac;
-
- case PlatformID.Unix:
- return Platform.Linux;
-
- default:
- return Platform.Windows;
- }
- }
-
/// Detect whether the code is running on Android.
///
/// This code is derived from https://stackoverflow.com/a/47521647/262123. It detects Android by calling the
@@ -149,7 +142,7 @@ namespace StardewModdingAPI.Toolkit.Utilities
try
{
buffer = Marshal.AllocHGlobal(8192);
- if (EnvironmentUtility.uname(buffer) == 0)
+ if (LowLevelEnvironmentUtility.uname(buffer) == 0)
{
string os = Marshal.PtrToStringAnsi(buffer);
return os == "Darwin";
diff --git a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs
new file mode 100644
index 00000000..4ef578f7
--- /dev/null
+++ b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using StardewModdingAPI.Toolkit.Framework;
+
+namespace StardewModdingAPI.Toolkit.Utilities
+{
+ /// Provides methods for fetching environment information.
+ public static class EnvironmentUtility
+ {
+ /*********
+ ** Fields
+ *********/
+ /// The cached platform.
+ private static Platform? CachedPlatform;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Detect the current OS.
+ public static Platform DetectPlatform()
+ {
+ Platform? platform = EnvironmentUtility.CachedPlatform;
+
+ if (platform == null)
+ {
+ string rawPlatform = LowLevelEnvironmentUtility.DetectPlatform();
+ EnvironmentUtility.CachedPlatform = platform = (Platform)Enum.Parse(typeof(Platform), rawPlatform, ignoreCase: true);
+ }
+
+ return platform.Value;
+ }
+
+
+ /// Get the human-readable OS name and version.
+ /// The current platform.
+ [SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")]
+ public static string GetFriendlyPlatformName(Platform platform)
+ {
+ return LowLevelEnvironmentUtility.GetFriendlyPlatformName(platform.ToString());
+ }
+
+ /// Get the name of the Stardew Valley executable.
+ /// The current platform.
+ public static string GetExecutableName(Platform platform)
+ {
+ return LowLevelEnvironmentUtility.GetExecutableName(platform.ToString());
+ }
+
+ /// Get whether the platform uses Mono.
+ /// The current platform.
+ public static bool IsMono(this Platform platform)
+ {
+ return LowLevelEnvironmentUtility.IsMono(platform.ToString());
+ }
+ }
+}
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index c1c99150..b7977fb7 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -5,11 +5,42 @@ using System.Reflection;
using StardewModdingAPI.Enums;
using StardewModdingAPI.Framework;
using StardewModdingAPI.Framework.ModLoading;
+using StardewModdingAPI.Toolkit.Framework;
using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
namespace StardewModdingAPI
{
+ /// Contains constants that are accessed before the game itself has been loaded.
+ /// Most code should use instead of this class directly.
+ internal static class EarlyConstants
+ {
+ //
+ // Note: this class *must not* depend on any external DLL beyond .NET Framework itself.
+ // That includes the game or SMAPI toolkit, since it's accessed before those are loaded.
+ //
+ // Adding an external dependency may seem to work in some cases, but will prevent SMAPI
+ // from showing a human-readable error if the game isn't available. To test this, just
+ // rename "Stardew Valley.exe" in the game folder; you should see an error like "Oops!
+ // SMAPI can't find the game", not a technical exception.
+ //
+
+ /*********
+ ** Accessors
+ *********/
+ /// The path to the game folder.
+ public static string ExecutionPath { get; } = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+
+ /// The absolute path to the folder containing SMAPI's internal files.
+ public static readonly string InternalFilesPath = Path.Combine(EarlyConstants.ExecutionPath, "smapi-internal");
+
+ /// The target game platform.
+ internal static GamePlatform Platform { get; } = (GamePlatform)Enum.Parse(typeof(GamePlatform), LowLevelEnvironmentUtility.DetectPlatform());
+
+ /// The game's assembly name.
+ internal static string GameAssemblyName => EarlyConstants.Platform == GamePlatform.Windows ? "Stardew Valley" : "StardewValley";
+ }
+
/// Contains SMAPI's constants and assumptions.
public static class Constants
{
@@ -29,10 +60,10 @@ namespace StardewModdingAPI
public static ISemanticVersion MaximumGameVersion { get; } = null;
/// The target game platform.
- public static GamePlatform TargetPlatform => (GamePlatform)Constants.Platform;
+ public static GamePlatform TargetPlatform { get; } = EarlyConstants.Platform;
/// The path to the game folder.
- public static string ExecutionPath { get; } = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+ public static string ExecutionPath { get; } = EarlyConstants.ExecutionPath;
/// The directory path containing Stardew Valley's app data.
public static string DataPath { get; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley");
@@ -67,7 +98,7 @@ namespace StardewModdingAPI
internal const string GamePerformanceCounterName = "";
/// The absolute path to the folder containing SMAPI's internal files.
- internal static readonly string InternalFilesPath = Program.DllSearchPath;
+ internal static readonly string InternalFilesPath = EarlyConstants.InternalFilesPath;
/// The file path for the SMAPI configuration file.
internal static string ApiConfigPath => Path.Combine(Constants.InternalFilesPath, "config.json");
@@ -105,11 +136,8 @@ namespace StardewModdingAPI
/// The game's current semantic version.
internal static ISemanticVersion GameVersion { get; } = new GameVersion(Game1.version);
- /// The target game platform.
- internal static Platform Platform { get; } = EnvironmentUtility.DetectPlatform();
-
- /// The game's assembly name.
- internal static string GameAssemblyName => Constants.Platform == Platform.Windows ? "Stardew Valley" : "StardewValley";
+ /// The target game platform as a SMAPI toolkit constant.
+ internal static Platform Platform { get; } = (Platform)Constants.TargetPlatform;
/// The language code for non-translated mod assets.
internal static LocalizedContentManager.LanguageCode DefaultLanguage { get; } = LocalizedContentManager.LanguageCode.en;
diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs
index a8f20c69..05251070 100644
--- a/src/SMAPI/Program.cs
+++ b/src/SMAPI/Program.cs
@@ -1,13 +1,9 @@
using System;
-using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
-#if SMAPI_FOR_WINDOWS
-#endif
using StardewModdingAPI.Framework;
-using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI
{
@@ -18,9 +14,7 @@ namespace StardewModdingAPI
** Fields
*********/
/// The absolute path to search for SMAPI's internal DLLs.
- /// We can't use directly, since depends on DLLs loaded from this folder.
- [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "The assembly location is never null in this context.")]
- internal static readonly string DllSearchPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "smapi-internal");
+ internal static readonly string DllSearchPath = EarlyConstants.InternalFilesPath;
/*********
@@ -37,10 +31,9 @@ namespace StardewModdingAPI
Program.AssertGameVersion();
Program.Start(args);
}
- catch (BadImageFormatException ex) when (ex.FileName == "StardewValley" || ex.FileName == "Stardew Valley") // NOTE: don't use StardewModdingAPI.Constants here, assembly resolution isn't hooked up at this point
+ catch (BadImageFormatException ex) when (ex.FileName == "StardewValley" || ex.FileName == "Stardew Valley") // don't use EarlyConstants.GameAssemblyName, since we want to check both possible names
{
- string executableName = Program.GetExecutableAssemblyName();
- Console.WriteLine($"SMAPI failed to initialize because your game's {executableName}.exe seems to be invalid.\nThis may be a pirated version which modified the executable in an incompatible way; if so, you can try a different download or buy a legitimate version.\n\nTechnical details:\n{ex}");
+ Console.WriteLine($"SMAPI failed to initialize because your game's {ex.FileName}.exe seems to be invalid.\nThis may be a pirated version which modified the executable in an incompatible way; if so, you can try a different download or buy a legitimate version.\n\nTechnical details:\n{ex}");
}
catch (Exception ex)
{
@@ -76,11 +69,10 @@ namespace StardewModdingAPI
}
/// Assert that the game is available.
- /// This must be checked *before* any references to , and this method should not reference itself to avoid errors in Mono.
+ /// This must be checked *before* any references to , and this method should not reference itself to avoid errors in Mono or when the game isn't present.
private static void AssertGamePresent()
{
- string gameAssemblyName = Program.GetExecutableAssemblyName();
- if (Type.GetType($"StardewValley.Game1, {gameAssemblyName}", throwOnError: false) == null)
+ if (Type.GetType($"StardewValley.Game1, {EarlyConstants.GameAssemblyName}", throwOnError: false) == null)
Program.PrintErrorAndExit("Oops! SMAPI can't find the game. Make sure you're running StardewModdingAPI.exe in your game folder. See the readme.txt file for details.");
}
@@ -100,14 +92,6 @@ namespace StardewModdingAPI
// max version
else if (Constants.MaximumGameVersion != null && Constants.GameVersion.IsNewerThan(Constants.MaximumGameVersion))
Program.PrintErrorAndExit($"Oops! You're running Stardew Valley {Constants.GameVersion}, but this version of SMAPI is only compatible up to Stardew Valley {Constants.MaximumGameVersion}. Please check for a newer version of SMAPI: https://smapi.io.");
-
- }
-
- /// Get the game's executable assembly name.
- private static string GetExecutableAssemblyName()
- {
- Platform platform = EnvironmentUtility.DetectPlatform();
- return platform == Platform.Windows ? "Stardew Valley" : "StardewValley";
}
/// Initialize SMAPI and launch the game.
diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj
index a3dbf52f..7d2b8199 100644
--- a/src/SMAPI/SMAPI.csproj
+++ b/src/SMAPI/SMAPI.csproj
@@ -59,8 +59,11 @@
+
+
+