Refactor batch editor to smaller components

Did something similar for NHSE years ago. Allows for much easier reuse elsewhere and clearer usage.
This commit is contained in:
Kurt 2026-02-21 00:17:33 -06:00
parent 0b42f57534
commit 2efa19e5e3
36 changed files with 1135 additions and 1009 deletions

View File

@ -0,0 +1,535 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Reflection;
using static PKHeX.Core.BatchEditingUtil;
namespace PKHeX.Core;
/// <summary>
/// Base logic for editing entities with user provided <see cref="StringInstruction"/> list.
/// </summary>
/// <remarks>
/// Caches reflection results for the provided types, and provides utility methods for fetching properties and applying instructions.
/// </remarks>
public abstract class BatchEditingBase<TObject, TMeta> : IBatchEditor<TObject> where TObject : notnull
{
private readonly Type[] _types;
private readonly string[] _customProperties;
private readonly Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>>[] _props;
private readonly Lazy<string[][]> _properties;
protected BatchEditingBase(Type[] types, string[] customProperties, int expectedMax)
{
_types = types;
_customProperties = customProperties;
_props = GetPropertyDictionaries(types, expectedMax);
_properties = new Lazy<string[][]>(() => GetPropArray(_props, customProperties));
}
/// <summary>
/// Property names, indexed by <see cref="Types"/>.
/// </summary>
public string[][] Properties => _properties.Value;
/// <summary>
/// Gets the list of supported entity types.
/// </summary>
public IReadOnlyList<Type> Types => _types;
protected abstract TMeta CreateMeta(TObject entity);
protected abstract bool ShouldModify(TObject entity);
protected abstract bool TryHandleSetOperation(StringInstruction cmd, TMeta info, TObject entity, out ModifyResult result);
protected abstract bool TryHandleFilter(StringInstruction cmd, TMeta info, TObject entity, out bool isMatch);
/// <summary>
/// Tries to fetch the entity property from the cache of available properties.
/// </summary>
public bool TryGetHasProperty(TObject entity, ReadOnlySpan<char> name, [NotNullWhen(true)] out PropertyInfo? pi)
=> TryGetHasProperty(entity.GetType(), name, out pi);
/// <summary>
/// Tries to fetch the entity property from the cache of available properties.
/// </summary>
public bool TryGetHasProperty(Type type, ReadOnlySpan<char> name, [NotNullWhen(true)] out PropertyInfo? pi)
{
var index = _types.IndexOf(type);
if (index < 0)
{
pi = null;
return false;
}
var localProps = _props[index];
return localProps.TryGetValue(name, out pi);
}
/// <summary>
/// Gets a list of entity types that implement the requested property.
/// </summary>
public IEnumerable<string> GetTypesImplementing(string property)
{
for (int i = 0; i < _types.Length; i++)
{
var type = _types[i];
var localProps = _props[i];
if (!localProps.TryGetValue(property, out var pi))
continue;
yield return $"{type.Name}: {pi.PropertyType.Name}";
}
}
/// <summary>
/// Gets the type of the entity property using the saved cache of properties.
/// </summary>
public bool TryGetPropertyType(string propertyName, [NotNullWhen(true)] out string? result, int typeIndex = 0)
{
if (_customProperties.Contains(propertyName))
{
result = "Custom";
return true;
}
result = null;
if (typeIndex == 0)
{
foreach (var p in _props)
{
if (!p.TryGetValue(propertyName, out var pi))
continue;
result = pi.PropertyType.Name;
return true;
}
return false;
}
int index = typeIndex - 1;
if ((uint)index >= _props.Length)
index = 0;
var pr = _props[index];
if (!pr.TryGetValue(propertyName, out var info))
return false;
result = info.PropertyType.Name;
return true;
}
/// <summary>
/// Checks if the entity is filtered by the provided filters.
/// </summary>
public bool IsFilterMatch(IEnumerable<StringInstruction> filters, TObject entity)
{
var info = CreateMeta(entity);
var localProps = GetProps(entity);
foreach (var filter in filters)
{
if (!IsFilterMatch(filter, info, entity, localProps))
return false;
}
return true;
}
/// <summary>
/// Tries to modify the entity.
/// </summary>
public bool TryModifyIsSuccess(TObject entity, IEnumerable<StringInstruction> filters, IEnumerable<StringInstruction> modifications, Func<TObject, bool>? modifier = null)
=> TryModify(entity, filters, modifications, modifier) is ModifyResult.Modified;
/// <summary>
/// Tries to modify the entity using instructions and a custom modifier delegate.
/// </summary>
public ModifyResult TryModify(TObject entity, IEnumerable<StringInstruction> filters, IEnumerable<StringInstruction> modifications, Func<TObject, bool>? modifier = null)
{
if (!ShouldModify(entity))
return ModifyResult.Skipped;
var info = CreateMeta(entity);
var localProps = GetProps(entity);
foreach (var cmd in filters)
{
try
{
if (!IsFilterMatch(cmd, info, entity, localProps))
return ModifyResult.Filtered;
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
return ModifyResult.Error;
}
}
var error = false;
var result = ModifyResult.Skipped;
if (modifier is { } func)
{
try
{
if (!func(entity))
return ModifyResult.Skipped;
result = ModifyResult.Modified;
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
return ModifyResult.Error;
}
}
foreach (var cmd in modifications)
{
try
{
var tmp = SetProperty(cmd, entity, info, localProps);
if (tmp == ModifyResult.Error)
error = true;
else if (tmp != ModifyResult.Skipped)
result = tmp;
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
error = true;
}
}
if (error)
result |= ModifyResult.Error;
return result;
}
private static Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>>[] GetPropertyDictionaries(IReadOnlyList<Type> types, int expectedMax)
{
var result = new Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>>[types.Count];
for (int i = 0; i < types.Count; i++)
result[i] = GetPropertyDictionary(types[i], ReflectUtil.GetAllPropertyInfoPublic, expectedMax).GetAlternateLookup<ReadOnlySpan<char>>();
return result;
}
private static Dictionary<string, PropertyInfo> GetPropertyDictionary(Type type, Func<Type, IEnumerable<PropertyInfo>> selector, int expectedMax)
{
var dict = new Dictionary<string, PropertyInfo>(expectedMax);
var localProps = selector(type);
foreach (var p in localProps)
dict.TryAdd(p.Name, p);
return dict;
}
private static string[][] GetPropArray<T>(Dictionary<string, T>.AlternateLookup<ReadOnlySpan<char>>[] types, ReadOnlySpan<string> extra)
{
var result = new string[types.Length + 2][];
var p = result.AsSpan(1, types.Length);
for (int i = 0; i < p.Length; i++)
{
var type = types[i].Dictionary;
string[] combine = [..type.Keys, ..extra];
combine.Sort();
p[i] = combine;
}
var first = p[0];
var any = new HashSet<string>(first);
var all = new HashSet<string>(first);
foreach (var set in p[1..])
{
any.UnionWith(set);
all.IntersectWith(set);
}
var arrAny = any.ToArray();
arrAny.Sort();
result[0] = arrAny;
var arrAll = all.ToArray();
arrAll.Sort();
result[^1] = arrAll;
return result;
}
private Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> GetProps(TObject entity)
{
var type = entity.GetType();
var typeIndex = _types.IndexOf(type);
return _props[typeIndex];
}
private bool IsFilterMatch(StringInstruction cmd, TMeta info, TObject entity, Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> localProps)
{
if (TryHandleFilter(cmd, info, entity, out var isMatch))
return isMatch;
return IsPropertyFiltered(cmd, entity, localProps);
}
private static bool IsPropertyFiltered(StringInstruction cmd, TObject entity, Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> localProps)
{
if (!localProps.TryGetValue(cmd.PropertyName, out var pi))
return false;
if (!pi.CanRead)
return false;
var val = cmd.PropertyValue;
if (val.StartsWith(PointerToken) && localProps.TryGetValue(val.AsSpan(1), out var opi))
{
var result = opi.GetValue(entity) ?? throw new NullReferenceException();
return cmd.Comparer.IsCompareOperator(pi.CompareTo(entity, result));
}
return cmd.Comparer.IsCompareOperator(pi.CompareTo(entity, val));
}
private ModifyResult SetProperty(StringInstruction cmd, TObject entity, TMeta info, Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> localProps)
{
if (cmd.Operation == InstructionOperation.Set && TryHandleSetOperation(cmd, info, entity, out var result))
return result;
if (!localProps.TryGetValue(cmd.PropertyName, out var pi))
return ModifyResult.Error;
if (!pi.CanWrite)
return ModifyResult.Error;
if (cmd.Operation != InstructionOperation.Set)
return ApplyNumericOperation(entity, cmd, pi, localProps);
if (!TryResolveOperandValue(cmd, entity, localProps, out var value))
return ModifyResult.Error;
ReflectUtil.SetValue(pi, entity, value);
return ModifyResult.Modified;
}
private static ModifyResult ApplyNumericOperation(TObject entity, StringInstruction cmd, PropertyInfo pi, Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> localProps)
{
if (!pi.CanRead)
return ModifyResult.Error;
if (!TryGetNumericType(pi.PropertyType, out var numericType))
return ModifyResult.Error;
var currentValue = pi.GetValue(entity);
if (currentValue is null)
return ModifyResult.Error;
if (!TryResolveOperandValue(cmd, entity, localProps, out var operandValue))
return ModifyResult.Error;
if (!TryApplyNumericOperation(numericType, cmd.Operation, currentValue, operandValue, out var value))
return ModifyResult.Error;
ReflectUtil.SetValue(pi, entity, value);
return ModifyResult.Modified;
}
private static bool TryResolveOperandValue(StringInstruction cmd, TObject entity, Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> localProps, [NotNullWhen(true)] out object? value)
{
if (cmd.Random)
{
value = cmd.RandomValue;
return true;
}
var propertyValue = cmd.PropertyValue;
if (propertyValue.StartsWith(PointerToken) && localProps.TryGetValue(propertyValue.AsSpan(1), out var opi))
{
value = opi.GetValue(entity);
return value is not null;
}
value = propertyValue;
return true;
}
private static bool TryGetNumericType(Type type, out Type numericType)
{
numericType = Nullable.GetUnderlyingType(type) ?? type;
// bool isNullable = type != numericType;
return numericType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(INumber<>));
}
private static bool TryApplyNumericOperation(Type numericType, InstructionOperation operation, object currentValue, object operandValue, [NotNullWhen(true)] out object? result)
{
result = null;
if (numericType == typeof(byte))
return ApplyBinaryInteger<byte>(currentValue, operandValue, operation, out result);
if (numericType == typeof(sbyte))
return ApplyBinaryInteger<sbyte>(currentValue, operandValue, operation, out result);
if (numericType == typeof(short))
return ApplyBinaryInteger<short>(currentValue, operandValue, operation, out result);
if (numericType == typeof(ushort))
return ApplyBinaryInteger<ushort>(currentValue, operandValue, operation, out result);
if (numericType == typeof(int))
return ApplyBinaryInteger<int>(currentValue, operandValue, operation, out result);
if (numericType == typeof(uint))
return ApplyBinaryInteger<uint>(currentValue, operandValue, operation, out result);
if (numericType == typeof(long))
return ApplyBinaryInteger<long>(currentValue, operandValue, operation, out result);
if (numericType == typeof(ulong))
return ApplyBinaryInteger<ulong>(currentValue, operandValue, operation, out result);
if (numericType == typeof(nint))
return ApplyBinaryInteger<nint>(currentValue, operandValue, operation, out result);
if (numericType == typeof(nuint))
return ApplyBinaryInteger<nuint>(currentValue, operandValue, operation, out result);
if (numericType == typeof(BigInteger))
return ApplyBinaryInteger<BigInteger>(currentValue, operandValue, operation, out result);
if (numericType == typeof(float))
return ApplyNumeric<float>(currentValue, operandValue, operation, out result);
if (numericType == typeof(double))
return ApplyNumeric<double>(currentValue, operandValue, operation, out result);
if (numericType == typeof(decimal))
return ApplyNumeric<decimal>(currentValue, operandValue, operation, out result);
return false;
}
private static bool ApplyNumeric<T>(object currentValue, object operandValue, InstructionOperation operation, [NotNullWhen(true)] out object? result)
where T : INumber<T>
{
if (operation.IsBitwise)
{
result = null;
return false;
}
var success = TryApplyNumericOperationCore<T>(operation, currentValue, operandValue, out var typed);
result = typed;
return success;
}
private static bool ApplyBinaryInteger<T>(object currentValue, object operandValue, InstructionOperation operation, [NotNullWhen(true)] out object? result)
where T : IBinaryInteger<T>
{
var success = operation.IsBitwise
? TryApplyBinaryIntegerOperationCore<T>(operation, currentValue, operandValue, out var typed)
: TryApplyNumericOperationCore(operation, currentValue, operandValue, out typed);
result = typed;
return success;
}
private static bool TryApplyNumericOperationCore<T>(InstructionOperation operation, object currentValue, object operandValue, [NotNullWhen(true)] out T? result)
where T : INumber<T>
{
if (!TryConvertNumeric<T>(currentValue, out var left) || !TryConvertNumeric<T>(operandValue, out var right))
{
result = default;
return false;
}
return TryApplyNumericOperationCore(operation, left, right, out result);
}
private static bool TryApplyNumericOperationCore<T>(InstructionOperation operation, T left, T right, [NotNullWhen(true)] out T? result)
where T : INumber<T>
{
try
{
result = operation switch
{
InstructionOperation.Add => left + right,
InstructionOperation.Subtract => left - right,
InstructionOperation.Multiply => left * right,
InstructionOperation.Divide => left / right,
InstructionOperation.Modulo => left % right,
_ => right,
};
return true;
}
catch (DivideByZeroException)
{
result = default;
return false;
}
}
private static bool TryApplyBinaryIntegerOperationCore<T>(InstructionOperation operation, object currentValue, object operandValue, [NotNullWhen(true)] out T? result)
where T : IBinaryInteger<T>
{
if (!TryConvertNumeric<T>(currentValue, out var left) || !TryConvertNumeric<T>(operandValue, out var right))
{
result = default;
return false;
}
return TryApplyBinaryIntegerOperationCore(operation, left, right, out result);
}
private static bool TryApplyBinaryIntegerOperationCore<T>(InstructionOperation operation, T left, T right, [NotNullWhen(true)] out T? result)
where T : IBinaryInteger<T>
{
try
{
switch (operation)
{
case InstructionOperation.BitwiseAnd:
result = left & right;
return true;
case InstructionOperation.BitwiseOr:
result = left | right;
return true;
case InstructionOperation.BitwiseXor:
result = left ^ right;
return true;
case InstructionOperation.BitwiseShiftLeft:
result = left << int.CreateChecked(right);
return true;
case InstructionOperation.BitwiseShiftRight:
result = left >> int.CreateChecked(right);
return true;
default:
result = default;
return false;
}
}
catch (OverflowException)
{
result = default;
return false;
}
}
private static bool TryConvertNumeric<T>(object value, [NotNullWhen(true)] out T? result) where T : INumber<T>
{
if (value is T typed)
{
result = typed;
return true;
}
if (value is string text)
{
if (T.TryParse(text, CultureInfo.InvariantCulture, out var parsed))
{
result = parsed;
return true;
}
result = default;
return false;
}
if (value is IConvertible)
{
try
{
var converted = Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture);
if (converted is T convertedValue)
{
result = convertedValue;
return true;
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
result = default;
return false;
}
}

View File

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace PKHeX.Core;
public static class BatchEditingUtil
{
public const string PROP_TYPENAME = "ObjectType";
public const char PointerToken = '*';
/// <summary>
/// Checks if the object is filtered by the provided <see cref="filters"/>.
/// </summary>
/// <remarks>
/// Does not use cached reflection; less performant than a cached <see cref="BatchEditingBase{TObject,TMeta}"/> implementation.
/// </remarks>
/// <param name="filters">Filters which must be satisfied.</param>
/// <param name="obj">Object to check.</param>
/// <returns>True if <see cref="obj"/> matches all filters.</returns>
public static bool IsFilterMatch<T>(IEnumerable<StringInstruction> filters, T obj) where T : notnull
{
foreach (var cmd in filters)
{
var name = cmd.PropertyName;
var value = cmd.PropertyValue;
if (name is PROP_TYPENAME)
{
var type = obj.GetType();
var typeName = type.Name;
if (!cmd.Comparer.IsCompareEquivalence(value == typeName))
return false;
continue;
}
if (!ReflectUtil.HasProperty(obj, name, out var pi))
return false;
try
{
if (cmd.Comparer.IsCompareOperator(pi.CompareTo(obj, value)))
continue;
}
// User provided inputs can mismatch the type's required value format, and fail to be compared.
catch (Exception e)
{
Debug.WriteLine($"Unable to compare {name} to {value}.");
Debug.WriteLine(e.Message);
}
return false;
}
return true;
}
}

View File

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
namespace PKHeX.Core;
/// <summary>
/// Provides batch editing helpers for an entity type.
/// </summary>
public interface IBatchEditor<TObject> where TObject : notnull
{
/// <summary>
/// Gets the list of supported entity types.
/// </summary>
IReadOnlyList<Type> Types { get; }
/// <summary>
/// Gets the property names, indexed by <see cref="Types"/>.
/// </summary>
string[][] Properties { get; }
/// <summary>
/// Tries to fetch the entity property from the cache of available properties.
/// </summary>
bool TryGetHasProperty(TObject entity, ReadOnlySpan<char> name, [NotNullWhen(true)] out PropertyInfo? pi);
/// <summary>
/// Tries to fetch the entity property from the cache of available properties.
/// </summary>
bool TryGetHasProperty(Type type, ReadOnlySpan<char> name, [NotNullWhen(true)] out PropertyInfo? pi);
/// <summary>
/// Gets a list of entity types that implement the requested property.
/// </summary>
IEnumerable<string> GetTypesImplementing(string property);
/// <summary>
/// Gets the type of the entity property using the saved cache of properties.
/// </summary>
bool TryGetPropertyType(string propertyName, [NotNullWhen(true)] out string? result, int typeIndex = 0);
/// <summary>
/// Checks if the entity is filtered by the provided filters.
/// </summary>
bool IsFilterMatch(IEnumerable<StringInstruction> filters, TObject entity);
/// <summary>
/// Tries to modify the entity.
/// </summary>
bool TryModifyIsSuccess(TObject entity, IEnumerable<StringInstruction> filters, IEnumerable<StringInstruction> modifications, Func<TObject, bool>? modifier = null);
/// <summary>
/// Tries to modify the entity using instructions and a custom modifier delegate.
/// </summary>
ModifyResult TryModify(TObject entity, IEnumerable<StringInstruction> filters, IEnumerable<StringInstruction> modifications, Func<TObject, bool>? modifier = null);
}

View File

@ -0,0 +1,71 @@
using System;
using static PKHeX.Core.InstructionComparer;
namespace PKHeX.Core;
/// <summary>
/// Value comparison type
/// </summary>
public enum InstructionComparer : byte
{
None,
IsEqual,
IsNotEqual,
IsGreaterThan,
IsGreaterThanOrEqual,
IsLessThan,
IsLessThanOrEqual,
}
/// <summary>
/// Extension methods for <see cref="InstructionComparer"/>
/// </summary>
public static class InstructionComparerExtensions
{
extension(InstructionComparer comparer)
{
/// <summary>
/// Indicates if the <see cref="comparer"/> is supported by the logic.
/// </summary>
/// <returns>True if supported, false if unsupported.</returns>
public bool IsSupported => comparer switch
{
IsEqual => true,
IsNotEqual => true,
IsGreaterThan => true,
IsGreaterThanOrEqual => true,
IsLessThan => true,
IsLessThanOrEqual => true,
_ => false,
};
/// <summary>
/// Checks if the compare operator is satisfied by a boolean comparison result.
/// </summary>
/// <param name="compareResult">Result from Equals comparison</param>
/// <returns>True if satisfied</returns>
/// <remarks>Only use this method if the comparison is boolean only. Use the <see cref="IsCompareOperator"/> otherwise.</remarks>
public bool IsCompareEquivalence(bool compareResult) => comparer switch
{
IsEqual => compareResult,
IsNotEqual => !compareResult,
_ => false,
};
/// <summary>
/// Checks if the compare operator is satisfied by the <see cref="IComparable{T}.CompareTo"/> result.
/// </summary>
/// <param name="compareResult">Result from CompareTo</param>
/// <returns>True if satisfied</returns>
public bool IsCompareOperator(int compareResult) => comparer switch
{
IsEqual => compareResult is 0,
IsNotEqual => compareResult is not 0,
IsGreaterThan => compareResult > 0,
IsGreaterThanOrEqual => compareResult >= 0,
IsLessThan => compareResult < 0,
IsLessThanOrEqual => compareResult <= 0,
_ => false,
};
}
}

View File

@ -0,0 +1,37 @@
using static PKHeX.Core.InstructionOperation;
namespace PKHeX.Core;
/// <summary>
/// Operation type for applying a modification.
/// </summary>
public enum InstructionOperation : byte
{
Set,
Add,
Subtract,
Multiply,
Divide,
Modulo,
BitwiseAnd,
BitwiseOr,
BitwiseXor,
BitwiseShiftRight,
BitwiseShiftLeft,
}
public static class InstructionOperationExtensions
{
extension(InstructionOperation operation)
{
public bool IsBitwise => operation switch
{
BitwiseAnd => true,
BitwiseOr => true,
BitwiseXor => true,
BitwiseShiftRight => true,
BitwiseShiftLeft => true,
_ => false,
};
}
}

View File

@ -378,88 +378,3 @@ public static bool TryGetOperation(char opCode, out InstructionOperation operati
}
}
}
/// <summary>
/// Value comparison type
/// </summary>
public enum InstructionComparer : byte
{
None,
IsEqual,
IsNotEqual,
IsGreaterThan,
IsGreaterThanOrEqual,
IsLessThan,
IsLessThanOrEqual,
}
/// <summary>
/// Operation type for applying a modification.
/// </summary>
public enum InstructionOperation : byte
{
Set,
Add,
Subtract,
Multiply,
Divide,
Modulo,
BitwiseAnd,
BitwiseOr,
BitwiseXor,
BitwiseShiftRight,
BitwiseShiftLeft,
}
/// <summary>
/// Extension methods for <see cref="InstructionComparer"/>
/// </summary>
public static class InstructionComparerExtensions
{
extension(InstructionComparer comparer)
{
/// <summary>
/// Indicates if the <see cref="comparer"/> is supported by the logic.
/// </summary>
/// <returns>True if supported, false if unsupported.</returns>
public bool IsSupported => comparer switch
{
IsEqual => true,
IsNotEqual => true,
IsGreaterThan => true,
IsGreaterThanOrEqual => true,
IsLessThan => true,
IsLessThanOrEqual => true,
_ => false,
};
/// <summary>
/// Checks if the compare operator is satisfied by a boolean comparison result.
/// </summary>
/// <param name="compareResult">Result from Equals comparison</param>
/// <returns>True if satisfied</returns>
/// <remarks>Only use this method if the comparison is boolean only. Use the <see cref="IsCompareOperator"/> otherwise.</remarks>
public bool IsCompareEquivalence(bool compareResult) => comparer switch
{
IsEqual => compareResult,
IsNotEqual => !compareResult,
_ => false,
};
/// <summary>
/// Checks if the compare operator is satisfied by the <see cref="IComparable.CompareTo"/> result.
/// </summary>
/// <param name="compareResult">Result from CompareTo</param>
/// <returns>True if satisfied</returns>
public bool IsCompareOperator(int compareResult) => comparer switch
{
IsEqual => compareResult is 0,
IsNotEqual => compareResult is not 0,
IsGreaterThan => compareResult > 0,
IsGreaterThanOrEqual => compareResult >= 0,
IsLessThan => compareResult < 0,
IsLessThanOrEqual => compareResult <= 0,
_ => false,
};
}
}

View File

@ -1,850 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Reflection;
using static PKHeX.Core.MessageStrings;
using static PKHeX.Core.BatchModifications;
namespace PKHeX.Core;
/// <summary>
/// Logic for editing many <see cref="PKM"/> with user provided <see cref="StringInstruction"/> list.
/// </summary>
public static class BatchEditing
{
public static readonly Type[] Types =
[
typeof (PK9), typeof (PA9),
typeof (PK8), typeof (PA8), typeof (PB8),
typeof (PB7),
typeof (PK7), typeof (PK6), typeof (PK5), typeof (PK4), typeof(BK4), typeof(RK4),
typeof (PK3), typeof (XK3), typeof (CK3),
typeof (PK2), typeof (SK2), typeof (PK1),
];
/// <summary>
/// Extra properties to show in the list of selectable properties (GUI)
/// </summary>
private static readonly string[] CustomProperties =
[
PROP_LEGAL, PROP_TYPENAME, PROP_RIBBONS, PROP_EVS, PROP_CONTESTSTATS, PROP_MOVEMASTERY, PROP_MOVEPLUS,
PROP_TYPE1, PROP_TYPE2, PROP_TYPEEITHER,
IdentifierContains, nameof(ISlotInfo.Slot), nameof(SlotInfoBox.Box),
];
/// <summary>
/// Property names, indexed by <see cref="Types"/>.
/// </summary>
public static string[][] Properties => GetProperties.Value;
private static readonly Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>>[] Props = GetPropertyDictionaries(Types);
private static readonly Lazy<string[][]> GetProperties = new(() => GetPropArray(Props, CustomProperties));
private static Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>>[] GetPropertyDictionaries(IReadOnlyList<Type> types)
{
var result = new Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>>[types.Count];
for (int i = 0; i < types.Count; i++)
result[i] = GetPropertyDictionary(types[i], ReflectUtil.GetAllPropertyInfoPublic).GetAlternateLookup<ReadOnlySpan<char>>();
return result;
}
private static Dictionary<string, PropertyInfo> GetPropertyDictionary(Type type, Func<Type, IEnumerable<PropertyInfo>> selector)
{
const int expectedMax = 0x200; // currently 0x160 as of 2022
var dict = new Dictionary<string, PropertyInfo>(expectedMax);
var props = selector(type);
foreach (var p in props)
dict.TryAdd(p.Name, p);
return dict;
}
internal const string CONST_RAND = "$rand";
internal const string CONST_SHINY = "$shiny";
internal const string CONST_SUGGEST = "$suggest";
private const string CONST_BYTES = "$[]";
private const char CONST_POINTER = '*';
internal const char CONST_SPECIAL = '$';
internal const string PROP_LEGAL = "Legal";
internal const string PROP_TYPENAME = "ObjectType";
internal const string PROP_TYPEEITHER = "HasType";
internal const string PROP_TYPE1 = "PersonalType1";
internal const string PROP_TYPE2 = "PersonalType2";
internal const string PROP_RIBBONS = "Ribbons";
internal const string PROP_EVS = "EVs";
internal const string PROP_CONTESTSTATS = "ContestStats";
internal const string PROP_MOVEMASTERY = "MoveMastery";
internal const string PROP_MOVEPLUS = "PlusMoves";
internal const string IdentifierContains = nameof(IdentifierContains);
private static string[][] GetPropArray<T>(Dictionary<string, T>.AlternateLookup<ReadOnlySpan<char>>[] types, ReadOnlySpan<string> extra)
{
// Create a list for all types, [inAny, ..types, inAll]
var result = new string[types.Length + 2][];
var p = result.AsSpan(1, types.Length);
for (int i = 0; i < p.Length; i++)
{
var type = types[i].Dictionary;
string[] combine = [..type.Keys, ..extra];
Array.Sort(combine);
p[i] = combine;
}
// Properties for any PKM
// Properties shared by all PKM
var first = p[0];
var any = new HashSet<string>(first);
var all = new HashSet<string>(first);
foreach (var set in p[1..])
{
any.UnionWith(set);
all.IntersectWith(set);
}
var arrAny = any.ToArray();
Array.Sort(arrAny);
result[0] = arrAny;
var arrAll = all.ToArray();
Array.Sort(arrAll);
result[^1] = arrAll;
return result;
}
/// <summary>
/// Tries to fetch the <see cref="PKM"/> property from the cache of available properties.
/// </summary>
/// <param name="pk">Pokémon to check</param>
/// <param name="name">Property Name to check</param>
/// <param name="pi">Property Info retrieved (if any).</param>
/// <returns>True if it has property, false if it does not.</returns>
public static bool TryGetHasProperty(PKM pk, ReadOnlySpan<char> name, [NotNullWhen(true)] out PropertyInfo? pi)
{
var type = pk.GetType();
return TryGetHasProperty(type, name, out pi);
}
/// <summary>
/// Tries to fetch the <see cref="PKM"/> property from the cache of available properties.
/// </summary>
/// <param name="type">Type to check</param>
/// <param name="name">Property Name to check</param>
/// <param name="pi">Property Info retrieved (if any).</param>
/// <returns>True if it has property, false if it does not.</returns>
public static bool TryGetHasProperty(Type type, ReadOnlySpan<char> name, [NotNullWhen(true)] out PropertyInfo? pi)
{
var index = Types.IndexOf(type);
if (index < 0)
{
pi = null;
return false;
}
var props = Props[index];
return props.TryGetValue(name, out pi);
}
/// <summary>
/// Gets a list of <see cref="PKM"/> types that implement the requested <see cref="property"/>.
/// </summary>
public static IEnumerable<string> GetTypesImplementing(string property)
{
for (int i = 0; i < Types.Length; i++)
{
var type = Types[i];
var props = Props[i];
if (!props.TryGetValue(property, out var pi))
continue;
yield return $"{type.Name}: {pi.PropertyType.Name}";
}
}
/// <summary>
/// Gets the type of the <see cref="PKM"/> property using the saved cache of properties.
/// </summary>
/// <param name="propertyName">Property Name to fetch the type for</param>
/// <param name="result">Type name of the property</param>
/// <param name="typeIndex">Type index (within <see cref="Types"/>). Leave empty (0) for a nonspecific format.</param>
/// <returns>Short name of the property's type.</returns>
public static bool TryGetPropertyType(string propertyName, [NotNullWhen(true)] out string? result, int typeIndex = 0)
{
if (CustomProperties.Contains(propertyName))
{
result = "Custom";
return true;
}
result = null;
if (typeIndex == 0) // Any
{
foreach (var p in Props)
{
if (!p.TryGetValue(propertyName, out var pi))
continue;
result = pi.PropertyType.Name;
return true;
}
return false;
}
int index = typeIndex - 1;
if ((uint)index >= Props.Length)
index = 0; // All vs Specific
var pr = Props[index];
if (!pr.TryGetValue(propertyName, out var info))
return false;
result = info.PropertyType.Name;
return true;
}
/// <summary>
/// Initializes the <see cref="StringInstruction"/> list with a context-sensitive value. If the provided value is a string, it will attempt to convert that string to its corresponding index.
/// </summary>
/// <param name="il">Instructions to initialize.</param>
public static void ScreenStrings(IEnumerable<StringInstruction> il)
{
foreach (var i in il)
{
var pv = i.PropertyValue;
if (pv.All(char.IsDigit))
continue;
if (pv.StartsWith(CONST_SPECIAL) && !pv.StartsWith(CONST_BYTES, StringComparison.Ordinal))
{
var str = pv.AsSpan(1);
if (StringInstruction.IsRandomRange(str))
{
i.SetRandomRange(str);
continue;
}
}
SetInstructionScreenedValue(i);
}
}
/// <summary>
/// Initializes the <see cref="StringInstruction"/> with a context-sensitive value. If the provided value is a string, it will attempt to convert that string to its corresponding index.
/// </summary>
/// <param name="i">Instruction to initialize.</param>
private static void SetInstructionScreenedValue(StringInstruction i)
{
ReadOnlySpan<string> set;
switch (i.PropertyName)
{
case nameof(PKM.Species): set = GameInfo.Strings.specieslist; break;
case nameof(PKM.HeldItem): set = GameInfo.Strings.itemlist; break;
case nameof(PKM.Ability): set = GameInfo.Strings.abilitylist; break;
case nameof(PKM.Nature): set = GameInfo.Strings.natures; break;
case nameof(PKM.Ball): set = GameInfo.Strings.balllist; break;
case nameof(PKM.Move1) or nameof(PKM.Move2) or nameof(PKM.Move3) or nameof(PKM.Move4):
case nameof(PKM.RelearnMove1) or nameof(PKM.RelearnMove2) or nameof(PKM.RelearnMove3) or nameof(PKM.RelearnMove4):
set = GameInfo.Strings.movelist; break;
default:
return;
}
i.SetScreenedValue(set);
}
private static Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> GetProps(PKM pk)
{
var type = pk.GetType();
var typeIndex = Types.IndexOf(type);
return Props[typeIndex];
}
/// <summary>
/// Checks if the object is filtered by the provided <see cref="filters"/>.
/// </summary>
/// <param name="filters">Filters which must be satisfied.</param>
/// <param name="pk">Object to check.</param>
/// <returns>True if <see cref="pk"/> matches all filters.</returns>
public static bool IsFilterMatch(IEnumerable<StringInstruction> filters, PKM pk)
{
var props = GetProps(pk);
foreach (var filter in filters)
{
if (!IsFilterMatch(filter, pk, props))
return false;
}
return true;
}
/// <summary>
/// Checks if the object is filtered by the provided <see cref="filters"/>.
/// </summary>
/// <param name="filters">Filters which must be satisfied.</param>
/// <param name="pk">Object to check.</param>
/// <returns>True if <see cref="pk"/> matches all filters.</returns>
public static bool IsFilterMatchMeta(IEnumerable<StringInstruction> filters, SlotCache pk)
{
foreach (var i in filters)
{
foreach (var filter in BatchFilters.FilterMeta)
{
if (!filter.IsMatch(i.PropertyName))
continue;
if (!filter.IsFiltered(pk, i))
return false;
break;
}
}
return true;
}
/// <summary>
/// Checks if the object is filtered by the provided <see cref="filters"/>.
/// </summary>
/// <param name="filters">Filters which must be satisfied.</param>
/// <param name="obj">Object to check.</param>
/// <returns>True if <see cref="obj"/> matches all filters.</returns>
public static bool IsFilterMatch(IEnumerable<StringInstruction> filters, object obj)
{
foreach (var cmd in filters)
{
if (cmd.PropertyName is PROP_TYPENAME)
{
var type = obj.GetType();
var typeName = type.Name;
if (!cmd.Comparer.IsCompareEquivalence(cmd.PropertyValue == typeName))
return false;
continue;
}
if (!ReflectUtil.HasProperty(obj, cmd.PropertyName, out var pi))
return false;
try
{
if (cmd.Comparer.IsCompareOperator(pi.CompareTo(obj, cmd.PropertyValue)))
continue;
}
// User provided inputs can mismatch the type's required value format, and fail to be compared.
catch (Exception e)
{
Debug.WriteLine($"Unable to compare {cmd.PropertyName} to {cmd.PropertyValue}.");
Debug.WriteLine(e.Message);
}
return false;
}
return true;
}
/// <summary>
/// Tries to modify the <see cref="PKM"/>.
/// </summary>
/// <param name="pk">Object to modify.</param>
/// <param name="filters">Filters which must be satisfied prior to any modifications being made.</param>
/// <param name="modifications">Modifications to perform on the <see cref="pk"/>.</param>
/// <param name="modifier">Custom modifier delegate.</param>
/// <returns>Result of the attempted modification.</returns>
public static bool TryModify(PKM pk, IEnumerable<StringInstruction> filters, IEnumerable<StringInstruction> modifications, Func<PKM, bool>? modifier = null)
=> TryModifyPKM(pk, filters, modifications, modifier) is ModifyResult.Modified;
/// <summary>
/// Tries to modify the <see cref="PKM"/> using instructions and a custom modifier delegate.
/// </summary>
/// <param name="pk">Object to modify.</param>
/// <param name="filters">Filters which must be satisfied prior to any modifications being made.</param>
/// <param name="modifications">Modifications to perform on the <see cref="pk"/>.</param>
/// <param name="modifier">Custom modifier delegate.</param>
/// <returns>Result of the attempted modification.</returns>
internal static ModifyResult TryModifyPKM(PKM pk, IEnumerable<StringInstruction> filters, IEnumerable<StringInstruction> modifications, Func<PKM, bool>? modifier = null)
{
if (!pk.ChecksumValid || pk.Species == 0)
return ModifyResult.Skipped;
var info = new BatchInfo(pk);
var props = GetProps(pk);
// Check if any filter requires us to exclude this from modification scope.
foreach (var cmd in filters)
{
try
{
if (!IsFilterMatch(cmd, info, props))
return ModifyResult.Filtered;
}
catch (Exception ex)
{
Debug.WriteLine(MsgBEModifyFailCompare + " " + ex.Message, cmd.PropertyName, cmd.PropertyValue);
return ModifyResult.Error;
}
}
// Run all modifications, and track if any modifications were made or if any errors occurred.
var error = false;
var result = ModifyResult.Skipped;
// If a compiled modifier is provided, execute it. If it returns false, skip further modifications.
if (modifier is { } func)
{
try
{
if (!func(pk))
return ModifyResult.Skipped;
result = ModifyResult.Modified;
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
return ModifyResult.Error;
}
}
foreach (var cmd in modifications)
{
try
{
var tmp = SetPKMProperty(cmd, info, props);
if (tmp == ModifyResult.Error)
error = true;
else if (tmp != ModifyResult.Skipped)
result = tmp;
}
catch (Exception ex)
{
Debug.WriteLine(MsgBEModifyFail + " " + ex.Message, cmd.PropertyName, cmd.PropertyValue);
error = true;
}
}
if (error)
result |= ModifyResult.Error;
return result;
}
/// <summary>
/// Sets the property if the <see cref="BatchInfo"/> should be filtered due to the <see cref="StringInstruction"/> provided.
/// </summary>
/// <param name="cmd">Command Filter</param>
/// <param name="info">Pokémon to check.</param>
/// <param name="props">PropertyInfo cache (optional)</param>
/// <returns>True if filtered, else false.</returns>
private static ModifyResult SetPKMProperty(StringInstruction cmd, BatchInfo info, Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> props)
{
var pk = info.Entity;
if (cmd.Operation == InstructionOperation.Set)
{
if (cmd.PropertyValue.StartsWith(CONST_BYTES, StringComparison.Ordinal))
return SetByteArrayProperty(pk, cmd);
if (cmd.PropertyValue.StartsWith(CONST_SUGGEST, StringComparison.OrdinalIgnoreCase))
return SetSuggestedPKMProperty(cmd.PropertyName, info, cmd.PropertyValue);
if (cmd is { PropertyValue: CONST_RAND, PropertyName: nameof(PKM.Moves) })
return SetSuggestedMoveset(info, true);
if (SetComplexProperty(pk, cmd))
return ModifyResult.Modified;
}
if (!props.TryGetValue(cmd.PropertyName, out var pi))
return ModifyResult.Error;
if (!pi.CanWrite)
return ModifyResult.Error;
if (cmd.Operation != InstructionOperation.Set)
return ApplyNumericOperation(pk, cmd, pi, props);
if (!TryResolveOperandValue(cmd, pk, props, out var value))
return ModifyResult.Error;
ReflectUtil.SetValue(pi, pk, value);
return ModifyResult.Modified;
}
private static ModifyResult ApplyNumericOperation<T>(T pk, StringInstruction cmd, PropertyInfo pi, Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> props) where T : notnull
{
if (!pi.CanRead)
return ModifyResult.Error;
if (!TryGetNumericType(pi.PropertyType, out var numericType))
return ModifyResult.Error;
var currentValue = pi.GetValue(pk);
if (currentValue is null)
return ModifyResult.Error;
if (!TryResolveOperandValue(cmd, pk, props, out var operandValue))
return ModifyResult.Error;
if (!TryApplyNumericOperation(numericType, currentValue, operandValue, cmd.Operation, out var value))
return ModifyResult.Error;
ReflectUtil.SetValue(pi, pk, value);
return ModifyResult.Modified;
}
private static bool TryResolveOperandValue<T>(StringInstruction cmd, T pk, Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> props, [NotNullWhen(true)] out object? value)
{
if (cmd.Random)
{
value = cmd.RandomValue;
return true;
}
var propertyValue = cmd.PropertyValue;
if (propertyValue.StartsWith(CONST_POINTER) && props.TryGetValue(propertyValue.AsSpan(1), out var opi))
{
value = opi.GetValue(pk);
return value is not null;
}
value = propertyValue;
return true;
}
private static bool TryGetNumericType(Type type, out Type numericType)
{
numericType = Nullable.GetUnderlyingType(type) ?? type;
// isNullable = numericType != type;
return numericType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(INumber<>));
}
private static bool TryApplyNumericOperation(Type numericType, object currentValue, object operandValue, InstructionOperation operation, [NotNullWhen(true)] out object? result)
{
result = null;
if (numericType == typeof(byte))
return ApplyBinaryInteger<byte>(currentValue, operandValue, operation, out result);
if (numericType == typeof(sbyte))
return ApplyBinaryInteger<sbyte>(currentValue, operandValue, operation, out result);
if (numericType == typeof(short))
return ApplyBinaryInteger<short>(currentValue, operandValue, operation, out result);
if (numericType == typeof(ushort))
return ApplyBinaryInteger<ushort>(currentValue, operandValue, operation, out result);
if (numericType == typeof(int))
return ApplyBinaryInteger<int>(currentValue, operandValue, operation, out result);
if (numericType == typeof(uint))
return ApplyBinaryInteger<uint>(currentValue, operandValue, operation, out result);
if (numericType == typeof(long))
return ApplyBinaryInteger<long>(currentValue, operandValue, operation, out result);
if (numericType == typeof(ulong))
return ApplyBinaryInteger<ulong>(currentValue, operandValue, operation, out result);
if (numericType == typeof(nint))
return ApplyBinaryInteger<nint>(currentValue, operandValue, operation, out result);
if (numericType == typeof(nuint))
return ApplyBinaryInteger<nuint>(currentValue, operandValue, operation, out result);
if (numericType == typeof(BigInteger))
return ApplyBinaryInteger<BigInteger>(currentValue, operandValue, operation, out result);
if (numericType == typeof(float))
return ApplyNumeric<float>(currentValue, operandValue, operation, out result);
if (numericType == typeof(double))
return ApplyNumeric<double>(currentValue, operandValue, operation, out result);
if (numericType == typeof(decimal))
return ApplyNumeric<decimal>(currentValue, operandValue, operation, out result);
return false;
}
private static bool ApplyNumeric<T>(object currentValue, object operandValue, InstructionOperation operation, [NotNullWhen(true)] out object? result)
where T : INumber<T>
{
if (IsBitwiseOperation(operation))
{
result = null;
return false;
}
var success = TryApplyNumericOperationCore<T>(currentValue, operandValue, operation, out var typed);
result = typed;
return success;
}
private static bool ApplyBinaryInteger<T>(object currentValue, object operandValue, InstructionOperation operation, [NotNullWhen(true)] out object? result)
where T : IBinaryInteger<T>
{
var success = IsBitwiseOperation(operation)
? TryApplyBinaryIntegerOperationCore<T>(currentValue, operandValue, operation, out var typed)
: TryApplyNumericOperationCore(currentValue, operandValue, operation, out typed);
result = typed;
return success;
}
private static bool TryApplyNumericOperationCore<T>(object currentValue, object operandValue, InstructionOperation operation, [NotNullWhen(true)] out T? result) where T : INumber<T>
{
if (!TryConvertNumeric<T>(currentValue, out var left) || !TryConvertNumeric<T>(operandValue, out var right))
{
result = default;
return false;
}
try
{
result = operation switch
{
InstructionOperation.Add => left + right,
InstructionOperation.Subtract => left - right,
InstructionOperation.Multiply => left * right,
InstructionOperation.Divide => left / right,
InstructionOperation.Modulo => left % right,
_ => right,
};
return true;
}
catch (DivideByZeroException)
{
result = default;
return false;
}
}
private static bool TryApplyBinaryIntegerOperationCore<T>(object currentValue, object operandValue, InstructionOperation operation, [NotNullWhen(true)] out T? result)
where T : IBinaryInteger<T>
{
if (!TryConvertNumeric<T>(currentValue, out var left) || !TryConvertNumeric<T>(operandValue, out var right))
{
result = default;
return false;
}
try
{
switch (operation)
{
case InstructionOperation.BitwiseAnd:
result = left & right;
return true;
case InstructionOperation.BitwiseOr:
result = left | right;
return true;
case InstructionOperation.BitwiseXor:
result = left ^ right;
return true;
case InstructionOperation.BitwiseShiftLeft:
result = left << int.CreateChecked(right);
return true;
case InstructionOperation.BitwiseShiftRight:
result = left >> int.CreateChecked(right);
return true;
default:
result = default;
return false;
}
}
catch (OverflowException)
{
result = default;
return false;
}
}
private static bool IsBitwiseOperation(InstructionOperation operation)
=> operation is InstructionOperation.BitwiseAnd or InstructionOperation.BitwiseOr or InstructionOperation.BitwiseXor
or InstructionOperation.BitwiseShiftLeft or InstructionOperation.BitwiseShiftRight;
private static bool TryConvertNumeric<T>(object value, [NotNullWhen(true)] out T? result) where T : INumber<T>
{
if (value is T typed)
{
result = typed;
return true;
}
if (value is string text)
{
if (T.TryParse(text, CultureInfo.InvariantCulture, out var parsed))
{
result = parsed;
return true;
}
result = default;
return false;
}
if (value is IConvertible)
{
try
{
var converted = Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture);
if (converted is T convertedValue)
{
result = convertedValue;
return true;
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
result = default;
return false;
}
/// <summary>
/// Checks if the <see cref="BatchInfo"/> should be filtered due to the <see cref="StringInstruction"/> provided.
/// </summary>
/// <param name="cmd">Command Filter</param>
/// <param name="info">Pokémon to check.</param>
/// <param name="props">PropertyInfo cache (optional)</param>
/// <returns>True if filter matches, else false.</returns>
private static bool IsFilterMatch(StringInstruction cmd, BatchInfo info, Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> props)
{
var match = BatchFilters.FilterMods.Find(z => z.IsMatch(cmd.PropertyName));
if (match is not null)
return match.IsFiltered(info, cmd);
return IsPropertyFiltered(cmd, info.Entity, props);
}
/// <summary>
/// Checks if the <see cref="PKM"/> should be filtered due to the <see cref="StringInstruction"/> provided.
/// </summary>
/// <param name="cmd">Command Filter</param>
/// <param name="pk">Pokémon to check.</param>
/// <param name="props">PropertyInfo cache (optional)</param>
/// <returns>True if filter matches, else false.</returns>
private static bool IsFilterMatch(StringInstruction cmd, PKM pk, Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> props)
{
var match = BatchFilters.FilterMods.Find(z => z.IsMatch(cmd.PropertyName));
if (match is not null)
return match.IsFiltered(pk, cmd);
return IsPropertyFiltered(cmd, pk, props);
}
/// <summary>
/// Checks if the <see cref="PKM"/> should be filtered due to the <see cref="StringInstruction"/> provided.
/// </summary>
/// <param name="cmd">Command Filter</param>
/// <param name="pk">Pokémon to check.</param>
/// <param name="props">PropertyInfo cache</param>
/// <returns>True if filtered, else false.</returns>
private static bool IsPropertyFiltered(StringInstruction cmd, PKM pk, Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>> props)
{
if (!props.TryGetValue(cmd.PropertyName, out var pi))
return false;
if (!pi.CanRead)
return false;
var val = cmd.PropertyValue;
if (val.StartsWith(CONST_POINTER) && props.TryGetValue(val.AsSpan(1), out var opi))
{
var result = opi.GetValue(pk) ?? throw new NullReferenceException();
return cmd.Comparer.IsCompareOperator(pi.CompareTo(pk, result));
}
return cmd.Comparer.IsCompareOperator(pi.CompareTo(pk, val));
}
/// <summary>
/// Sets the <see cref="PKM"/> data with a suggested value based on its <see cref="LegalityAnalysis"/>.
/// </summary>
/// <param name="name">Property to modify.</param>
/// <param name="info">Cached info storing Legal data.</param>
/// <param name="propValue">Suggestion string which starts with <see cref="CONST_SUGGEST"/></param>
private static ModifyResult SetSuggestedPKMProperty(ReadOnlySpan<char> name, BatchInfo info, ReadOnlySpan<char> propValue)
{
foreach (var mod in BatchMods.SuggestionMods)
{
if (mod.IsMatch(name, propValue, info))
return mod.Modify(name, propValue, info);
}
return ModifyResult.Error;
}
/// <summary>
/// Sets the <see cref="PKM"/> byte array property to a specified value.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="cmd">Modification</param>
private static ModifyResult SetByteArrayProperty(PKM pk, StringInstruction cmd)
{
Span<byte> dest;
switch (cmd.PropertyName)
{
case nameof(PKM.NicknameTrash) or nameof(PKM.Nickname): dest = pk.NicknameTrash; break;
case nameof(PKM.OriginalTrainerTrash): dest = pk.OriginalTrainerTrash; break;
case nameof(PKM.HandlingTrainerTrash): dest = pk.HandlingTrainerTrash; break;
default:
return ModifyResult.Error;
}
var src = cmd.PropertyValue.AsSpan(CONST_BYTES.Length); // skip prefix
StringUtil.LoadHexBytesTo(src, dest, 3);
return ModifyResult.Modified;
}
/// <summary>
/// Sets the <see cref="PKM"/> property to a non-specific smart value.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="cmd">Modification</param>
/// <returns>True if modified, false if no modifications done.</returns>
private static bool SetComplexProperty(PKM pk, StringInstruction cmd)
{
ReadOnlySpan<char> name = cmd.PropertyName;
ReadOnlySpan<char> value = cmd.PropertyValue;
if (name.StartsWith("IV") && value is CONST_RAND)
{
SetRandomIVs(pk, name);
return true;
}
foreach (var mod in BatchMods.ComplexMods)
{
if (!mod.IsMatch(name, value))
continue;
mod.Modify(pk, cmd);
return true;
}
return false;
}
/// <summary>
/// Sets the <see cref="PKM"/> IV(s) to a random value.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="propertyName">Property to modify</param>
private static void SetRandomIVs(PKM pk, ReadOnlySpan<char> propertyName)
{
if (propertyName is nameof(PKM.IVs))
{
var la = new LegalityAnalysis(pk);
var enc = la.EncounterMatch;
if (enc is IFlawlessIVCount { FlawlessIVCount: not 0 } fc)
pk.SetRandomIVs(fc.FlawlessIVCount);
else if (enc is IFixedIVSet { IVs: {IsSpecified: true} iv})
pk.SetRandomIVs(iv);
else if (enc is IFlawlessIVCountConditional c && c.GetFlawlessIVCount(pk) is { Max: not 0 } x)
pk.SetRandomIVs(Util.Rand.Next(x.Min, x.Max + 1));
else
pk.SetRandomIVs();
return;
}
if (TryGetHasProperty(pk, propertyName, out var pi))
{
const string IV32 = nameof(PK9.IV32);
if (propertyName is IV32)
{
var value = (uint)Util.Rand.Next(0x3FFFFFFF + 1);
if (pk is BK4 bk) // Big Endian, reverse IV ordering
{
value <<= 2; // flags are the lowest bits, and our random value is still fine.
value |= bk.IV32 & 3; // preserve the flags
bk.IV32 = value;
return;
}
var exist = ReflectUtil.GetValue(pk, IV32);
value |= exist switch
{
uint iv => iv & (3u << 30), // preserve the flags
_ => 0,
};
ReflectUtil.SetValue(pi, pk, value);
}
else
{
var value = Util.Rand.Next(pk.MaxIV + 1);
ReflectUtil.SetValue(pi, pk, value);
}
}
}
}

View File

@ -0,0 +1,23 @@
using System.Diagnostics.CodeAnalysis;
namespace PKHeX.Core;
/// <summary>
/// Default property provider that uses an <see cref="IBatchEditor{TObject}"/> for reflection.
/// </summary>
public class BatchPropertyProvider<TEditor, TObject>(TEditor editor) : IPropertyProvider<TObject> where TObject : notnull where TEditor : IBatchEditor<TObject>
{
/// <summary>
/// Initializes a new instance of the <see cref="BatchPropertyProvider{TEditor, TObject}"/> class with the specified editor.
/// </summary>
public bool TryGetProperty(TObject obj, string prop, [NotNullWhen(true)] out string? result)
{
result = null;
if (!editor.TryGetHasProperty(obj, prop, out var pi))
return false;
var value = pi.GetValue(obj);
result = value?.ToString();
return result is not null;
}
}

View File

@ -1,5 +1,5 @@
using System.Collections.Generic;
using static PKHeX.Core.BatchEditing;
using static PKHeX.Core.EntityBatchEditor;
namespace PKHeX.Core;
@ -9,7 +9,7 @@ namespace PKHeX.Core;
public static class BatchFilters
{
/// <summary>
/// Filters to use for <see cref="BatchEditing"/> that are derived from the <see cref="PKM"/> data.
/// Filters to use for <see cref="EntityBatchEditor"/> that are derived from the <see cref="PKM"/> data.
/// </summary>
public static readonly List<IComplexFilter> FilterMods =
[
@ -17,7 +17,7 @@ public static class BatchFilters
(pk, cmd) => bool.TryParse(cmd.PropertyValue, out var b) && cmd.Comparer.IsCompareEquivalence(b == new LegalityAnalysis(pk).Valid),
(info, cmd) => bool.TryParse(cmd.PropertyValue, out var b) && cmd.Comparer.IsCompareEquivalence(b == info.Legality.Valid)),
new ComplexFilter(PROP_TYPENAME,
new ComplexFilter(BatchEditingUtil.PROP_TYPENAME,
(pk, cmd) => cmd.Comparer.IsCompareEquivalence(pk.GetType().Name == cmd.PropertyValue),
(info, cmd) => cmd.Comparer.IsCompareEquivalence(info.Entity.GetType().Name == cmd.PropertyValue)),
@ -35,7 +35,7 @@ public static class BatchFilters
];
/// <summary>
/// Filters to use for <see cref="BatchEditing"/> that are derived from the <see cref="PKM"/> source.
/// Filters to use for <see cref="EntityBatchEditor"/> that are derived from the <see cref="PKM"/> source.
/// </summary>
public static readonly List<IComplexFilterMeta> FilterMeta =
[

View File

@ -6,12 +6,10 @@ namespace PKHeX.Core;
/// <param name="Entity"> Entity to be modified. </param>
public sealed record BatchInfo(PKM Entity)
{
private LegalityAnalysis? la; // c# 14 replace with get-field
/// <summary>
/// Legality analysis of the entity.
/// </summary>
public LegalityAnalysis Legality => la ??= new LegalityAnalysis(Entity);
public LegalityAnalysis Legality => field ??= new LegalityAnalysis(Entity);
/// <inheritdoc cref="LegalityAnalysis.Valid"/>
public bool Legal => Legality.Valid;

View File

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using static PKHeX.Core.BatchEditing;
using static PKHeX.Core.EntityBatchEditor;
namespace PKHeX.Core;

View File

@ -0,0 +1,304 @@
using System;
using System.Collections.Generic;
using System.Linq;
using static PKHeX.Core.BatchModifications;
namespace PKHeX.Core;
/// <summary>
/// Logic for editing many <see cref="PKM"/> with user provided <see cref="StringInstruction"/> list.
/// </summary>
public sealed class EntityBatchEditor() : BatchEditingBase<PKM, BatchInfo>(EntityTypes, EntityCustomProperties, expectedMax: 0x200)
{
private static readonly Type[] EntityTypes =
[
typeof (PK9), typeof (PA9),
typeof (PK8), typeof (PA8), typeof (PB8),
typeof (PB7),
typeof (PK7), typeof (PK6), typeof (PK5), typeof (PK4), typeof(BK4), typeof(RK4),
typeof (PK3), typeof (XK3), typeof (CK3),
typeof (PK2), typeof (SK2), typeof (PK1),
];
/// <summary>
/// Extra properties to show in the list of selectable properties (GUI) with special handling.
/// </summary>
/// <remarks>
/// These are not necessarily properties of the <see cref="PKM"/> themselves,
/// but can be any context-sensitive value related to the <see cref="PKM"/> or its legality,
/// such as "Legal" or "HasType". The handling of these properties must be implemented in the <see cref="TryHandleSetOperation"/> and <see cref="TryHandleFilter"/> methods.
/// </remarks>
private static readonly string[] EntityCustomProperties =
[
// General
BatchEditingUtil.PROP_TYPENAME,
// Entity/PersonalInfo
PROP_LEGAL, PROP_RIBBONS, PROP_EVS, PROP_CONTESTSTATS, PROP_MOVEMASTERY, PROP_MOVEPLUS,
PROP_TYPE1, PROP_TYPE2, PROP_TYPEEITHER,
// SlotCache
IdentifierContains, nameof(ISlotInfo.Slot), nameof(SlotInfoBox.Box),
];
public static EntityBatchEditor Instance { get; } = new();
// Custom Identifiers for special handling.
private const string CONST_BYTES = "$[]"; // Define a byte array with separated hex byte values, e.g. "$[]FF,02,03" or "$[]A0 02 0A FF"
// Custom Values to apply.
internal const string CONST_RAND = "$rand";
internal const string CONST_SHINY = "$shiny";
internal const string CONST_SUGGEST = "$suggest";
internal const char CONST_SPECIAL = '$';
// Custom Properties to change.
internal const string PROP_LEGAL = "Legal";
internal const string PROP_TYPEEITHER = "HasType";
internal const string PROP_TYPE1 = "PersonalType1";
internal const string PROP_TYPE2 = "PersonalType2";
internal const string PROP_RIBBONS = "Ribbons";
internal const string PROP_EVS = "EVs";
internal const string PROP_CONTESTSTATS = "ContestStats";
internal const string PROP_MOVEMASTERY = "MoveMastery";
internal const string PROP_MOVEPLUS = "PlusMoves";
internal const string IdentifierContains = nameof(IdentifierContains);
/// <summary>
/// Initializes the <see cref="StringInstruction"/> list with a context-sensitive value. If the provided value is a string, it will attempt to convert that string to its corresponding index.
/// </summary>
/// <param name="il">Instructions to initialize.</param>
public static void ScreenStrings(IEnumerable<StringInstruction> il)
{
foreach (var i in il)
{
var pv = i.PropertyValue;
if (pv.All(char.IsDigit))
continue;
if (pv.StartsWith(CONST_SPECIAL) && !pv.StartsWith(CONST_BYTES, StringComparison.Ordinal))
{
var str = pv.AsSpan(1);
if (StringInstruction.IsRandomRange(str))
{
i.SetRandomRange(str);
continue;
}
}
SetInstructionScreenedValue(i);
}
}
/// <summary>
/// Initializes the <see cref="StringInstruction"/> with a context-sensitive value. If the provided value is a string, it will attempt to convert that string to its corresponding index.
/// </summary>
/// <param name="i">Instruction to initialize.</param>
private static void SetInstructionScreenedValue(StringInstruction i)
{
ReadOnlySpan<string> set;
switch (i.PropertyName)
{
case nameof(PKM.Species): set = GameInfo.Strings.specieslist; break;
case nameof(PKM.HeldItem): set = GameInfo.Strings.itemlist; break;
case nameof(PKM.Ability): set = GameInfo.Strings.abilitylist; break;
case nameof(PKM.Nature): set = GameInfo.Strings.natures; break;
case nameof(PKM.Ball): set = GameInfo.Strings.balllist; break;
case nameof(PKM.Move1) or nameof(PKM.Move2) or nameof(PKM.Move3) or nameof(PKM.Move4):
case nameof(PKM.RelearnMove1) or nameof(PKM.RelearnMove2) or nameof(PKM.RelearnMove3) or nameof(PKM.RelearnMove4):
set = GameInfo.Strings.movelist; break;
default:
return;
}
i.SetScreenedValue(set);
}
/// <summary>
/// Checks if the object is filtered by the provided <see cref="filters"/>.
/// </summary>
/// <param name="filters">Filters which must be satisfied.</param>
/// <param name="pk">Object to check.</param>
/// <returns>True if <see cref="pk"/> matches all filters.</returns>
public static bool IsFilterMatchMeta(IEnumerable<StringInstruction> filters, SlotCache pk)
{
foreach (var i in filters)
{
foreach (var filter in BatchFilters.FilterMeta)
{
if (!filter.IsMatch(i.PropertyName))
continue;
if (!filter.IsFiltered(pk, i))
return false;
break;
}
}
return true;
}
protected override BatchInfo CreateMeta(PKM entity) => new(entity);
protected override bool ShouldModify(PKM entity) => entity.ChecksumValid && entity.Species != 0;
protected override bool TryHandleSetOperation(StringInstruction cmd, BatchInfo info, PKM entity, out ModifyResult result)
{
if (cmd.PropertyValue.StartsWith(CONST_BYTES, StringComparison.Ordinal))
{
result = SetByteArrayProperty(entity, cmd);
return true;
}
if (cmd.PropertyValue.StartsWith(CONST_SUGGEST, StringComparison.OrdinalIgnoreCase))
{
result = SetSuggestedProperty(cmd.PropertyName, info, cmd.PropertyValue);
return true;
}
if (cmd is { PropertyValue: CONST_RAND, PropertyName: nameof(PKM.Moves) })
{
result = SetSuggestedMoveset(info, true);
return true;
}
if (SetComplexProperty(info, cmd))
{
result = ModifyResult.Modified;
return true;
}
result = ModifyResult.Skipped;
return false;
}
protected override bool TryHandleFilter(StringInstruction cmd, BatchInfo info, PKM entity, out bool isMatch)
{
var match = BatchFilters.FilterMods.Find(z => z.IsMatch(cmd.PropertyName));
if (match is null)
{
isMatch = false;
return false;
}
isMatch = match.IsFiltered(info, cmd);
return true;
}
/// <summary>
/// Sets the <see cref="PKM"/> data with a suggested value based on its <see cref="LegalityAnalysis"/>.
/// </summary>
/// <param name="name">Property to modify.</param>
/// <param name="info">Cached info storing Legal data.</param>
/// <param name="propValue">Suggestion string which starts with <see cref="CONST_SUGGEST"/></param>
private static ModifyResult SetSuggestedProperty(ReadOnlySpan<char> name, BatchInfo info, ReadOnlySpan<char> propValue)
{
foreach (var mod in BatchMods.SuggestionMods)
{
if (mod.IsMatch(name, propValue, info))
return mod.Modify(name, propValue, info);
}
return ModifyResult.Error;
}
/// <summary>
/// Sets the <see cref="PKM"/> byte array property to a specified value.
/// </summary>
/// <param name="pk">Pokémon to modify.</param>
/// <param name="cmd">Modification</param>
private static ModifyResult SetByteArrayProperty(PKM pk, StringInstruction cmd)
{
Span<byte> dest;
switch (cmd.PropertyName)
{
case nameof(PKM.NicknameTrash) or nameof(PKM.Nickname): dest = pk.NicknameTrash; break;
case nameof(PKM.OriginalTrainerTrash): dest = pk.OriginalTrainerTrash; break;
case nameof(PKM.HandlingTrainerTrash): dest = pk.HandlingTrainerTrash; break;
default:
return ModifyResult.Error;
}
var src = cmd.PropertyValue.AsSpan(CONST_BYTES.Length); // skip prefix
StringUtil.LoadHexBytesTo(src, dest, 3);
return ModifyResult.Modified;
}
/// <summary>
/// Sets the <see cref="PKM"/> property to a non-specific smart value.
/// </summary>
/// <param name="info">Pokémon to modify.</param>
/// <param name="cmd">Modification</param>
/// <returns>True if modified, false if no modifications done.</returns>
private bool SetComplexProperty(BatchInfo info, StringInstruction cmd)
{
ReadOnlySpan<char> name = cmd.PropertyName;
ReadOnlySpan<char> value = cmd.PropertyValue;
if (name.StartsWith("IV") && value is CONST_RAND)
{
SetRandomIVs(info, name);
return true;
}
foreach (var mod in BatchMods.ComplexMods)
{
if (!mod.IsMatch(name, value))
continue;
mod.Modify(info.Entity, cmd);
return true;
}
return false;
}
/// <summary>
/// Sets the <see cref="PKM"/> IV(s) to a random value.
/// </summary>
/// <param name="info">Pokémon to modify.</param>
/// <param name="propertyName">Property to modify</param>
private void SetRandomIVs(BatchInfo info, ReadOnlySpan<char> propertyName)
{
var pk = info.Entity;
if (propertyName is nameof(PKM.IVs))
{
var la = info.Legality;
var enc = la.EncounterMatch;
if (enc is IFlawlessIVCount { FlawlessIVCount: not 0 } fc)
pk.SetRandomIVs(fc.FlawlessIVCount);
else if (enc is IFixedIVSet { IVs: {IsSpecified: true} iv})
pk.SetRandomIVs(iv);
else if (enc is IFlawlessIVCountConditional c && c.GetFlawlessIVCount(pk) is { Max: not 0 } x)
pk.SetRandomIVs(Util.Rand.Next(x.Min, x.Max + 1));
else
pk.SetRandomIVs();
return;
}
if (TryGetHasProperty(pk, propertyName, out var pi))
{
const string IV32 = nameof(PK9.IV32);
if (propertyName is IV32)
{
var value = (uint)Util.Rand.Next(0x3FFFFFFF + 1);
if (pk is BK4 bk) // Big Endian, reverse IV ordering
{
value <<= 2; // flags are the lowest bits, and our random value is still fine.
value |= bk.IV32 & 3; // preserve the flags
bk.IV32 = value;
return;
}
var exist = ReflectUtil.GetValue(pk, IV32);
value |= exist switch
{
uint iv => iv & (3u << 30), // preserve the flags
_ => 0,
};
ReflectUtil.SetValue(pi, pk, value);
}
else
{
var value = Util.Rand.Next(pk.MaxIV + 1);
ReflectUtil.SetValue(pi, pk, value);
}
}
}
}

View File

@ -8,12 +8,14 @@ namespace PKHeX.Core;
/// <summary>
/// Carries out a batch edit and contains information summarizing the results.
/// </summary>
public sealed class BatchEditor
public sealed class EntityBatchProcessor
{
private int Modified { get; set; }
private int Iterated { get; set; }
private int Failed { get; set; }
private static EntityBatchEditor Editor => EntityBatchEditor.Instance;
/// <summary>
/// Tries to modify the <see cref="PKM"/> using instructions and a custom modifier delegate.
/// </summary>
@ -34,7 +36,7 @@ public bool Process(PKM pk, IEnumerable<StringInstruction> filters, IEnumerable<
return false;
}
var result = BatchEditing.TryModifyPKM(pk, filters, modifications, modifier);
var result = Editor.TryModify(pk, filters, modifications, modifier);
if (result != ModifyResult.Skipped)
Iterated++;
if (result.HasFlag(ModifyResult.Error))
@ -75,9 +77,9 @@ public string GetEditorResults(IReadOnlyCollection<StringInstructionSet> sets)
/// <param name="data">Entities to modify</param>
/// <param name="modifier">Custom modifier delegate.</param>
/// <returns>Editor object if follow-up modifications are desired.</returns>
public static BatchEditor Execute(ReadOnlySpan<string> lines, IEnumerable<PKM> data, Func<PKM, bool>? modifier = null)
public static EntityBatchProcessor Execute(ReadOnlySpan<string> lines, IEnumerable<PKM> data, Func<PKM, bool>? modifier = null)
{
var editor = new BatchEditor();
var editor = new EntityBatchProcessor();
var sets = StringInstructionSet.GetBatchSets(lines);
foreach (var pk in data)
{

View File

@ -176,7 +176,7 @@ public static ModifyResult SetEVs(PKM pk)
/// <param name="option">Option to apply with</param>
public static ModifyResult SetContestStats(PKM pk, LegalityAnalysis la, ReadOnlySpan<char> option)
{
if (option.Length != 0 && option[BatchEditing.CONST_SUGGEST.Length..] is not "0")
if (option.Length != 0 && option[EntityBatchEditor.CONST_SUGGEST.Length..] is not "0")
pk.SetMaxContestStats(la.EncounterMatch, la.Info.EvoChainsAllGens);
else
pk.SetSuggestedContestStats(la.EncounterMatch, la.Info.EvoChainsAllGens);

View File

@ -3,38 +3,16 @@
namespace PKHeX.Core;
/// <summary>
/// Interface for retrieving properties from a <see cref="PKM"/>.
/// Interface for retrieving properties from a <see cref="T"/>.
/// </summary>
public interface IPropertyProvider
public interface IPropertyProvider<in T> where T : notnull
{
/// <summary>
/// Attempts to retrieve a property's value (as string) from a <see cref="PKM"/> instance.
/// Attempts to retrieve a property's value (as string) from an entity instance.
/// </summary>
/// <param name="pk">Entity to retrieve the property from.</param>
/// <param name="obj">Entity to retrieve the property from.</param>
/// <param name="prop">Property name to retrieve.</param>
/// <param name="result">Property value as string.</param>
/// <returns><see langword="true"/> if the property was found and retrieved successfully; otherwise, <see langword="false"/>.</returns>
bool TryGetProperty(PKM pk, string prop, [NotNullWhen(true)] out string? result);
}
public sealed class DefaultPropertyProvider : IPropertyProvider
{
public static readonly DefaultPropertyProvider Instance = new();
public bool TryGetProperty(PKM pk, string prop, [NotNullWhen(true)] out string? result)
{
result = null;
if (!BatchEditing.TryGetHasProperty(pk, prop, out var pi))
return false;
try
{
var value = pi.GetValue(pk);
result = value?.ToString();
return result is not null;
}
catch
{
return false;
}
}
bool TryGetProperty(T obj, string prop, [NotNullWhen(true)] out string? result);
}

View File

@ -1018,16 +1018,16 @@ static IEnumerable<Type> GetImplementingTypes(Type t)
foreach (var property in shared)
{
// Setter sanity check: a derived type may not implement a setter if its parent type has one.
if (!BatchEditing.TryGetHasProperty(result, property, out var pi))
if (!EntityBatchEditor.Instance.TryGetHasProperty(result, property, out var pi))
continue;
if (!pi.CanWrite)
continue;
// Fetch the current value.
if (!BatchEditing.TryGetHasProperty(this, property, out var src))
if (!EntityBatchEditor.Instance.TryGetHasProperty(this, property, out var src))
continue;
var prop = src.GetValue(this);
if (prop is byte[] or null)
if (prop is byte[] or Memory<byte> or null)
continue; // not a valid property transfer
if (pi.PropertyType != src.PropertyType)
continue; // property type mismatch (not really a 1:1 shared property)

View File

@ -126,7 +126,7 @@ private IEnumerable<SlotCache> SearchInner(IEnumerable<SlotCache> list, Func<PKM
foreach (var entry in list)
{
var pk = entry.Entity;
if (BatchFiltersMeta.Count != 0 && !BatchEditing.IsFilterMatchMeta(BatchFiltersMeta, entry))
if (BatchFiltersMeta.Count != 0 && !EntityBatchEditor.IsFilterMatchMeta(BatchFiltersMeta, entry))
continue;
if (!predicate(pk))
continue;

View File

@ -90,7 +90,7 @@ public static bool SatisfiesFilterMoves(PKM pk, ReadOnlySpan<ushort> requiredMov
public static bool SatisfiesFilterBatchInstruction(PKM pk, IReadOnlyList<StringInstruction> filters)
{
return BatchEditing.IsFilterMatch(filters, pk); // Compare across all filters
return EntityBatchEditor.Instance.IsFilterMatch(filters, pk); // Compare across all filters
}
public static Func<PKM, string> GetCloneDetectMethod(CloneDetectionMethod method) => method switch

View File

@ -19,7 +19,7 @@ public static class ReflectUtil
/// <param name="obj">Object to fetch property from</param>
/// <param name="value">Value to compare to</param>
/// <returns>Comparison result</returns>
public static int CompareTo(this PropertyInfo pi, object obj, object value)
public static int CompareTo<T>(this PropertyInfo pi, T obj, object value)
{
var v = pi.GetValue(obj, null);
var c = ConvertValue(value, pi.PropertyType);
@ -30,20 +30,20 @@ public static int CompareTo(this PropertyInfo pi, object obj, object value)
return 0;
}
public static void SetValue(PropertyInfo pi, object obj, object value)
public static void SetValue<T>(PropertyInfo pi, T obj, object value)
{
var c = ConvertValue(value, pi.PropertyType);
pi.SetValue(obj, c, null);
}
public static object? GetValue(object obj, string name)
public static object? GetValue<T>(T obj, string name) where T : notnull
{
if (obj.GetType().GetTypeInfo().TryGetPropertyInfo(name, out var pi))
return pi.GetValue(obj, null);
return null;
}
public static bool SetValue(object obj, string name, object value)
public static bool SetValue<T>(T obj, string name, object value) where T : notnull
{
if (!obj.GetType().GetTypeInfo().TryGetPropertyInfo(name, out var pi))
return false;
@ -78,13 +78,14 @@ public static IEnumerable<PropertyInfo> GetAllPropertyInfoCanWritePublic(Type ty
public static IEnumerable<PropertyInfo> GetAllPropertyInfoPublic(Type type)
{
return type.GetTypeInfo().GetAllTypeInfo().SelectMany(GetAllProperties)
.Where(p => p.CanReadPublic() || p.CanWritePublic());
.Where(CanReadOrWritePublic);
}
extension(PropertyInfo p)
{
private bool CanReadPublic() => p.CanRead && (p.GetMethod?.IsPublic ?? false);
private bool CanWritePublic() => p.CanWrite && (p.SetMethod?.IsPublic ?? false);
private bool CanReadOrWritePublic() => p.CanReadPublic() || p.CanWritePublic();
}
public static IEnumerable<string> GetPropertiesPublic(Type type)
@ -150,7 +151,7 @@ public static IEnumerable<TypeInfo> GetAllTypeInfo(this TypeInfo? typeInfo)
/// <param name="name">Name of the property.</param>
/// <param name="pi">Reference to the property info for the object, if it exists.</param>
/// <returns>True if it has property, and false if it does not have property. <see cref="pi"/> is null when returning false.</returns>
public static bool HasProperty(object obj, string name, [NotNullWhen(true)] out PropertyInfo? pi)
public static bool HasProperty<T>(T obj, string name, [NotNullWhen(true)] out PropertyInfo? pi) where T : notnull
{
var type = obj.GetType();
return type.GetTypeInfo().TryGetPropertyInfo(name, out pi);
@ -184,14 +185,14 @@ private IEnumerable<T> GetAll<T>(Func<TypeInfo, IEnumerable<T>> accessor)
extension(Type type)
{
public Dictionary<T, string> GetAllConstantsOfType<T>() where T : struct
public Dictionary<T, string> GetAllConstantsOfType<T>() where T : unmanaged
{
var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.FlattenHierarchy);
var consts = fields.Where(fi => fi is { IsLiteral: true, IsInitOnly: false } && fi.FieldType == typeof(T));
return consts.ToDictionary(z => (T)(z.GetRawConstantValue() ?? throw new NullReferenceException(nameof(z.Name))), z => z.Name);
}
public Dictionary<string, T> GetAllPropertiesOfType<T>(object obj) where T : class
public Dictionary<string, T> GetAllPropertiesOfType<T>(object obj)
{
var props = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
var result = new Dictionary<string, T>(props.Length);

View File

@ -46,7 +46,7 @@ public EntityInstructionBuilder(Func<PKM> pk)
CB_Format.Items.Clear();
CB_Format.Items.Add(MsgAny);
foreach (Type t in BatchEditing.Types)
foreach (Type t in EntityBatchEditor.Instance.Types)
CB_Format.Items.Add(t.Name.ToLowerInvariant());
CB_Format.Items.Add(MsgAll);
@ -65,18 +65,18 @@ private void CB_Format_SelectedIndexChanged(object sender, EventArgs e)
byte format = (byte)CB_Format.SelectedIndex;
CB_Property.Items.Clear();
CB_Property.Items.AddRange(BatchEditing.Properties[format]);
CB_Property.Items.AddRange(EntityBatchEditor.Instance.Properties[format]);
CB_Property.SelectedIndex = 0;
currentFormat = format;
}
private void CB_Property_SelectedIndexChanged(object sender, EventArgs e)
{
if (!BatchEditing.TryGetPropertyType(CB_Property.Text, out var type, CB_Format.SelectedIndex))
if (!EntityBatchEditor.Instance.TryGetPropertyType(CB_Property.Text, out var type, CB_Format.SelectedIndex))
type = "Unknown";
L_PropType.Text = type;
if (BatchEditing.TryGetHasProperty(Entity, CB_Property.Text, out var pi))
if (EntityBatchEditor.Instance.TryGetHasProperty(Entity, CB_Property.Text, out var pi))
{
L_PropType.ResetForeColor();

View File

@ -14,7 +14,7 @@ public partial class BatchEditor : Form
private readonly SaveFile SAV;
// Mass Editing
private Core.BatchEditor editor = new();
private EntityBatchProcessor editor = new();
private readonly EntityInstructionBuilder UC_Builder;
private static string LastUsedCommands = string.Empty;
@ -135,15 +135,15 @@ private void RunBackgroundWorker()
foreach (var set in sets)
{
BatchEditing.ScreenStrings(set.Filters);
BatchEditing.ScreenStrings(set.Instructions);
EntityBatchEditor.ScreenStrings(set.Filters);
EntityBatchEditor.ScreenStrings(set.Instructions);
}
RunBatchEdit(sets, TB_Folder.Text, destPath);
}
private void RunBatchEdit(StringInstructionSet[] sets, string source, string? destination)
{
editor = new Core.BatchEditor();
editor = new EntityBatchProcessor();
bool finished = false, displayed = false; // hack cuz DoWork event isn't cleared after completion
b.DoWork += (_, _) =>
{
@ -241,7 +241,7 @@ private void ProcessSAV(IList<SlotCache> data, IReadOnlyList<StringInstruction>
if (entry.Source is SlotInfoBox info && SAV.GetBoxSlotFlags(info.Box, info.Slot).IsOverwriteProtected())
editor.AddSkipped();
else if (!BatchEditing.IsFilterMatchMeta(filterMeta, entry))
else if (!EntityBatchEditor.IsFilterMatchMeta(filterMeta, entry))
editor.AddSkipped();
else
editor.Process(pk, Filters, Instructions);
@ -276,7 +276,7 @@ private void TryProcess(string source, string destDir, IReadOnlyList<StringInstr
var info = new SlotInfoFileSingle(source);
var entry = new SlotCache(info, pk);
if (!BatchEditing.IsFilterMatchMeta(metaFilters, entry))
if (!EntityBatchEditor.IsFilterMatchMeta(metaFilters, entry))
{
editor.AddSkipped();
return;

View File

@ -15,7 +15,9 @@ namespace PKHeX.WinForms;
public partial class ReportGrid : Form
{
public IPropertyProvider PropertyProvider { get; init; } = DefaultPropertyProvider.Instance;
public IPropertyProvider<PKM> PropertyProvider { get; init; } =
new BatchPropertyProvider<EntityBatchEditor, PKM>(EntityBatchEditor.Instance);
private sealed class PokemonList<T> : SortableBindingList<T> where T : class;
public ReportGrid()

View File

@ -351,8 +351,8 @@ private IEnumerable<IEncounterInfo> SearchDatabase(CancellationToken token)
if (batchText.Length != 0 && !StringInstructionSet.HasEmptyLine(batchText))
{
var filters = StringInstruction.GetFilters(batchText);
BatchEditing.ScreenStrings(filters);
results = results.Where(enc => BatchEditing.IsFilterMatch(filters, enc)); // Compare across all filters
EntityBatchEditor.ScreenStrings(filters);
results = results.Where(enc => BatchEditingUtil.IsFilterMatch(filters, enc)); // Compare across all filters
}
return results;

View File

@ -359,8 +359,8 @@ private void B_Search_Click(object sender, EventArgs e)
if (batchText.Length != 0 && !StringInstructionSet.HasEmptyLine(batchText))
{
var filters = StringInstruction.GetFilters(batchText);
BatchEditing.ScreenStrings(filters);
res = res.Where(pk => BatchEditing.IsFilterMatch(filters, pk)); // Compare across all filters
EntityBatchEditor.ScreenStrings(filters);
res = res.Where(pk => BatchEditingUtil.IsFilterMatch(filters, pk)); // Compare across all filters
}
var results = res.ToArray();

View File

@ -53,7 +53,7 @@ public void ApplyNumericOperation(string instruction, InstructionOperation opera
entry.Operation.Should().Be(operation);
var pk = CreateTestPK7(initialValue);
var modified = BatchEditing.TryModify(pk, [], [entry]);
var modified = EntityBatchEditor.Instance.TryModifyIsSuccess(pk, [], [entry]);
modified.Should().BeTrue();
pk.EXP.Should().Be(expectedValue);
@ -74,7 +74,7 @@ private static PK7 CreateTestPK7(uint exp)
public void ProcessDelegateReturnsTrueWhenModified()
{
var pk = CreateTestPK7(100);
var editor = new BatchEditor();
var editor = new EntityBatchProcessor();
bool modified = editor.Process(pk, [], [], static p =>
{
@ -89,7 +89,7 @@ public void ProcessDelegateReturnsTrueWhenModified()
public void ProcessDelegateUpdatesExpWhenModified()
{
var pk = CreateTestPK7(100);
var editor = new BatchEditor();
var editor = new EntityBatchProcessor();
_ = editor.Process(pk, [], [], static p =>
{
@ -104,7 +104,7 @@ public void ProcessDelegateUpdatesExpWhenModified()
public void ProcessInstructionsAndDelegateUpdatesExp()
{
var pk = CreateTestPK7(100);
var editor = new BatchEditor();
var editor = new EntityBatchProcessor();
_ = editor.Process(pk, [], [], static p =>
{
@ -119,7 +119,7 @@ public void ProcessInstructionsAndDelegateUpdatesExp()
public void ProcessInstructionsAndDelegateSkipsWhenDelegateReturnsFalse()
{
var pk = CreateTestPK7(100);
var editor = new BatchEditor();
var editor = new EntityBatchProcessor();
StringInstruction.TryParseInstruction(".EXP=200", out var instruction).Should().BeTrue();
instruction.Should().NotBeNull();
@ -132,7 +132,7 @@ public void ProcessInstructionsAndDelegateSkipsWhenDelegateReturnsFalse()
public void ProcessInstructionsAndDelegatePreservesExpWhenDelegateReturnsFalse()
{
var pk = CreateTestPK7(100);
var editor = new BatchEditor();
var editor = new EntityBatchProcessor();
StringInstruction.TryParseInstruction(".EXP=200", out var instruction).Should().BeTrue();
instruction.Should().NotBeNull();