using System; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Threading; #if SMAPI_FOR_WINDOWS using System.Windows.Forms; #endif using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewModdingAPI.Inheritance; using StardewValley; namespace StardewModdingAPI { /// The main entry point for SMAPI, responsible for hooking into and launching the game. public class Program { /********* ** Properties *********/ /// The full path to the Stardew Valley executable. private static readonly string GameExecutablePath = File.Exists(Path.Combine(Constants.ExecutionPath, "StardewValley.exe")) ? Path.Combine(Constants.ExecutionPath, "StardewValley.exe") // Linux or Mac : Path.Combine(Constants.ExecutionPath, "Stardew Valley.exe"); // Windows /// The full path to the folder containing mods. private static readonly string ModPath = Path.Combine(Constants.ExecutionPath, "Mods"); /********* ** Accessors *********/ /// The number of mods currently loaded by SMAPI. public static int ModsLoaded; /// The underlying game instance. public static SGame gamePtr; /// Whether the game is currently running. public static bool ready; /// The underlying game assembly. public static Assembly StardewAssembly; /// The underlying type. public static Type StardewProgramType; /// The field containing game's main instance. public static FieldInfo StardewGameInfo; // ReSharper disable once PossibleNullReferenceException /// The game's build type (i.e. GOG vs Steam). public static int BuildType => (int)Program.StardewProgramType.GetField("buildType", BindingFlags.Public | BindingFlags.Static).GetValue(null); /********* ** Public methods *********/ /// The main entry point which hooks into and launches the game. /// The command-line arguments. private static void Main(string[] args) { // set thread culture for consistent log formatting Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB"); // hook into & launch the game try { Log.AsyncY($"SMAPI {Constants.Version}"); Log.AsyncY($"Stardew Valley {Game1.version} on {Environment.OSVersion}"); Program.ConfigureConsoleWindow(); Program.CheckForUpdateAsync(); Program.CreateDirectories(); Program.StartGame(); } catch (Exception ex) { Console.WriteLine(ex); Console.ReadKey(); Log.AsyncR($"Critical error: {ex}"); } // print message when game ends Log.AsyncY("The API will now terminate. Press any key to continue..."); Console.ReadKey(); } /********* ** Private methods *********/ /// Configure the console window. private static void ConfigureConsoleWindow() { Console.Title = Constants.ConsoleTitle; #if DEBUG Console.Title += " - DEBUG IS NOT FALSE, AUTHOUR NEEDS TO REUPLOAD THIS VERSION"; #endif } /// Create and verify the SMAPI directories. private static void CreateDirectories() { Log.AsyncY("Validating file paths..."); Program.VerifyPath(Program.ModPath); Program.VerifyPath(Constants.LogDir); if (!File.Exists(Program.GameExecutablePath)) throw new FileNotFoundException($"Could not find executable: {Program.GameExecutablePath}"); } /// Asynchronously check for a new version of SMAPI, and print a message to the console if an update is available. private static void CheckForUpdateAsync() { new Thread(() => { try { GitRelease release = UpdateHelper.GetLatestVersionAsync(Constants.GitHubRepository).Result; Version latestVersion = new Version(release.Tag); if (latestVersion.CompareTo(Constants.Version) > 0) Log.AsyncColour($"You can update SMAPI from version {Constants.Version} to {latestVersion}", ConsoleColor.Magenta); } catch (Exception ex) { Log.Debug($"Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.\n{ex}"); } }).Start(); } /// Hook into Stardew Valley and launch the game. private static void StartGame() { // load the game assembly (ignore security) Log.AsyncY("Initializing SDV Assembly..."); Program.StardewAssembly = Assembly.UnsafeLoadFrom(Program.GameExecutablePath); Program.StardewProgramType = Program.StardewAssembly.GetType("StardewValley.Program", true); Program.StardewGameInfo = Program.StardewProgramType.GetField("gamePtr"); // change the game's version Log.AsyncY("Injecting New SDV Version..."); Game1.version += $"-Z_MODDED | SMAPI {Constants.Version}"; // add error interceptors #if SMAPI_FOR_WINDOWS Application.ThreadException += Log.Application_ThreadException; Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); #endif AppDomain.CurrentDomain.UnhandledException += Log.CurrentDomain_UnhandledException; // initialise game try { Log.AsyncY("Initializing SDV..."); Program.gamePtr = new SGame(); // hook events Program.gamePtr.Exiting += (sender, e) => Program.ready = false; Program.gamePtr.Window.ClientSizeChanged += GraphicsEvents.InvokeResize; // patch graphics Log.AsyncY("Patching SDV Graphics Profile..."); Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; // load mods Program.LoadMods(); // initialise Program.StardewGameInfo.SetValue(Program.StardewProgramType, Program.gamePtr); Log.AsyncY("Applying Final SDV Tweaks..."); Program.gamePtr.IsMouseVisible = false; Program.gamePtr.Window.Title = $"Stardew Valley - Version {Game1.version}"; } catch (Exception ex) { Log.AsyncR($"Game failed to initialise: {ex}"); return; } // initialise after game launches new Thread(() => { // wait for the game to load up while (!Program.ready) Thread.Sleep(1000); // register help command Command.RegisterCommand("help", "Lists all commands | 'help ' returns command description").CommandFired += Program.help_CommandFired; // raise game loaded event Log.AsyncY("Game Loaded"); GameEvents.InvokeGameLoaded(); // listen for command line input Log.AsyncY("Starting console..."); Log.AsyncY("Type 'help' for help, or 'help ' for a command's usage"); Thread consoleInputThread = new Thread(Program.ConsoleInputLoop); consoleInputThread.Start(); while (Program.ready) Thread.Sleep(1000 / 10); // Check if the game is still running 10 times a second // abort the console thread, we're closing if (consoleInputThread.ThreadState == ThreadState.Running) consoleInputThread.Abort(); Log.AsyncY("Game Execution Finished"); Log.AsyncY("Shutting Down..."); Thread.Sleep(100); Environment.Exit(0); }).Start(); // start game loop Log.AsyncY("Starting SDV..."); try { Program.ready = true; Program.gamePtr.Run(); } catch (Exception ex) { Program.ready = false; Log.AsyncR($"Game failed to start: {ex}"); } } /// Create a directory path if it doesn't exist. /// The directory path. private static void VerifyPath(string path) { try { if (!Directory.Exists(path)) Directory.CreateDirectory(path); } catch (Exception ex) { Log.AsyncR($"Could not create a path: {path}\n\n{ex}"); } } /// Load and hook up all mods in the mod directory. private static void LoadMods() { Log.AsyncY("LOADING MODS"); foreach (string directory in Directory.GetDirectories(Program.ModPath)) { foreach (string manifestPath in Directory.GetFiles(directory, "manifest.json")) { // read manifest Log.AsyncG($"Found Manifest: {manifestPath}"); Manifest manifest = new Manifest(); try { // read manifest text string json = File.ReadAllText(manifestPath); if (string.IsNullOrEmpty(json)) { Log.AsyncR($"Failed to read mod manifest '{manifestPath}'. Manifest is empty!"); continue; } // deserialise manifest manifest = manifest.InitializeConfig(manifestPath); if (string.IsNullOrEmpty(manifest.EntryDll)) { Log.AsyncR($"Failed to read mod manifest '{manifestPath}'. EntryDll is empty!"); continue; } } catch (Exception ex) { Log.AsyncR($"Failed to read mod manifest '{manifestPath}'. Exception details:\n{ex}"); continue; } // create per-save directory string targDir = Path.GetDirectoryName(manifestPath); string psDir = Path.Combine(targDir, "psconfigs"); Log.AsyncY($"Created psconfigs directory @{psDir}"); try { if (manifest.PerSaveConfigs) { if (!Directory.Exists(psDir)) { Directory.CreateDirectory(psDir); Log.AsyncY($"Created psconfigs directory @{psDir}"); } if (!Directory.Exists(psDir)) { Log.AsyncR($"Failed to create psconfigs directory '{psDir}'. No exception occured."); continue; } } } catch (Exception ex) { Log.AsyncR($"Failed to create psconfigs directory '{targDir}'. Exception details:\n{ex}"); continue; } // load DLL & hook up mod string targDll = string.Empty; try { targDll = Path.Combine(targDir, manifest.EntryDll); if (!File.Exists(targDll)) { Log.AsyncR($"Failed to load mod '{manifest.EntryDll}'. File {targDll} does not exist!"); continue; } Assembly modAssembly = Assembly.UnsafeLoadFrom(targDll); if (modAssembly.DefinedTypes.Count(x => x.BaseType == typeof(Mod)) > 0) { Log.AsyncY("Loading Mod DLL..."); TypeInfo tar = modAssembly.DefinedTypes.First(x => x.BaseType == typeof(Mod)); Mod modEntry = (Mod)modAssembly.CreateInstance(tar.ToString()); if (modEntry != null) { modEntry.PathOnDisk = targDir; modEntry.Manifest = manifest; Log.AsyncG($"LOADED MOD: {modEntry.Manifest.Name} by {modEntry.Manifest.Author} - Version {modEntry.Manifest.Version} | Description: {modEntry.Manifest.Description} (@ {targDll})"); Program.ModsLoaded += 1; modEntry.Entry(); } } else Log.AsyncR("Invalid Mod DLL"); } catch (Exception ex) { Log.AsyncR($"Failed to load mod '{targDll}'. Exception details:\n{ex}"); } } } // print result Log.AsyncG($"LOADED {Program.ModsLoaded} MODS"); Console.Title = Constants.ConsoleTitle; } /// Run a loop handling console input. private static void ConsoleInputLoop() { while (true) Command.CallCommand(Console.ReadLine()); } /// The method called when the user submits the help command in the console. /// The event sender. /// The event data. private static void help_CommandFired(object sender, EventArgsCommand e) { if (e.Command.CalledArgs.Length > 0) { var command = Command.FindCommand(e.Command.CalledArgs[0]); if (command == null) Log.AsyncR("The specified command could not be found"); else Log.AsyncY(command.CommandArgs.Length > 0 ? $"{command.CommandName}: {command.CommandDesc} - {string.Join(", ", command.CommandArgs)}" : $"{command.CommandName}: {command.CommandDesc}"); } else Log.AsyncY("Commands: " + string.Join(", ", Command.RegisteredCommands.Select(x => x.CommandName))); } } }