using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using static PKHeX.Core.MessageStrings; using static PKHeX.Core.BatchModifications; namespace PKHeX.Core; /// /// Logic for editing many with user provided list. /// 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), }; /// /// Extra properties to show in the list of selectable properties (GUI) /// public static readonly List CustomProperties = new() { PROP_LEGAL, PROP_TYPENAME, PROP_RIBBONS, PROP_CONTESTSTATS, PROP_MOVEMASTERY, IdentifierContains, nameof(ISlotInfo.Slot), nameof(SlotInfoBox.Box), }; /// /// Property names, indexed by . /// public static string[][] Properties => GetProperties.Value; private static readonly Lazy GetProperties = new(() => GetPropArray(Types, CustomProperties)); private static readonly Dictionary[] Props = GetPropertyDictionaries(Types); private static Dictionary[] GetPropertyDictionaries(IReadOnlyList types) { var result = new Dictionary[types.Count]; for (int i = 0; i < types.Count; i++) result[i] = GetPropertyDictionary(types[i], ReflectUtil.GetAllPropertyInfoPublic); return result; } private static Dictionary GetPropertyDictionary(Type type, Func> selector) { var dict = new Dictionary(); 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 char CONST_POINTER = '*'; internal const char CONST_SPECIAL = '$'; 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 types, IReadOnlyList 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(first); var all = new HashSet(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; } /// /// Tries to fetch the property from the cache of available properties. /// /// Pokémon to check /// Property Name to check /// Property Info retrieved (if any). /// True if has property, false if does not. public static bool TryGetHasProperty(PKM pk, string name, [NotNullWhen(true)] out PropertyInfo? pi) { var type = pk.GetType(); return TryGetHasProperty(type, name, out pi); } /// /// Tries to fetch the property from the cache of available properties. /// /// Type to check /// Property Name to check /// Property Info retrieved (if any). /// True if has property, false if does not. 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); } /// /// Gets a list of types that implement the requested . /// public static IEnumerable 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}"; } } /// /// Gets the type of the property using the saved cache of properties. /// /// Property Name to fetch the type for /// Type index (within . Leave empty (0) for a nonspecific format. /// Short name of the property's type. 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; } /// /// Initializes the list with a context-sensitive value. If the provided value is a string, it will attempt to convert that string to its corresponding index. /// /// Instructions to initialize. public static void ScreenStrings(IEnumerable il) { foreach (var i in il.Where(i => !i.PropertyValue.All(char.IsDigit))) { string pv = i.PropertyValue; if (pv.StartsWith(CONST_SPECIAL) && !pv.StartsWith(CONST_BYTES, StringComparison.Ordinal) && pv.Contains(',')) i.SetRandRange(pv); SetInstructionScreenedValue(i); } } /// /// Initializes the with a context-sensitive value. If the provided value is a string, it will attempt to convert that string to its corresponding index. /// /// Instruction to initialize. 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; } } /// /// Checks if the object is filtered by the provided . /// /// Filters which must be satisfied. /// Object to check. /// True if matches all filters. public static bool IsFilterMatch(IEnumerable filters, PKM pk) => filters.All(z => IsFilterMatch(z, pk, Props[Array.IndexOf(Types, pk.GetType())])); /// /// Checks if the object is filtered by the provided . /// /// Filters which must be satisfied. /// Object to check. /// True if matches all filters. public static bool IsFilterMatchMeta(IEnumerable 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; } /// /// Checks if the object is filtered by the provided . /// /// Filters which must be satisfied. /// Object to check. /// True if matches all filters. public static bool IsFilterMatch(IEnumerable 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; } /// /// Tries to modify the . /// /// Object to modify. /// Filters which must be satisfied prior to any modifications being made. /// Modifications to perform on the . /// Result of the attempted modification. public static bool TryModify(PKM pk, IEnumerable filters, IEnumerable modifications) { var result = TryModifyPKM(pk, filters, modifications); return result == ModifyResult.Modified; } /// /// Tries to modify the . /// /// Command Filter /// Filters which must be satisfied prior to any modifications being made. /// Modifications to perform on the . /// Result of the attempted modification. internal static ModifyResult TryModifyPKM(PKM pk, IEnumerable filters, IEnumerable 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; } /// /// Sets the if the should be filtered due to the provided. /// /// Command Filter /// Pokémon to check. /// PropertyInfo cache (optional) /// True if filtered, else false. private static ModifyResult SetPKMProperty(StringInstruction cmd, BatchInfo info, IReadOnlyDictionary props) { var pk = info.Entity; 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.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; } /// /// Checks if the should be filtered due to the provided. /// /// Command Filter /// Pokémon to check. /// PropertyInfo cache (optional) /// True if filter matches, else false. private static bool IsFilterMatch(StringInstruction cmd, BatchInfo info, IReadOnlyDictionary 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); } /// /// Checks if the should be filtered due to the provided. /// /// Command Filter /// Pokémon to check. /// PropertyInfo cache (optional) /// True if filter matches, else false. private static bool IsFilterMatch(StringInstruction cmd, PKM pk, IReadOnlyDictionary props) { var match = BatchFilters.FilterMods.Find(z => z.IsMatch(cmd.PropertyName)); if (match != null) return match.IsFiltered(pk, cmd); return IsPropertyFiltered(cmd, pk, props); } /// /// Checks if the should be filtered due to the provided. /// /// Command Filter /// Pokémon to check. /// PropertyInfo cache /// True if filtered, else false. private static bool IsPropertyFiltered(StringInstruction cmd, PKM pk, IReadOnlyDictionary props) { if (!props.TryGetValue(cmd.PropertyName, out var pi)) return false; if (!pi.CanRead) return false; return pi.IsValueEqual(pk, cmd.PropertyValue) == cmd.Evaluator; } /// /// Sets the data with a suggested value based on its . /// /// Property to modify. /// Cached info storing Legal data. /// Suggestion string which starts with 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; } /// /// Sets the byte array property to a specified value. /// /// Pokémon to modify. /// Modification 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)); } } /// /// Sets the property to a non-specific smart value. /// /// Pokémon to modify. /// Modification /// True if modified, false if no modifications done. private static bool SetComplexProperty(PKM pk, StringInstruction cmd) { if (cmd.PropertyName.StartsWith("IV", StringComparison.Ordinal) && 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; } /// /// Sets the IV(s) to a random value. /// /// Pokémon to modify. /// Modification 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)); } }