SMAPI/StardewModdingAPI/Program.cs

495 lines
20 KiB
C#
Raw Normal View History

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
2016-03-09 23:05:47 +08:00
using Mono.Cecil;
2016-03-10 00:38:12 +08:00
using Mono.Cecil.Cil;
using StardewModdingAPI.Events;
using StardewModdingAPI.Helpers;
using StardewModdingAPI.Inheritance;
using StardewModdingAPI.Inheritance.Menus;
using StardewValley;
using StardewValley.Menus;
using System;
2016-02-28 19:55:35 +08:00
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Windows.Forms;
2016-02-28 19:55:35 +08:00
namespace StardewModdingAPI
{
public class Program
{
private static List<string> _modPaths;
private static List<string> _modContentPaths;
public static Texture2D DebugPixel { get; private set; }
2016-02-29 09:26:36 +08:00
public static SGame gamePtr;
2016-02-28 19:55:35 +08:00
public static bool ready;
public static Assembly StardewAssembly;
public static Type StardewProgramType;
public static FieldInfo StardewGameInfo;
public static Form StardewForm;
public static Thread gameThread;
public static Thread consoleInputThread;
public static CecilContext StardewContext;
public static CecilContext SmapiContext;
2016-03-04 03:01:32 +08:00
2016-02-28 19:55:35 +08:00
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary>
/// Main method holding the API execution
/// </summary>
/// <param name="args"></param>
2016-02-28 19:55:35 +08:00
private static void Main(string[] args)
{
try
{
ConfigureUI();
ConfigurePaths();
2016-03-09 23:05:47 +08:00
ConfigureMethodInjection();
2016-03-10 00:38:12 +08:00
//ConfigureSDV();
2016-03-09 23:05:47 +08:00
//GameRunInvoker();
}
catch (Exception e)
{
// Catch and display all exceptions.
2016-03-07 04:25:14 +08:00
StardewModdingAPI.Log.Error("Critical error: " + e);
}
2016-03-07 04:25:14 +08:00
StardewModdingAPI.Log.Comment("The API will now terminate. Press any key to continue...");
Console.ReadKey();
}
2016-03-09 23:05:47 +08:00
/// <summary>
/// Configures Mono.Cecil injections
/// </summary>
private static void ConfigureMethodInjection()
{
StardewContext = new CecilContext(CecilContextType.Stardew);
SmapiContext = new CecilContext(CecilContextType.SMAPI);
2016-03-10 00:38:12 +08:00
}
private static void WeaveOnEnterMethod(Mono.Cecil.Cil.ILProcessor ilProcessor, Instruction target, MethodReference callback)
{
// Instruction loadNameInstruction = ilProcessor.Create(OpCodes.Ldstr, name);
//ilProcessor.InsertBefore(target, loadNameInstruction);
Instruction callEnterInstruction = ilProcessor.Create(OpCodes.Call, callback);
ilProcessor.InsertBefore(target, callEnterInstruction);
}
private static void WeaveOnExitMethod(Mono.Cecil.Cil.ILProcessor ilProcessor, List<Instruction> targets, MethodReference callback)
{
// Instruction loadNameInstruction = ilProcessor.Create(OpCodes.Ldstr, name);
//ilProcessor.InsertBefore(target, loadNameInstruction);
foreach (var returnInstruction in targets)
{
Instruction callEnterInstruction = ilProcessor.Create(OpCodes.Call, callback);
ilProcessor.InsertBefore(returnInstruction, callEnterInstruction);
}
2016-03-09 23:05:47 +08:00
}
2016-02-28 19:55:35 +08:00
/// <summary>
/// Set up the console properties
/// </summary>
private static void ConfigureUI()
{
Console.Title = Constants.ConsoleTitle;
2016-02-28 19:55:35 +08:00
#if DEBUG
Console.Title += " - DEBUG IS NOT FALSE, AUTHOUR NEEDS TO REUPLOAD THIS VERSION";
#endif
}
/// <summary>
/// Setup the required paths and logging
/// </summary>
private static void ConfigurePaths()
{
2016-03-07 04:25:14 +08:00
StardewModdingAPI.Log.Info("Validating api paths...");
_modPaths = new List<string>();
_modContentPaths = new List<string>();
//TODO: Have an app.config and put the paths inside it so users can define locations to load mods from
_modPaths.Add(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley", "Mods"));
_modPaths.Add(Path.Combine(Constants.ExecutionPath, "Mods"));
_modContentPaths.Add(Path.Combine(Constants.ExecutionPath, "Mods", "Content"));
_modContentPaths.Add(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley", "Mods", "Content"));
2016-03-03 01:24:15 +08:00
//Checks that all defined modpaths exist as directories
_modPaths.ForEach(path => VerifyPath(path));
_modContentPaths.ForEach(path => VerifyPath(path));
VerifyPath(Constants.LogPath);
2016-03-01 04:01:41 +08:00
StardewModdingAPI.Log.Initialize(Constants.LogPath);
if (!File.Exists(Constants.ExecutionPath + "\\Stardew Valley.exe"))
{
throw new FileNotFoundException(string.Format("Could not found: {0}\\Stardew Valley.exe", Constants.ExecutionPath));
2016-03-01 04:01:41 +08:00
}
2016-03-07 04:51:22 +08:00
}
/// <summary>
/// Load Stardev Valley and control features
/// </summary>
private static void ConfigureSDV()
{
2016-03-07 04:25:14 +08:00
StardewModdingAPI.Log.Info("Initializing SDV Assembly...");
// Load in the assembly - ignores security
StardewAssembly = Assembly.UnsafeLoadFrom(Constants.ExecutionPath + "\\Stardew Valley.exe");
2016-02-28 19:55:35 +08:00
StardewProgramType = StardewAssembly.GetType("StardewValley.Program", true);
StardewGameInfo = StardewProgramType.GetField("gamePtr");
// Change the game's version
StardewModdingAPI.Log.Verbose("Injecting New SDV Version...");
Game1.version += string.Format("-Z_MODDED | SMAPI {0}", Constants.VersionString);
2016-03-04 03:01:32 +08:00
// Create the thread for the game to run in.
2016-02-28 19:55:35 +08:00
gameThread = new Thread(RunGame);
StardewModdingAPI.Log.Info("Starting SDV...");
gameThread.Start();
// Wait for the game to load up
while (!ready) ;
2016-02-28 19:55:35 +08:00
2016-03-03 01:24:15 +08:00
//SDV is running
StardewModdingAPI.Log.Comment("SDV Loaded Into Memory");
2016-02-28 19:55:35 +08:00
2016-03-03 01:24:15 +08:00
//Create definition to listen for input
StardewModdingAPI.Log.Verbose("Initializing Console Input Thread...");
2016-03-03 01:24:15 +08:00
consoleInputThread = new Thread(ConsoleInputThread);
// The only command in the API (at least it should be, for now)
Command.RegisterCommand("help", "Lists all commands | 'help <cmd>' returns command description").CommandFired += help_CommandFired;
2016-03-03 01:24:15 +08:00
//Command.RegisterCommand("crash", "crashes sdv").CommandFired += delegate { Game1.player.draw(null); };
2016-02-28 19:55:35 +08:00
2016-03-03 01:24:15 +08:00
//Subscribe to events
Events.ControlEvents.KeyPressed += Events_KeyPressed;
Events.GameEvents.LoadContent += Events_LoadContent;
//Events.MenuChanged += Events_MenuChanged; //Idk right now
2016-02-28 19:55:35 +08:00
StardewModdingAPI.Log.Verbose("Applying Final SDV Tweaks...");
2016-02-28 19:55:35 +08:00
StardewInvoke(() =>
{
gamePtr.IsMouseVisible = false;
gamePtr.Window.Title = "Stardew Valley - Version " + Game1.version;
StardewForm.Resize += Events.GraphicsEvents.InvokeResize;
2016-02-28 19:55:35 +08:00
});
}
2016-02-28 19:55:35 +08:00
/// <summary>
/// Wrap the 'RunGame' method for console output
/// </summary>
private static void GameRunInvoker()
{
2016-03-03 01:24:15 +08:00
//Game's in memory now, send the event
StardewModdingAPI.Log.Verbose("Game Loaded");
Events.GameEvents.InvokeGameLoaded();
2016-03-01 04:01:41 +08:00
2016-03-07 04:51:22 +08:00
StardewModdingAPI.Log.Comment("Type 'help' for help, or 'help <cmd>' for a command's usage");
2016-03-03 01:24:15 +08:00
//Begin listening to input
consoleInputThread.Start();
2016-03-01 04:01:41 +08:00
while (ready)
{
//Check if the game is still running 10 times a second
Thread.Sleep(1000 / 10);
}
2016-03-03 01:24:15 +08:00
//abort the thread, we're closing
2016-03-01 04:01:41 +08:00
if (consoleInputThread != null && consoleInputThread.ThreadState == ThreadState.Running)
consoleInputThread.Abort();
StardewModdingAPI.Log.Verbose("Game Execution Finished");
StardewModdingAPI.Log.Verbose("Shutting Down...");
2016-03-03 01:24:15 +08:00
Thread.Sleep(100);
2016-03-01 04:01:41 +08:00
Environment.Exit(0);
}
/// <summary>
/// Create the given directory path if it does not exist
/// </summary>
/// <param name="path">Desired directory path</param>
private static void VerifyPath(string path)
{
try
{
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
}
catch (Exception ex)
2016-03-07 04:25:14 +08:00
{
StardewModdingAPI.Log.Error("Could not create a path: " + path + "\n\n" + ex);
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2016-02-29 05:01:24 +08:00
2016-02-28 19:55:35 +08:00
public static void RunGame()
{
Application.ThreadException += StardewModdingAPI.Log.Application_ThreadException;
2016-03-03 01:24:15 +08:00
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
AppDomain.CurrentDomain.UnhandledException += StardewModdingAPI.Log.CurrentDomain_UnhandledException;
2016-03-03 01:24:15 +08:00
2016-02-28 19:55:35 +08:00
try
{
2016-02-29 09:26:36 +08:00
gamePtr = new SGame();
StardewModdingAPI.Log.Verbose("Patching SDV Graphics Profile...");
2016-02-28 19:55:35 +08:00
Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef;
LoadMods();
StardewForm = Control.FromHandle(Program.gamePtr.Window.Handle).FindForm();
2016-03-01 04:01:41 +08:00
StardewForm.Closing += StardewForm_Closing;
2016-02-28 19:55:35 +08:00
ready = true;
2016-03-04 06:08:40 +08:00
StardewGameInfo.SetValue(StardewProgramType, gamePtr);
gamePtr.Run();
#region deprecated
if (false)
2016-03-04 03:01:32 +08:00
{
2016-03-04 06:08:40 +08:00
//Nope, I can't get it to work. I depend on Game1 being an SGame, and can't cast a parent to a child
//I'm leaving this here in case the community is interested
2016-03-04 03:01:32 +08:00
//StardewInjectorMod.Entry(true);
2016-03-04 06:08:40 +08:00
Type gt = StardewAssembly.GetType("StardewValley.Game1", true);
gamePtr = (SGame)Activator.CreateInstance(gt);
ready = true;
2016-03-04 04:06:25 +08:00
StardewGameInfo.SetValue(StardewProgramType, gamePtr);
gamePtr.Run();
}
2016-03-04 06:08:40 +08:00
#endregion
}
catch (Exception ex)
{
StardewModdingAPI.Log.Error("Game failed to start: " + ex);
}
}
static void StardewForm_Closing(object sender, CancelEventArgs e)
{
e.Cancel = true;
2016-03-04 06:08:40 +08:00
if (true || MessageBox.Show("Are you sure you would like to quit Stardew Valley?\nUnsaved progress will be lost!", "Confirm Exit", MessageBoxButtons.YesNo, MessageBoxIcon.Exclamation) == DialogResult.Yes)
{
gamePtr.Exit();
gamePtr.Dispose();
StardewForm.Hide();
ready = false;
}
}
public static void LoadMods()
{
StardewModdingAPI.Log.Verbose("LOADING MODS");
int loadedMods = 0;
foreach (string ModPath in _modPaths)
{
foreach (String s in Directory.GetFiles(ModPath, "*.dll"))
{
2016-03-04 04:06:25 +08:00
if (s.Contains("StardewInjector"))
continue;
StardewModdingAPI.Log.Success("Found DLL: " + s);
try
{
Assembly mod = Assembly.UnsafeLoadFrom(s); //to combat internet-downloaded DLLs
if (mod.DefinedTypes.Count(x => x.BaseType == typeof(Mod)) > 0)
{
StardewModdingAPI.Log.Verbose("Loading Mod DLL...");
TypeInfo tar = mod.DefinedTypes.First(x => x.BaseType == typeof(Mod));
Mod m = (Mod)mod.CreateInstance(tar.ToString());
Console.WriteLine("LOADED MOD: {0} by {1} - Version {2} | Description: {3}", m.Name, m.Authour, m.Version, m.Description);
loadedMods += 1;
m.Entry();
}
else
{
StardewModdingAPI.Log.Error("Invalid Mod DLL");
}
}
catch (Exception ex)
{
StardewModdingAPI.Log.Error("Failed to load mod '{0}'. Exception details:\n" + ex, s);
}
}
}
StardewModdingAPI.Log.Success("LOADED {0} MODS", loadedMods);
}
public static void ConsoleInputThread()
{
string input = string.Empty;
while (true)
{
Command.CallCommand(Console.ReadLine());
}
}
2016-03-04 04:06:25 +08:00
static void Events_LoadContent(object o, EventArgs e)
{
StardewModdingAPI.Log.Info("Initializing Debug Assets...");
DebugPixel = new Texture2D(Game1.graphics.GraphicsDevice, 1, 1);
DebugPixel.SetData(new Color[] { Color.White });
#if DEBUG
StardewModdingAPI.Log.Verbose("REGISTERING BASE CUSTOM ITEM");
SObject so = new SObject();
so.Name = "Mario Block";
so.CategoryName = "SMAPI Test Mod";
so.Description = "It's a block from Mario!\nLoaded in realtime by SMAPI.";
so.Texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, new FileStream(_modContentPaths[0] + "\\Test.png", FileMode.Open));
so.IsPassable = true;
so.IsPlaceable = true;
StardewModdingAPI.Log.Verbose("REGISTERED WITH ID OF: " + SGame.RegisterModItem(so));
//StardewModdingAPI.Log.Verbose("REGISTERING SECOND CUSTOM ITEM");
//SObject so2 = new SObject();
//so2.Name = "Mario Painting";
//so2.CategoryName = "SMAPI Test Mod";
//so2.Description = "It's a painting of a creature from Mario!\nLoaded in realtime by SMAPI.";
//so2.Texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, new FileStream(_modContentPaths[0] + "\\PaintingTest.png", FileMode.Open));
//so2.IsPassable = true;
//so2.IsPlaceable = true;
//StardewModdingAPI.Log.Verbose("REGISTERED WITH ID OF: " + SGame.RegisterModItem(so2));
Command.CallCommand("load");
#endif
}
2016-03-04 04:06:25 +08:00
static void Events_KeyPressed(object o, EventArgsKeyPressed e)
{
}
static void Events_MenuChanged(IClickableMenu newMenu)
{
StardewModdingAPI.Log.Verbose("NEW MENU: " + newMenu.GetType());
if (newMenu is GameMenu)
{
Game1.activeClickableMenu = SGameMenu.ConstructFromBaseClass(Game1.activeClickableMenu as GameMenu);
}
}
static void Events_LocationsChanged(List<GameLocation> newLocations)
{
#if DEBUG
SGame.ModLocations = SGameLocation.ConstructFromBaseClasses(Game1.locations);
#endif
}
static void Events_CurrentLocationChanged(GameLocation newLocation)
{
//SGame.CurrentLocation = null;
//System.Threading.Thread.Sleep(10);
#if DEBUG
Console.WriteLine(newLocation.name);
SGame.CurrentLocation = SGame.LoadOrCreateSGameLocationFromName(newLocation.name);
#endif
//Game1.currentLocation = SGame.CurrentLocation;
//Log.LogComment(((SGameLocation) newLocation).name);
//Log.LogComment("LOC CHANGED: " + SGame.currentLocation.name);
}
public static void StardewInvoke(Action a)
{
StardewForm.Invoke(a);
}
static void help_CommandFired(object o, EventArgsCommand e)
{
if (e.Command.CalledArgs.Length > 0)
{
Command fnd = Command.FindCommand(e.Command.CalledArgs[0]);
if (fnd == null)
StardewModdingAPI.Log.Error("The command specified could not be found");
else
{
if (fnd.CommandArgs.Length > 0)
StardewModdingAPI.Log.Info("{0}: {1} - {2}", fnd.CommandName, fnd.CommandDesc, fnd.CommandArgs.ToSingular());
else
StardewModdingAPI.Log.Info("{0}: {1}", fnd.CommandName, fnd.CommandDesc);
}
}
else
StardewModdingAPI.Log.Info("Commands: " + Command.RegisteredCommands.Select(x => x.CommandName).ToSingular());
}
#region Logging
[Obsolete("This method is obsolete and will be removed in v0.39, please use the appropriate methods in the Log class")]
public static void Log(object o, params object[] format)
{
StardewModdingAPI.Log.Info(o, format);
}
[Obsolete("This method is obsolete and will be removed in v0.39, please use the appropriate methods in the Log class")]
public static void LogColour(ConsoleColor c, object o, params object[] format)
{
StardewModdingAPI.Log.Info(o, format);
}
[Obsolete("This method is obsolete and will be removed in v0.39, please use the appropriate methods in the Log class")]
public static void LogInfo(object o, params object[] format)
{
StardewModdingAPI.Log.Info(o, format);
}
[Obsolete("This method is obsolete and will be removed in v0.39, please use the appropriate methods in the Log class")]
public static void LogError(object o, params object[] format)
{
StardewModdingAPI.Log.Error(o, format);
}
[Obsolete("This method is obsolete and will be removed in v0.39, please use the appropriate methods in the Log class")]
public static void LogDebug(object o, params object[] format)
{
StardewModdingAPI.Log.Debug(o, format);
}
[Obsolete("This method is obsolete and will be removed in v0.39, please use the appropriate methods in the Log class")]
public static void LogValueNotSpecified()
{
StardewModdingAPI.Log.Error("<value> must be specified");
}
[Obsolete("This method is obsolete and will be removed in v0.39, please use the appropriate methods in the Log class")]
public static void LogObjectValueNotSpecified()
{
StardewModdingAPI.Log.Error("<object> and <value> must be specified");
}
[Obsolete("This method is obsolete and will be removed in v0.39, please use the appropriate methods in the Log class")]
public static void LogValueInvalid()
{
StardewModdingAPI.Log.Error("<value> is invalid");
}
[Obsolete("This method is obsolete and will be removed in v0.39, please use the appropriate methods in the Log class")]
public static void LogObjectInvalid()
{
StardewModdingAPI.Log.Error("<object> is invalid");
}
[Obsolete("This method is obsolete and will be removed in v0.39, please use the appropriate methods in the Log class")]
public static void LogValueNotInt32()
{
StardewModdingAPI.Log.Error("<value> must be a whole number (Int32)");
}
#endregion
}
2016-03-04 04:06:25 +08:00
}