using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.AssemblyRewriters.Finders; using StardewModdingAPI.AssemblyRewriters.Rewriters; using StardewModdingAPI.AssemblyRewriters.Rewriters.Wrappers; using StardewModdingAPI.Events; 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); /********* ** Accessors *********/ /**** ** Public ****/ /// SMAPI's current semantic version. public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(1, 11, 0); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.26"); /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; /// 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 ****/ /// The GitHub repository to check for updates. internal const string GitHubRepository = "Pathoschild/SMAPI"; /// 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 to the log where the latest output should be saved. internal static string DefaultLogPath => Path.Combine(Constants.LogDir, "SMAPI-latest.txt"); /// 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; } = Constants.GetGameVersion(); /// The target game platform. internal static Platform TargetPlatform { get; } = #if SMAPI_FOR_WINDOWS Platform.Windows; #else Platform.Mono; #endif /********* ** 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 rewriters which detect or fix incompatible CIL instructions in mod assemblies. internal static IEnumerable GetRewriters() { return new IInstructionRewriter[] { /**** ** Finders throw an exception when incompatible code is found. ****/ // changes in Stardew Valley 1.2 (with no rewriters) new FieldFinder("StardewValley.Item", "set_Name"), // APIs removed in SMAPI 1.9 new TypeFinder("StardewModdingAPI.Advanced.ConfigFile"), new TypeFinder("StardewModdingAPI.Advanced.IConfigFile"), new TypeFinder("StardewModdingAPI.Entities.SPlayer"), new TypeFinder("StardewModdingAPI.Extensions"), new TypeFinder("StardewModdingAPI.Inheritance.SGame"), new TypeFinder("StardewModdingAPI.Inheritance.SObject"), new TypeFinder("StardewModdingAPI.LogWriter"), new TypeFinder("StardewModdingAPI.Manifest"), new TypeFinder("StardewModdingAPI.Version"), new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawDebug"), new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawTick"), new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderHudEventNoCheck"), new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderGuiEventNoCheck"), new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderHudEventNoCheck"), new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderGuiEventNoCheck"), /**** ** Rewriters change CIL as needed to fix incompatible code ****/ // crossplatform new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchWrapper), onlyIfPlatformChanged: true), // Stardew Valley 1.2 new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.activeClickableMenu)), new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.currentMinigame)), new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.gameMode)), new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.player)), new FieldReplaceRewriter(typeof(Game1), "borderFont", nameof(Game1.smallFont)), new FieldReplaceRewriter(typeof(Game1), "smoothFont", nameof(Game1.smallFont)), // SMAPI 1.9 new TypeReferenceRewriter("StardewModdingAPI.Inheritance.ItemStackChange", typeof(ItemStackChange)) }; } /// Get game current version as it should be displayed to players. /// The semantic game version. internal static ISemanticVersion GetGameDisplayVersion(ISemanticVersion version) { switch (version.ToString()) { case "1.1.1": return new SemanticVersion(1, 11, 0); // The 1.1 patch was released as 1.11 default: return version; } } /// 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 semantic version. private static ISemanticVersion GetGameVersion() { // get raw version // 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."); string version = (string)field.GetValue(null); // get semantic version if (version == "1.11") version = "1.1.1"; // The 1.1 patch was released as 1.11, which means it's out of order for semantic version checks return new SemanticVersion(version); } } }