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 ;
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 ;
2016-03-27 13:09:09 +08:00
//private static List<string> _modContentPaths;
public static Texture2D DebugPixel { get ; private set ; }
2016-03-21 03:43:31 +08:00
2016-03-27 18:40:09 +08:00
// ReSharper disable once PossibleNullReferenceException
public static int BuildType = > ( int ) StardewProgramType . GetField ( "buildType" , BindingFlags . Public | BindingFlags . Static ) . GetValue ( null ) ;
2016-03-21 03:43:31 +08:00
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2016-03-07 04:19:59 +08:00
/// <summary>
2016-03-27 13:09:09 +08:00
/// Main method holding the API execution
2016-03-07 04:19:59 +08:00
/// </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
{
2016-04-01 04:47:19 +08:00
Log . AsyncY ( "SDV Version: " + Game1 . version ) ;
Log . AsyncY ( "SMAPI Version: " + Constants . Version . VersionString ) ;
2016-03-07 04:19:59 +08:00
ConfigureUI ( ) ;
ConfigurePaths ( ) ;
ConfigureSDV ( ) ;
GameRunInvoker ( ) ;
}
catch ( Exception e )
{
// Catch and display all exceptions.
2016-03-27 09:50:47 +08:00
Console . WriteLine ( e ) ;
Console . ReadKey ( ) ;
2016-03-27 13:09:09 +08:00
Log . AsyncR ( "Critical error: " + e ) ;
2016-03-07 04:19:59 +08:00
}
2016-03-27 13:09:09 +08:00
Log . AsyncY ( "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>
2016-03-27 13:09:09 +08:00
/// 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>
2016-03-27 13:09:09 +08:00
/// Setup the required paths and logging
2016-03-07 04:19:59 +08:00
/// </summary>
private static void ConfigurePaths ( )
{
2016-03-27 13:09:09 +08:00
Log . AsyncY ( "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-27 09:50:47 +08:00
VerifyPath ( Constants . LogDir ) ;
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>
2016-03-27 13:09:09 +08:00
/// 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-27 13:09:09 +08:00
Log . AsyncY ( "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-27 13:09:09 +08:00
Log . AsyncY ( "Injecting New SDV Version..." ) ;
Game1 . version + = $"-Z_MODDED | SMAPI {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-27 13:09:09 +08:00
Log . AsyncY ( "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-27 13:09:09 +08:00
while ( ! ready )
{
}
2016-03-21 03:43:31 +08:00
//SDV is running
2016-03-27 13:09:09 +08:00
Log . AsyncY ( "SDV Loaded Into Memory" ) ;
2016-03-21 03:43:31 +08:00
//Create definition to listen for input
2016-03-27 13:09:09 +08:00
Log . AsyncY ( "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-27 13:09:09 +08:00
Log . AsyncY ( "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>
2016-03-27 13:09:09 +08:00
/// 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-27 13:09:09 +08:00
Log . AsyncY ( "Game Loaded" ) ;
2016-03-23 08:36:04 +08:00
GameEvents . InvokeGameLoaded ( ) ;
2016-03-21 03:43:31 +08:00
2016-03-27 13:09:09 +08:00
Log . AsyncY ( "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-27 13:09:09 +08:00
Log . AsyncY ( "Game Execution Finished" ) ;
Log . AsyncY ( "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>
2016-03-27 13:09:09 +08:00
/// Create the given directory path if it does not exist
2016-03-07 04:19:59 +08:00
/// </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-27 13:09:09 +08:00
Log . AsyncR ( "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-27 13:09:09 +08:00
Log . AsyncY ( "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-27 13:09:09 +08:00
Log . AsyncR ( "Game failed to start: " + ex ) ;
2016-03-21 03:43:31 +08:00
}
}
2016-03-27 13:09:09 +08:00
private static void StardewForm_Closing ( object sender , CancelEventArgs e )
2016-03-21 03:43:31 +08:00
{
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-27 13:09:09 +08:00
Log . AsyncY ( "LOADING MODS" ) ;
foreach ( var ModPath in _modPaths )
2016-03-21 03:43:31 +08:00
{
2016-03-27 13:09:09 +08:00
foreach ( var d in Directory . GetDirectories ( ModPath ) )
2016-03-21 03:43:31 +08:00
{
2016-03-27 13:09:09 +08:00
foreach ( var 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-27 13:09:09 +08:00
Log . AsyncG ( "Found Manifest: " + s ) ;
var manifest = new Manifest ( ) ;
2016-03-21 05:19:02 +08:00
try
2016-03-21 03:43:31 +08:00
{
2016-03-27 13:09:09 +08:00
var t = File . ReadAllText ( s ) ;
2016-03-21 05:19:02 +08:00
if ( string . IsNullOrEmpty ( t ) )
{
2016-03-27 13:09:09 +08:00
Log . AsyncR ( $"Failed to read mod manifest '{s}'. Manifest is empty!" ) ;
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-27 13:09:09 +08:00
Log . AsyncR ( $"Failed to read mod manifest '{s}'. EntryDll is empty!" ) ;
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-27 13:09:09 +08:00
Log . AsyncR ( $"Failed to read mod manifest '{s}'. Exception details:\n" + ex ) ;
2016-03-21 05:19:02 +08:00
continue ;
}
2016-03-27 13:09:09 +08:00
var targDir = Path . GetDirectoryName ( s ) ;
var psDir = Path . Combine ( targDir , "psconfigs" ) ;
Log . AsyncY ( $"Created psconfigs directory @{psDir}" ) ;
2016-03-21 05:19:02 +08:00
try
2016-03-23 08:36:04 +08:00
{
if ( manifest . PerSaveConfigs )
{
2016-03-27 09:50:47 +08:00
if ( ! Directory . Exists ( psDir ) )
{
Directory . CreateDirectory ( psDir ) ;
2016-03-27 13:09:09 +08:00
Log . AsyncY ( $"Created psconfigs directory @{psDir}" ) ;
2016-03-27 09:50:47 +08:00
}
2016-03-23 08:36:04 +08:00
2016-03-27 09:50:47 +08:00
if ( ! Directory . Exists ( psDir ) )
2016-03-23 08:36:04 +08:00
{
2016-03-27 13:09:09 +08:00
Log . AsyncR ( $"Failed to create psconfigs directory '{psDir}'. No exception occured." ) ;
2016-03-23 08:36:04 +08:00
continue ;
}
}
}
catch ( Exception ex )
{
2016-03-27 13:09:09 +08:00
Log . AsyncR ( $"Failed to create psconfigs directory '{targDir}'. Exception details:\n" + ex ) ;
2016-03-23 08:36:04 +08:00
continue ;
}
2016-03-27 13:09:09 +08:00
var targDll = string . Empty ;
2016-03-23 08:36:04 +08:00
try
2016-03-21 05:19:02 +08:00
{
2016-03-27 09:50:47 +08:00
targDll = Path . Combine ( targDir , manifest . EntryDll ) ;
2016-03-21 05:19:02 +08:00
if ( ! File . Exists ( targDll ) )
{
2016-03-27 13:09:09 +08:00
Log . AsyncR ( $"Failed to load mod '{manifest.EntryDll}'. File {targDll} does not exist!" ) ;
2016-03-21 05:19:02 +08:00
continue ;
}
2016-03-27 13:09:09 +08:00
var mod = Assembly . UnsafeLoadFrom ( targDll ) ;
2016-03-21 05:19:02 +08:00
if ( mod . DefinedTypes . Count ( x = > x . BaseType = = typeof ( Mod ) ) > 0 )
{
2016-03-27 13:09:09 +08:00
Log . AsyncY ( "Loading Mod DLL..." ) ;
var tar = mod . DefinedTypes . First ( x = > x . BaseType = = typeof ( Mod ) ) ;
var m = ( Mod ) mod . CreateInstance ( tar . ToString ( ) ) ;
2016-03-27 09:50:47 +08:00
m . PathOnDisk = targDir ;
2016-03-21 05:19:02 +08:00
m . Manifest = manifest ;
2016-03-27 13:09:09 +08:00
Log . AsyncG ( $"LOADED MOD: {m.Manifest.Name} by {m.Manifest.Authour} - Version {m.Manifest.Version} | Description: {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-27 13:09:09 +08:00
Log . AsyncR ( "Invalid Mod DLL" ) ;
2016-03-21 05:19:02 +08:00
}
}
catch ( Exception ex )
{
2016-03-27 13:09:09 +08:00
Log . AsyncR ( $"Failed to load mod '{targDll}'. Exception details:\n" + ex ) ;
2016-03-21 03:43:31 +08:00
}
}
}
}
2016-03-27 13:09:09 +08:00
Log . AsyncG ( $"LOADED {Constants.ModsLoaded} MODS" ) ;
2016-03-23 08:52:06 +08:00
Console . Title = Constants . ConsoleTitle ;
2016-03-21 03:43:31 +08:00
}
public static void ConsoleInputThread ( )
{
2016-03-27 13:09:09 +08:00
var input = string . Empty ;
2016-03-21 03:43:31 +08:00
while ( true )
{
Command . CallCommand ( Console . ReadLine ( ) ) ;
}
}
2016-03-27 13:09:09 +08:00
private static void Events_LoadContent ( object o , EventArgs e )
2016-03-21 03:43:31 +08:00
{
2016-03-27 13:09:09 +08:00
Log . AsyncY ( "Initializing Debug Assets..." ) ;
2016-03-21 03:43:31 +08:00
DebugPixel = new Texture2D ( Game1 . graphics . GraphicsDevice , 1 , 1 ) ;
2016-03-27 13:09:09 +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
2016-03-27 13:09:09 +08:00
StardewModdingAPI . Log . Async ( "REGISTERING BASE CUSTOM ITEM" ) ;
2016-03-21 03:43:31 +08:00
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 ;
2016-03-27 13:09:09 +08:00
StardewModdingAPI . Log . Async ( "REGISTERED WITH ID OF: " + SGame . RegisterModItem ( so ) ) ;
2016-03-21 03:43:31 +08:00
2016-03-27 13:09:09 +08:00
//StardewModdingAPI.Log.Async("REGISTERING SECOND CUSTOM ITEM");
2016-03-21 03:43:31 +08:00
//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;
2016-03-27 13:09:09 +08:00
//StardewModdingAPI.Log.Async("REGISTERED WITH ID OF: " + SGame.RegisterModItem(so2));
2016-03-21 03:43:31 +08:00
Command . CallCommand ( "load" ) ;
#endif
}
2016-03-27 13:09:09 +08:00
private static void Events_KeyPressed ( object o , EventArgsKeyPressed e )
2016-03-21 03:43:31 +08:00
{
}
2016-03-27 13:09:09 +08:00
private static void Events_MenuChanged ( IClickableMenu newMenu )
2016-03-05 14:32:04 +08:00
{
2016-03-27 13:09:09 +08:00
Log . AsyncY ( "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-27 13:09:09 +08:00
private 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
}
2016-03-27 13:09:09 +08:00
private 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
}
2016-03-27 13:09:09 +08:00
private static void help_CommandFired ( object o , EventArgsCommand e )
2016-03-05 14:32:04 +08:00
{
if ( e . Command . CalledArgs . Length > 0 )
{
2016-03-27 13:09:09 +08:00
var fnd = Command . FindCommand ( e . Command . CalledArgs [ 0 ] ) ;
2016-03-05 14:32:04 +08:00
if ( fnd = = null )
2016-03-27 13:09:09 +08:00
Log . AsyncR ( "The command specified could not be found" ) ;
2016-03-05 14:32:04 +08:00
else
{
if ( fnd . CommandArgs . Length > 0 )
2016-03-27 13:09:09 +08:00
Log . AsyncY ( $"{fnd.CommandName}: {fnd.CommandDesc} - {fnd.CommandArgs.ToSingular()}" ) ;
2016-03-05 14:32:04 +08:00
else
2016-03-27 13:09:09 +08:00
Log . AsyncY ( $"{fnd.CommandName}: {fnd.CommandDesc}" ) ;
2016-03-05 14:32:04 +08:00
}
}
else
2016-03-27 13:09:09 +08:00
Log . AsyncY ( "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
}