using System; using System.IO; using System.Linq; using System.Reflection; using System.Threading; using StardewModdingAPI.Framework; using StardewModdingAPI.Toolkit.Framework; using StardewModdingAPI.Toolkit.Serialization.Models; namespace StardewModdingAPI { /// The main entry point for SMAPI, responsible for hooking into and launching the game. internal class Program { /********* ** Fields *********/ /// The absolute path to search for SMAPI's internal DLLs. internal static readonly string DllSearchPath = EarlyConstants.InternalFilesPath; /********* ** Public methods *********/ /// The main entry point which hooks into and launches the game. /// The command-line arguments. public static void Main(string[] args) { Console.Title = $"SMAPI {EarlyConstants.RawApiVersion} - {Console.Title}"; try { AppDomain.CurrentDomain.AssemblyResolve += Program.CurrentDomain_AssemblyResolve; Program.AssertGamePresent(); Program.AssertGameVersion(); Program.AssertSmapiVersions(); Program.Start(args); } catch (BadImageFormatException ex) when (ex.FileName == "StardewValley" || ex.FileName == "Stardew Valley") // don't use EarlyConstants.GameAssemblyName, since we want to check both possible names { 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) { Console.WriteLine($"SMAPI failed to initialize: {ex}"); Program.PressAnyKeyToExit(true); } } /********* ** Private methods *********/ /// Method called when assembly resolution fails, which may return a manually resolved assembly. /// The event sender. /// The event arguments. private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs e) { try { AssemblyName name = new AssemblyName(e.Name); foreach (FileInfo dll in new DirectoryInfo(Program.DllSearchPath).EnumerateFiles("*.dll")) { if (name.Name.Equals(AssemblyName.GetAssemblyName(dll.FullName).Name, StringComparison.OrdinalIgnoreCase)) return Assembly.LoadFrom(dll.FullName); } return null; } catch (Exception ex) { Console.WriteLine($"Error resolving assembly: {ex}"); return null; } } /// 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 or when the game isn't present. private static void AssertGamePresent() { try { _ = Type.GetType($"StardewValley.Game1, {EarlyConstants.GameAssemblyName}", throwOnError: true); } catch (Exception ex) { // unofficial 64-bit if (EarlyConstants.Platform == GamePlatform.Windows) { FileInfo linuxExecutable = new FileInfo(Path.Combine(EarlyConstants.ExecutionPath, "StardewValley.exe")); if (linuxExecutable.Exists && LowLevelEnvironmentUtility.Is64BitAssembly(linuxExecutable.FullName)) Program.PrintErrorAndExit("Oops! You're running Stardew Valley in unofficial 64-bit mode, which is no longer supported. You can update to Stardew Valley 1.5.5 or later instead. See https://stardewvalleywiki.com/Modding:Migrate_to_64-bit_on_Windows for more info."); } // file doesn't exist if (!File.Exists(Path.Combine(EarlyConstants.ExecutionPath, $"{EarlyConstants.GameAssemblyName}.exe"))) Program.PrintErrorAndExit("Oops! SMAPI can't find the game. Make sure you're running StardewModdingAPI.exe in your game folder."); // Stardew Valley 1.5.5+ if (File.Exists(Path.Combine(EarlyConstants.ExecutionPath, "Stardew Valley.dll"))) Program.PrintErrorAndExit("Oops! You're running Stardew Valley 1.5.5 or later, but this version of SMAPI is only compatible up to Stardew Valley 1.5.4. Please check for a newer version of SMAPI: https://smapi.io."); // can't load file Program.PrintErrorAndExit( message: "Oops! SMAPI couldn't load the game executable. The technical details below may have more info.", technicalMessage: $"Technical details: {ex}" ); } } /// Assert that the game version is within and . private static void AssertGameVersion() { // min version if (Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion)) { ISemanticVersion suggestedApiVersion = Constants.GetCompatibleApiVersion(Constants.GameVersion); Program.PrintErrorAndExit(suggestedApiVersion != null ? $"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. You can install SMAPI {suggestedApiVersion} instead to fix this error, or update your game to the latest version." : $"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI." ); } // max version 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."); } /// Assert that the versions of all SMAPI components are correct. /// Players sometimes have mismatched versions (particularly when installed through Vortex), which can cause some very confusing bugs without this check. private static void AssertSmapiVersions() { // get SMAPI version without prerelease suffix (since we can't get that from the assembly versions) ISemanticVersion smapiVersion = new SemanticVersion(Constants.ApiVersion.MajorVersion, Constants.ApiVersion.MinorVersion, Constants.ApiVersion.PatchVersion); // compare with assembly versions foreach (var type in new[] { typeof(IManifest), typeof(Manifest) }) { AssemblyName assemblyName = type.Assembly.GetName(); ISemanticVersion assemblyVersion = new SemanticVersion(assemblyName.Version); if (!assemblyVersion.Equals(smapiVersion)) Program.PrintErrorAndExit($"Oops! The 'smapi-internal/{assemblyName.Name}.dll' file is version {assemblyVersion} instead of the required {Constants.ApiVersion}. SMAPI doesn't seem to be installed correctly."); } } /// Initialize SMAPI and launch the game. /// The command-line arguments. /// This method is separate from because that can't contain any references to assemblies loaded by (e.g. via ), or Mono will incorrectly show an assembly resolution error before assembly resolution is set up. private static void Start(string[] args) { // get flags bool writeToConsole = !args.Contains("--no-terminal") && Environment.GetEnvironmentVariable("SMAPI_NO_TERMINAL") == null; // get mods path string modsPath; { string rawModsPath = null; // get from command line args int pathIndex = Array.LastIndexOf(args, "--mods-path") + 1; if (pathIndex >= 1 && args.Length >= pathIndex) rawModsPath = args[pathIndex]; // get from environment variables if (string.IsNullOrWhiteSpace(rawModsPath)) rawModsPath = Environment.GetEnvironmentVariable("SMAPI_MODS_PATH"); // normalise modsPath = !string.IsNullOrWhiteSpace(rawModsPath) ? Path.Combine(Constants.ExecutionPath, rawModsPath) : Constants.DefaultModsPath; } // load SMAPI using SCore core = new SCore(modsPath, writeToConsole); core.RunInteractively(); } /// Write an error directly to the console and exit. /// The error message to display. /// An additional message to log with technical details. private static void PrintErrorAndExit(string message, string technicalMessage = null) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine(message); Console.ResetColor(); if (technicalMessage != null) { Console.WriteLine(); Console.ForegroundColor = ConsoleColor.Gray; Console.WriteLine(technicalMessage); Console.ResetColor(); Console.WriteLine(); } Program.PressAnyKeyToExit(showMessage: true); } /// Show a 'press any key to exit' message, and exit when they press a key. /// Whether to print a 'press any key to exit' message to the console. private static void PressAnyKeyToExit(bool showMessage) { if (showMessage) Console.WriteLine("Game has ended. Press any key to exit."); Thread.Sleep(100); Console.ReadKey(); Environment.Exit(0); } } }