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