diff --git a/PKHeX.Core/Legality/Bulk/BulkAnalysis.cs b/PKHeX.Core/Legality/Bulk/BulkAnalysis.cs new file mode 100644 index 000000000..0bb7e3563 --- /dev/null +++ b/PKHeX.Core/Legality/Bulk/BulkAnalysis.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace PKHeX.Core.Bulk; + +/// +/// Analyzes content within a for overall legality analysis. +/// +public sealed class BulkAnalysis +{ + public readonly IReadOnlyList AllData; + public readonly IReadOnlyList AllAnalysis; + public readonly ITrainerInfo Trainer; + public readonly List Parse = new(); + public readonly Dictionary Trackers = new(); + public readonly bool Valid; + + public readonly IBulkAnalysisSettings Settings; + private readonly bool[] CloneFlags; + + public bool GetIsClone(int entryIndex) => CloneFlags[entryIndex]; + + public bool SetIsClone(int entryIndex, bool value = true) => CloneFlags[entryIndex] = value; + + public BulkAnalysis(SaveFile sav, IBulkAnalysisSettings settings) + { + Trainer = sav; + Settings = settings; + var list = new List(sav.BoxSlotCount + (sav.HasParty ? 6 : 0) + 5); + SlotInfoLoader.AddFromSaveFile(sav, list); + list.RemoveAll(IsEmptyData); + AllData = list; + AllAnalysis = GetIndividualAnalysis(list); + CloneFlags = new bool[AllData.Count]; + + ScanAll(); + Valid = Parse.Count == 0 || Parse.All(z => z.Valid); + } + + // Remove things that aren't actual stored data, or already flagged by legality checks. + private static bool IsEmptyData(SlotCache obj) + { + var pk = obj.Entity; + if ((uint)(pk.Species - 1) >= pk.MaxSpeciesID) + return true; + if (!pk.ChecksumValid) + return true; + return false; + } + + public static readonly List Analyzers = new() + { + new StandardCloneChecker(), + new DuplicateTrainerChecker(), + new DuplicatePIDChecker(), + new DuplicateEncryptionChecker(), + new HandlerChecker(), + new DuplicateGiftChecker(), + }; + + private void ScanAll() + { + foreach (var analyzer in Analyzers) + analyzer.Analyze(this); + } + + private static string GetSummary(SlotCache entry) => $"[{entry.Identify()}] {entry.Entity.FileName}"; + + public void AddLine(SlotCache first, SlotCache second, string msg, CheckIdentifier i, Severity s = Severity.Invalid) + { + var c = $"{msg}{Environment.NewLine}{GetSummary(first)}{Environment.NewLine}{GetSummary(second)}"; + var chk = new CheckResult(s, c, i); + Parse.Add(chk); + } + + public void AddLine(SlotCache first, string msg, CheckIdentifier i, Severity s = Severity.Invalid) + { + var c = $"{msg}{Environment.NewLine}{GetSummary(first)}"; + var chk = new CheckResult(s, c, i); + Parse.Add(chk); + } + + private static IReadOnlyList GetIndividualAnalysis(IReadOnlyList pkms) + { + var results = new LegalityAnalysis[pkms.Count]; + for (int i = 0; i < pkms.Count; i++) + results[i] = Get(pkms[i]); + return results; + } + + private static LegalityAnalysis Get(SlotCache cache) => new(cache.Entity, cache.SAV.Personal, cache.Source.Origin); +} diff --git a/PKHeX.Core/Legality/Bulk/CombinedReference.cs b/PKHeX.Core/Legality/Bulk/CombinedReference.cs new file mode 100644 index 000000000..fb6daa30b --- /dev/null +++ b/PKHeX.Core/Legality/Bulk/CombinedReference.cs @@ -0,0 +1,3 @@ +namespace PKHeX.Core.Bulk; + +public sealed record CombinedReference(SlotCache Slot, LegalityAnalysis Analysis); diff --git a/PKHeX.Core/Legality/Bulk/DuplicateEncryptionChecker.cs b/PKHeX.Core/Legality/Bulk/DuplicateEncryptionChecker.cs new file mode 100644 index 000000000..2583520e6 --- /dev/null +++ b/PKHeX.Core/Legality/Bulk/DuplicateEncryptionChecker.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using System.Diagnostics; +using static PKHeX.Core.CheckIdentifier; + +namespace PKHeX.Core.Bulk; + +public sealed class DuplicateEncryptionChecker : IBulkAnalyzer +{ + public void Analyze(BulkAnalysis input) + { + if (input.Trainer.Generation < 6) + return; // no EC yet + CheckECReuse(input); + } + + private static void CheckECReuse(BulkAnalysis input) + { + var dict = new Dictionary(); + for (int i = 0; i < input.AllData.Count; i++) + { + if (input.GetIsClone(i)) + continue; // already flagged + var cp = input.AllData[i]; + var ca = input.AllAnalysis[i]; + Verify(input, dict, cp, ca); + } + } + + private static void Verify(BulkAnalysis input, IDictionary dict, SlotCache cp, LegalityAnalysis ca) + { + Debug.Assert(cp.Entity.Format >= 6); + var id = cp.Entity.EncryptionConstant; + + var cr = new CombinedReference(cp, ca); + if (!dict.TryGetValue(id, out var pa)) + { + dict.Add(id, cr); + return; + } + + VerifyECShare(input, pa, cr); + } + + private static void VerifyECShare(BulkAnalysis input, CombinedReference pr, CombinedReference cr) + { + var (ps, pa) = pr; + var (cs, ca) = cr; + + const CheckIdentifier ident = PID; + int gen = pa.Info.Generation; + bool gbaNDS = gen is 3 or 4 or 5; + + if (!gbaNDS) + { + if (ca.Info.Generation != gen) + { + input.AddLine(ps, cs, "EC sharing across generations detected.", ident); + return; + } + input.AddLine(ps, cs, "EC sharing for 3DS-onward origin detected.", ident); + return; + } + + // eggs/mystery gifts shouldn't share with wild encounters + var cenc = ca.Info.EncounterMatch; + bool eggMysteryCurrent = cenc is EncounterEgg or MysteryGift; + var penc = pa.Info.EncounterMatch; + bool eggMysteryPrevious = penc is EncounterEgg or MysteryGift; + + if (eggMysteryCurrent != eggMysteryPrevious) + { + input.AddLine(ps, cs, "EC sharing across RNG encounters detected.", ident); + } + } +} diff --git a/PKHeX.Core/Legality/Bulk/DuplicateGiftChecker.cs b/PKHeX.Core/Legality/Bulk/DuplicateGiftChecker.cs new file mode 100644 index 000000000..b8a212ea6 --- /dev/null +++ b/PKHeX.Core/Legality/Bulk/DuplicateGiftChecker.cs @@ -0,0 +1,41 @@ +using System.Linq; +using static PKHeX.Core.CheckIdentifier; + +namespace PKHeX.Core.Bulk; + +public sealed class DuplicateGiftChecker : IBulkAnalyzer +{ + public void Analyze(BulkAnalysis input) + { + if (input.Trainer.Generation <= 2) + return; + CheckDuplicateOwnedGifts(input); + } + + private static void CheckDuplicateOwnedGifts(BulkAnalysis input) + { + var all = input.AllData; + var combined = new CombinedReference[all.Count]; + for (int i = 0; i < combined.Length; i++) + combined[i] = new CombinedReference(all[i], input.AllAnalysis[i]); + + var dupes = combined.Where(z => + z.Analysis.Info.Generation >= 3 + && z.Analysis.EncounterMatch is MysteryGift { EggEncounter: true } && !z.Slot.Entity.WasTradedEgg) + .GroupBy(z => ((MysteryGift)z.Analysis.EncounterMatch).CardTitle); + + foreach (var dupe in dupes) + { + var tidGroup = dupe.GroupBy(z => z.Slot.Entity.ID32) + .Select(z => z.ToList()) + .Where(z => z.Count >= 2).ToList(); + if (tidGroup.Count == 0) + continue; + + var grp = tidGroup[0]; + var first = grp[0].Slot; + var second = grp[1].Slot; + input.AddLine(first, second, $"Receipt of the same egg mystery gifts detected: {dupe.Key}", Encounter); + } + } +} diff --git a/PKHeX.Core/Legality/Bulk/DuplicatePIDChecker.cs b/PKHeX.Core/Legality/Bulk/DuplicatePIDChecker.cs new file mode 100644 index 000000000..e6389acc2 --- /dev/null +++ b/PKHeX.Core/Legality/Bulk/DuplicatePIDChecker.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using static PKHeX.Core.CheckIdentifier; + +namespace PKHeX.Core.Bulk; + +public sealed class DuplicatePIDChecker : IBulkAnalyzer +{ + public void Analyze(BulkAnalysis input) + { + if (input.Trainer.Generation < 3) + return; // no PID yet + CheckPIDReuse(input); + } + + private static void CheckPIDReuse(BulkAnalysis input) + { + var dict = new Dictionary(); + for (int i = 0; i < input.AllData.Count; i++) + { + if (input.GetIsClone(i)) + continue; // already flagged + var cp = input.AllData[i]; + var ca = input.AllAnalysis[i]; + Verify(input, dict, ca, cp); + } + } + + private static void Verify(BulkAnalysis input, IDictionary dict, LegalityAnalysis ca, SlotCache cp) + { + bool g345 = ca.Info.Generation is 3 or 4 or 5; + var id = g345 ? cp.Entity.EncryptionConstant : cp.Entity.PID; + + var cr = new CombinedReference(cp, ca); + if (!dict.TryGetValue(id, out var pr)) + { + dict.Add(id, cr); + return; + } + + VerifyPIDShare(input, pr, cr); + } + + private static void VerifyPIDShare(BulkAnalysis input, CombinedReference pr, CombinedReference cr) + { + var ps = pr.Slot; + var pa = pr.Analysis; + var cs = cr.Slot; + var ca = cr.Analysis; + const CheckIdentifier ident = PID; + int gen = pa.Info.Generation; + + if (ca.Info.Generation != gen) + { + input.AddLine(ps, cs, "PID sharing across generations detected.", ident); + return; + } + + bool gbaNDS = gen is 3 or 4 or 5; + if (!gbaNDS) + { + input.AddLine(ps, cs, "PID sharing for 3DS-onward origin detected.", ident); + return; + } + + // eggs/mystery gifts shouldn't share with wild encounters + var cenc = ca.Info.EncounterMatch; + bool eggMysteryCurrent = cenc is EncounterEgg or MysteryGift; + var penc = pa.Info.EncounterMatch; + bool eggMysteryPrevious = penc is EncounterEgg or MysteryGift; + + if (eggMysteryCurrent != eggMysteryPrevious) + { + input.AddLine(ps, cs, "PID sharing across RNG encounters detected.", ident); + } + } +} diff --git a/PKHeX.Core/Legality/Bulk/DuplicateTrainerChecker.cs b/PKHeX.Core/Legality/Bulk/DuplicateTrainerChecker.cs new file mode 100644 index 000000000..2964c858a --- /dev/null +++ b/PKHeX.Core/Legality/Bulk/DuplicateTrainerChecker.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using static PKHeX.Core.CheckIdentifier; + +namespace PKHeX.Core.Bulk; + +public sealed class DuplicateTrainerChecker : IBulkAnalyzer +{ + public void Analyze(BulkAnalysis input) => CheckIDReuse(input); + + private static void CheckIDReuse(BulkAnalysis input) + { + var dict = new Dictionary(); + for (int i = 0; i < input.AllData.Count; i++) + { + if (input.GetIsClone(i)) + continue; // already flagged + var cs = input.AllData[i]; + var ca = input.AllAnalysis[i]; + Verify(input, dict, cs, ca); + } + } + + private static void Verify(BulkAnalysis input, IDictionary dict, SlotCache cs, LegalityAnalysis ca) + { + var id = cs.Entity.ID32; + + if (!dict.TryGetValue(id, out var pr)) + { + var r = new CombinedReference(cs, ca); + dict.Add(id, r); + return; + } + + var pa = pr.Analysis; + // ignore GB era collisions + // a 16bit TID16 can reasonably occur for multiple trainers, and versions + if (ca.Info.Generation <= 2 && pa.Info.Generation <= 2) + return; + + var ps = pr.Slot; + if (VerifyIDReuse(input, ps, pa, cs, ca)) + return; + + // egg encounters can be traded before hatching + // store the current loop pk if it's a better reference + if (ps.Entity.WasTradedEgg && !cs.Entity.WasTradedEgg) + dict[id] = new CombinedReference(cs, ca); + } + + private static bool VerifyIDReuse(BulkAnalysis input, SlotCache ps, LegalityAnalysis pa, SlotCache cs, LegalityAnalysis ca) + { + if (pa.EncounterMatch is MysteryGift { EggEncounter: false }) + return false; + if (ca.EncounterMatch is MysteryGift { EggEncounter: false }) + return false; + + const CheckIdentifier ident = Trainer; + + var pp = ps.Entity; + var cp = cs.Entity; + + // 32bit ID-SID16 should only occur for one generation + // Trainer-ID-SID16 should only occur for one version + if (IsSharedVersion(pp, pa, cp, ca)) + { + input.AddLine(ps, cs, "TID sharing across versions detected.", ident); + return true; + } + + // ID-SID16 should only occur for one Trainer name + if (pp.OT_Name != cp.OT_Name) + { + var severity = ca.Info.Generation == 4 ? Severity.Fishy : Severity.Invalid; + input.AddLine(ps, cs, "TID sharing across different trainer names detected.", ident, severity); + } + + return false; + } + + private static bool IsSharedVersion(PKM pp, LegalityAnalysis pa, PKM cp, LegalityAnalysis ca) + { + if (pp.Version == cp.Version || pp.Version == 0 || cp.Version == 0) + return false; + + // Traded eggs retain the original version ID, only on the same generation + if (pa.Info.Generation != ca.Info.Generation) + return false; + + // Gen3/4 traded eggs do not have an Egg Location, and do not update the Version upon hatch. + // These eggs can obtain another trainer's TID16/SID16/OT and be valid with a different version ID. + if (pa.EncounterMatch.EggEncounter && IsTradedEggVersionNoUpdate(pp, pa)) + return false; // version doesn't update on trade + if (ca.EncounterMatch.EggEncounter && IsTradedEggVersionNoUpdate(cp, ca)) + return false; // version doesn't update on trade + + static bool IsTradedEggVersionNoUpdate(PKM pk, LegalityAnalysis la) => la.Info.Generation switch + { + 3 => true, // No egg location, assume can be traded. Doesn't update version upon hatch. + 4 => pk.WasTradedEgg, // Gen4 traded eggs do not update version upon hatch. + _ => false, // Gen5+ eggs have an egg location, and update the version upon hatch. + }; + + return true; + } +} diff --git a/PKHeX.Core/Legality/Bulk/HandlerChecker.cs b/PKHeX.Core/Legality/Bulk/HandlerChecker.cs new file mode 100644 index 000000000..210043eea --- /dev/null +++ b/PKHeX.Core/Legality/Bulk/HandlerChecker.cs @@ -0,0 +1,43 @@ +using static PKHeX.Core.CheckIdentifier; + +namespace PKHeX.Core.Bulk; + +public sealed class HandlerChecker : IBulkAnalyzer +{ + public void Analyze(BulkAnalysis input) + { + if (input.Trainer.Generation < 6 || !input.Settings.CheckActiveHandler) + return; // no HT yet + CheckHandlerFlag(input); + } + + private static void CheckHandlerFlag(BulkAnalysis input) + { + for (var i = 0; i < input.AllData.Count; i++) + { + if (!input.AllAnalysis[i].Valid) + continue; + var cs = input.AllData[i]; + Verify(input, cs); + } + } + + private static void Verify(BulkAnalysis input, SlotCache cs) + { + var pk = cs.Entity; + var tr = cs.SAV; + var withOT = tr.IsFromTrainer(pk); + var flag = pk.CurrentHandler; + var expect = withOT ? 0 : 1; + if (flag != expect) + input.AddLine(cs, LegalityCheckStrings.LTransferCurrentHandlerInvalid, Trainer); + + if (flag != 1) + return; + + if (pk.HT_Name != tr.OT) + input.AddLine(cs, LegalityCheckStrings.LTransferHTMismatchName, Trainer); + if (pk is IHandlerLanguage h && h.HT_Language != tr.Language) + input.AddLine(cs, LegalityCheckStrings.LTransferHTMismatchLanguage, Trainer); + } +} diff --git a/PKHeX.Core/Legality/Bulk/IBulkAnalyzer.cs b/PKHeX.Core/Legality/Bulk/IBulkAnalyzer.cs new file mode 100644 index 000000000..24665085f --- /dev/null +++ b/PKHeX.Core/Legality/Bulk/IBulkAnalyzer.cs @@ -0,0 +1,6 @@ +namespace PKHeX.Core.Bulk; + +public interface IBulkAnalyzer +{ + void Analyze(BulkAnalysis input); +} diff --git a/PKHeX.Core/Legality/Bulk/StandardCloneChecker.cs b/PKHeX.Core/Legality/Bulk/StandardCloneChecker.cs new file mode 100644 index 000000000..65f0f852a --- /dev/null +++ b/PKHeX.Core/Legality/Bulk/StandardCloneChecker.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Diagnostics; +using PKHeX.Core.Searching; +using static PKHeX.Core.CheckIdentifier; + +namespace PKHeX.Core.Bulk; + +public sealed class StandardCloneChecker : IBulkAnalyzer +{ + public void Analyze(BulkAnalysis input) => CheckClones(input); + + private static void CheckClones(BulkAnalysis input) + { + var dict = new Dictionary(); + for (int i = 0; i < input.AllData.Count; i++) + { + var cs = input.AllData[i]; + var ca = input.AllAnalysis[i]; + Debug.Assert(cs.Entity.Format == input.Trainer.Generation); + + // Check the upload tracker to see if there's any duplication. + if (cs.Entity is IHomeTrack home) + CheckClonedTrackerHOME(input, home, cs, ca); + + // Hash Details like EC/IV to see if there's any duplication. + var identity = SearchUtil.HashByDetails(cs.Entity); + if (!dict.TryGetValue(identity, out var ps)) + { + dict.Add(identity, cs); + continue; + } + + input.SetIsClone(i, true); + input.AddLine(ps, cs, "Clone detected (Details).", Encounter); + } + } + + private const ulong DefaultUnsetTrackerHOME = 0ul; + + private static void CheckClonedTrackerHOME(BulkAnalysis input, IHomeTrack home, SlotCache cs, LegalityAnalysis ca) + { + var tracker = home.Tracker; + if (tracker == DefaultUnsetTrackerHOME) + CheckTrackerMissing(input, cs, ca); + else + CheckTrackerPresent(input, cs, tracker); + } + + private static void CheckTrackerMissing(BulkAnalysis input, SlotCache cs, LegalityAnalysis ca) + { + if (ca.Info.Generation is (< 8 and not -1)) + input.AddLine(cs, "Missing tracker.", Encounter); + } + + private static void CheckTrackerPresent(BulkAnalysis input, SlotCache cs, ulong tracker) + { + if (input.Trackers.TryGetValue(tracker, out var clone)) + input.AddLine(cs, clone, "Clone detected (Duplicate Tracker).", Encounter); + else + input.Trackers.Add(tracker, cs); + } +} diff --git a/PKHeX.Core/Legality/BulkAnalysis.cs b/PKHeX.Core/Legality/BulkAnalysis.cs deleted file mode 100644 index 70adaad8b..000000000 --- a/PKHeX.Core/Legality/BulkAnalysis.cs +++ /dev/null @@ -1,387 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; - -using PKHeX.Core.Searching; -using static PKHeX.Core.CheckIdentifier; - -namespace PKHeX.Core; - -/// -/// Analyzes content within a for overall legality analysis. -/// -public sealed class BulkAnalysis -{ - public readonly IReadOnlyList AllData; - public readonly IReadOnlyList AllAnalysis; - public readonly ITrainerInfo Trainer; - public readonly List Parse = new(); - public readonly Dictionary Trackers = new(); - public readonly bool Valid; - - private readonly IBulkAnalysisSettings Settings; - private readonly bool[] CloneFlags; - - public BulkAnalysis(SaveFile sav, IBulkAnalysisSettings settings) - { - Trainer = sav; - Settings = settings; - var list = new List(sav.BoxSlotCount + (sav.HasParty ? 6 : 0) + 5); - SlotInfoLoader.AddFromSaveFile(sav, list); - list.RemoveAll(IsEmptyData); - AllData = list; - AllAnalysis = GetIndividualAnalysis(list); - CloneFlags = new bool[AllData.Count]; - - ScanAll(); - Valid = Parse.Count == 0 || Parse.All(z => z.Valid); - } - - // Remove things that aren't actual stored data, or already flagged by legality checks. - private static bool IsEmptyData(SlotCache obj) - { - var pk = obj.Entity; - if ((uint)(pk.Species - 1) >= pk.MaxSpeciesID) - return true; - if (!pk.ChecksumValid) - return true; - return false; - } - - private void ScanAll() - { - CheckClones(); - if (Trainer.Generation <= 2) - return; - - CheckIDReuse(); - CheckPIDReuse(); - if (Trainer.Generation >= 6) - { - CheckECReuse(); - if (Settings.CheckActiveHandler) - CheckHandlerFlag(); - } - - CheckDuplicateOwnedGifts(); - } - - private static string GetSummary(SlotCache entry) => $"[{entry.Identify()}] {entry.Entity.FileName}"; - - private void AddLine(SlotCache first, SlotCache second, string msg, CheckIdentifier i, Severity s = Severity.Invalid) - { - var c = $"{msg}{Environment.NewLine}{GetSummary(first)}{Environment.NewLine}{GetSummary(second)}"; - var chk = new CheckResult(s, c, i); - Parse.Add(chk); - } - - private void AddLine(SlotCache first, string msg, CheckIdentifier i, Severity s = Severity.Invalid) - { - var c = $"{msg}{Environment.NewLine}{GetSummary(first)}"; - var chk = new CheckResult(s, c, i); - Parse.Add(chk); - } - - private void CheckClones() - { - var dict = new Dictionary(); - for (int i = 0; i < AllData.Count; i++) - { - var cs = AllData[i]; - var ca = AllAnalysis[i]; - Debug.Assert(cs.Entity.Format == Trainer.Generation); - - // Check the upload tracker to see if there's any duplication. - if (cs.Entity is IHomeTrack home) - { - if (home.Tracker != 0) - { - var tracker = home.Tracker; - if (Trackers.TryGetValue(tracker, out var clone)) - AddLine(cs, clone, "Clone detected (Duplicate Tracker).", Encounter); - else - Trackers.Add(tracker, cs); - } - else if (ca.Info.Generation is (< 8 and not -1)) - { - AddLine(cs, "Missing tracker.", Encounter); - } - } - - // Hash Details like EC/IV to see if there's any duplication. - var identity = SearchUtil.HashByDetails(cs.Entity); - if (!dict.TryGetValue(identity, out var ps)) - { - dict.Add(identity, cs); - continue; - } - - CloneFlags[i] = true; - AddLine(ps, cs, "Clone detected (Details).", Encounter); - } - } - - private void CheckDuplicateOwnedGifts() - { - var combined = new CombinedReference[AllData.Count]; - for (int i = 0; i < combined.Length; i++) - combined[i] = new CombinedReference(AllData[i], AllAnalysis[i]); - - var dupes = combined.Where(z => - z.Analysis.Info.Generation >= 3 - && z.Analysis.EncounterMatch is MysteryGift {EggEncounter: true} && !z.Slot.Entity.WasTradedEgg) - .GroupBy(z => ((MysteryGift)z.Analysis.EncounterMatch).CardTitle); - - foreach (var dupe in dupes) - { - var tidGroup = dupe.GroupBy(z => z.Slot.Entity.TID16 | (z.Slot.Entity.SID16 << 16)) - .Select(z => z.ToList()) - .Where(z => z.Count >= 2).ToList(); - if (tidGroup.Count == 0) - continue; - - var first = tidGroup[0][0].Slot; - var second = tidGroup[0][1].Slot; - AddLine(first, second, $"Receipt of the same egg mystery gifts detected: {dupe.Key}", Encounter); - } - } - - private void CheckECReuse() - { - var dict = new Dictionary(); - for (int i = 0; i < AllData.Count; i++) - { - if (CloneFlags[i]) - continue; // already flagged - var cp = AllData[i]; - var ca = AllAnalysis[i]; - Debug.Assert(cp.Entity.Format >= 6); - var id = cp.Entity.EncryptionConstant; - - var cr = new CombinedReference(cp, ca); - if (!dict.TryGetValue(id, out var pa)) - { - dict.Add(id, cr); - continue; - } - VerifyECShare(pa, cr); - } - } - - private void CheckPIDReuse() - { - var dict = new Dictionary(); - for (int i = 0; i < AllData.Count; i++) - { - if (CloneFlags[i]) - continue; // already flagged - var cp = AllData[i]; - var ca = AllAnalysis[i]; - bool g345 = ca.Info.Generation is 3 or 4 or 5; - var id = g345 ? cp.Entity.EncryptionConstant : cp.Entity.PID; - - var cr = new CombinedReference(cp, ca); - if (!dict.TryGetValue(id, out var pr)) - { - dict.Add(id, cr); - continue; - } - VerifyPIDShare(pr, cr); - } - } - - private void CheckHandlerFlag() - { - for (var i = 0; i < AllData.Count; i++) - { - if (!AllAnalysis[i].Valid) - continue; - var cs = AllData[i]; - var pk = cs.Entity; - var tr = cs.SAV; - var withOT = tr.IsFromTrainer(pk); - var flag = pk.CurrentHandler; - var expect = withOT ? 0 : 1; - if (flag != expect) - AddLine(cs, LegalityCheckStrings.LTransferCurrentHandlerInvalid, CheckIdentifier.Trainer); - - if (flag == 1) - { - if (pk.HT_Name != tr.OT) - AddLine(cs, LegalityCheckStrings.LTransferHTMismatchName, CheckIdentifier.Trainer); - if (pk is IHandlerLanguage h && h.HT_Language != tr.Language) - AddLine(cs, LegalityCheckStrings.LTransferHTMismatchLanguage, CheckIdentifier.Trainer); - } - } - } - - private sealed record CombinedReference(SlotCache Slot, LegalityAnalysis Analysis); - - private void CheckIDReuse() - { - var dict = new Dictionary(); - for (int i = 0; i < AllData.Count; i++) - { - if (CloneFlags[i]) - continue; // already flagged - var cs = AllData[i]; - var ca = AllAnalysis[i]; - var id = cs.Entity.ID32; - - if (!dict.TryGetValue(id, out var pr)) - { - var r = new CombinedReference(cs, ca); - dict.Add(id, r); - continue; - } - - var pa = pr.Analysis; - // ignore GB era collisions - // a 16bit TID16 can reasonably occur for multiple trainers, and versions - if (ca.Info.Generation <= 2 && pa.Info.Generation <= 2) - continue; - - var ps = pr.Slot; - if (VerifyIDReuse(ps, pa, cs, ca)) - continue; - - // egg encounters can be traded before hatching - // store the current loop pk if it's a better reference - if (ps.Entity.WasTradedEgg && !cs.Entity.WasTradedEgg) - dict[id] = new CombinedReference(cs, ca); - } - } - - private void VerifyECShare(CombinedReference pr, CombinedReference cr) - { - var (ps, pa) = pr; - var (cs, ca) = cr; - - const CheckIdentifier ident = PID; - int gen = pa.Info.Generation; - bool gbaNDS = gen is 3 or 4 or 5; - - if (!gbaNDS) - { - if (ca.Info.Generation != gen) - { - AddLine(ps, cs, "EC sharing across generations detected.", ident); - return; - } - AddLine(ps, cs, "EC sharing for 3DS-onward origin detected.", ident); - return; - } - - // eggs/mystery gifts shouldn't share with wild encounters - var cenc = ca.Info.EncounterMatch; - bool eggMysteryCurrent = cenc is EncounterEgg or MysteryGift; - var penc = pa.Info.EncounterMatch; - bool eggMysteryPrevious = penc is EncounterEgg or MysteryGift; - - if (eggMysteryCurrent != eggMysteryPrevious) - { - AddLine(ps, cs, "EC sharing across RNG encounters detected.", ident); - } - } - - private void VerifyPIDShare(CombinedReference pr, CombinedReference cr) - { - var ps = pr.Slot; - var pa = pr.Analysis; - var cs = cr.Slot; - var ca = cr.Analysis; - const CheckIdentifier ident = PID; - int gen = pa.Info.Generation; - - if (ca.Info.Generation != gen) - { - AddLine(ps, cs, "PID sharing across generations detected.", ident); - return; - } - - bool gbaNDS = gen is 3 or 4 or 5; - if (!gbaNDS) - { - AddLine(ps, cs, "PID sharing for 3DS-onward origin detected.", ident); - return; - } - - // eggs/mystery gifts shouldn't share with wild encounters - var cenc = ca.Info.EncounterMatch; - bool eggMysteryCurrent = cenc is EncounterEgg or MysteryGift; - var penc = pa.Info.EncounterMatch; - bool eggMysteryPrevious = penc is EncounterEgg or MysteryGift; - - if (eggMysteryCurrent != eggMysteryPrevious) - { - AddLine(ps, cs, "PID sharing across RNG encounters detected.", ident); - } - } - - private bool VerifyIDReuse(SlotCache ps, LegalityAnalysis pa, SlotCache cs, LegalityAnalysis ca) - { - if (pa.EncounterMatch is MysteryGift {EggEncounter: false}) - return false; - if (ca.EncounterMatch is MysteryGift {EggEncounter: false}) - return false; - - const CheckIdentifier ident = CheckIdentifier.Trainer; - - var pp = ps.Entity; - var cp = cs.Entity; - - // 32bit ID-SID16 should only occur for one generation - // Trainer-ID-SID16 should only occur for one version - if (IsSharedVersion(pp, pa, cp, ca)) - { - AddLine(ps, cs, "TID sharing across versions detected.", ident); - return true; - } - - // ID-SID16 should only occur for one Trainer name - if (pp.OT_Name != cp.OT_Name) - { - var severity = ca.Info.Generation == 4 ? Severity.Fishy : Severity.Invalid; - AddLine(ps, cs, "TID sharing across different trainer names detected.", ident, severity); - } - - return false; - } - - private static bool IsSharedVersion(PKM pp, LegalityAnalysis pa, PKM cp, LegalityAnalysis ca) - { - if (pp.Version == cp.Version || pp.Version == 0 || cp.Version == 0) - return false; - - // Traded eggs retain the original version ID, only on the same generation - if (pa.Info.Generation != ca.Info.Generation) - return false; - - // Gen3/4 traded eggs do not have an Egg Location, and do not update the Version upon hatch. - // These eggs can obtain another trainer's TID16/SID16/OT and be valid with a different version ID. - if (pa.EncounterMatch.EggEncounter && IsTradedEggVersionNoUpdate(pp, pa)) - return false; // version doesn't update on trade - if (ca.EncounterMatch.EggEncounter && IsTradedEggVersionNoUpdate(cp, ca)) - return false; // version doesn't update on trade - - static bool IsTradedEggVersionNoUpdate(PKM pk, LegalityAnalysis la) => la.Info.Generation switch - { - 3 => true, // No egg location, assume can be traded. Doesn't update version upon hatch. - 4 => pk.WasTradedEgg, // Gen4 traded eggs do not update version upon hatch. - _ => false, // Gen5+ eggs have an egg location, and update the version upon hatch. - }; - - return true; - } - - private static IReadOnlyList GetIndividualAnalysis(IReadOnlyList pkms) - { - var results = new LegalityAnalysis[pkms.Count]; - for (int i = 0; i < pkms.Count; i++) - results[i] = Get(pkms[i]); - return results; - } - - private static LegalityAnalysis Get(SlotCache cache) => new(cache.Entity, cache.SAV.Personal, cache.Source.Origin); -} diff --git a/PKHeX.Core/PKM/Util/Conversion/IEntityRejuvenator.cs b/PKHeX.Core/PKM/Util/Conversion/IEntityRejuvenator.cs index 654c11c45..0b284ab35 100644 --- a/PKHeX.Core/PKM/Util/Conversion/IEntityRejuvenator.cs +++ b/PKHeX.Core/PKM/Util/Conversion/IEntityRejuvenator.cs @@ -16,7 +16,7 @@ public interface IEntityRejuvenator /// /// Uses to auto-fill missing data after conversion. /// -public class LegalityRejuvenator : IEntityRejuvenator +public sealed class LegalityRejuvenator : IEntityRejuvenator { public void Rejuvenate(PKM result, PKM original) { diff --git a/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs b/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs index d5f13e912..c855b3b61 100644 --- a/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs +++ b/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs @@ -745,7 +745,7 @@ private void ClickVerifyCHK(object sender, EventArgs e) private void ClickVerifyStoredEntities(object sender, EventArgs e) { - var bulk = new BulkAnalysis(SAV, Main.Settings.Bulk); + var bulk = new Core.Bulk.BulkAnalysis(SAV, Main.Settings.Bulk); if (bulk.Valid) { WinFormsUtil.Alert("Clean!");