mirror of
https://github.com/kwsch/PKHeX
synced 2024-11-23 12:33:06 +00:00
33537465ca
Lazy init the all-any-byType sorted string list, static ctor unneeded for some nuget users
504 lines
22 KiB
C#
504 lines
22 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
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 (PK8), typeof (PA8), typeof (PB8),
|
|
typeof (PB7),
|
|
typeof (PK7), typeof (PK6), typeof (PK5), typeof (PK4), typeof(BK4),
|
|
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>
|
|
public static readonly List<string> CustomProperties = new()
|
|
{
|
|
PROP_LEGAL, PROP_TYPENAME, PROP_RIBBONS, PROP_CONTESTSTATS, PROP_MOVEMASTERY,
|
|
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 Lazy<string[][]> GetProperties = new(() => GetPropArray(Types, CustomProperties));
|
|
|
|
private static readonly Dictionary<string, PropertyInfo>[] Props = GetPropertyDictionaries(Types);
|
|
|
|
private static Dictionary<string, PropertyInfo>[] GetPropertyDictionaries(IReadOnlyList<Type> types)
|
|
{
|
|
var result = new Dictionary<string, PropertyInfo>[types.Count];
|
|
for (int i = 0; i < types.Count; i++)
|
|
result[i] = GetPropertyDictionary(types[i], ReflectUtil.GetAllPropertyInfoPublic);
|
|
return result;
|
|
}
|
|
|
|
private static Dictionary<string, PropertyInfo> GetPropertyDictionary(Type type, Func<Type, IEnumerable<PropertyInfo>> selector)
|
|
{
|
|
var dict = new Dictionary<string, PropertyInfo>();
|
|
var props = selector(type);
|
|
foreach (var p in props)
|
|
{
|
|
if (!dict.ContainsKey(p.Name))
|
|
dict.Add(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 string CONST_POINTER = "*";
|
|
|
|
internal const string PROP_LEGAL = "Legal";
|
|
internal const string PROP_TYPENAME = "ObjectType";
|
|
internal const string PROP_RIBBONS = "Ribbons";
|
|
internal const string PROP_CONTESTSTATS = "ContestStats";
|
|
internal const string PROP_MOVEMASTERY = "MoveMastery";
|
|
internal const string IdentifierContains = nameof(IdentifierContains);
|
|
|
|
private static string[][] GetPropArray(IReadOnlyList<Type> types, IReadOnlyList<string> extra)
|
|
{
|
|
var result = new string[types.Count + 2][];
|
|
var p = result.AsSpan(1, types.Count);
|
|
|
|
for (int i = 0; i < p.Length; i++)
|
|
{
|
|
var props = ReflectUtil.GetPropertiesPublic(types[i]);
|
|
p[i] = props.Concat(extra).OrderBy(a => a).ToArray();
|
|
}
|
|
|
|
// 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);
|
|
for (int i = 1; i < p.Length; i++)
|
|
{
|
|
any.UnionWith(p[i]);
|
|
all.IntersectWith(p[i]);
|
|
}
|
|
|
|
result[0] = any.OrderBy(z => z).ToArray();
|
|
result[^1] = all.OrderBy(z => z).ToArray();
|
|
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 has property, false if does not.</returns>
|
|
public static bool TryGetHasProperty(PKM pk, string 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 has property, false if does not.</returns>
|
|
public static bool TryGetHasProperty(Type type, string name, [NotNullWhen(true)] out PropertyInfo? pi)
|
|
{
|
|
var index = Array.IndexOf(Types, 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="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 string? GetPropertyType(string propertyName, int typeIndex = 0)
|
|
{
|
|
if (CustomProperties.Contains(propertyName))
|
|
return "Custom";
|
|
|
|
if (typeIndex == 0) // Any
|
|
{
|
|
foreach (var p in Props)
|
|
{
|
|
if (p.TryGetValue(propertyName, out var pi))
|
|
return pi.PropertyType.Name;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
int index = typeIndex - 1 >= Props.Length ? 0 : typeIndex - 1; // All vs Specific
|
|
var pr = Props[index];
|
|
if (!pr.TryGetValue(propertyName, out var info))
|
|
return null;
|
|
return info.PropertyType.Name;
|
|
}
|
|
|
|
/// <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.Where(i => !i.PropertyValue.All(char.IsDigit)))
|
|
{
|
|
string pv = i.PropertyValue;
|
|
if (pv.StartsWith("$") && !pv.StartsWith(CONST_BYTES) && pv.Contains(','))
|
|
i.SetRandRange(pv);
|
|
|
|
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)
|
|
{
|
|
switch (i.PropertyName)
|
|
{
|
|
case nameof(PKM.Species): i.SetScreenedValue(GameInfo.Strings.specieslist); return;
|
|
case nameof(PKM.HeldItem): i.SetScreenedValue(GameInfo.Strings.itemlist); return;
|
|
case nameof(PKM.Ability): i.SetScreenedValue(GameInfo.Strings.abilitylist); return;
|
|
case nameof(PKM.Nature): i.SetScreenedValue(GameInfo.Strings.natures); return;
|
|
case nameof(PKM.Ball): i.SetScreenedValue(GameInfo.Strings.balllist); return;
|
|
|
|
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):
|
|
i.SetScreenedValue(GameInfo.Strings.movelist); return;
|
|
}
|
|
}
|
|
|
|
/// <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) => filters.All(z => IsFilterMatch(z, pk, Props[Array.IndexOf(Types, pk.GetType())]));
|
|
|
|
/// <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)
|
|
{
|
|
if ((obj.GetType().Name == cmd.PropertyValue) != cmd.Evaluator)
|
|
return false;
|
|
continue;
|
|
}
|
|
|
|
if (!ReflectUtil.HasProperty(obj, cmd.PropertyName, out var pi))
|
|
return false;
|
|
try
|
|
{
|
|
if (pi.IsValueEqual(obj, cmd.PropertyValue) == cmd.Evaluator)
|
|
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>
|
|
/// <returns>Result of the attempted modification.</returns>
|
|
public static bool TryModify(PKM pk, IEnumerable<StringInstruction> filters, IEnumerable<StringInstruction> modifications)
|
|
{
|
|
var result = TryModifyPKM(pk, filters, modifications);
|
|
return result == ModifyResult.Modified;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to modify the <see cref="BatchInfo"/>.
|
|
/// </summary>
|
|
/// <param name="pk">Command Filter</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>
|
|
/// <returns>Result of the attempted modification.</returns>
|
|
internal static ModifyResult TryModifyPKM(PKM pk, IEnumerable<StringInstruction> filters, IEnumerable<StringInstruction> modifications)
|
|
{
|
|
if (!pk.ChecksumValid || pk.Species == 0)
|
|
return ModifyResult.Invalid;
|
|
|
|
var info = new BatchInfo(pk);
|
|
var pi = Props[Array.IndexOf(Types, pk.GetType())];
|
|
foreach (var cmd in filters)
|
|
{
|
|
try
|
|
{
|
|
if (!IsFilterMatch(cmd, info, pi))
|
|
return ModifyResult.Filtered;
|
|
}
|
|
// Swallow any error because this can be malformed user input.
|
|
catch (Exception ex)
|
|
{
|
|
Debug.WriteLine(MsgBEModifyFailCompare + " " + ex.Message, cmd.PropertyName, cmd.PropertyValue);
|
|
return ModifyResult.Error;
|
|
}
|
|
}
|
|
|
|
ModifyResult result = ModifyResult.Modified;
|
|
foreach (var cmd in modifications)
|
|
{
|
|
try
|
|
{
|
|
var tmp = SetPKMProperty(cmd, info, pi);
|
|
if (tmp != ModifyResult.Modified)
|
|
result = tmp;
|
|
}
|
|
// Swallow any error because this can be malformed user input.
|
|
catch (Exception ex)
|
|
{
|
|
Debug.WriteLine(MsgBEModifyFail + " " + ex.Message, cmd.PropertyName, cmd.PropertyValue);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the 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, IReadOnlyDictionary<string, PropertyInfo> props)
|
|
{
|
|
var pk = info.Entity;
|
|
if (cmd.PropertyValue.StartsWith(CONST_BYTES))
|
|
return SetByteArrayProperty(pk, cmd);
|
|
|
|
if (cmd.PropertyValue.StartsWith(CONST_SUGGEST, true, CultureInfo.CurrentCulture))
|
|
return SetSuggestedPKMProperty(cmd.PropertyName, info, cmd.PropertyValue);
|
|
if (cmd.PropertyValue == CONST_RAND && cmd.PropertyName == nameof(PKM.Moves))
|
|
return SetMoves(pk, info.Legality.GetMoveSet(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;
|
|
|
|
object val;
|
|
if (cmd.Random)
|
|
val = cmd.RandomValue;
|
|
else if (cmd.PropertyValue.StartsWith(CONST_POINTER) && props.TryGetValue(cmd.PropertyValue[1..], out var opi))
|
|
val = opi.GetValue(pk);
|
|
else
|
|
val = cmd.PropertyValue;
|
|
|
|
ReflectUtil.SetValue(pi, pk, val);
|
|
return ModifyResult.Modified;
|
|
}
|
|
|
|
/// <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, IReadOnlyDictionary<string, PropertyInfo> props)
|
|
{
|
|
var match = BatchFilters.FilterMods.Find(z => z.IsMatch(cmd.PropertyName));
|
|
if (match != 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, IReadOnlyDictionary<string, PropertyInfo> props)
|
|
{
|
|
var match = BatchFilters.FilterMods.Find(z => z.IsMatch(cmd.PropertyName));
|
|
if (match != 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, IReadOnlyDictionary<string, PropertyInfo> props)
|
|
{
|
|
if (!props.TryGetValue(cmd.PropertyName, out var pi))
|
|
return false;
|
|
if (!pi.CanRead)
|
|
return false;
|
|
return pi.IsValueEqual(pk, cmd.PropertyValue) == cmd.Evaluator;
|
|
}
|
|
|
|
/// <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(string name, BatchInfo info, string propValue)
|
|
{
|
|
var first = BatchMods.SuggestionMods.Find(z => z.IsMatch(name, propValue, info));
|
|
if (first != null)
|
|
return first.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)
|
|
{
|
|
switch (cmd.PropertyName)
|
|
{
|
|
case nameof(PKM.Nickname_Trash): ConvertToBytes(cmd.PropertyValue).CopyTo(pk.Nickname_Trash); return ModifyResult.Modified;
|
|
case nameof(PKM.OT_Trash): ConvertToBytes(cmd.PropertyValue).CopyTo(pk.OT_Trash); return ModifyResult.Modified;
|
|
case nameof(PKM.HT_Trash): ConvertToBytes(cmd.PropertyValue).CopyTo(pk.HT_Trash); return ModifyResult.Modified;
|
|
default:
|
|
return ModifyResult.Error;
|
|
}
|
|
static byte[] ConvertToBytes(string str)
|
|
{
|
|
var arr = str[CONST_BYTES.Length..].Split(',');
|
|
return Array.ConvertAll(arr, z => Convert.ToByte(z.Trim(), 16));
|
|
}
|
|
}
|
|
|
|
/// <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)
|
|
{
|
|
if (cmd.PropertyName.StartsWith("IV") && cmd.PropertyValue == CONST_RAND)
|
|
{
|
|
SetRandomIVs(pk, cmd);
|
|
return true;
|
|
}
|
|
|
|
var match = BatchMods.ComplexMods.Find(z => z.IsMatch(cmd.PropertyName, cmd.PropertyValue));
|
|
if (match == null)
|
|
return false;
|
|
|
|
match.Modify(pk, cmd);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the <see cref="PKM"/> IV(s) to a random value.
|
|
/// </summary>
|
|
/// <param name="pk">Pokémon to modify.</param>
|
|
/// <param name="cmd">Modification</param>
|
|
private static void SetRandomIVs(PKM pk, StringInstruction cmd)
|
|
{
|
|
if (cmd.PropertyName == nameof(PKM.IVs))
|
|
{
|
|
pk.SetRandomIVs();
|
|
return;
|
|
}
|
|
|
|
if (TryGetHasProperty(pk, cmd.PropertyName, out var pi))
|
|
ReflectUtil.SetValue(pi, pk, Util.Rand.Next(pk.MaxIV + 1));
|
|
}
|
|
}
|
|
}
|