Support for deep cloning battles and Pokemon
This commit is contained in:
parent
b3529fa22f
commit
40803f0269
|
@ -11,7 +11,7 @@ namespace PkmnLib.Dynamic.Models;
|
|||
/// A battle is a representation of a battle in the Pokemon games. It contains all the information needed
|
||||
/// to simulate a battle, and can be used to simulate a battle between two parties.
|
||||
/// </summary>
|
||||
public interface IBattle : IScriptSource
|
||||
public interface IBattle : IScriptSource, IDeepCloneable
|
||||
{
|
||||
/// <summary>
|
||||
/// The library the battle uses for handling.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using PkmnLib.Dynamic.Models.Choices;
|
||||
using PkmnLib.Dynamic.ScriptHandling;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Dynamic.Models;
|
||||
|
||||
|
@ -11,7 +12,7 @@ namespace PkmnLib.Dynamic.Models;
|
|||
/// It holds several helper functions to change the turn order while doing the execution. This is needed, as several
|
||||
/// moves in Pokémon actively mess with this order.
|
||||
/// </remarks>
|
||||
public class BattleChoiceQueue
|
||||
public class BattleChoiceQueue : IDeepCloneable
|
||||
{
|
||||
private readonly ITurnChoice?[] _choices;
|
||||
private int _currentIndex;
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Dynamic.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A battle party is a wrapper around a Pokemon party that provides additional functionality for battles.
|
||||
/// It indicates for which side and position the party is responsible.
|
||||
/// </summary>
|
||||
public interface IBattleParty
|
||||
public interface IBattleParty : IDeepCloneable
|
||||
{
|
||||
/// <summary>
|
||||
/// The backing Pokemon party.
|
||||
|
|
|
@ -8,7 +8,7 @@ namespace PkmnLib.Dynamic.Models;
|
|||
/// <summary>
|
||||
/// A side in a battle.
|
||||
/// </summary>
|
||||
public interface IBattleSide : IScriptSource
|
||||
public interface IBattleSide : IScriptSource, IDeepCloneable
|
||||
{
|
||||
/// <summary>
|
||||
/// The index of the side on the battle.
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
using PkmnLib.Dynamic.ScriptHandling;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Dynamic.Models.Choices;
|
||||
|
||||
/// <summary>
|
||||
/// A choice that is made at the beginning of a turn. This can be a switch, flee, item, or pass choice.
|
||||
/// </summary>
|
||||
public interface ITurnChoice : IScriptSource
|
||||
public interface ITurnChoice : IScriptSource, IDeepCloneable
|
||||
{
|
||||
/// <summary>
|
||||
/// The user of the turn choice
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using PkmnLib.Static.Moves;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Dynamic.Models;
|
||||
|
||||
|
@ -42,7 +43,7 @@ public enum MoveLearnMethod
|
|||
/// A learned move is the data attached to a Pokemon for a move it has learned. It has information
|
||||
/// such as the remaining amount of users, how it has been learned, etc.
|
||||
/// </summary>
|
||||
public interface ILearnedMove
|
||||
public interface ILearnedMove : IDeepCloneable
|
||||
{
|
||||
/// <summary>
|
||||
/// The immutable move information of the move.
|
||||
|
|
|
@ -12,7 +12,7 @@ namespace PkmnLib.Dynamic.Models;
|
|||
/// <summary>
|
||||
/// The data of a Pokemon.
|
||||
/// </summary>
|
||||
public interface IPokemon : IScriptSource
|
||||
public interface IPokemon : IScriptSource, IDeepCloneable
|
||||
{
|
||||
/// <summary>
|
||||
/// The library data of the Pokemon.
|
||||
|
@ -352,7 +352,7 @@ public interface IPokemon : IScriptSource
|
|||
/// The data of the Pokémon related to being in a battle.
|
||||
/// This is only set when the Pokémon is on the field in a battle.
|
||||
/// </summary>
|
||||
public interface IPokemonBattleData
|
||||
public interface IPokemonBattleData : IDeepCloneable
|
||||
{
|
||||
/// <summary>
|
||||
/// The battle the Pokémon is in.
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
using System.Collections;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Dynamic.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A collection of Pokemon.
|
||||
/// </summary>
|
||||
public interface IPokemonParty : IReadOnlyList<IPokemon?>
|
||||
public interface IPokemonParty : IReadOnlyList<IPokemon?>, IDeepCloneable
|
||||
{
|
||||
event EventHandler<(IPokemon?, int index)>? OnSwapInto;
|
||||
event EventHandler<(int index1, int index2)>? OnSwap;
|
||||
|
|
|
@ -12,7 +12,7 @@ namespace PkmnLib.Dynamic.ScriptHandling;
|
|||
/// changes. This allows for easily defining generational differences, and add effects that the
|
||||
/// developer might require.
|
||||
/// </summary>
|
||||
public abstract class Script
|
||||
public abstract class Script : IDeepCloneable
|
||||
{
|
||||
internal event Action<Script>? OnRemoveEvent;
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Dynamic.ScriptHandling;
|
||||
|
||||
|
@ -7,7 +8,7 @@ namespace PkmnLib.Dynamic.ScriptHandling;
|
|||
/// A holder class for a script. This is used so we can cache a list of these, and iterate over them, even when
|
||||
/// the underlying script changes.
|
||||
/// </summary>
|
||||
public class ScriptContainer : IEnumerable<ScriptContainer>
|
||||
public class ScriptContainer : IEnumerable<ScriptContainer>, IDeepCloneable
|
||||
{
|
||||
/// <inheritdoc cref="ScriptContainer"/>
|
||||
public ScriptContainer()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Static;
|
||||
|
||||
|
@ -83,7 +84,7 @@ public record ImmutableStatisticSet<T>
|
|||
/// A set of statistics that can be changed.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public record StatisticSet<T> : ImmutableStatisticSet<T>, IEnumerable<T>
|
||||
public record StatisticSet<T> : ImmutableStatisticSet<T>, IEnumerable<T>, IDeepCloneable
|
||||
where T : struct
|
||||
{
|
||||
/// <inheritdoc cref="StatisticSet{T}"/>
|
||||
|
|
|
@ -0,0 +1,196 @@
|
|||
using System.Collections;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace PkmnLib.Static.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// Marks the type as deep cloneable. This means that when a deep clone is made, the object will be cloned recursively.
|
||||
/// Any reference types that are not marked as deep cloneable will be copied by reference, and not cloned.
|
||||
/// </summary>
|
||||
public interface IDeepCloneable;
|
||||
|
||||
/// <summary>
|
||||
/// Handles deep cloning of objects.
|
||||
/// </summary>
|
||||
public static class DeepCloneHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// This will clone an object including all fields and properties, and recursively clone any reference types
|
||||
/// that are marked as deep cloneable. Value types will be copied by value, and reference types that are not marked as
|
||||
/// deep cloneable will be copied by reference.
|
||||
///
|
||||
/// Recursive references will be handled correctly, and will only be cloned once, to prevent infinite loops and invalid
|
||||
/// references.
|
||||
/// </summary>
|
||||
public static T DeepClone<T>(this T? obj, Dictionary<(Type, int), object>? objects = null) where T : IDeepCloneable
|
||||
{
|
||||
if (obj == null)
|
||||
return default!;
|
||||
if (objects != null && objects.TryGetValue((obj.GetType(), obj.GetHashCode()), out var value))
|
||||
return (T)value;
|
||||
|
||||
var type = obj.GetType();
|
||||
// We use GetUninitializedObject to create an object without calling the constructor. This is necessary to prevent
|
||||
// side effects from the constructor, and to not require a parameterless constructor.
|
||||
var newObj = FormatterServices.GetUninitializedObject(type)!;
|
||||
|
||||
// If the objects dictionary is null, we create a new one. We use this dictionary to keep track of objects that have
|
||||
// already been cloned, so we can re-use them instead of cloning them again. This is necessary to prevent infinite
|
||||
// loops and invalid references.
|
||||
objects ??= new Dictionary<(Type, int), object>();
|
||||
objects.Add((obj.GetType(), obj.GetHashCode()), newObj);
|
||||
|
||||
var expressions = GetDeepCloneExpressions(type);
|
||||
foreach (var (getter, setter) in expressions)
|
||||
{
|
||||
var v = getter.Invoke(obj);
|
||||
if (v == null)
|
||||
continue;
|
||||
var cloned = DeepCloneInternal(v, v.GetType(), objects);
|
||||
setter.Invoke(newObj, cloned);
|
||||
}
|
||||
|
||||
return (T)newObj;
|
||||
}
|
||||
|
||||
private static object DeepCloneInternal(object? obj, Type type, Dictionary<(Type, int), object> objects)
|
||||
{
|
||||
if (obj == null)
|
||||
return null!;
|
||||
// If the object is a value type or a string, we can just return it.
|
||||
if (type.IsValueType || type == typeof(string))
|
||||
return obj;
|
||||
|
||||
// If the object is marked as deep cloneable, we will clone it.
|
||||
if (type.GetInterface(nameof(IDeepCloneable)) != null)
|
||||
{
|
||||
// If the object is already cloned, we return the cloned object to prevent infinite loops and invalid references.
|
||||
if (objects.TryGetValue((obj.GetType(), obj.GetHashCode()), out var value))
|
||||
return value;
|
||||
var o = DeepClone((IDeepCloneable)obj, objects);
|
||||
return o;
|
||||
}
|
||||
|
||||
if (type.IsArray)
|
||||
{
|
||||
// ReSharper disable once SuspiciousTypeConversion.Global
|
||||
var array = (Array)obj;
|
||||
var newArray = Array.CreateInstance(type.GetElementType()!, array.Length);
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
newArray.SetValue(DeepCloneInternal(array.GetValue(i), type.GetElementType()!, objects),
|
||||
i);
|
||||
return newArray;
|
||||
}
|
||||
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
var genericType = type.GetGenericTypeDefinition();
|
||||
if (genericType == typeof(List<>))
|
||||
{
|
||||
// ReSharper disable once SuspiciousTypeConversion.Global
|
||||
var list = (IList)obj;
|
||||
var newList = (IList)Activator.CreateInstance(type);
|
||||
foreach (var item in list)
|
||||
newList.Add(DeepCloneInternal(item, type.GetGenericArguments()[0], objects));
|
||||
return newList;
|
||||
}
|
||||
|
||||
if (genericType == typeof(Dictionary<,>))
|
||||
{
|
||||
// ReSharper disable once SuspiciousTypeConversion.Global
|
||||
var dictionary = (IDictionary)obj;
|
||||
var newDictionary = (IDictionary)Activator.CreateInstance(type);
|
||||
foreach (DictionaryEntry entry in dictionary)
|
||||
newDictionary.Add(
|
||||
DeepCloneInternal(entry.Key, type.GetGenericArguments()[0], objects),
|
||||
DeepCloneInternal(entry.Value, type.GetGenericArguments()[1], objects));
|
||||
return newDictionary;
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to get the compiled expressions for deep cloning a type. This will create a getter and setter for each field
|
||||
/// in the type, which can be used to clone the object.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is thread safe, and will only create the expressions once for each type. It returns compiled expressions for
|
||||
/// each field in the type, so that we can get high performance deep cloning.
|
||||
/// </remarks>
|
||||
private static (Func<object, object> getter, Action<object, object> setter)[]
|
||||
GetDeepCloneExpressions(Type type)
|
||||
{
|
||||
// We use a lock here to prevent multiple threads from trying to create the expressions at the same time.
|
||||
lock (DeepCloneExpressions)
|
||||
{
|
||||
if (DeepCloneExpressions.TryGetValue(type, out var value))
|
||||
return value;
|
||||
|
||||
var fields = GetFields(type).ToArray();
|
||||
var expressions = new (Func<object, object> getter, Action<object, object> setter)[fields.Length];
|
||||
for (var i = 0; i < fields.Length; i++)
|
||||
{
|
||||
var field = fields[i];
|
||||
// Create a compiled getter for the field.
|
||||
// 1. Set up the instance parameter.
|
||||
var obj = Expression.Parameter(typeof(object));
|
||||
// 2. Cast the instance (which we want to pass as an object) to the correct type.
|
||||
var cast = Expression.Convert(obj, type);
|
||||
// 3. Get the field value from the instance.
|
||||
var get = Expression.Field(cast, field);
|
||||
// 4. Cast the field value to an object.
|
||||
var getCasted = Expression.Convert(get, typeof(object));
|
||||
// 5. Wrap the cast in a lambda so we can compile it.
|
||||
var lambda = Expression.Lambda<Func<object, object>>(getCasted, obj);
|
||||
|
||||
// This is a slight hack to allow us to set readonly fields. We can't set them directly through expression trees,
|
||||
// as Expression.Assign checks for this. We can however set them through reflection, so we create a setter that
|
||||
// does this. This is not ideal as it is slower, but works for now.
|
||||
if (field.IsInitOnly)
|
||||
{
|
||||
void Setter(object instance, object v)
|
||||
{
|
||||
field.SetValue(instance, v);
|
||||
}
|
||||
|
||||
expressions[i] = (getter: lambda.Compile(), Setter);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create a compiled setter for the field.
|
||||
// 1. Set up the parameter for the value.
|
||||
var valueLambda = Expression.Parameter(typeof(object));
|
||||
// 2. Cast the value to the correct type.
|
||||
var valueCast = Expression.Convert(valueLambda, field.FieldType);
|
||||
// 3. Assign the value to the field.
|
||||
var assign = Expression.Assign(Expression.Field(cast, field), valueCast);
|
||||
// 4. Wrap the assign in a lambda so we can compile it.
|
||||
var set = Expression.Lambda<Action<object, object>>(assign, obj, valueLambda);
|
||||
expressions[i] = (getter: lambda.Compile(), setter: set.Compile());
|
||||
}
|
||||
}
|
||||
|
||||
DeepCloneExpressions.Add(type, expressions);
|
||||
return expressions;
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<FieldInfo> GetFields(Type type)
|
||||
{
|
||||
IEnumerable<FieldInfo> fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance |
|
||||
BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
|
||||
// Note that we do the above with DeclaredOnly, while we do want to get the fields from the base type.
|
||||
// This is because even without DeclaredOnly, we will only get public fields from our base type. As we want
|
||||
// to get all fields, we need to do this recursively.
|
||||
if (type.BaseType != null)
|
||||
fields = fields.Concat(GetFields(type.BaseType));
|
||||
return fields;
|
||||
}
|
||||
|
||||
private static readonly Dictionary<Type, (Func<object, object> getter, Action<object, object> setter)[]>
|
||||
DeepCloneExpressions = new();
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using PkmnLib.Dynamic.Models;
|
||||
using PkmnLib.Static;
|
||||
using PkmnLib.Static.Species;
|
||||
using PkmnLib.Static.Utils;
|
||||
using PkmnLib.Tests.Integration;
|
||||
|
||||
namespace PkmnLib.Tests.Static;
|
||||
|
||||
public class DeepCloneTests
|
||||
{
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Local")]
|
||||
[SuppressMessage("ReSharper", "ValueParameterNotUsed")]
|
||||
private class TestClass : IDeepCloneable
|
||||
{
|
||||
public int Value { get; set; }
|
||||
public int Field;
|
||||
private int PrivateValue { get; set; }
|
||||
#pragma warning disable CS0169 // Field is never used
|
||||
private int _privateField;
|
||||
#pragma warning restore CS0169 // Field is never used
|
||||
private int OnlyGetter => 0;
|
||||
|
||||
private int OnlySetter
|
||||
{
|
||||
set { }
|
||||
}
|
||||
|
||||
public TestClass? Self { get; set; }
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DeepCloneTestProperty()
|
||||
{
|
||||
var obj = new TestClass { Value = 1 };
|
||||
var clone = obj.DeepClone();
|
||||
await Assert.That(clone).IsNotEqualTo(obj);
|
||||
await Assert.That(clone.Value).IsEqualTo(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DeepCloneTestField()
|
||||
{
|
||||
var obj = new TestClass { Field = 1 };
|
||||
var clone = obj.DeepClone();
|
||||
await Assert.That(clone).IsNotEqualTo(obj);
|
||||
await Assert.That(clone.Field).IsEqualTo(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DeepCloneTestPrivateProperty()
|
||||
{
|
||||
var obj = new TestClass();
|
||||
obj.GetType().GetProperty("PrivateValue", BindingFlags.NonPublic | BindingFlags.Instance)!.SetValue(obj, 1);
|
||||
var clone = obj.DeepClone();
|
||||
await Assert.That(clone).IsNotEqualTo(obj);
|
||||
var clonePrivateValue =
|
||||
clone.GetType().GetProperty("PrivateValue", BindingFlags.NonPublic | BindingFlags.Instance)!
|
||||
.GetValue(clone);
|
||||
await Assert.That(clonePrivateValue).IsEqualTo(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DeepCloneTestPrivateField()
|
||||
{
|
||||
var obj = new TestClass();
|
||||
obj.GetType().GetField("_privateField", BindingFlags.NonPublic | BindingFlags.Instance)!.SetValue(obj, 1);
|
||||
var clone = obj.DeepClone();
|
||||
await Assert.That(clone).IsNotEqualTo(obj);
|
||||
var clonePrivateField =
|
||||
clone.GetType().GetField("_privateField", BindingFlags.NonPublic | BindingFlags.Instance)!
|
||||
.GetValue(clone);
|
||||
await Assert.That(clonePrivateField).IsEqualTo(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DeepCloneTestRecursion()
|
||||
{
|
||||
var obj = new TestClass();
|
||||
obj.Self = obj;
|
||||
var clone = obj.DeepClone();
|
||||
await Assert.That(clone).IsNotEqualTo(obj);
|
||||
await Assert.That(clone.Self).IsNotEqualTo(obj);
|
||||
await Assert.That(clone.Self).IsEqualTo(clone);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DeepCloneIntegrationTestsBattle()
|
||||
{
|
||||
var library = LibraryHelpers.LoadLibrary();
|
||||
await Assert.That(library.StaticLibrary.Species.TryGet("bulbasaur", out var bulbasaur)).IsTrue();
|
||||
await Assert.That(library.StaticLibrary.Species.TryGet("charmander", out var charmander)).IsTrue();
|
||||
var party1 = new PokemonParty(6);
|
||||
party1.SwapInto(new PokemonImpl(library, bulbasaur!,
|
||||
bulbasaur!.GetDefaultForm(), new AbilityIndex
|
||||
{
|
||||
IsHidden = false,
|
||||
Index = 0,
|
||||
}, 50, 0,
|
||||
Gender.Male, 0, "hardy"), 0);
|
||||
var party2 = new PokemonParty(6);
|
||||
party2.SwapInto(new PokemonImpl(library, charmander!,
|
||||
charmander!.GetDefaultForm(), new AbilityIndex
|
||||
{
|
||||
IsHidden = false,
|
||||
Index = 0,
|
||||
}, 50, 0,
|
||||
Gender.Male, 0, "hardy"), 0);
|
||||
|
||||
var parties = new[]
|
||||
{
|
||||
new BattlePartyImpl(party1, [new ResponsibleIndex(0, 0)]),
|
||||
new BattlePartyImpl(party2, [new ResponsibleIndex(1, 0)]),
|
||||
};
|
||||
var battle = new BattleImpl(library, parties, false, 2, 3, randomSeed: 0);
|
||||
battle.Sides[0].SwapPokemon(0, party1[0]);
|
||||
battle.Sides[1].SwapPokemon(0, party2[0]);
|
||||
party1[0]!.ChangeStatBoost(Statistic.Defense, 2, true);
|
||||
await Assert.That(party1[0]!.StatBoost.Defense).IsEqualTo((sbyte)2);
|
||||
|
||||
var clone = battle.DeepClone();
|
||||
await Assert.That(clone).IsNotEqualTo(battle);
|
||||
await Assert.That(clone.Sides[0].Pokemon[0]).IsNotEqualTo(battle.Sides[0].Pokemon[0]);
|
||||
await Assert.That(clone.Sides[1].Pokemon[0]).IsNotEqualTo(battle.Sides[1].Pokemon[0]);
|
||||
|
||||
await Assert.That(clone.Sides[0].Pokemon[0]!.Species).IsEqualTo(battle.Sides[0].Pokemon[0]!.Species);
|
||||
await Assert.That(clone.Sides[1].Pokemon[0]!.Species).IsEqualTo(battle.Sides[1].Pokemon[0]!.Species);
|
||||
|
||||
await Assert.That(clone.Library).IsEqualTo(battle.Library);
|
||||
|
||||
var pokemon = clone.Sides[0].Pokemon[0]!;
|
||||
await Assert.That(pokemon.BattleData).IsNotNull();
|
||||
await Assert.That(pokemon.BattleData).IsNotEqualTo(battle.Sides[0].Pokemon[0]!.BattleData);
|
||||
await Assert.That(pokemon.BattleData!.Battle).IsEqualTo(clone);
|
||||
await Assert.That(pokemon.BattleData!.SeenOpponents).Contains(clone.Sides[1].Pokemon[0]!);
|
||||
await Assert.That(pokemon.BattleData!.SeenOpponents).DoesNotContain(battle.Sides[1].Pokemon[0]!);
|
||||
await Assert.That(pokemon.StatBoost.Defense).IsEqualTo((sbyte)2);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue