mirror of
https://github.com/kwsch/PKHeX
synced 2024-11-26 14:00:21 +00:00
Extract Bulk Analysis components
This commit is contained in:
parent
f77e605cae
commit
39a7007541
12 changed files with 506 additions and 389 deletions
93
PKHeX.Core/Legality/Bulk/BulkAnalysis.cs
Normal file
93
PKHeX.Core/Legality/Bulk/BulkAnalysis.cs
Normal file
|
@ -0,0 +1,93 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace PKHeX.Core.Bulk;
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes content within a <see cref="SaveFile"/> for overall <see cref="PKM"/> legality analysis.
|
||||
/// </summary>
|
||||
public sealed class BulkAnalysis
|
||||
{
|
||||
public readonly IReadOnlyList<SlotCache> AllData;
|
||||
public readonly IReadOnlyList<LegalityAnalysis> AllAnalysis;
|
||||
public readonly ITrainerInfo Trainer;
|
||||
public readonly List<CheckResult> Parse = new();
|
||||
public readonly Dictionary<ulong, SlotCache> 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<SlotCache>(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<IBulkAnalyzer> 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<LegalityAnalysis> GetIndividualAnalysis(IReadOnlyList<SlotCache> 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);
|
||||
}
|
3
PKHeX.Core/Legality/Bulk/CombinedReference.cs
Normal file
3
PKHeX.Core/Legality/Bulk/CombinedReference.cs
Normal file
|
@ -0,0 +1,3 @@
|
|||
namespace PKHeX.Core.Bulk;
|
||||
|
||||
public sealed record CombinedReference(SlotCache Slot, LegalityAnalysis Analysis);
|
75
PKHeX.Core/Legality/Bulk/DuplicateEncryptionChecker.cs
Normal file
75
PKHeX.Core/Legality/Bulk/DuplicateEncryptionChecker.cs
Normal file
|
@ -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<uint, CombinedReference>();
|
||||
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<uint, CombinedReference> 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);
|
||||
}
|
||||
}
|
||||
}
|
41
PKHeX.Core/Legality/Bulk/DuplicateGiftChecker.cs
Normal file
41
PKHeX.Core/Legality/Bulk/DuplicateGiftChecker.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
76
PKHeX.Core/Legality/Bulk/DuplicatePIDChecker.cs
Normal file
76
PKHeX.Core/Legality/Bulk/DuplicatePIDChecker.cs
Normal file
|
@ -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<uint, CombinedReference>();
|
||||
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<uint, CombinedReference> 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);
|
||||
}
|
||||
}
|
||||
}
|
105
PKHeX.Core/Legality/Bulk/DuplicateTrainerChecker.cs
Normal file
105
PKHeX.Core/Legality/Bulk/DuplicateTrainerChecker.cs
Normal file
|
@ -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<uint, CombinedReference>();
|
||||
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<uint, CombinedReference> 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;
|
||||
}
|
||||
}
|
43
PKHeX.Core/Legality/Bulk/HandlerChecker.cs
Normal file
43
PKHeX.Core/Legality/Bulk/HandlerChecker.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
6
PKHeX.Core/Legality/Bulk/IBulkAnalyzer.cs
Normal file
6
PKHeX.Core/Legality/Bulk/IBulkAnalyzer.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace PKHeX.Core.Bulk;
|
||||
|
||||
public interface IBulkAnalyzer
|
||||
{
|
||||
void Analyze(BulkAnalysis input);
|
||||
}
|
62
PKHeX.Core/Legality/Bulk/StandardCloneChecker.cs
Normal file
62
PKHeX.Core/Legality/Bulk/StandardCloneChecker.cs
Normal file
|
@ -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<string, SlotCache>();
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes content within a <see cref="SaveFile"/> for overall <see cref="PKM"/> legality analysis.
|
||||
/// </summary>
|
||||
public sealed class BulkAnalysis
|
||||
{
|
||||
public readonly IReadOnlyList<SlotCache> AllData;
|
||||
public readonly IReadOnlyList<LegalityAnalysis> AllAnalysis;
|
||||
public readonly ITrainerInfo Trainer;
|
||||
public readonly List<CheckResult> Parse = new();
|
||||
public readonly Dictionary<ulong, SlotCache> 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<SlotCache>(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<string, SlotCache>();
|
||||
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<uint, CombinedReference>();
|
||||
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<uint, CombinedReference>();
|
||||
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<uint, CombinedReference>();
|
||||
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<LegalityAnalysis> GetIndividualAnalysis(IReadOnlyList<SlotCache> 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);
|
||||
}
|
|
@ -16,7 +16,7 @@ public interface IEntityRejuvenator
|
|||
/// <summary>
|
||||
/// Uses <see cref="LegalityAnalysis"/> to auto-fill missing data after conversion.
|
||||
/// </summary>
|
||||
public class LegalityRejuvenator : IEntityRejuvenator
|
||||
public sealed class LegalityRejuvenator : IEntityRejuvenator
|
||||
{
|
||||
public void Rejuvenate(PKM result, PKM original)
|
||||
{
|
||||
|
|
|
@ -745,7 +745,7 @@ public partial class SAVEditor : UserControl, ISlotViewer<PictureBox>, ISaveFile
|
|||
|
||||
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!");
|
||||
|
|
Loading…
Reference in a new issue