using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Threading; using System.Windows.Forms; 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 private static List _modPaths; 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; //private static List _modContentPaths; 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("SDV Version: " + Game1.version); Log.AsyncY("SMAPI Version: " + Constants.Version.VersionString); ConfigureUI(); ConfigurePaths(); ConfigureSDV(); GameRunInvoker(); } 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 } /// /// Setup the required paths and logging /// private static void ConfigurePaths() { Log.AsyncY("Validating api paths..."); _modPaths = new List {Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley", "Mods"), Path.Combine(Constants.ExecutionPath, "Mods")}; //_modContentPaths = new List(); //TODO: Have an app.config and put the paths inside it so users can define locations to load mods from //Mods need to make their own content paths, since we're doing a different, manifest-driven, approach. //_modContentPaths.Add(Path.Combine(Constants.ExecutionPath, "Mods", "Content")); //_modContentPaths.Add(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley", "Mods", "Content")); //Checks that all defined modpaths exist as directories _modPaths.ForEach(VerifyPath); //_modContentPaths.ForEach(path => VerifyPath(path)); VerifyPath(Constants.LogDir); if (!File.Exists(GameExecutablePath)) { throw new FileNotFoundException($"Could not found: {GameExecutablePath}"); } } /// /// Load Stardev Valley and control features /// private static void ConfigureSDV() { Log.AsyncY("Initializing SDV Assembly..."); // Load in the assembly - ignores security 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.VersionString}"; // Create the thread for the game to run in. gameThread = new Thread(RunGame); Log.AsyncY("Starting SDV..."); gameThread.Start(); // Wait for the game to load up while (!ready) { } //SDV is running Log.AsyncY("SDV Loaded Into Memory"); //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; Log.AsyncY("Applying Final SDV Tweaks..."); gamePtr.IsMouseVisible = false; gamePtr.Window.Title = "Stardew Valley - Version " + Game1.version; gamePtr.Window.ClientSizeChanged += GraphicsEvents.InvokeResize; } /// /// Wrap the 'RunGame' method for console output /// private static void GameRunInvoker() { //Game's in memory now, send the event Log.AsyncY("Game Loaded"); GameEvents.InvokeGameLoaded(); Log.AsyncY("Type 'help' for help, or 'help ' for a command's usage"); //Begin listening to input consoleInputThread.Start(); while (ready) { //Check if the game is still running 10 times a second Thread.Sleep(1000 / 10); } //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); } /// /// Create the given directory path if it does not exist /// /// Desired 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 RunGame() { Application.ThreadException += Log.Application_ThreadException; Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); AppDomain.CurrentDomain.UnhandledException += Log.CurrentDomain_UnhandledException; try { gamePtr = new SGame(); Log.AsyncY("Patching SDV Graphics Profile..."); Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; LoadMods(); ready = true; gamePtr.Exiting += (sender, e) => ready = false; StardewGameInfo.SetValue(StardewProgramType, gamePtr); gamePtr.Run(); } catch (Exception ex) { Log.AsyncR("Game failed to start: " + ex); } } public static void LoadMods() { Log.AsyncY("LOADING MODS"); foreach (var ModPath in _modPaths) { foreach (var d in Directory.GetDirectories(ModPath)) { foreach (var s in Directory.GetFiles(d, "manifest.json")) { if (s.Contains("StardewInjector")) continue; Log.AsyncG("Found Manifest: " + s); var manifest = new Manifest(); try { var t = File.ReadAllText(s); if (string.IsNullOrEmpty(t)) { Log.AsyncR($"Failed to read mod manifest '{s}'. Manifest is empty!"); continue; } manifest = manifest.InitializeConfig(s); if (string.IsNullOrEmpty(manifest.EntryDll)) { Log.AsyncR($"Failed to read mod manifest '{s}'. EntryDll is empty!"); continue; } } catch (Exception ex) { Log.AsyncR($"Failed to read mod manifest '{s}'. Exception details:\n" + ex); continue; } var targDir = Path.GetDirectoryName(s); var 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; } var 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; } var mod = Assembly.UnsafeLoadFrom(targDll); if (mod.DefinedTypes.Count(x => x.BaseType == typeof(Mod)) > 0) { Log.AsyncY("Loading Mod DLL..."); var tar = mod.DefinedTypes.First(x => x.BaseType == typeof(Mod)); var m = (Mod) mod.CreateInstance(tar.ToString()); if (m != null) { m.PathOnDisk = targDir; m.Manifest = manifest; Log.AsyncG($"LOADED MOD: {m.Manifest.Name} by {m.Manifest.Authour} - Version {m.Manifest.Version} | Description: {m.Manifest.Description} (@ {targDll})"); Constants.ModsLoaded += 1; m.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()); } } }