Removed own memberwise cast function, and replaced with one by jpmikkers. This needs converting to a recursion-less version and having the loop issue I think it has fixed

This commit is contained in:
ClxS 2016-03-13 01:54:22 +00:00
parent 1ec0570ed9
commit 65f1e5bec4
10 changed files with 316 additions and 214 deletions

View File

@ -6,7 +6,8 @@ using System.Threading.Tasks;
namespace StardewModdingAPI.API
{
public class Game
class Game
{
}
}

View File

@ -46,6 +46,8 @@ namespace StardewModdingAPI.Events
{
try
{
Program.IsGameReferenceDirty = true;
var test = Program.gamePtr;
UpdateTick.Invoke(null, EventArgs.Empty);
}
catch (Exception ex)

View File

@ -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<Array, int[]> 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<Array, int[]> 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;
}
}
}

View File

@ -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<T>(this object original)
{
return (T)new DeepCopyContext().InternalCopy(original, true);
}
private class DeepCopyContext
{
private static readonly Func<object, object> CloneMethod;
private readonly Dictionary<Object, Object> m_Visited;
private readonly Dictionary<Type, FieldInfo[]> 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<Func<object, object>>(body, p1).Compile();
//Console.WriteLine("typeof(object) contains {0} nonshallow fields", NonShallowFields(typeof(object)).Count());
}
public DeepCopyContext()
{
m_Visited = new Dictionary<object, object>(new ReferenceEqualityComparer());
m_NonShallowFieldCache = new Dictionary<Type, FieldInfo[]>();
}
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;
}
/// <summary>
/// From the given type hierarchy (i.e. including all base types), return all fields that should be deep-copied
/// </summary>
/// <param name="typeToReflect"></param>
/// <returns></returns>
private static IEnumerable<FieldInfo> 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<Object>
{
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);
}
}
}

View File

@ -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);

View File

@ -9,140 +9,5 @@ namespace StardewModdingAPI.Helpers
{
public static class ReflectionHelper
{
private class FieldCompatability
{
public List<Tuple<FieldInfo, FieldInfo>> MatchingFields = new List<Tuple<FieldInfo, FieldInfo>>();
public List<Tuple<FieldInfo, FieldInfo>> FixableMismatchingFields = new List<Tuple<FieldInfo, FieldInfo>>();
public List<Tuple<FieldInfo, FieldInfo>> UnfixableMismatchingFields = new List<Tuple<FieldInfo, FieldInfo>>();
public List<FieldInfo> FieldsMissingFromA = new List<FieldInfo>();
public List<FieldInfo> FieldsMissingFromB = new List<FieldInfo>();
}
private static FieldCompatability AssessFieldCompatbility(FieldInfo[] a, FieldInfo[] b)
{
FieldCompatability results = new FieldCompatability();
List<FieldInfo> editableB = new List<FieldInfo>(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<Tuple<Type, Type, object, object>> pendingCasts = new Stack<Tuple<Type, Type, object, object>>();
List<object> alreadyProcessed = new List<object>();
pendingCasts.Push(Tuple.Create(toType, fromType,
to, from));
while (pendingCasts.Count > 0)
{
Tuple<Type, Type, object, object> 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<T>(this object @base) where T : new()
{
T retObj = new T();
RecursiveMemberwiseCast(@base.GetType(), retObj.GetType().BaseType, retObj, @base);
return retObj;
}
}
}

View File

@ -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");
}
}
}

View File

@ -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<Game1>();
if(IsGameReferenceDirty && gameInst != null)
{
_gamePtr = gameInst.Copy<Game1>();
}
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
/// </summary>
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;
}
/// <summary>
/// Set up the console properties
/// </summary>
@ -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 <cmd>' 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 <cmd>' 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();
}
/// <summary>
@ -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)
{

View File

@ -73,8 +73,7 @@
<PlatformTarget>x86</PlatformTarget>
<OutputPath>bin\x86\Debug\</OutputPath>
<Prefer32Bit>false</Prefer32Bit>
<DefineConstants>
</DefineConstants>
<DefineConstants>DEBUG</DefineConstants>
<UseVSHostingProcess>true</UseVSHostingProcess>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
@ -106,7 +105,7 @@
<HintPath>..\packages\Mono.Cecil.0.9.6.1\lib\net45\Mono.Cecil.Rocks.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Stardew Valley, Version=1.0.5905.5747, Culture=neutral, processorArchitecture=x86">
<Reference Include="Stardew Valley, Version=1.0.5912.41135, Culture=neutral, processorArchitecture=x86">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\Games\SteamLibrary\steamapps\common\Stardew Valley\Stardew Valley.exe</HintPath>
</Reference>
@ -119,8 +118,7 @@
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
<Reference Include="xTile, Version=2.0.4.0, Culture=neutral, processorArchitecture=x86">
<SpecificVersion>False</SpecificVersion>
<Reference Include="xTile">
<HintPath>..\..\..\..\Games\SteamLibrary\steamapps\common\Stardew Valley\xTile.dll</HintPath>
</Reference>
</ItemGroup>
@ -143,10 +141,13 @@
<Compile Include="Events\Mine.cs" />
<Compile Include="Events\Player.cs" />
<Compile Include="Events\Time.cs" />
<Compile Include="ExtensionMethods\Array.cs" />
<Compile Include="ExtensionMethods\Object.cs" />
<Compile Include="Extensions.cs" />
<Compile Include="Helpers\CecilContext.cs" />
<Compile Include="Helpers\CecilHelper.cs" />
<Compile Include="Helpers\ReflectionHelper.cs" />
<Compile Include="Helpers\StardewAssembly.cs" />
<Compile Include="Inheritance\ItemStackChange.cs" />
<Compile Include="Inheritance\Menus\SBobberBar.cs" />
<Compile Include="Inheritance\Menus\SGameMenu.cs" />
@ -163,9 +164,6 @@
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<Content Include="Manifest Resources\Microsoft.Xna.Framework.RuntimeProfile.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>