2016-03-23 08:36:04 +08:00
using System ;
2016-03-21 03:43:31 +08:00
using System.Collections.Generic ;
using System.ComponentModel ;
2016-03-22 09:05:07 +08:00
using System.Globalization ;
2016-03-21 03:43:31 +08:00
using System.IO ;
using System.Linq ;
using System.Reflection ;
using System.Threading ;
using System.Windows.Forms ;
2016-03-23 08:36:04 +08:00
using Microsoft.Xna.Framework ;
using Microsoft.Xna.Framework.Graphics ;
using StardewModdingAPI.Events ;
using StardewModdingAPI.Inheritance ;
using StardewModdingAPI.Inheritance.Menus ;
using StardewValley ;
using StardewValley.Menus ;
2016-03-21 03:43:31 +08:00
namespace StardewModdingAPI
{
public class Program
{
private static List < string > _modPaths ;
2016-03-21 05:51:49 +08:00
//private static List<string> _modContentPaths;
2016-03-21 03:43:31 +08:00
public static Texture2D DebugPixel { get ; private set ; }
public static SGame gamePtr ;
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 bool StardewInjectorLoaded { get ; private set ; }
public static Mod StardewInjectorMod { get ; private set ; }
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2016-03-07 04:19:59 +08:00
/// <summary>
/// Main method holding the API execution
/// </summary>
2016-03-21 03:43:31 +08:00
/// <param name="args"></param>
private static void Main ( string [ ] args )
{
2016-03-22 09:05:07 +08:00
Thread . CurrentThread . CurrentCulture = CultureInfo . CreateSpecificCulture ( "en-GB" ) ;
2016-03-07 04:19:59 +08:00
try
{
ConfigureUI ( ) ;
ConfigurePaths ( ) ;
ConfigureSDV ( ) ;
GameRunInvoker ( ) ;
}
catch ( Exception e )
{
// Catch and display all exceptions.
2016-03-24 01:43:11 +08:00
Log . Error ( "Critical error: " + e ) ;
2016-03-07 04:19:59 +08:00
}
2016-03-24 01:43:11 +08:00
Log . Comment ( "The API will now terminate. Press any key to continue..." ) ;
2016-03-07 04:19:59 +08:00
Console . ReadKey ( ) ;
2016-03-21 03:43:31 +08:00
}
2016-03-07 04:19:59 +08:00
/// <summary>
/// Set up the console properties
2016-03-21 03:43:31 +08:00
/// </summary>
2016-03-07 04:19:59 +08:00
private static void ConfigureUI ( )
2016-03-21 03:43:31 +08:00
{
2016-03-07 05:58:40 +08:00
Console . Title = Constants . ConsoleTitle ;
2016-03-21 03:43:31 +08:00
#if DEBUG
2016-03-05 14:32:04 +08:00
Console . Title + = " - DEBUG IS NOT FALSE, AUTHOUR NEEDS TO REUPLOAD THIS VERSION" ;
#endif
2016-03-07 04:19:59 +08:00
}
/// <summary>
/// Setup the required paths and logging
/// </summary>
private static void ConfigurePaths ( )
{
2016-03-24 01:43:11 +08:00
Log . Info ( "Validating api paths..." ) ;
2016-03-07 04:19:59 +08:00
_modPaths = new List < string > ( ) ;
2016-03-21 05:51:49 +08:00
//_modContentPaths = new List<string>();
2016-03-05 14:32:04 +08:00
//TODO: Have an app.config and put the paths inside it so users can define locations to load mods from
2016-03-21 03:43:31 +08:00
_modPaths . Add ( Path . Combine ( Environment . GetFolderPath ( Environment . SpecialFolder . ApplicationData ) , "StardewValley" , "Mods" ) ) ;
_modPaths . Add ( Path . Combine ( Constants . ExecutionPath , "Mods" ) ) ;
2016-03-21 05:51:49 +08:00
//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"));
2016-03-21 03:43:31 +08:00
//Checks that all defined modpaths exist as directories
_modPaths . ForEach ( path = > VerifyPath ( path ) ) ;
2016-03-21 05:51:49 +08:00
//_modContentPaths.ForEach(path => VerifyPath(path));
2016-03-21 03:43:31 +08:00
VerifyPath ( Constants . LogPath ) ;
2016-03-24 01:43:11 +08:00
Log . Initialize ( Constants . LogPath ) ;
2016-03-07 04:19:59 +08:00
2016-03-07 05:58:40 +08:00
if ( ! File . Exists ( Constants . ExecutionPath + "\\Stardew Valley.exe" ) )
2016-03-21 03:43:31 +08:00
{
2016-03-07 05:58:40 +08:00
throw new FileNotFoundException ( string . Format ( "Could not found: {0}\\Stardew Valley.exe" , Constants . ExecutionPath ) ) ;
2016-03-21 03:43:31 +08:00
}
}
2016-03-07 04:19:59 +08:00
/// <summary>
/// Load Stardev Valley and control features
2016-03-21 03:43:31 +08:00
/// </summary>
2016-03-07 04:19:59 +08:00
private static void ConfigureSDV ( )
2016-03-21 03:43:31 +08:00
{
2016-03-24 01:43:11 +08:00
Log . Info ( "Initializing SDV Assembly..." ) ;
2016-03-21 03:43:31 +08:00
// Load in the assembly - ignores security
StardewAssembly = Assembly . UnsafeLoadFrom ( Constants . ExecutionPath + "\\Stardew Valley.exe" ) ;
StardewProgramType = StardewAssembly . GetType ( "StardewValley.Program" , true ) ;
StardewGameInfo = StardewProgramType . GetField ( "gamePtr" ) ;
// Change the game's version
2016-03-24 01:43:11 +08:00
Log . Verbose ( "Injecting New SDV Version..." ) ;
Game1 . version + = string . Format ( "-Z_MODDED | SMAPI {0}" , Constants . Version . VersionString ) ;
2016-03-21 03:43:31 +08:00
// Create the thread for the game to run in.
gameThread = new Thread ( RunGame ) ;
2016-03-24 01:43:11 +08:00
Log . Info ( "Starting SDV..." ) ;
2016-03-07 05:58:40 +08:00
gameThread . Start ( ) ;
2016-03-05 14:32:04 +08:00
2016-03-07 05:58:40 +08:00
// Wait for the game to load up
2016-03-21 03:43:31 +08:00
while ( ! ready ) ;
//SDV is running
2016-03-24 01:43:11 +08:00
Log . Comment ( "SDV Loaded Into Memory" ) ;
2016-03-21 03:43:31 +08:00
//Create definition to listen for input
2016-03-24 01:43:11 +08:00
Log . Verbose ( "Initializing Console Input Thread..." ) ;
2016-03-21 03:43:31 +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 ;
//Command.RegisterCommand("crash", "crashes sdv").CommandFired += delegate { Game1.player.draw(null); };
//Subscribe to events
2016-03-23 08:36:04 +08:00
ControlEvents . KeyPressed + = Events_KeyPressed ;
GameEvents . LoadContent + = Events_LoadContent ;
2016-03-21 03:43:31 +08:00
//Events.MenuChanged += Events_MenuChanged; //Idk right now
2016-03-24 01:43:11 +08:00
Log . Verbose ( "Applying Final SDV Tweaks..." ) ;
2016-03-21 03:43:31 +08:00
StardewInvoke ( ( ) = >
2016-03-05 14:32:04 +08:00
{
gamePtr . IsMouseVisible = false ;
gamePtr . Window . Title = "Stardew Valley - Version " + Game1 . version ;
2016-03-23 08:36:04 +08:00
StardewForm . Resize + = GraphicsEvents . InvokeResize ;
2016-03-21 03:43:31 +08:00
} ) ;
}
2016-03-07 04:19:59 +08:00
/// <summary>
/// Wrap the 'RunGame' method for console output
2016-03-21 03:43:31 +08:00
/// </summary>
2016-03-07 04:19:59 +08:00
private static void GameRunInvoker ( )
2016-03-21 03:43:31 +08:00
{
//Game's in memory now, send the event
2016-03-24 01:43:11 +08:00
Log . Verbose ( "Game Loaded" ) ;
2016-03-23 08:36:04 +08:00
GameEvents . InvokeGameLoaded ( ) ;
2016-03-21 03:43:31 +08:00
2016-03-24 01:43:11 +08:00
Log . Comment ( "Type 'help' for help, or 'help <cmd>' for a command's usage" ) ;
2016-03-21 03:43:31 +08:00
//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 ( ) ;
2016-03-24 01:43:11 +08:00
Log . Verbose ( "Game Execution Finished" ) ;
Log . Verbose ( "Shutting Down..." ) ;
2016-03-21 03:43:31 +08:00
Thread . Sleep ( 100 ) ;
Environment . Exit ( 0 ) ;
2016-03-05 14:32:04 +08:00
}
2016-03-07 04:19:59 +08:00
/// <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-21 03:43:31 +08:00
{
2016-03-24 01:43:11 +08:00
Log . Error ( "Could not create a path: " + path + "\n\n" + ex ) ;
2016-03-21 03:43:31 +08:00
}
2016-03-07 04:19:59 +08:00
}
2016-03-05 14:32:04 +08:00
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2016-03-21 03:43:31 +08:00
public static void RunGame ( )
{
2016-03-24 01:43:11 +08:00
Application . ThreadException + = Log . Application_ThreadException ;
2016-03-21 03:43:31 +08:00
Application . SetUnhandledExceptionMode ( UnhandledExceptionMode . CatchException ) ;
2016-03-24 01:43:11 +08:00
AppDomain . CurrentDomain . UnhandledException + = Log . CurrentDomain_UnhandledException ;
2016-03-21 03:43:31 +08:00
try
{
gamePtr = new SGame ( ) ;
2016-03-24 01:43:11 +08:00
Log . Verbose ( "Patching SDV Graphics Profile..." ) ;
2016-03-21 03:43:31 +08:00
Game1 . graphics . GraphicsProfile = GraphicsProfile . HiDef ;
LoadMods ( ) ;
2016-03-23 08:36:04 +08:00
StardewForm = Control . FromHandle ( gamePtr . Window . Handle ) . FindForm ( ) ;
2016-03-21 03:43:31 +08:00
StardewForm . Closing + = StardewForm_Closing ;
ready = true ;
StardewGameInfo . SetValue ( StardewProgramType , gamePtr ) ;
gamePtr . Run ( ) ;
}
catch ( Exception ex )
{
2016-03-24 01:43:11 +08:00
Log . Error ( "Game failed to start: " + ex ) ;
2016-03-21 03:43:31 +08:00
}
}
static void StardewForm_Closing ( object sender , CancelEventArgs e )
{
e . Cancel = true ;
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 ( )
{
2016-03-24 01:43:11 +08:00
Log . Verbose ( "LOADING MODS" ) ;
2016-03-21 03:43:31 +08:00
foreach ( string ModPath in _modPaths )
{
2016-03-24 01:43:11 +08:00
foreach ( string d in Directory . GetDirectories ( ModPath ) )
2016-03-21 03:43:31 +08:00
{
2016-03-24 01:43:11 +08:00
foreach ( string s in Directory . GetFiles ( d , "manifest.json" ) )
2016-03-21 03:43:31 +08:00
{
2016-03-21 05:19:02 +08:00
if ( s . Contains ( "StardewInjector" ) )
continue ;
2016-03-24 01:43:11 +08:00
Log . Success ( "Found Manifest: " + s ) ;
2016-03-21 05:19:02 +08:00
Manifest manifest = new Manifest ( ) ;
try
2016-03-21 03:43:31 +08:00
{
2016-03-21 05:19:02 +08:00
string t = File . ReadAllText ( s ) ;
if ( string . IsNullOrEmpty ( t ) )
{
2016-03-24 01:43:11 +08:00
Log . Error ( "Failed to read mod manifest '{0}'. Manifest is empty!" , s ) ;
2016-03-21 05:19:02 +08:00
continue ;
}
2016-03-23 08:36:04 +08:00
2016-03-23 16:22:47 +08:00
manifest = manifest . InitializeConfig ( s ) ;
2016-03-21 05:19:02 +08:00
if ( string . IsNullOrEmpty ( manifest . EntryDll ) )
{
2016-03-24 01:43:11 +08:00
Log . Error ( "Failed to read mod manifest '{0}'. EntryDll is empty!" , s ) ;
2016-03-21 05:19:02 +08:00
continue ;
}
2016-03-21 03:43:31 +08:00
}
2016-03-21 05:19:02 +08:00
catch ( Exception ex )
2016-03-21 03:43:31 +08:00
{
2016-03-24 01:43:11 +08:00
Log . Error ( "Failed to read mod manifest '{0}'. Exception details:\n" + ex , s ) ;
2016-03-21 05:19:02 +08:00
continue ;
}
try
2016-03-23 08:36:04 +08:00
{
if ( manifest . PerSaveConfigs )
{
if ( ! Directory . Exists ( Path . GetDirectoryName ( s ) ) )
Directory . CreateDirectory ( Path . GetDirectoryName ( s ) ) ;
if ( ! Directory . Exists ( Path . GetDirectoryName ( s ) ) )
{
2016-03-24 01:43:11 +08:00
Log . Error ( "Failed to create psconfigs directory '{0}'. No exception occured." , Path . GetDirectoryName ( s ) ) ;
2016-03-23 08:36:04 +08:00
continue ;
}
}
}
catch ( Exception ex )
{
2016-03-24 01:43:11 +08:00
Log . Error ( "Failed to create psconfigs directory '{0}'. Exception details:\n" + ex , Path . GetDirectoryName ( s ) ) ;
2016-03-23 08:36:04 +08:00
continue ;
}
2016-03-24 04:43:36 +08:00
string targDll = string . Empty ;
2016-03-23 08:36:04 +08:00
try
2016-03-21 05:19:02 +08:00
{
2016-03-24 04:43:36 +08:00
targDll = Path . Combine ( Path . GetDirectoryName ( s ) , manifest . EntryDll ) ;
2016-03-21 05:19:02 +08:00
if ( ! File . Exists ( targDll ) )
{
2016-03-24 04:43:36 +08:00
Log . Error ( "Failed to load mod '{0}'. File {1} does not exist!" , manifest . EntryDll , targDll ) ;
2016-03-21 05:19:02 +08:00
continue ;
}
Assembly mod = Assembly . UnsafeLoadFrom ( targDll ) ;
if ( mod . DefinedTypes . Count ( x = > x . BaseType = = typeof ( Mod ) ) > 0 )
{
2016-03-24 01:43:11 +08:00
Log . Verbose ( "Loading Mod DLL..." ) ;
2016-03-21 05:19:02 +08:00
TypeInfo tar = mod . DefinedTypes . First ( x = > x . BaseType = = typeof ( Mod ) ) ;
Mod m = ( Mod ) mod . CreateInstance ( tar . ToString ( ) ) ;
m . PathOnDisk = Path . GetDirectoryName ( s ) ;
m . Manifest = manifest ;
2016-03-24 01:43:11 +08:00
Log . Success ( "LOADED MOD: {0} by {1} - Version {2} | Description: {3} (@ {4})" , m . Manifest . Name , m . Manifest . Authour , m . Manifest . Version , m . Manifest . Description , targDll ) ;
2016-03-23 08:36:04 +08:00
Constants . ModsLoaded + = 1 ;
2016-03-21 05:19:02 +08:00
m . Entry ( ) ;
}
else
{
2016-03-24 01:43:11 +08:00
Log . Error ( "Invalid Mod DLL" ) ;
2016-03-21 05:19:02 +08:00
}
}
catch ( Exception ex )
{
2016-03-24 04:43:36 +08:00
Log . Error ( "Failed to load mod '{0}'. Exception details:\n" + ex , targDll ) ;
2016-03-21 03:43:31 +08:00
}
}
}
}
2016-03-24 01:43:11 +08:00
Log . Success ( "LOADED {0} MODS" , Constants . ModsLoaded ) ;
2016-03-23 08:52:06 +08:00
Console . Title = Constants . ConsoleTitle ;
2016-03-21 03:43:31 +08:00
}
public static void ConsoleInputThread ( )
{
string input = string . Empty ;
while ( true )
{
Command . CallCommand ( Console . ReadLine ( ) ) ;
}
}
static void Events_LoadContent ( object o , EventArgs e )
{
2016-03-24 01:43:11 +08:00
Log . Info ( "Initializing Debug Assets..." ) ;
2016-03-21 03:43:31 +08:00
DebugPixel = new Texture2D ( Game1 . graphics . GraphicsDevice , 1 , 1 ) ;
2016-03-23 08:36:04 +08:00
DebugPixel . SetData ( new [ ] { Color . White } ) ;
2016-03-05 14:32:04 +08:00
2016-03-21 03:43:31 +08:00
#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
}
static void Events_KeyPressed ( object o , EventArgsKeyPressed e )
{
}
static void Events_MenuChanged ( IClickableMenu newMenu )
2016-03-05 14:32:04 +08:00
{
2016-03-24 01:43:11 +08:00
Log . Verbose ( "NEW MENU: " + newMenu . GetType ( ) ) ;
2016-03-21 03:43:31 +08:00
if ( newMenu is GameMenu )
{
Game1 . activeClickableMenu = SGameMenu . ConstructFromBaseClass ( Game1 . activeClickableMenu as GameMenu ) ;
}
}
2016-03-05 14:32:04 +08:00
2016-03-21 03:43:31 +08:00
static void Events_LocationsChanged ( List < GameLocation > newLocations )
2016-03-05 14:32:04 +08:00
{
2016-03-21 03:43:31 +08:00
#if DEBUG
SGame . ModLocations = SGameLocation . ConstructFromBaseClasses ( Game1 . locations ) ;
#endif
}
static void Events_CurrentLocationChanged ( GameLocation newLocation )
2016-03-05 14:32:04 +08:00
{
//SGame.CurrentLocation = null;
//System.Threading.Thread.Sleep(10);
2016-03-21 03:43:31 +08:00
#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 ) ;
2016-03-05 14:32:04 +08:00
}
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 )
2016-03-24 01:43:11 +08:00
Log . Error ( "The command specified could not be found" ) ;
2016-03-05 14:32:04 +08:00
else
{
if ( fnd . CommandArgs . Length > 0 )
2016-03-24 01:43:11 +08:00
Log . Info ( "{0}: {1} - {2}" , fnd . CommandName , fnd . CommandDesc , fnd . CommandArgs . ToSingular ( ) ) ;
2016-03-05 14:32:04 +08:00
else
2016-03-24 01:43:11 +08:00
Log . Info ( "{0}: {1}" , fnd . CommandName , fnd . CommandDesc ) ;
2016-03-05 14:32:04 +08:00
}
}
else
2016-03-24 01:43:11 +08:00
Log . Info ( "Commands: " + Command . RegisteredCommands . Select ( x = > x . CommandName ) . ToSingular ( ) ) ;
2016-03-05 14:32:04 +08:00
}
2016-03-21 03:43:31 +08:00
}
2016-03-04 04:06:25 +08:00
}