using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Internal; using StardewValley; namespace StardewModdingAPI { /// Contains SMAPI's constants and assumptions. public static class Constants { /********* ** Properties *********/ /// The directory path containing the current save's data (if a save is loaded). private static string RawSavePath => Context.IsSaveLoaded ? Path.Combine(Constants.SavesPath, Constants.GetSaveFolderName()) : null; /// Whether the directory containing the current save's data exists on disk. private static bool SavePathReady => Context.IsSaveLoaded && Directory.Exists(Constants.RawSavePath); /// Maps vendor keys (like Nexus) to their mod URL template (where {0} is the mod ID). This doesn't affect update checks, which defer to the remote web API. private static readonly IDictionary VendorModUrls = new Dictionary(StringComparer.InvariantCultureIgnoreCase) { ["Chucklefish"] = "https://community.playstarbound.com/resources/{0}", ["GitHub"] = "https://github.com/{0}/releases", ["Nexus"] = "https://www.nexusmods.com/stardewvalley/mods/{0}" }; /********* ** Accessors *********/ /**** ** Public ****/ /// SMAPI's current semantic version. public static ISemanticVersion ApiVersion { get; } /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; /// The target game platform. public static GamePlatform TargetPlatform => (GamePlatform)Constants.Platform; /// The path to the game folder. public static string ExecutionPath { get; } = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); /// 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 directory name containing the current save's data (if a save is loaded and the directory exists). public static string SaveFolderName => Context.IsSaveLoaded ? Constants.GetSaveFolderName() : ""; /// The directory path containing the current save's data (if a save is loaded and the directory exists). public static string CurrentSavePath => Constants.SavePathReady ? Path.Combine(Constants.SavesPath, Constants.GetSaveFolderName()) : ""; /**** ** Internal ****/ /// SMAPI's current semantic version as a mod toolkit version. internal static Toolkit.ISemanticVersion ApiVersionForToolkit { get; } /// The URL of the SMAPI home page. internal const string HomePageUrl = "https://smapi.io"; /// The file path for the SMAPI configuration file. internal static string ApiConfigPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.config.json"); /// The file path for the SMAPI metadata file. internal static string ApiMetadataPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.metadata.json"); /// The file path to the log where the latest output should be saved. internal static string DefaultLogPath => Path.Combine(Constants.LogDir, "SMAPI-latest.txt"); /// A copy of the log leading up to 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.ExecutionPath, "StardewModdingAPI.crash.marker"); /// The file path which stores the detected update version for the next run. internal static string UpdateMarker => Path.Combine(Constants.ExecutionPath, "StardewModdingAPI.update.marker"); /// The full path to the folder containing mods. internal static string ModPath { get; } = Path.Combine(Constants.ExecutionPath, "Mods"); /// The game's current semantic version. internal static ISemanticVersion GameVersion { get; } = new GameVersion(Constants.GetGameVersion()); /// The target game platform. internal static Platform Platform { get; } = EnvironmentUtility.DetectPlatform(); /********* ** Internal methods *********/ /// Initialise the static values. static Constants() { Constants.ApiVersionForToolkit = new Toolkit.SemanticVersion("2.6-beta.15"); Constants.MinimumGameVersion = new GameVersion("1.3.13"); Constants.ApiVersion = new SemanticVersion(Constants.ApiVersionForToolkit); } /// 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.Linux: case Platform.Mac: removeAssemblyReferences = new[] { "Netcode", "Stardew Valley", "Microsoft.Xna.Framework", "Microsoft.Xna.Framework.Game", "Microsoft.Xna.Framework.Graphics" }; targetAssemblies = new[] { typeof(StardewValley.Game1).Assembly, // note: includes Netcode types on Linux/Mac typeof(Microsoft.Xna.Framework.Vector2).Assembly }; break; case Platform.Windows: removeAssemblyReferences = new[] { "StardewValley", "MonoGame.Framework" }; targetAssemblies = new[] { typeof(Netcode.NetBool).Assembly, 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 an update URL for an update key (if valid). /// The update key. internal static string GetUpdateUrl(string updateKey) { string[] parts = updateKey.Split(new[] { ':' }, 2); if (parts.Length != 2) return null; string vendorKey = parts[0].Trim(); string modID = parts[1].Trim(); if (Constants.VendorModUrls.TryGetValue(vendorKey, out string urlTemplate)) return string.Format(urlTemplate, modID); return null; } /********* ** Private methods *********/ /// Get the name of a save directory for the current player. private static string GetSaveFolderName() { string prefix = new string(Game1.player.Name.Where(char.IsLetterOrDigit).ToArray()); return $"{prefix}_{Game1.uniqueIDForThisGame}"; } /// Get the game's current version string. private static string GetGameVersion() { // we need reflection because it's a constant, so SMAPI's references to it are inlined at compile-time FieldInfo field = typeof(Game1).GetField(nameof(Game1.version), BindingFlags.Public | BindingFlags.Static); if (field == null) throw new InvalidOperationException($"The {nameof(Game1)}.{nameof(Game1.version)} field could not be found."); return (string)field.GetValue(null); } } }