diff --git a/StardewModdingAPI/API/Game.cs b/StardewModdingAPI/API/Game.cs index 2469fb20..a3efded8 100644 --- a/StardewModdingAPI/API/Game.cs +++ b/StardewModdingAPI/API/Game.cs @@ -6,7 +6,8 @@ using System.Threading.Tasks; namespace StardewModdingAPI.API { - public class Game + class Game { + } } diff --git a/StardewModdingAPI/Events/Game.cs b/StardewModdingAPI/Events/Game.cs index f6e18cee..819f0304 100644 --- a/StardewModdingAPI/Events/Game.cs +++ b/StardewModdingAPI/Events/Game.cs @@ -46,6 +46,8 @@ namespace StardewModdingAPI.Events { try { + Program.IsGameReferenceDirty = true; + var test = Program.gamePtr; UpdateTick.Invoke(null, EventArgs.Empty); } catch (Exception ex) diff --git a/StardewModdingAPI/ExtensionMethods/Array.cs b/StardewModdingAPI/ExtensionMethods/Array.cs new file mode 100644 index 00000000..bbe0e5f4 --- /dev/null +++ b/StardewModdingAPI/ExtensionMethods/Array.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StardewModdingAPI.ExtensionMethods +{ + public static class ArrayExtensions + { + public static void ForEach(this Array array, Action action) + { + if (array.LongLength == 0) return; + ArrayTraverse walker = new ArrayTraverse(array); + do action(array, walker.Position); + while (walker.Step()); + } + + //public static void ForEach(Array array, Action action) + //{ + // if (array.LongLength == 0) return; + // ArrayTraverse walker = new ArrayTraverse(array); + // do action(array, walker.Position); + // while (walker.Step()); + //} + } + + internal class ArrayTraverse + { + public int[] Position; + private int[] maxLengths; + + public ArrayTraverse(Array array) + { + maxLengths = new int[array.Rank]; + for (int i = 0; i < array.Rank; ++i) + { + maxLengths[i] = array.GetLength(i) - 1; + } + Position = new int[array.Rank]; + } + + public bool Step() + { + for (int i = 0; i < Position.Length; ++i) + { + if (Position[i] < maxLengths[i]) + { + Position[i]++; + for (int j = 0; j < i; j++) + { + Position[j] = 0; + } + return true; + } + } + return false; + } + } +} diff --git a/StardewModdingAPI/ExtensionMethods/Object.cs b/StardewModdingAPI/ExtensionMethods/Object.cs new file mode 100644 index 00000000..2400df7f --- /dev/null +++ b/StardewModdingAPI/ExtensionMethods/Object.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace StardewModdingAPI.ExtensionMethods +{ + //From https://github.com/jpmikkers/net-object-deep-copy/blob/master/ObjectExtensions.cs + public static class ObjectExtensions + { + public static T Copy(this object original) + { + return (T)new DeepCopyContext().InternalCopy(original, true); + } + + private class DeepCopyContext + { + private static readonly Func CloneMethod; + private readonly Dictionary m_Visited; + private readonly Dictionary m_NonShallowFieldCache; + + static DeepCopyContext() + { + MethodInfo cloneMethod = typeof(Object).GetMethod("MemberwiseClone", BindingFlags.NonPublic | BindingFlags.Instance); + var p1 = Expression.Parameter(typeof(object)); + var body = Expression.Call(p1, cloneMethod); + CloneMethod = Expression.Lambda>(body, p1).Compile(); + //Console.WriteLine("typeof(object) contains {0} nonshallow fields", NonShallowFields(typeof(object)).Count()); + } + + public DeepCopyContext() + { + m_Visited = new Dictionary(new ReferenceEqualityComparer()); + m_NonShallowFieldCache = new Dictionary(); + } + + private static bool IsPrimitive(Type type) + { + if (type.IsValueType && type.IsPrimitive) return true; + if (type == typeof(String)) return true; + if (type == typeof(Decimal)) return true; + if (type == typeof(DateTime)) return true; + return false; + } + + public Object InternalCopy(Object originalObject, bool includeInObjectGraph) + { + if (originalObject == null) return null; + var typeToReflect = originalObject.GetType(); + if (IsPrimitive(typeToReflect)) return originalObject; + + if (typeof(XElement).IsAssignableFrom(typeToReflect)) return new XElement(originalObject as XElement); + if (typeof(Delegate).IsAssignableFrom(typeToReflect)) return null; + + if (includeInObjectGraph) + { + object result; + if (m_Visited.TryGetValue(originalObject, out result)) return result; + } + + var cloneObject = CloneMethod(originalObject); + + if (includeInObjectGraph) + { + m_Visited.Add(originalObject, cloneObject); + } + + if (typeToReflect.IsArray) + { + var arrayElementType = typeToReflect.GetElementType(); + + if (IsPrimitive(arrayElementType)) + { + // for an array of primitives, do nothing. The shallow clone is enough. + } + else if (arrayElementType.IsValueType) + { + // if its an array of structs, there's no need to check and add the individual elements to 'visited', because in .NET it's impossible to create + // references to individual array elements. + Array clonedArray = (Array)cloneObject; + clonedArray.ForEach((array, indices) => array.SetValue(InternalCopy(clonedArray.GetValue(indices), false), indices)); + } + else + { + // it's an array of ref types + Array clonedArray = (Array)cloneObject; + clonedArray.ForEach((array, indices) => array.SetValue(InternalCopy(clonedArray.GetValue(indices), true), indices)); + } + } + else + { + foreach (var fieldInfo in CachedNonShallowFields(typeToReflect)) + { + var originalFieldValue = fieldInfo.GetValue(originalObject); + // a valuetype field can never have a reference pointing to it, so don't check the object graph in that case + + Log.Error("Replace this with a recurse-less version"); + var clonedFieldValue = InternalCopy(originalFieldValue, !fieldInfo.FieldType.IsValueType); + fieldInfo.SetValue(cloneObject, clonedFieldValue); + } + } + + return cloneObject; + } + + private FieldInfo[] CachedNonShallowFields(Type typeToReflect) + { + FieldInfo[] result; + + if (!m_NonShallowFieldCache.TryGetValue(typeToReflect, out result)) + { + result = NonShallowFields(typeToReflect).ToArray(); + m_NonShallowFieldCache[typeToReflect] = result; + } + + return result; + } + + /// + /// From the given type hierarchy (i.e. including all base types), return all fields that should be deep-copied + /// + /// + /// + private static IEnumerable NonShallowFields(Type typeToReflect) + { + while (typeToReflect != typeof(object)) + { + foreach (var fieldInfo in typeToReflect.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly)) + { + if (IsPrimitive(fieldInfo.FieldType)) continue; // this is 5% faster than a where clause.. + yield return fieldInfo; + } + typeToReflect = typeToReflect.BaseType; + } + } + } + } + + public class ReferenceEqualityComparer : EqualityComparer + { + public override bool Equals(object x, object y) + { + return ReferenceEquals(x, y); + } + + public override int GetHashCode(object obj) + { + if (obj == null) return 0; + // The RuntimeHelpers.GetHashCode method always calls the Object.GetHashCode method non-virtually, + // even if the object's type has overridden the Object.GetHashCode method. + return RuntimeHelpers.GetHashCode(obj); + } + } + +} diff --git a/StardewModdingAPI/Helpers/CecilHelper.cs b/StardewModdingAPI/Helpers/CecilHelper.cs index aed7fb82..1ace1bf8 100644 --- a/StardewModdingAPI/Helpers/CecilHelper.cs +++ b/StardewModdingAPI/Helpers/CecilHelper.cs @@ -17,6 +17,13 @@ namespace StardewModdingAPI.Helpers private static void InjectMethod(ILProcessor ilProcessor, Instruction target, MethodReference method) { Instruction callEnterInstruction = ilProcessor.Create(OpCodes.Call, method); + + if(method.HasThis) + { + Instruction loadObjInstruction = ilProcessor.Create(OpCodes.Ldarg_0); + ilProcessor.InsertBefore(target, loadObjInstruction); + } + if (method.HasParameters) { Instruction loadObjInstruction = ilProcessor.Create(OpCodes.Ldarg_0); diff --git a/StardewModdingAPI/Helpers/ReflectionHelper.cs b/StardewModdingAPI/Helpers/ReflectionHelper.cs index 7e644e51..204740f6 100644 --- a/StardewModdingAPI/Helpers/ReflectionHelper.cs +++ b/StardewModdingAPI/Helpers/ReflectionHelper.cs @@ -9,140 +9,5 @@ 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/Helpers/StardewAssembly.cs b/StardewModdingAPI/Helpers/StardewAssembly.cs new file mode 100644 index 00000000..0c8b0756 --- /dev/null +++ b/StardewModdingAPI/Helpers/StardewAssembly.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace StardewModdingAPI.Helpers +{ + public static class StardewAssembly + { + private static Assembly ModifiedGameAssembly { get; set; } + private static CecilContext StardewContext { get; set; } + private static CecilContext SmapiContext { get; set; } + + public static void ModifyStardewAssembly() + { + StardewContext = new CecilContext(CecilContextType.Stardew); + SmapiContext = new CecilContext(CecilContextType.SMAPI); + + CecilHelper.InjectEntryMethod(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"); + } + + public static void LoadStardewAssembly() + { + ModifiedGameAssembly = Assembly.Load(StardewContext.ModifiedAssembly.GetBuffer()); + //ModifiedGameAssembly = Assembly.UnsafeLoadFrom(Constants.ExecutionPath + "\\Stardew Valley.exe"); + } + + internal static void Launch() + { + ModifiedGameAssembly.EntryPoint.Invoke(null, new object[] { new string[] { } }); + } + + internal static void WriteModifiedExe() + { + StardewContext.WriteAssembly("StardewValley-Modified.exe"); + } + } +} diff --git a/StardewModdingAPI/Manifest Resources/Microsoft.Xna.Framework.RuntimeProfile.txt b/StardewModdingAPI/Manifest Resources/Microsoft.Xna.Framework.RuntimeProfile.txt deleted file mode 100644 index 34e4189a..00000000 --- a/StardewModdingAPI/Manifest Resources/Microsoft.Xna.Framework.RuntimeProfile.txt +++ /dev/null @@ -1 +0,0 @@ -HiDef \ No newline at end of file diff --git a/StardewModdingAPI/Program.cs b/StardewModdingAPI/Program.cs index 3d0a4c5e..fe9acbd7 100644 --- a/StardewModdingAPI/Program.cs +++ b/StardewModdingAPI/Program.cs @@ -3,6 +3,7 @@ using Microsoft.Xna.Framework.Graphics; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Events; +using StardewModdingAPI.ExtensionMethods; using StardewModdingAPI.Helpers; using StardewModdingAPI.Inheritance; using StardewModdingAPI.Inheritance.Menus; @@ -26,27 +27,31 @@ namespace StardewModdingAPI public static Texture2D DebugPixel { get; private set; } - private static object modifiedAssemblyGame; - //public static Game1 gamePtr; + public static bool IsGameReferenceDirty { get; set; } + + public static object gameInst; + + public static Game1 _gamePtr; public static Game1 gamePtr { get { - return modifiedAssemblyGame.MemberwiseCast(); + if(IsGameReferenceDirty && gameInst != null) + { + _gamePtr = gameInst.Copy(); + } + return _gamePtr; } } - public static bool ready; - public static Assembly StardewAssembly; - public static Type StardewProgramType; - public static FieldInfo StardewGameInfo; + public static bool ready; + public static Form StardewForm; public static Thread gameThread; public static Thread consoleInputThread; - public static CecilContext StardewContext; - public static CecilContext SmapiContext; + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -79,27 +84,20 @@ namespace StardewModdingAPI /// private static void ConfigureMethodInjection() { - StardewContext = new CecilContext(CecilContextType.Stardew); - SmapiContext = new CecilContext(CecilContextType.SMAPI); + StardewAssembly.ModifyStardewAssembly(); - //StardewContext.ReplaceMethodInstruction(OpCodes.Newobj, "System.Void StardewValley.Game1::.ctor()"); - //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 +#if DEBUG + StardewAssembly.WriteModifiedExe(); +#endif + } - } - public static void Test(object instance) { - modifiedAssemblyGame = instance; - } - + gameInst = instance; + IsGameReferenceDirty = true; + } + /// /// Set up the console properties /// @@ -138,7 +136,8 @@ namespace StardewModdingAPI if (!File.Exists(Constants.ExecutionPath + "\\Stardew Valley.exe")) { - throw new FileNotFoundException(string.Format("Could not found: {0}\\Stardew Valley.exe", Constants.ExecutionPath)); + StardewModdingAPI.Log.Error("Replace this"); + //throw new FileNotFoundException(string.Format("Could not found: {0}\\Stardew Valley.exe", Constants.ExecutionPath)); } } @@ -150,44 +149,26 @@ namespace StardewModdingAPI StardewModdingAPI.Log.Info("Initializing SDV Assembly..."); // Load in the assembly - ignores security - StardewAssembly = Assembly.Load(StardewContext.ModifiedAssembly.GetBuffer()); - StardewProgramType = StardewAssembly.GetType("StardewValley.Program", true); - StardewGameInfo = StardewProgramType.GetField("gamePtr"); - + StardewAssembly.LoadStardewAssembly(); + StardewModdingAPI.Log.Comment("SDV Loaded Into Memory"); + // Change the game's version StardewModdingAPI.Log.Verbose("Injecting New SDV Version..."); Game1.version += string.Format("-Z_MODDED | SMAPI {0}", Constants.VersionString); - - // Create the thread for the game to run in. - gameThread = new Thread(RunGame); - StardewModdingAPI.Log.Info("Starting SDV..."); - gameThread.Start(); - // Wait for the game to load up - while (!ready) ; - - //SDV is running - StardewModdingAPI.Log.Comment("SDV Loaded Into Memory"); + // Create the thread for the game to run in. + Application.ThreadException += StardewModdingAPI.Log.Application_ThreadException; + Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); + AppDomain.CurrentDomain.UnhandledException += StardewModdingAPI.Log.CurrentDomain_UnhandledException; + //Create definition to listen for input StardewModdingAPI.Log.Verbose("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; - //Command.RegisterCommand("crash", "crashes sdv").CommandFired += delegate { Game1.player.draw(null); }; - StardewModdingAPI.Log.Verbose("Applying Final SDV Tweaks..."); + Command.RegisterCommand("help", "Lists all commands | 'help ' returns command description").CommandFired += help_CommandFired; - StardewAssembly.EntryPoint.Invoke(null, new object[] { new string[] { } }); - //StardewInvoke(() => - //{ - // gamePtr.IsMouseVisible = false; - // gamePtr.Window.Title = "Stardew Valley - Version " + Game1.version; - // StardewForm.Resize += Events.GraphicsEvents.InvokeResize; - //}); - - //var test = (Game1)StardewGameInfo.GetValue(StardewProgramType); + StardewAssembly.Launch(); } /// @@ -241,21 +222,6 @@ namespace StardewModdingAPI ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - public static void RunGame() - { - Application.ThreadException += StardewModdingAPI.Log.Application_ThreadException; - Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); - AppDomain.CurrentDomain.UnhandledException += StardewModdingAPI.Log.CurrentDomain_UnhandledException; - - try - { - ready = true; - } - catch (Exception ex) - { - StardewModdingAPI.Log.Error("Game failed to start: " + ex); - } - } static void StardewForm_Closing(object sender, CancelEventArgs e) { diff --git a/StardewModdingAPI/StardewModdingAPI.csproj b/StardewModdingAPI/StardewModdingAPI.csproj index b233eb81..6a0dbe25 100644 --- a/StardewModdingAPI/StardewModdingAPI.csproj +++ b/StardewModdingAPI/StardewModdingAPI.csproj @@ -73,8 +73,7 @@ x86 bin\x86\Debug\ false - - + DEBUG true @@ -106,7 +105,7 @@ ..\packages\Mono.Cecil.0.9.6.1\lib\net45\Mono.Cecil.Rocks.dll True - + False ..\..\..\..\Games\SteamLibrary\steamapps\common\Stardew Valley\Stardew Valley.exe @@ -119,8 +118,7 @@ - - False + ..\..\..\..\Games\SteamLibrary\steamapps\common\Stardew Valley\xTile.dll @@ -143,10 +141,13 @@ + + + @@ -163,9 +164,6 @@ - - Always -