mirror of
https://github.com/kwsch/PKHeX.git
synced 2026-04-25 08:10:48 -05:00
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:
parent
0b42f57534
commit
2efa19e5e3
535
PKHeX.Core/Editing/Bulk/Base/BatchEditingBase.cs
Normal file
535
PKHeX.Core/Editing/Bulk/Base/BatchEditingBase.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
53
PKHeX.Core/Editing/Bulk/Base/BatchEditingUtil.cs
Normal file
53
PKHeX.Core/Editing/Bulk/Base/BatchEditingUtil.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
57
PKHeX.Core/Editing/Bulk/Base/IBatchEditor.cs
Normal file
57
PKHeX.Core/Editing/Bulk/Base/IBatchEditor.cs
Normal 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);
|
||||
}
|
||||
71
PKHeX.Core/Editing/Bulk/Base/InstructionComparer.cs
Normal file
71
PKHeX.Core/Editing/Bulk/Base/InstructionComparer.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
37
PKHeX.Core/Editing/Bulk/Base/InstructionOperation.cs
Normal file
37
PKHeX.Core/Editing/Bulk/Base/InstructionOperation.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
PKHeX.Core/Editing/Bulk/BatchPropertyProvider.cs
Normal file
23
PKHeX.Core/Editing/Bulk/BatchPropertyProvider.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 =
|
||||
[
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
||||
304
PKHeX.Core/Editing/Bulk/Entity/EntityBatchEditor.cs
Normal file
304
PKHeX.Core/Editing/Bulk/Entity/EntityBatchEditor.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user