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!");