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)));
}
}
}