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; } } }