using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Mono.Cecil;
using StardewModdingAPI.Enums;
using StardewModdingAPI.Framework;
#if SMAPI_DEPRECATED
using StardewModdingAPI.Framework.Deprecations;
#endif
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 GamePath { 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.GamePath, "smapi-internal");
/// The target game platform.
internal static GamePlatform Platform { get; } = (GamePlatform)Enum.Parse(typeof(GamePlatform), LowLevelEnvironmentUtility.DetectPlatform());
/// The game framework running the game.
internal static GameFramework GameFramework { get; } = GameFramework.MonoGame;
/// The game's assembly name.
internal static string GameAssemblyName { get; } = "Stardew Valley";
/// The value which should appear in the SMAPI log, if any.
internal static int? LogScreenId { get; set; }
/// SMAPI's current raw semantic version.
internal static string RawApiVersion = "3.17.2";
}
/// Contains SMAPI's constants and assumptions.
public static class Constants
{
/*********
** Accessors
*********/
/****
** Public
****/
/// SMAPI's current semantic version.
public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion(EarlyConstants.RawApiVersion);
/// The minimum supported version of Stardew Valley.
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.6");
/// The maximum supported version of Stardew Valley, if any.
public static ISemanticVersion? MaximumGameVersion { get; } = null;
/// The target game platform.
public static GamePlatform TargetPlatform { get; } = EarlyConstants.Platform;
/// The game framework running the game.
public static GameFramework GameFramework { get; } = EarlyConstants.GameFramework;
#if SMAPI_DEPRECATED
/// The path to the game folder.
[Obsolete($"Use {nameof(Constants)}.{nameof(GamePath)} instead. This property will be removed in SMAPI 4.0.0.")]
public static string ExecutionPath
{
get
{
SCore.DeprecationManager.Warn(
source: null,
nounPhrase: $"{nameof(Constants)}.{nameof(Constants.ExecutionPath)}",
version: "3.14.0",
severity: DeprecationLevel.Info
);
return Constants.GamePath;
}
}
#endif
/// The path to the game folder.
public static string GamePath { get; } = EarlyConstants.GamePath;
/// The path to the game's Content folder.
public static string ContentPath { get; } = Constants.GetContentFolderPath();
/// The directory path containing Stardew Valley's app data.
public static string DataPath { get; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley");
/// The directory path in which error logs should be stored.
public static string LogDir { get; } = Path.Combine(Constants.DataPath, "ErrorLogs");
/// The directory path where all saves are stored.
public static string SavesPath { get; } = Path.Combine(Constants.DataPath, "Saves");
/// The name of the current save folder (if save info is available, regardless of whether the save file exists yet).
public static string? SaveFolderName => Constants.GetSaveFolderName();
/// The absolute path to the current save folder (if save info is available and the save file exists).
public static string? CurrentSavePath => Constants.GetSaveFolderPathIfExists();
/****
** Internal
****/
/// Whether SMAPI was compiled in debug mode.
internal const bool IsDebugBuild =
#if DEBUG
true;
#else
false;
#endif
/// The URL of the SMAPI home page.
internal const string HomePageUrl = "https://smapi.io";
/// The absolute path to the folder containing SMAPI's internal files.
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");
/// The file path for the overrides file for , which is applied over it.
internal static string ApiUserConfigPath => Path.Combine(Constants.InternalFilesPath, "config.user.json");
/// The file path for the SMAPI metadata file.
internal static string ApiMetadataPath => Path.Combine(Constants.InternalFilesPath, "metadata.json");
/// The filename prefix used for all SMAPI logs.
internal static string LogNamePrefix { get; } = "SMAPI-";
/// The filename for SMAPI's main log, excluding the .
internal static string LogFilename { get; } = $"{Constants.LogNamePrefix}latest";
/// The filename extension for SMAPI log files.
internal static string LogExtension { get; } = "txt";
/// The file path for the log containing the previous fatal crash, if any.
internal static string FatalCrashLog => Path.Combine(Constants.LogDir, "SMAPI-crash.txt");
/// The file path which stores a fatal crash message for the next run.
internal static string FatalCrashMarker => Path.Combine(Constants.InternalFilesPath, "StardewModdingAPI.crash.marker");
/// The file path which stores the detected update version for the next run.
internal static string UpdateMarker => Path.Combine(Constants.InternalFilesPath, "StardewModdingAPI.update.marker");
/// The default full path to search for mods.
internal static string DefaultModsPath { get; } = Path.Combine(Constants.GamePath, "Mods");
/// The actual full path to search for mods.
internal static string ModsPath { get; set; } = null!; // initialized early during SMAPI startup
/// The game's current semantic version.
internal static ISemanticVersion GameVersion { get; } = new GameVersion(Game1.version);
/// The target game platform as a SMAPI toolkit constant.
internal static Platform Platform { get; } = (Platform)Constants.TargetPlatform;
/*********
** Internal methods
*********/
/// Get the SMAPI version to recommend for an older game version, if any.
/// The game version to search.
/// Returns the compatible SMAPI version, or null if none was found.
internal static ISemanticVersion? GetCompatibleApiVersion(ISemanticVersion version)
{
// This covers all officially supported public game updates. It might seem like version
// ranges would be better, but the given SMAPI versions may not be compatible with
// intermediate unlisted versions (e.g. private beta updates).
//
// Nonstandard versions are normalized by GameVersion (e.g. 1.07 => 1.0.7).
switch (version.ToString())
{
case "1.4.1":
case "1.4.0":
return new SemanticVersion("3.0.1");
case "1.3.36":
return new SemanticVersion("2.11.2");
case "1.3.33":
case "1.3.32":
return new SemanticVersion("2.10.2");
case "1.3.28":
return new SemanticVersion("2.7.0");
case "1.2.33":
case "1.2.32":
case "1.2.31":
case "1.2.30":
return new SemanticVersion("2.5.5");
case "1.2.29":
case "1.2.28":
case "1.2.27":
case "1.2.26":
return new SemanticVersion("1.13.1");
case "1.1.1":
case "1.1.0":
return new SemanticVersion("1.9.0");
case "1.0.7.1":
case "1.0.7":
case "1.0.6":
case "1.0.5.2":
case "1.0.5.1":
case "1.0.5":
case "1.0.4":
case "1.0.3":
case "1.0.2":
case "1.0.1":
case "1.0.0":
return new SemanticVersion("0.40.0");
default:
return null;
}
}
/// Configure the Mono.Cecil assembly resolver.
/// The assembly resolver.
internal static void ConfigureAssemblyResolver(AssemblyDefinitionResolver resolver)
{
// add search paths
resolver.TryAddSearchDirectory(Constants.GamePath);
resolver.TryAddSearchDirectory(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 was separate on Windows only before Stardew Valley 1.5.5.
resolver.AddWithExplicitNames(AssemblyDefinition.ReadAssembly(typeof(Game1).Assembly.Location), "StardewValley", "Stardew Valley", "Netcode");
}
/// Get metadata for mapping assemblies to the current platform.
/// The target game platform.
internal static PlatformAssemblyMap GetAssemblyMap(Platform targetPlatform)
{
var removeAssemblyReferences = new List();
var targetAssemblies = new List();
// get assembly renamed in SMAPI 3.0
removeAssemblyReferences.Add("StardewModdingAPI.Toolkit.CoreInterfaces");
targetAssemblies.Add(typeof(StardewModdingAPI.IManifest).Assembly);
// XNA Framework before Stardew Valley 1.5.5
removeAssemblyReferences.AddRange(new[]
{
"Microsoft.Xna.Framework",
"Microsoft.Xna.Framework.Game",
"Microsoft.Xna.Framework.Graphics",
"Microsoft.Xna.Framework.Xact"
});
targetAssemblies.Add(
typeof(Microsoft.Xna.Framework.Vector2).Assembly
);
// `Netcode.dll` merged into the game assembly in Stardew Valley 1.5.5
removeAssemblyReferences.Add(
"Netcode"
);
// Stardew Valley reference
removeAssemblyReferences.Add("StardewValley");
targetAssemblies.Add(typeof(StardewValley.Game1).Assembly);
return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences.ToArray(), targetAssemblies.ToArray());
}
/*********
** Private methods
*********/
/// Get the absolute path to the game's Content folder.
private static string GetContentFolderPath()
{
//
// We can't use Path.Combine(Constants.GamePath, Game1.content.RootDirectory) here,
// since Game1.content isn't initialized until later in the game startup.
//
string gamePath = EarlyConstants.GamePath;
// most platforms
if (EarlyConstants.Platform != GamePlatform.Mac)
return Path.Combine(gamePath, "Content");
// macOS
string[] paths = new[]
{
// GOG
// - game: Stardew Valley.app/Contents/MacOS
// - content: Stardew Valley.app/Resources/Content
"../../Resources/Content",
// Steam
// - game: StardewValley/Contents/MacOS
// - content: StardewValley/Contents/Resources/Content
"../Resources/Content"
}
.Select(path => Path.GetFullPath(Path.Combine(gamePath, path)))
.ToArray();
foreach (string path in paths)
{
if (Directory.Exists(path))
return path;
}
return paths.Last();
}
/// Get the name of the save folder, if any.
private static string? GetSaveFolderName()
{
return Constants.GetSaveFolder()?.Name;
}
/// Get the absolute path to the current save folder, if any.
private static string? GetSaveFolderPathIfExists()
{
DirectoryInfo? saveFolder = Constants.GetSaveFolder();
return saveFolder?.Exists == true
? saveFolder.FullName
: null;
}
/// Get the current save folder, if any.
private static DirectoryInfo? GetSaveFolder()
{
// save not available
if (Context.LoadStage == LoadStage.None)
return null;
// get basic info
string rawSaveName = Game1.GetSaveGameName(set_value: false);
ulong saveID = Context.LoadStage == LoadStage.SaveParsed
? SaveGame.loaded.uniqueIDForThisGame
: Game1.uniqueIDForThisGame;
// get best match (accounting for rare case where folder name isn't sanitized)
DirectoryInfo? folder = null;
foreach (string saveName in new[] { rawSaveName, new string(rawSaveName.Where(char.IsLetterOrDigit).ToArray()) })
{
try
{
folder = new DirectoryInfo(Path.Combine(Constants.SavesPath, $"{saveName}_{saveID}"));
if (folder.Exists)
return folder;
}
catch (ArgumentException)
{
// ignore invalid path
}
}
// if save doesn't exist yet, return the default one we expect to be created
return folder;
}
/// Get a display label for the game's build number.
internal static string GetBuildVersionLabel()
{
string version = typeof(Game1).Assembly.GetName().Version?.ToString() ?? "unknown";
if (version.StartsWith($"{Game1.version}."))
version = version.Substring(Game1.version.Length + 1);
return version;
}
}
}