diff --git a/StardewModdingAPI/API/Game.cs b/StardewModdingAPI/API/Game.cs new file mode 100644 index 00000000..2469fb20 --- /dev/null +++ b/StardewModdingAPI/API/Game.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StardewModdingAPI.API +{ + public class Game + { + } +} diff --git a/StardewModdingAPI/Events/Game.cs b/StardewModdingAPI/Events/Game.cs index 6290d2c7..f6e18cee 100644 --- a/StardewModdingAPI/Events/Game.cs +++ b/StardewModdingAPI/Events/Game.cs @@ -17,7 +17,7 @@ namespace StardewModdingAPI.Events { GameLoaded.Invoke(null, EventArgs.Empty); } - + public static void InvokeInitialize() { try diff --git a/StardewModdingAPI/Helpers/CecilContext.cs b/StardewModdingAPI/Helpers/CecilContext.cs index b25a9ba6..706acd41 100644 --- a/StardewModdingAPI/Helpers/CecilContext.cs +++ b/StardewModdingAPI/Helpers/CecilContext.cs @@ -154,5 +154,10 @@ namespace StardewModdingAPI.Helpers } return reference; } + + internal void WriteAssembly(string file) + { + _assemblyDefinition.Write(file); + } } } diff --git a/StardewModdingAPI/Helpers/CecilHelper.cs b/StardewModdingAPI/Helpers/CecilHelper.cs index ded40946..aed7fb82 100644 --- a/StardewModdingAPI/Helpers/CecilHelper.cs +++ b/StardewModdingAPI/Helpers/CecilHelper.cs @@ -16,15 +16,18 @@ namespace StardewModdingAPI.Helpers private static void InjectMethod(ILProcessor ilProcessor, Instruction target, MethodReference method) { - var callTarget = target; + Instruction callEnterInstruction = ilProcessor.Create(OpCodes.Call, method); if (method.HasParameters) { Instruction loadObjInstruction = ilProcessor.Create(OpCodes.Ldarg_0); ilProcessor.InsertBefore(target, loadObjInstruction); - callTarget = loadObjInstruction; + ilProcessor.InsertAfter(loadObjInstruction, callEnterInstruction); } - Instruction callEnterInstruction = ilProcessor.Create(OpCodes.Call, method); - ilProcessor.InsertAfter(callTarget, callEnterInstruction); + else + { + ilProcessor.InsertBefore(target, callEnterInstruction); + } + } private static void InjectMethod(ILProcessor ilProcessor, IEnumerable targets, MethodReference method) diff --git a/StardewModdingAPI/Helpers/ReflectionHelper.cs b/StardewModdingAPI/Helpers/ReflectionHelper.cs index 72c484b9..7e644e51 100644 --- a/StardewModdingAPI/Helpers/ReflectionHelper.cs +++ b/StardewModdingAPI/Helpers/ReflectionHelper.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Text; using System.Threading.Tasks; @@ -8,6 +9,140 @@ namespace StardewModdingAPI.Helpers { public static class ReflectionHelper { + private class FieldCompatability + { + public List> MatchingFields = new List>(); + public List> FixableMismatchingFields = new List>(); + public List> UnfixableMismatchingFields = new List>(); + public List FieldsMissingFromA = new List(); + public List FieldsMissingFromB = new List(); + } + private static FieldCompatability AssessFieldCompatbility(FieldInfo[] a, FieldInfo[] b) + { + FieldCompatability results = new FieldCompatability(); + List editableB = new List(b); + + foreach (var leftField in a) + { + FieldInfo matchingField = null; + foreach(var rightField in editableB) + { + if(leftField.Name == rightField.Name) + { + matchingField = rightField; + if (leftField.FieldType == rightField.FieldType) + { + results.MatchingFields.Add(Tuple.Create(leftField, rightField)); + } + else if(leftField.FieldType.FullName == rightField.FieldType.FullName) + { + results.FixableMismatchingFields.Add(Tuple.Create(leftField, rightField)); + } + else + { + results.UnfixableMismatchingFields.Add(Tuple.Create(leftField, rightField)); + } + break; + } + } + + if (matchingField != null) + { + editableB.Remove(matchingField); + } + else + { + results.FieldsMissingFromB.Add(leftField); + } + } + + results.FieldsMissingFromA.AddRange(editableB); + return results; + } + + private static void WarnMismatch(FieldCompatability compatibility, string source) + { + if (compatibility.UnfixableMismatchingFields.Any()) + { + Log.Warning("Unfixable type mismatch in {0}. Mods which depend on these types may fail to work properly", source); + foreach (var mismatch in compatibility.UnfixableMismatchingFields) + { + Log.Warning("- {0} is not of type {1}, is of type {2}", mismatch.Item1.Name, mismatch.Item1.FieldType.FullName, mismatch.Item2.FieldType.FullName); + } + } + } + + private static void WarnMissing(FieldCompatability compatibility, string source) + { + if (compatibility.FieldsMissingFromA.Any() || compatibility.FieldsMissingFromB.Any()) + { + Log.Warning("The following fields are not present in both objects", source); + + foreach (var mismatch in compatibility.FieldsMissingFromA) + Log.Warning("- Left Object Missing {0} ({1})", mismatch.Name, mismatch.FieldType.FullName); + foreach (var mismatch in compatibility.FieldsMissingFromA) + Log.Warning("- Right Object Missing {0} ({1})", mismatch.Name, mismatch.FieldType.FullName); + } + } + + private static void RecursiveMemberwiseCast(Type toType, Type fromType, object to, object from) + { + Log.Verbose("Writing {0}", toType.Name); + + Stack> pendingCasts = new Stack>(); + List alreadyProcessed = new List(); + + pendingCasts.Push(Tuple.Create(toType, fromType, + to, from)); + + while (pendingCasts.Count > 0) + { + Tuple current = pendingCasts.Pop(); + + Type castTo = current.Item1; + Type castFrom = current.Item2; + object objectToSet = current.Item3; + object baseObject = current.Item4; + + var targetFields = castTo + .GetFields().Where(n => !n.IsInitOnly && !n.IsLiteral).ToArray(); + var baseFields = castFrom + .GetFields().Where(n => !n.IsInitOnly && !n.IsLiteral).ToArray(); + + var compatibility = AssessFieldCompatbility(targetFields, baseFields); + + WarnMissing(compatibility, from.GetType().FullName); + WarnMismatch(compatibility, from.GetType().FullName); + + foreach (var match in compatibility.MatchingFields) + { + var toValue = match.Item1.GetValue(objectToSet); + var fromValue = match.Item2.GetValue(baseObject); + + match.Item2.SetValue(objectToSet, fromValue); + } + + foreach (var fixableMismatch in compatibility.FixableMismatchingFields) + { + var toValue = fixableMismatch.Item1.GetValue(objectToSet); + var fromValue = fixableMismatch.Item2.GetValue(baseObject); + + if (fromValue != null && !alreadyProcessed.Any(n => Object.ReferenceEquals(n, fromValue))) + { + alreadyProcessed.Add(fromValue); + //pendingCasts.Push(Tuple.Create(fixableMismatch.Item1.FieldType, fixableMismatch.Item2.FieldType, + // toValue, fromValue)); + } + } + } + } + + public static T MemberwiseCast(this object @base) where T : new() + { + T retObj = new T(); + RecursiveMemberwiseCast(@base.GetType(), retObj.GetType().BaseType, retObj, @base); + return retObj; + } } } diff --git a/StardewModdingAPI/Log.cs b/StardewModdingAPI/Log.cs index 2784b709..96a5875a 100644 --- a/StardewModdingAPI/Log.cs +++ b/StardewModdingAPI/Log.cs @@ -78,6 +78,16 @@ namespace StardewModdingAPI /// /// public static void Comment(object message, params object[] values) + { + Log.PrintLog(message?.ToString(), false, values); + } + + /// + /// Additional comment to display to console and logging. + /// + /// + /// + public static void Warning(object message, params object[] values) { Console.ForegroundColor = ConsoleColor.Yellow; Log.PrintLog(message?.ToString(), false, values); diff --git a/StardewModdingAPI/Program.cs b/StardewModdingAPI/Program.cs index e8e1ec17..3d0a4c5e 100644 --- a/StardewModdingAPI/Program.cs +++ b/StardewModdingAPI/Program.cs @@ -26,7 +26,15 @@ namespace StardewModdingAPI public static Texture2D DebugPixel { get; private set; } - public static SGame gamePtr; + private static object modifiedAssemblyGame; + //public static Game1 gamePtr; + public static Game1 gamePtr + { + get + { + return modifiedAssemblyGame.MemberwiseCast(); + } + } public static bool ready; public static Assembly StardewAssembly; @@ -78,13 +86,18 @@ namespace StardewModdingAPI //CecilHelper.RedirectConstructor(StardewContext, SmapiContext, "StardewValley.Program", "Main", // "StardewValley.Game1", ".ctor", "StardewModdingAPI.Inheritance.SGame", ".ctor"); CecilHelper.InjectExitMethod(StardewContext, SmapiContext, "StardewValley.Game1", ".ctor", "StardewModdingAPI.Program", "Test"); + CecilHelper.InjectExitMethod(StardewContext, SmapiContext, "StardewValley.Game1", "Initialize", "StardewModdingAPI.Events.GameEvents", "InvokeInitialize"); + CecilHelper.InjectExitMethod(StardewContext, SmapiContext, "StardewValley.Game1", "LoadContent", "StardewModdingAPI.Events.GameEvents", "InvokeLoadContent"); + CecilHelper.InjectExitMethod(StardewContext, SmapiContext, "StardewValley.Game1", "Update", "StardewModdingAPI.Events.GameEvents", "InvokeUpdateTick"); + CecilHelper.InjectExitMethod(StardewContext, SmapiContext, "StardewValley.Game1", "Draw", "StardewModdingAPI.Events.GraphicsEvents", "InvokeDrawTick"); + //TODO - Invoke Resize + + } - public static void Test(Game1 instance) + public static void Test(object instance) { - var inst = instance; - string test = Game1.samBandName; - var test2 = Game1.numberOfSelectedItems; + modifiedAssemblyGame = instance; } /// @@ -135,12 +148,11 @@ namespace StardewModdingAPI private static void ConfigureSDV() { StardewModdingAPI.Log.Info("Initializing SDV Assembly..."); - + // Load in the assembly - ignores security - //StardewAssembly = Assembly.UnsafeLoadFrom(Constants.ExecutionPath + "\\Stardew Valley.exe"); StardewAssembly = Assembly.Load(StardewContext.ModifiedAssembly.GetBuffer()); StardewProgramType = StardewAssembly.GetType("StardewValley.Program", true); - StardewGameInfo = StardewProgramType.GetField("gamePtr"); + StardewGameInfo = StardewProgramType.GetField("gamePtr"); // Change the game's version StardewModdingAPI.Log.Verbose("Injecting New SDV Version..."); @@ -164,12 +176,7 @@ namespace StardewModdingAPI // 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; //Command.RegisterCommand("crash", "crashes sdv").CommandFired += delegate { Game1.player.draw(null); }; - - //Subscribe to events - Events.ControlEvents.KeyPressed += Events_KeyPressed; - Events.GameEvents.LoadContent += Events_LoadContent; - //Events.MenuChanged += Events_MenuChanged; //Idk right now - + StardewModdingAPI.Log.Verbose("Applying Final SDV Tweaks..."); StardewAssembly.EntryPoint.Invoke(null, new object[] { new string[] { } }); @@ -241,20 +248,8 @@ namespace StardewModdingAPI AppDomain.CurrentDomain.UnhandledException += StardewModdingAPI.Log.CurrentDomain_UnhandledException; try - { - //gamePtr = new SGame(); - //StardewModdingAPI.Log.Verbose("Patching SDV Graphics Profile..."); - //Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; - //LoadMods(); - - //StardewForm = Control.FromHandle(Program.gamePtr.Window.Handle).FindForm(); - //StardewForm.Closing += StardewForm_Closing; - + { ready = true; - - //Game1 g1 = gamePtr as Game1; - //StardewGameInfo.SetValue(StardewProgramType, g1); - //gamePtr.Run(); } catch (Exception ex) { @@ -322,73 +317,7 @@ namespace StardewModdingAPI Command.CallCommand(Console.ReadLine()); } } - - 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 - } - - 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 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); diff --git a/StardewModdingAPI/StardewModdingAPI.csproj b/StardewModdingAPI/StardewModdingAPI.csproj index 929ba488..b233eb81 100644 --- a/StardewModdingAPI/StardewModdingAPI.csproj +++ b/StardewModdingAPI/StardewModdingAPI.csproj @@ -125,6 +125,7 @@ +