using System.Collections.Generic; namespace PKHeX.Core; /// /// Analyzes a to determine if the provided parameters are a valid end product. /// /// /// When generating a Trainer Team for , the game must generate PID values that match the programmed team specifications. /// The game is 'locked' into a PID generating loop until a valid PID is created, after which the routine is unlocked and proceeds to generate the next member. /// These locks cause the of the current to be rerolled until the requisite lock is satisfied. /// This locking is the same as Method H/J/K and Gen3 Cute Charm by extension, with the additional restriction of not allowing shinies. /// locks require a certain , which is derived from the . /// locks require a certain gender value, which is derived from the and ratio. /// locks require the member to not be shiny. This is enforced for non-shadow members for both games, and for shadow members in . /// public sealed class TeamLockResult { /// /// Seed prior to generating the team per . /// public readonly uint OriginSeed; /// /// Indicates if the input parameters can be generated by the provided. /// public readonly bool Valid; /// /// NPC Team data containing a list of members. /// internal readonly TeamLock Specifications; /// /// Frame the is from, based on frames reversed from the input seed provided. /// private int OriginFrame; /// /// Required CPU Trainer Shiny Value /// /// /// If this value is >= 0, the CPU Trainer Shiny Value must be equal to this value as a skipped over a matching interrupt frame. /// If this value is , the CPU Trainer Shiny Value can be anything (except matching any of the result members). /// private uint RCSV = NOT_FORCED; /// /// Player Trainer Shiny Value /// /// Only used by encounters, which disallow shiny shadow members for both player & CPU TSVs. private readonly uint TSV; private readonly Stack Locks; private readonly FrameCache Cache; // only save a list of valid nodes we've visited to save memory while we recursively search for a full team. private readonly Stack Team; private const uint NOT_FORCED = uint.MaxValue; internal TeamLockResult(TeamLock teamSpec, uint originSeed, uint tsv) { Locks = new Stack((Specifications = teamSpec).Locks); Team = new Stack(Locks.Count); Cache = new FrameCache(XDRNG.Prev2(originSeed)); TSV = tsv; Valid = FindLockSeed(); if (Valid) OriginSeed = Cache.GetSeed(OriginFrame); } /// /// Depth-first search traversal which finds a possible origin for the . /// /// Frame at which the search starts/continues at. /// Prior data. If this is the last lock in the CPU Team, this is null. /// True if the are valid. private bool FindLockSeed(int frame = 0, NPCLock? prior = null) { if (Locks.Count == 0) // full team reverse-generated return VerifyNPC(frame); var current = Locks.Pop(); var locks = GetPossibleLocks(frame, current, prior); foreach (var l in locks) { Team.Push(l); // possible match if (FindLockSeed(l.FrameID, current)) return true; // all locks are satisfied Team.Pop(); // no match, remove } Locks.Push(current); // return the lock, lock is impossible return false; } /// /// Generates a list of frames the lock data can be generated at. /// /// Starting frame for the traversal. /// Current lock criteria to satisfy. Used to find valid results to yield. /// Lock criteria for previous party member. Used for determining when the traversal stops. /// List of possible locks for the provided input. private IEnumerable GetPossibleLocks(int ctr, NPCLock current, NPCLock? prior) { if (prior is not { Shadow: false } x) return GetSingleLock(ctr, current); return GetAllLocks(ctr, current, x); } /// /// Returns a single as the lock must match precisely. /// /// Starting frame for the traversal. /// Current lock criteria to satisfy. Used to find valid results to yield. private IEnumerable GetSingleLock(int ctr, NPCLock current) { uint pid = (Cache[ctr + 1] << 16) | Cache[ctr]; if (current.MatchesLock(pid)) yield return new SeedFrame(pid, ctr + current.FramesConsumed); else yield break; // Reaching here means the single lock didn't cut it. Maybe the frame before it was an anti-shiny reroll? // Track if we ever require the CPU Trainer Shiny Value to be a value for a shiny skip. // We need to un-set this flag if future frames don't pan out. bool forcedOT = false; int start = 2; while (true) { var upper = Cache[start + 1]; var lower = Cache[start]; // uint cid = upper << 16 | lower; var sv = (upper ^ lower) >> 3; if (sv == TSV) // XD shiny checks all opponent PKM, even non-shadow. { // Anti-shiny rerolled! This is a possible frame. } else if (RCSV != NOT_FORCED) // CPU shiny value is required for a previous lock { if (sv == RCSV) { // No CPU shiny value forced yet. Let's try to skip this lock by requiring the eventual OT to get this shiny. RCSV = sv; forcedOT = true; continue; // don't break } if (forcedOT) // current call to this method had forced the OT; clear the forced OT before breaking. RCSV = NOT_FORCED; yield break; // Since we can't skip this interrupt, we're done. } // Yield the final rerolled pid instead of the bad anti-shiny (metadata/validation). yield return new SeedFrame(pid, start + current.FramesConsumed); start += 2; } } /// /// Generates nodes until the traversal is ended by an interrupt frame that matches the . /// /// Starting frame for the traversal. /// Current lock criteria to satisfy. Used to find valid results to yield. /// Lock criteria for previous party member. Used for determining when the traversal stops. /// List of possible locks for the provided input. /// /// An "interrupt" signals the end of the traversal. /// Any afterwards (when generated forward from the CPU Trainer) will use the interrupt rather than the previous that was found for the lock. /// private IEnumerable GetAllLocks(int ctr, NPCLock current, NPCLock prior) { // Since the prior(next) lock is generated 7+2*n frames after, the worst case break is 7 frames after the PID. // Continue reversing until a sequential generation case is found. int start = ctr; // Track if we ever require the CPU Trainer Shiny Value to be a value for a shiny skip. // We need to un-set this flag if future frames don't pan out. bool forcedOT = false; while (true) { int p7 = ctr - 7; if (p7 > start) { // check for interrupting cpu team cases var upper = Cache[p7 + 1]; var lower = Cache[p7]; uint cid = (upper << 16) | lower; var sv = (upper ^ lower) >> 3; if (sv == TSV) // XD shiny checks all opponent PKM, even non-shadow. { // This interrupt is ignored! The result is shiny. } else if (prior.MatchesLock(cid)) // lock matched cpu mon { if (RCSV != NOT_FORCED) // CPU shiny value is required for a previous lock { if (sv != RCSV) { if (forcedOT) // current call to this method had forced the OT; clear the forced OT before breaking. RCSV = NOT_FORCED; yield break; // Since we can't skip this interrupt, we're done. } } else // No CPU shiny value forced yet. Let's try to skip this lock by requiring the eventual OT to get this shiny. { RCSV = sv; forcedOT = true; // don't break } } } uint pid = (Cache[ctr + 1] << 16) | Cache[ctr]; if (current.MatchesLock(pid)) yield return new SeedFrame(pid, ctr + current.FramesConsumed); ctr += 2; } } /// /// Checks to see if the generated is compatible with the CPU Trainer Data. /// /// Ending frame of the traversal. /// True if the are valid. private bool VerifyNPC(int ctr) { var TID16 = Cache[ctr + 1]; var SID16 = Cache[ctr]; var CPUSV = (TID16 ^ SID16) >> 3; if (RCSV != NOT_FORCED && RCSV != CPUSV) return false; // required CPU Trainer's shiny value did not match the required value. int pos = Team.Count - 1; // stack can't do a for loop :( foreach (var member in Team) { var pid = member.PID; var psv = ((pid & 0xFFFF) ^ (pid >> 16)) >> 3; // check for shiny for Trainer -- XD only // if (psv == TSV) // XD shiny checks all opponent PKM, even non-shadow. // return false; // no shiny shadow mons // we already checked this when re-generating the team // check for shiny for CPU if (psv == CPUSV) { if (!Specifications.Locks[pos].Shadow) return false; // no shiny CPU mons if (TSV != NOT_FORCED) // XD return false; // no shiny shadow mons } pos--; } OriginFrame = ctr + 2; return true; } }