diff --git a/PKHeX.Core/Legality/Checks.cs b/PKHeX.Core/Legality/Checks.cs
index a223f3c11..aba8f5985 100644
--- a/PKHeX.Core/Legality/Checks.cs
+++ b/PKHeX.Core/Legality/Checks.cs
@@ -932,30 +932,18 @@ private void VerifyCXDStarterCorrelation(PIDIV pidiv)
if (pidiv.Type != PIDType.CXD)
return;
- var spec = EncounterMatch.Species;
- int rev; // pidiv reversed 2x yields SID, 3x yields TID. shift by 7 if another PKM is generated prior
- switch (spec)
+ bool valid;
+ switch (EncounterMatch.Species)
{
- // XD
- case 133: // Eevee
- rev = 2;
- break;
-
- // Colosseum
- case 197: // Umbreon (generated before Espeon)
- rev = 2;
- break;
- case 196: // Espeon (generated after Umbreon)
- rev = 2+7;
- break;
+ case 133:
+ valid = LockFinder.IsXDStarterValid(pidiv.OriginSeed, pkm.TID, pkm.SID); break;
+ case 196: case 197:
+ valid = pidiv.Type == PIDType.CXD_ColoStarter; break;
default:
return;
}
- var seed = pidiv.OriginSeed;
- var SIDf = pidiv.RNG.Reverse(seed, rev);
- var TIDf = pidiv.RNG.Prev(SIDf);
- if (SIDf >> 16 != pkm.SID || TIDf >> 16 != pkm.TID)
- AddLine(Severity.Invalid, V400 + $" {TIDf>>16}/{SIDf>>16}", CheckIdentifier.PID);
+ if (!valid)
+ AddLine(Severity.Invalid, V400, CheckIdentifier.PID);
}
private void VerifyAbility()
diff --git a/PKHeX.Core/Legality/Encounters/Data/Encounters3.cs b/PKHeX.Core/Legality/Encounters/Data/Encounters3.cs
index f680af810..5373fa336 100644
--- a/PKHeX.Core/Legality/Encounters/Data/Encounters3.cs
+++ b/PKHeX.Core/Legality/Encounters/Data/Encounters3.cs
@@ -391,8 +391,9 @@ private static void MarkG3SlotsSafariZones(ref EncounterArea[] Areas, int locati
#region Colosseum
internal static readonly EncounterStatic[] Encounter_Colo =
{
- new EncounterStatic { Gift = true, Species = 196, Level = 25, Location = 254 }, // Espeon
- new EncounterStatic { Gift = true, Species = 197, Level = 26, Location = 254, Moves = new[] {044} }, // Umbreon (Bite)
+ // Colosseum Starters: Gender locked to male
+ new EncounterStatic { Gift = true, Species = 196, Level = 25, Location = 254, Gender = 0 }, // Espeon
+ new EncounterStatic { Gift = true, Species = 197, Level = 26, Location = 254, Gender = 0, Moves = new[] {044} }, // Umbreon (Bite)
new EncounterStaticShadow { Species = 296, Level = 30, Gauge = 03000, Moves = new[] {193,116,233,238}, Location = 005 }, // Makuhita: Miror B.Peon Trudly @ Phenac City
diff --git a/PKHeX.Core/Legality/RNG/Locks/LockFinder.cs b/PKHeX.Core/Legality/RNG/Locks/LockFinder.cs
index 1d309fb92..8f265ce3a 100644
--- a/PKHeX.Core/Legality/RNG/Locks/LockFinder.cs
+++ b/PKHeX.Core/Legality/RNG/Locks/LockFinder.cs
@@ -2,13 +2,27 @@
namespace PKHeX.Core
{
- public class NPCLock
+ ///
+ /// Locks associated to a given NPC PKM that appears before a .
+ ///
+ public sealed class NPCLock
{
public int Species;
public uint? Nature = null;
public uint? Gender = null;
public uint? Ability = null;
}
+
+ ///
+ /// Contains various Colosseum/XD 'wait for value' logic related to PKM generation.
+ ///
+ ///
+ /// "Locks" are referring to the being "locked" to a certain value, e.g. requiring Nature to be neutral.
+ /// These locks cause the of the current to be rerolled until the requisite lock is satisfied.
+ /// locks require a certain , which is derived from the .
+ /// locks require a certain gender value, which is derived from the and ratio.
+ /// Not sure if Abilities are locked for the encounter, assume not. When this code is eventually utilized, our understanding can be tested!
+ ///
public static class LockFinder
{
// Message Passing
@@ -64,7 +78,7 @@ private static IEnumerable FindPossibleLockFrames(uint seed, RNG RNG, N
} while (true);
}
- private static bool VerifyNPC(uint seed, RNG RNG, Stack PIDs, bool XD, out uint origin)
+ private static bool VerifyNPC(uint seed, RNG RNG, IEnumerable PIDs, bool XD, out uint origin)
{
// todo: get trainer TID/SID/Origin Seed
origin = 0;
@@ -72,9 +86,8 @@ private static bool VerifyNPC(uint seed, RNG RNG, Stack PIDs, bool XD, out
var sid = 0;
// verify none are shiny
- var arr = PIDs.ToArray();
- for (int i = 0; i < PIDs.Count; i++)
- if (IsShiny(tid, sid, arr[i]))
+ foreach (var pid in PIDs)
+ if (IsShiny(tid, sid, pid))
return false;
return true;
}
@@ -91,5 +104,88 @@ private static bool MatchesLock(NPCLock k, uint PID, int Gender, int AbilityNumb
return false;
return true;
}
+
+ // Colosseum/XD Starters
+ public static bool IsXDStarterValid(uint seed, int TID, int SID)
+ {
+ // pidiv reversed 2x yields SID, 3x yields TID. shift by 7 if another PKM is generated prior
+ var SIDf = RNG.XDRNG.Reverse(seed, 2);
+ var TIDf = RNG.XDRNG.Prev(SIDf);
+ return SIDf >> 16 == SID && TIDf >> 16 == TID;
+ }
+ public static bool IsColoStarterValid(int species, ref uint seed, int TID, int SID, uint pkPID, uint IV1, uint IV2)
+ {
+ // reverse the seed the bare minimum
+ int rev = 2;
+ if (species == 196)
+ rev += 7;
+
+ var rng = RNG.XDRNG;
+ var SIDf = rng.Reverse(seed, rev);
+ int ctr = 0;
+ while (true)
+ {
+ if (SIDf >> 16 == SID && rng.Prev(SIDf) >> 16 == TID)
+ break;
+ SIDf = rng.Prev(SIDf);
+ if (ctr > 32) // arbitrary
+ return false;
+ ctr++;
+ }
+
+ var next = rng.Next(SIDf);
+
+ // generate Umbreon
+ var PIDIV = GenerateValidColoStarterPID(ref next, TID, SID);
+ if (species == 196) // need espeon, which is immediately next
+ PIDIV = GenerateValidColoStarterPID(ref next, TID, SID);
+
+ if (!PIDIV.Equals(pkPID, IV1, IV2))
+ return false;
+ seed = rng.Reverse(SIDf, 2);
+ return true;
+ }
+
+ private struct PIDIVGroup
+ {
+ public uint PID;
+ public uint IV1;
+ public uint IV2;
+
+ public bool Equals(uint pid, uint iv1, uint iv2) => PID == pid && IV1 == iv1 && IV2 == iv2;
+ }
+
+ private static PIDIVGroup GenerateValidColoStarterPID(ref uint uSeed, int TID, int SID)
+ {
+ var rng = RNG.XDRNG;
+ PIDIVGroup group = new PIDIVGroup();
+
+ uSeed = rng.Advance(uSeed, 2); // skip fakePID
+ group.IV1 = (uSeed >> 16) & 0x7FFF;
+ uSeed = rng.Next(uSeed);
+ group.IV2 = (uSeed >> 16) & 0x7FFF;
+ uSeed = rng.Next(uSeed);
+ uSeed = rng.Advance(uSeed, 1); // skip ability call
+ group.PID = GenerateStarterPID(ref uSeed, TID, SID);
+
+ uSeed = rng.Advance(uSeed, 2); // PID calls consumed
+
+ return group;
+ }
+ private static uint GenerateStarterPID(ref uint uSeed, int TID, int SID)
+ {
+ uint PID;
+ const byte ratio = 0x20; // 12.5% F (can't be female)
+ while (true)
+ {
+ var next = RNG.XDRNG.Next(uSeed);
+ PID = (uSeed & 0xFFFF0000) | (next >> 16);
+ if ((PID & 0xFF) > ratio && !IsShiny(TID, SID, PID))
+ break;
+ uSeed = RNG.XDRNG.Next(next);
+ }
+
+ return PID;
+ }
}
}
diff --git a/PKHeX.Core/Legality/RNG/MethodFinder.cs b/PKHeX.Core/Legality/RNG/MethodFinder.cs
index bfc5a3dfd..0512df0d3 100644
--- a/PKHeX.Core/Legality/RNG/MethodFinder.cs
+++ b/PKHeX.Core/Legality/RNG/MethodFinder.cs
@@ -34,6 +34,8 @@ public static PIDIV Analyze(PKM pk)
return pidiv;
if (pk.Species == 201 && GetLCRNGUnownMatch(top, bot, IVs, out pidiv)) // frlg only
return pidiv;
+ if (GetColoStarterMatch(pk, top, bot, IVs, out pidiv))
+ return pidiv;
if (GetXDRNGMatch(top, bot, IVs, out pidiv))
return pidiv;
@@ -448,6 +450,25 @@ private static bool GetPokewalkerMatch(PKM pk, uint oldpid, out PIDIV pidiv)
pidiv = new PIDIV {NoSeed = true, RNG = RNG.LCRNG, Type = PIDType.Pokewalker};
return true;
}
+ private static bool GetColoStarterMatch(PKM pk, uint top, uint bot, uint[] IVs, out PIDIV pidiv)
+ {
+ if (pk.Version != 15 || pk.Species != 196 && pk.Species != 197)
+ return GetNonMatch(out pidiv);
+
+ var iv1 = GetIVChunk(IVs, 0);
+ var iv2 = GetIVChunk(IVs, 3);
+ var xdc = GetSeedsFromPIDEuclid(RNG.XDRNG, top, bot);
+ foreach (var seed in xdc)
+ {
+ uint origin = seed;
+ if (!LockFinder.IsColoStarterValid(pk.Species, ref origin, pk.TID, pk.SID, pk.PID, iv1, iv2))
+ continue;
+
+ pidiv = new PIDIV { OriginSeed = origin, RNG = RNG.XDRNG, Type = PIDType.CXD_ColoStarter };
+ return true;
+ }
+ return GetNonMatch(out pidiv);
+ }
///
/// Returns false and no .
@@ -660,7 +681,7 @@ public static bool IsCompatible3(this PIDType val, IEncounterable encounter, PKM
case EncounterStatic s:
switch (pkm.Version)
{
- case (int)GameVersion.CXD: return val == PIDType.CXD;
+ case (int)GameVersion.CXD: return val == PIDType.CXD || val == PIDType.CXD_ColoStarter;
case (int)GameVersion.E: return val == PIDType.Method_1; // no roamer glitch
case (int)GameVersion.FR:
diff --git a/PKHeX.Core/Legality/RNG/PIDType.cs b/PKHeX.Core/Legality/RNG/PIDType.cs
index a2e6658bb..6f20dafaa 100644
--- a/PKHeX.Core/Legality/RNG/PIDType.cs
+++ b/PKHeX.Core/Legality/RNG/PIDType.cs
@@ -64,6 +64,7 @@ public enum PIDType
// XDRNG Based
CXD,
+ CXD_ColoStarter,
Channel,
PokeSpot,
diff --git a/PKHeX.Core/Legality/Structures/EncounterType.cs b/PKHeX.Core/Legality/Structures/EncounterType.cs
index 53a4c4964..1848cb97a 100644
--- a/PKHeX.Core/Legality/Structures/EncounterType.cs
+++ b/PKHeX.Core/Legality/Structures/EncounterType.cs
@@ -27,10 +27,6 @@ public enum EncounterType
public static class EncounterTypeExtension
{
- public static bool Contains(this EncounterType g1, int g2)
- {
- var type = (EncounterType)(1 << g2);
- return (g1 & type) != 0;
- }
+ public static bool Contains(this EncounterType g1, int g2) => g1.HasFlag((EncounterType)(1 << g2));
}
}