using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Reflection; using static PKHeX.Core.MessageStrings; namespace PKHeX.Core { public static class BatchEditing { public static readonly Type[] Types = { typeof (PB7), typeof (PK7), typeof (PK6), typeof (PK5), typeof (PK4), typeof(BK4), typeof (PK3), typeof (XK3), typeof (CK3), typeof (PK2), typeof (PK1), }; public static readonly string[] CustomProperties = { PROP_LEGAL }; public static readonly string[][] Properties = GetPropArray(); private static readonly Dictionary[] Props = Types.Select(z => ReflectUtil.GetAllPropertyInfoPublic(z) .GroupBy(p => p.Name).Select(g => g.First()).ToDictionary(p => p.Name)) .ToArray(); private const string CONST_RAND = "$rand"; private const string CONST_SHINY = "$shiny"; private const string CONST_SUGGEST = "$suggest"; private const string CONST_BYTES = "$[]"; private const string PROP_LEGAL = "Legal"; private const string IdentifierContains = nameof(PKM.Identifier) + "Contains"; private static string[][] GetPropArray() { var p = new string[Types.Length][]; for (int i = 0; i < p.Length; i++) { var pz = ReflectUtil.GetPropertiesPublic(Types[i]); p[i] = pz.Concat(CustomProperties).OrderBy(a => a).ToArray(); } // Properties for any PKM var any = ReflectUtil.GetPropertiesPublic(typeof(PK1)).Union(p.SelectMany(a => a)).OrderBy(a => a).ToArray(); // Properties shared by all PKM var all = p.Aggregate(new HashSet(p[0]), (h, e) => { h.IntersectWith(e); return h; }).OrderBy(a => a).ToArray(); var p1 = new string[Types.Length + 2][]; Array.Copy(p, 0, p1, 1, p.Length); p1[0] = any; p1[p1.Length - 1] = all; return p1; } /// /// 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 pkm, string name, out PropertyInfo pi) { var props = Props[Array.IndexOf(Types, pkm.GetType())]; return props.TryGetValue(name, out pi); } /// /// 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("$") && !pv.StartsWith(CONST_BYTES) && 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): case nameof(PKM.Move2): case nameof(PKM.Move3): case nameof(PKM.Move4): case nameof(PKM.RelearnMove1): case nameof(PKM.RelearnMove2): case nameof(PKM.RelearnMove3): case 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 pkm) => filters.All(z => IsFilterMatch(z, pkm, Props[Array.IndexOf(Types, pkm.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 IsFilterMatch(IEnumerable filters, object obj) { foreach (var cmd in filters) { if (!ReflectUtil.HasProperty(obj, cmd.PropertyName, out var pi)) return false; try { if (pi.IsValueEqual(obj, cmd.PropertyValue) == cmd.Evaluator) continue; } catch { Debug.WriteLine($"Unable to compare {cmd.PropertyName} to {cmd.PropertyValue}."); } 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 pkm, IEnumerable filters, IEnumerable modifications) { var result = TryModifyPKM(pkm, 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 pkm, IEnumerable filters, IEnumerable modifications) { if (!pkm.ChecksumValid || pkm.Species == 0) return ModifyResult.Invalid; PKMInfo info = new PKMInfo(pkm); var pi = Props[Array.IndexOf(Types, pkm.GetType())]; foreach (var cmd in filters) { try { if (!IsFilterMatch(cmd, info, pi)) return ModifyResult.Filtered; } 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; } 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, PKMInfo info, IReadOnlyDictionary props) { var pkm = info.pkm; if (cmd.PropertyValue.StartsWith(CONST_BYTES)) return SetByteArrayProperty(pkm, cmd); if (cmd.PropertyValue == CONST_SUGGEST) return SetSuggestedPKMProperty(cmd.PropertyName, info); if (cmd.PropertyValue == CONST_RAND && cmd.PropertyName == nameof(PKM.Moves)) return SetMoves(pkm, pkm.GetMoveSet(true, info.Legality)); if (SetComplexProperty(pkm, cmd)) return ModifyResult.Modified; if (!props.TryGetValue(cmd.PropertyName, out var pi)) return ModifyResult.Error; if (!pi.CanWrite) return ModifyResult.Error; object val = cmd.Random ? (object)cmd.RandomValue : cmd.PropertyValue; ReflectUtil.SetValue(pi, pkm, 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, PKMInfo info, IReadOnlyDictionary props) { if (IsLegalFiltered(cmd, () => info.Legal)) return true; return IsPropertyFiltered(cmd, info.pkm, 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 pkm, IReadOnlyDictionary props) { if (IsLegalFiltered(cmd, () => new LegalityAnalysis(pkm).Valid)) return true; return IsPropertyFiltered(cmd, pkm, 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 pkm, IReadOnlyDictionary props) { if (IsIdentifierFiltered(cmd, pkm)) return true; if (!props.TryGetValue(cmd.PropertyName, out var pi)) return false; if (!pi.CanRead) return false; return pi.IsValueEqual(pkm, cmd.PropertyValue) == cmd.Evaluator; } /// /// Checks if the should be filtered due to its containing a value. /// /// Command Filter /// Pokémon to check. /// True if filtered, else false. private static bool IsIdentifierFiltered(StringInstruction cmd, PKM pkm) { if (cmd.PropertyName != IdentifierContains) return false; bool result = pkm.Identifier.Contains(cmd.PropertyValue); return result == cmd.Evaluator; } /// /// Checks if the should be filtered due to its legality. /// /// Command Filter /// Function to check if the is legal. /// True if filtered, else false. private static bool IsLegalFiltered(StringInstruction cmd, Func isLegal) { if (cmd.PropertyName != PROP_LEGAL) return false; if (!bool.TryParse(cmd.PropertyValue, out bool legal)) return true; return legal == isLegal() == cmd.Evaluator; } /// /// Sets the data with a suggested value based on its . /// /// Property to modify. /// Cached info storing Legal data. private static ModifyResult SetSuggestedPKMProperty(string name, PKMInfo info) { var PKM = info.pkm; switch (name) { case nameof(IHyperTrain.HyperTrainFlags): PKM.SetSuggestedHyperTrainingData(); return ModifyResult.Modified; case nameof(PKM.RelearnMoves): PKM.RelearnMoves = info.SuggestedRelearn; return ModifyResult.Modified; case nameof(PKM.Met_Location): var encounter = info.SuggestedEncounter; if (encounter == null) return ModifyResult.Error; int level = encounter.Level; int location = encounter.Location; int minlvl = Legal.GetLowestLevel(PKM, encounter.LevelMin); PKM.Met_Level = level; PKM.Met_Location = location; PKM.CurrentLevel = Math.Max(minlvl, level); return ModifyResult.Modified; case nameof(PKM.Moves): return SetMoves(PKM, PKM.GetMoveSet(la: info.Legality)); default: return ModifyResult.Error; } } /// /// Sets the provided moves in a random order. /// /// Pokémon to modify. /// Moves to apply. private static ModifyResult SetMoves(PKM pkm, int[] moves) { pkm.SetMoves(moves); return ModifyResult.Modified; } /// /// Sets the byte array propery to a specified value. /// /// Pokémon to modify. /// Modification private static ModifyResult SetByteArrayProperty(PKM pkm, StringInstruction cmd) { switch (cmd.PropertyName) { case nameof(pkm.Nickname_Trash): pkm.Nickname_Trash = string2arr(cmd.PropertyValue); return ModifyResult.Modified; case nameof(pkm.OT_Trash): pkm.OT_Trash = string2arr(cmd.PropertyValue); return ModifyResult.Modified; default: return ModifyResult.Error; } byte[] string2arr(string str) => str.Substring(CONST_BYTES.Length).Split(',').Select(z => Convert.ToByte(z.Trim(), 16)).ToArray(); } /// /// Sets the property to a non-specific smart value. /// /// Pokémon to modify. /// Modification private static bool SetComplexProperty(PKM pkm, StringInstruction cmd) { if (cmd.PropertyName == nameof(pkm.MetDate)) pkm.MetDate = DateTime.ParseExact(cmd.PropertyValue, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None); else if (cmd.PropertyName == nameof(pkm.EggMetDate)) pkm.EggMetDate = DateTime.ParseExact(cmd.PropertyValue, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None); else if (cmd.PropertyName == nameof(pkm.EncryptionConstant) && cmd.PropertyValue == CONST_RAND) pkm.EncryptionConstant = Util.Rand32(); else if ((cmd.PropertyName == nameof(pkm.Ability) || cmd.PropertyName == nameof(pkm.AbilityNumber)) && cmd.PropertyValue.StartsWith("$")) pkm.RefreshAbility(Convert.ToInt16(cmd.PropertyValue[1]) - 0x30); else if (cmd.PropertyName == nameof(pkm.PID) && cmd.PropertyValue == CONST_RAND) pkm.SetPIDGender(pkm.Gender); else if (cmd.PropertyName == nameof(pkm.EncryptionConstant) && cmd.PropertyValue == nameof(pkm.PID)) pkm.EncryptionConstant = pkm.PID; else if (cmd.PropertyName == nameof(pkm.PID) && cmd.PropertyValue == CONST_SHINY) pkm.SetShiny(); else if (cmd.PropertyName == nameof(pkm.Species) && cmd.PropertyValue == "0") pkm.Data = new byte[pkm.Data.Length]; else if (cmd.PropertyName.StartsWith("IV") && cmd.PropertyValue == CONST_RAND) SetRandomIVs(pkm, cmd); else if (cmd.PropertyName == nameof(pkm.IsNicknamed) && string.Equals(cmd.PropertyValue, "false", StringComparison.OrdinalIgnoreCase)) pkm.SetDefaultNickname(); else return false; return true; } /// /// Sets the IV(s) to a random value. /// /// Pokémon to modify. /// Modification private static void SetRandomIVs(PKM pkm, StringInstruction cmd) { if (cmd.PropertyName == nameof(pkm.IVs)) { pkm.SetRandomIVs(); return; } if (TryGetHasProperty(pkm, cmd.PropertyName, out var pi)) ReflectUtil.SetValue(pi, pkm, Util.Rand.Next(pkm.MaxIV + 1)); } } }