PKHeX/PKHeX.Core/Legality/RNG/Locks/TeamLockResult.cs
Kurt 1d5f929981 Fix shiny check behavior for XD nonshadows
XD compares the player TSV against the non-shadow members, which is unnecessary but does result in them being able to cause shiny skips.

Update the VerifyNPC method to remove tsv check, as it's just repeating the logic we've done in the lock generator.

Added a test method that can be stepped through; compare the team results manually since they aren't exposed for interaction.

Closes #2751 , big thanks to @ijuintekka for being so detailed in their investigation/report!

Co-Authored-By: ijuintekka <ijuintekka@users.noreply.github.com>
2020-03-11 19:18:58 -07:00

227 lines
11 KiB
C#

using System.Collections.Generic;
namespace PKHeX.Core
{
/// <summary>
/// Analyzes a <see cref="TeamLock"/> to determine if the provided parameters are a valid end product.
/// </summary>
/// <remarks>
/// When generating a Trainer Team for <see cref="GameVersion.CXD"/>, 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 <see cref="PKM.PID"/> of the current <see cref="PKM"/> 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.
/// <see cref="PKM.Nature"/> locks require a certain <see cref="Nature"/>, which is derived from the <see cref="PKM.PID"/>.
/// <see cref="PKM.Gender"/> locks require a certain gender value, which is derived from the <see cref="PKM.PID"/> and <see cref="PersonalInfo.Gender"/> ratio.
/// <see cref="PKM.IsShiny"/> locks require the member to not be shiny. This is enforced for non-shadow members for both games, and for shadow members in <see cref="GameVersion.XD"/>.
/// </remarks>
public sealed class TeamLockResult
{
/// <summary>
/// Seed prior to generating the team per <see cref="Specifications"/>.
/// </summary>
public readonly uint OriginSeed;
/// <summary>
/// Indicates if the input parameters can be generated by the <see cref="Specifications"/> provided.
/// </summary>
public readonly bool Valid;
/// <summary>
/// NPC Team data containing a list of <see cref="NPCLock"/> members.
/// </summary>
internal readonly TeamLock Specifications;
/// <summary>
/// Frame the <see cref="OriginSeed"/> is from, based on frames reversed from the input seed provided.
/// </summary>
private int OriginFrame;
/// <summary>
/// Required CPU Trainer Shiny Value
/// </summary>
/// <remarks>
/// If this value is >= 0, the CPU Trainer Shiny Value must be equal to this value as a <see cref="SeedFrame"/> skipped over a matching interrupt frame.
/// If this value is <see cref="NOT_FORCED"/>, the CPU Trainer Shiny Value can be anything (except matching any of the <see cref="SeedFrame"/> result members.
/// </remarks>
private int RCSV = NOT_FORCED;
/// <summary>
/// Player Trainer Shiny Value
/// </summary>
/// <remarks>Only used by <see cref="GameVersion.XD"/> encounters, which disallow shiny shadow members for both player &amp; CPU TSVs.</remarks>
private readonly int TSV;
private readonly Stack<NPCLock> 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<SeedFrame> Team;
private const int NOT_FORCED = -1;
internal TeamLockResult(TeamLock teamSpec, uint originSeed, int tsv)
{
Locks = new Stack<NPCLock>((Specifications = teamSpec).Locks);
Team = new Stack<SeedFrame>();
Cache = new FrameCache(RNG.XDRNG.Reverse(originSeed, 2), RNG.XDRNG.Prev);
TSV = tsv;
Valid = FindLockSeed();
if (Valid)
OriginSeed = Cache.GetSeed(OriginFrame);
}
/// <summary>
/// Depth-first search traversal which finds a possible origin for the <see cref="Specifications"/>.
/// </summary>
/// <param name="frame">Frame at which the search starts/continues at.</param>
/// <param name="prior">Prior <see cref="NPCLock"/> data. If this is the last lock in the CPU Team, this is null.</param>
/// <returns>True if the <see cref="Specifications"/> are valid.</returns>
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;
}
/// <summary>
/// Generates a list of frames the <see cref="current"/> lock data can be generated at.
/// </summary>
/// <param name="ctr">Starting frame for the traversal.</param>
/// <param name="current">Current lock criteria to satisfy. Used to find valid <see cref="SeedFrame"/> results to yield.</param>
/// <param name="prior">Prior lock criteria. Used for determining when the traversal stops.</param>
/// <returns>List of possible locks for the provided input.</returns>
private IEnumerable<SeedFrame> GetPossibleLocks(int ctr, NPCLock current, NPCLock? prior)
{
if (prior?.Shadow != false)
return GetSingleLock(ctr, current);
return GetAllLocks(ctr, current, prior);
}
/// <summary>
/// Returns a single <see cref="SeedFrame"/> as the <see cref="current"/> lock must match precisely.
/// </summary>
/// <param name="ctr">Starting frame for the traversal.</param>
/// <param name="current">Current lock criteria to satisfy. Used to find valid <see cref="SeedFrame"/> results to yield.</param>
/// <returns></returns>
private IEnumerable<SeedFrame> GetSingleLock(int ctr, NPCLock current)
{
uint pid = Cache[ctr + 1] << 16 | Cache[ctr];
if (current.MatchesLock(pid))
yield return new SeedFrame(pid, ctr + (current.Seen ? 5 : 7));
}
/// <summary>
/// Generates <see cref="SeedFrame"/> nodes until the traversal is ended by an interrupt frame that matches the <see cref="prior"/>.
/// </summary>
/// <param name="ctr">Starting frame for the traversal.</param>
/// <param name="current">Current lock criteria to satisfy. Used to find valid <see cref="SeedFrame"/> results to yield.</param>
/// <param name="prior">Prior lock criteria. Used for determining when the traversal stops.</param>
/// <returns>List of possible locks for the provided input.</returns>
/// <remarks>
/// An "interrupt" signals the end of the traversal.
/// Any <see cref="SeedFrame"/> afterwards (when generated forward from the CPU Trainer) will use the interrupt rather than the previous <see cref="SeedFrame"/> that was found for the <see cref="prior"/> lock.
/// </remarks>
private IEnumerable<SeedFrame> 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. Lets try to skip this lock by requiring the eventual OT to get this shiny.
{
RCSV = (int)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.Seen ? 5 : 7));
ctr += 2;
}
}
/// <summary>
/// Checks to see if the generated <see cref="Team"/> is compatible with the CPU Trainer Data.
/// </summary>
/// <param name="ctr">Ending frame of the traversal.</param>
/// <returns>True if the <see cref="Specifications"/> are valid.</returns>
private bool VerifyNPC(int ctr)
{
var TID = Cache[ctr + 1];
var SID = Cache[ctr];
var CPUSV = (TID ^ SID) >> 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;
}
}
}