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; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Events; using StardewModdingAPI.Inheritance; using StardewValley; namespace StardewModdingAPI { public class Program { /// 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"); public static SGame gamePtr; public static bool ready; public static Assembly StardewAssembly; public static Type StardewProgramType; public static FieldInfo StardewGameInfo; public static Thread gameThread; public static Thread consoleInputThread; public static Texture2D DebugPixel { get; private set; } // ReSharper disable once PossibleNullReferenceException public static int BuildType => (int)StardewProgramType.GetField("buildType", BindingFlags.Public | BindingFlags.Static).GetValue(null); ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// /// Main method holding the API execution /// /// private static void Main(string[] args) { Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB"); try { Log.AsyncY($"SMAPI {Constants.Version}"); Log.AsyncY($"Stardew Valley {Game1.version} on {Environment.OSVersion}"); Program.ConfigureUI(); Program.CreateDirectories(); Program.StartGame(); } catch (Exception e) { // Catch and display all exceptions. Console.WriteLine(e); Console.ReadKey(); Log.AsyncR("Critical error: " + e); } Log.AsyncY("The API will now terminate. Press any key to continue..."); Console.ReadKey(); } /// /// Set up the console properties /// private static void ConfigureUI() { 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..."); VerifyPath(ModPath); VerifyPath(Constants.LogDir); if (!File.Exists(GameExecutablePath)) throw new FileNotFoundException($"Could not find executable: {GameExecutablePath}"); } /// /// Load Stardev Valley and control features, and launch the game. /// private static void StartGame() { // Load in the assembly - ignores security Log.AsyncY("Initializing SDV Assembly..."); StardewAssembly = Assembly.UnsafeLoadFrom(GameExecutablePath); StardewProgramType = StardewAssembly.GetType("StardewValley.Program", true); StardewGameInfo = 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..."); gamePtr = new SGame(); // hook events gamePtr.Exiting += (sender, e) => ready = false; gamePtr.Window.ClientSizeChanged += GraphicsEvents.InvokeResize; // patch graphics Log.AsyncY("Patching SDV Graphics Profile..."); Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; // load mods LoadMods(); // initialise StardewGameInfo.SetValue(StardewProgramType, gamePtr); Log.AsyncY("Applying Final SDV Tweaks..."); gamePtr.IsMouseVisible = false; 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 (!ready) Thread.Sleep(1000); // Create definition to listen for input Log.AsyncY("Initializing Console Input Thread..."); consoleInputThread = new Thread(ConsoleInputThread); // The only command in the API (at least it should be, for now) Command.RegisterCommand("help", "Lists all commands | 'help ' returns command description").CommandFired += help_CommandFired; // Subscribe to events ControlEvents.KeyPressed += Events_KeyPressed; GameEvents.LoadContent += Events_LoadContent; // Game's in memory now, send the event Log.AsyncY("Game Loaded"); GameEvents.InvokeGameLoaded(); // Listen for command line input Log.AsyncY("Type 'help' for help, or 'help ' for a command's usage"); consoleInputThread.Start(); while (ready) Thread.Sleep(1000 / 10); // Check if the game is still running 10 times a second // Abort the thread, we're closing if (consoleInputThread != null && 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 { ready = true; gamePtr.Run(); } catch (Exception ex) { 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); } } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// public static void LoadMods() { Log.AsyncY("LOADING MODS"); foreach (string directory in Directory.GetDirectories(ModPath)) { foreach (string manifestPath in Directory.GetFiles(directory, "manifest.json")) { if (manifestPath.Contains("StardewInjector")) continue; // 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; } 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; } 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})"); Constants.ModsLoaded += 1; modEntry.Entry(); } } else Log.AsyncR("Invalid Mod DLL"); } catch (Exception ex) { Log.AsyncR($"Failed to load mod '{targDll}'. Exception details:\n" + ex); } } } Log.AsyncG($"LOADED {Constants.ModsLoaded} MODS"); Console.Title = Constants.ConsoleTitle; } public static void ConsoleInputThread() { while (true) { Command.CallCommand(Console.ReadLine()); } } private static void Events_LoadContent(object o, EventArgs e) { Log.AsyncY("Initializing Debug Assets..."); DebugPixel = new Texture2D(Game1.graphics.GraphicsDevice, 1, 1); DebugPixel.SetData(new[] { Color.White }); } private static void Events_KeyPressed(object o, EventArgsKeyPressed e) { } private static void help_CommandFired(object o, EventArgsCommand e) { if (e.Command.CalledArgs.Length > 0) { var fnd = Command.FindCommand(e.Command.CalledArgs[0]); if (fnd == null) Log.AsyncR("The command specified could not be found"); else { Log.AsyncY(fnd.CommandArgs.Length > 0 ? $"{fnd.CommandName}: {fnd.CommandDesc} - {fnd.CommandArgs.ToSingular()}" : $"{fnd.CommandName}: {fnd.CommandDesc}"); } } else Log.AsyncY("Commands: " + Command.RegisteredCommands.Select(x => x.CommandName).ToSingular()); } } }