diff --git a/PKHeX.Core/Legality/Areas/EncounterArea9.cs b/PKHeX.Core/Legality/Areas/EncounterArea9.cs index ef74ba996..bb1afded3 100644 --- a/PKHeX.Core/Legality/Areas/EncounterArea9.cs +++ b/PKHeX.Core/Legality/Areas/EncounterArea9.cs @@ -12,6 +12,8 @@ public sealed record EncounterArea9 : EncounterArea { public readonly EncounterSlot9[] Slots; + public ushort CrossFrom { get; init; } + protected override IReadOnlyList Raw => Slots; public static EncounterArea9[] GetAreas(BinLinkerAccessor input, GameVersion game) @@ -25,12 +27,13 @@ public static EncounterArea9[] GetAreas(BinLinkerAccessor input, GameVersion gam private EncounterArea9(ReadOnlySpan areaData, GameVersion game) : base(game) { Location = areaData[0]; - Slots = ReadSlots(areaData[2..]); + CrossFrom = areaData[2]; + Slots = ReadSlots(areaData[4..]); } private EncounterSlot9[] ReadSlots(ReadOnlySpan areaData) { - const int size = 6; + const int size = 8; var result = new EncounterSlot9[areaData.Length / size]; for (int i = 0; i < result.Length; i++) { @@ -38,9 +41,12 @@ private EncounterSlot9[] ReadSlots(ReadOnlySpan areaData) var species = ReadUInt16LittleEndian(slot); var form = slot[2]; var gender = (sbyte)slot[3]; + var min = slot[4]; var max = slot[5]; - result[i] = new EncounterSlot9(this, species, form, min, max, gender); + var time = slot[6]; + + result[i] = new EncounterSlot9(this, species, form, min, max, gender, time); } return result; } diff --git a/PKHeX.Core/Legality/Encounters/EncounterSlot/AreaWeather9.cs b/PKHeX.Core/Legality/Encounters/EncounterSlot/AreaWeather9.cs new file mode 100644 index 000000000..20061634c --- /dev/null +++ b/PKHeX.Core/Legality/Encounters/EncounterSlot/AreaWeather9.cs @@ -0,0 +1,42 @@ +using System; +using static PKHeX.Core.RibbonIndex; +using static PKHeX.Core.AreaWeather9; + +namespace PKHeX.Core; + +/// +/// Encounter Conditions for +/// +[Flags] +public enum AreaWeather9 : ushort +{ + None, + Normal = 1, + Overcast = 1 << 1, + Raining = 1 << 2, + Thunderstorm = 1 << 3, + Intense_Sun = 1 << 4, + Snowing = 1 << 5, + Snowstorm = 1 << 6, + Sandstorm = 1 << 7, + Heavy_Fog = 1 << 8, + + Standard = Normal | Overcast | Raining | Thunderstorm, + Sand = Normal | Overcast | Raining | Sandstorm, + Snow = Normal | Overcast | Snowing | Snowstorm, + Inside = Normal | Overcast, +} + +public static class AreaWeather9Extensions +{ + public static bool IsMarkCompatible(this AreaWeather9 weather, RibbonIndex m) => m switch + { + MarkCloudy => (weather & Overcast) != 0, + MarkRainy => (weather & Raining) != 0, + MarkStormy => (weather & Thunderstorm) != 0, + MarkSnowy => (weather & Snowing) != 0, + MarkBlizzard => (weather & Snowstorm) != 0, + MarkSandstorm => (weather & Sandstorm) != 0, + _ => true, + }; +} diff --git a/PKHeX.Core/Legality/Encounters/EncounterSlot/EncounterSlot9.cs b/PKHeX.Core/Legality/Encounters/EncounterSlot/EncounterSlot9.cs index 86ad852d2..4e0daff4c 100644 --- a/PKHeX.Core/Legality/Encounters/EncounterSlot/EncounterSlot9.cs +++ b/PKHeX.Core/Legality/Encounters/EncounterSlot/EncounterSlot9.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; +using static PKHeX.Core.AreaWeather9; + namespace PKHeX.Core; /// @@ -9,10 +12,12 @@ public sealed record EncounterSlot9 : EncounterSlot public override int Generation => 9; public override EntityContext Context => EntityContext.Gen9; public sbyte Gender { get; } + public byte Time { get; } // disallow at time bit flag - public EncounterSlot9(EncounterArea9 area, ushort species, byte form, byte min, byte max, sbyte gender) : base(area, species, form, min, max) + public EncounterSlot9(EncounterArea9 area, ushort species, byte form, byte min, byte max, sbyte gender, byte time) : base(area, species, form, min, max) { Gender = gender; + Time = time; } protected override void ApplyDetails(ITrainerInfo sav, EncounterCriteria criteria, PKM pk) @@ -33,4 +38,61 @@ protected override void ApplyDetails(ITrainerInfo sav, EncounterCriteria criteri pk.Nature = ToxtricityUtil.GetRandomNature(ref rand, Form); pk9.EncryptionConstant = Util.Rand32(); } + + private static int GetTime(RibbonIndex mark) => mark switch + { + RibbonIndex.MarkLunchtime => 0, + RibbonIndex.MarkSleepyTime => 1, + RibbonIndex.MarkDusk => 2, + RibbonIndex.MarkDawn => 3, + _ => 4, + }; + + public bool CanSpawnAtTime(RibbonIndex mark) => (Time & (1 << GetTime(mark))) == 0; + + public bool CanSpawnInWeather(RibbonIndex mark) + { + if (AreaWeather.TryGetValue((byte)Area.Location, out var areaWeather)) + return areaWeather.IsMarkCompatible(mark); + return false; + } + + /// + /// Location IDs matched with possible weather types. + /// + internal static readonly Dictionary AreaWeather = new() + { + { 6, Standard }, // South Province (Area One) + { 10, Standard }, // Pokémon League + { 12, Standard }, // South Province (Area Two) + { 14, Standard }, // South Province (Area Four) + { 16, Standard }, // South Province (Area Six) + { 18, Standard }, // South Province (Area Five) + { 20, Standard }, // South Province (Area Three) + { 22, Standard }, // West Province (Area One) + { 24, Sand }, // Asado Desert + { 26, Standard }, // West Province (Area Two) + { 28, Standard }, // West Province (Area Three) + { 30, Standard }, // Tagtree Thicket + { 32, Standard }, // East Province (Area Three) + { 34, Standard }, // East Province (Area One) + { 36, Standard }, // East Province (Area Two) + { 38, Snow }, // Glaseado Mountain (1) + { 40, Standard }, // Casseroya Lake + { 44, Standard }, // North Province (Area Three) + { 46, Standard }, // North Province (Area One) + { 48, Standard }, // North Province (Area Two) + { 50, Standard }, // Great Crater of Paldea + { 56, Standard }, // South Paldean Sea + { 58, Standard }, // West Paldean Sea + { 60, Standard }, // East Paldean Sea + { 62, Standard }, // North Paldean Sea + { 64, Inside }, // Inlet Grotto + { 67, Inside }, // Alfornada Cavern + { 69, Standard | Inside | Snow | Snow },// Dalizapa Passage (Near Medali, Tunnels, Near Pokémon Center, Near Zapico) + { 70, Standard }, // Poco Path + { 80, Standard }, // Cabo Poco + { 109, Standard }, // Socarrat Trail + { 124, Inside }, // Area Zero (5) + }; } diff --git a/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterFixed9.cs b/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterFixed9.cs index 2ea97eb4e..cf4549f67 100644 --- a/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterFixed9.cs +++ b/PKHeX.Core/Legality/Encounters/EncounterStatic/EncounterFixed9.cs @@ -37,7 +37,8 @@ private static EncounterFixed9 ReadEncounter(ReadOnlySpan data) => new() Level = data[0x03], FlawlessIVCount = data[0x04], TeraType = (GemType)data[0x05], - // 2 bytes reserved + Gender = (sbyte)data[0x06], + // 1 byte reserved Moves = new Moveset( BinaryPrimitives.ReadUInt16LittleEndian(data[0x08..]), BinaryPrimitives.ReadUInt16LittleEndian(data[0x0A..]), @@ -57,6 +58,13 @@ protected override bool IsMatchLocation(PKM pk) return loc == Location0 || loc == Location1 || loc == Location2; } + protected override bool IsMatchForm(PKM pk, EvoCriteria evo) + { + if (Species is (int)Core.Species.Deerling or (int)Core.Species.Sawsbuck) + return pk.Form <= 3; + return base.IsMatchForm(pk, evo); + } + public override bool IsMatchExact(PKM pk, EvoCriteria evo) { if (TeraType != GemType.Random && pk is ITeraType t && !Tera9RNG.IsMatchTeraType(TeraType, Species, Form, (byte)t.TeraTypeOriginal)) diff --git a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator9.cs b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator9.cs index 621cd2394..eeed05fcc 100644 --- a/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator9.cs +++ b/PKHeX.Core/Legality/Encounters/Generator/ByGeneration/EncounterGenerator9.cs @@ -62,22 +62,26 @@ private static IEnumerable GetEncounters(PKM pk, EvoCriteria[] c yield break; } - // Static Encounters can collide with wild encounters (close match); don't break if a Static Encounter is yielded. - var encs = GetValidStaticEncounter(pk, chain, game); - foreach (var z in encs) + if (pk is not IRibbonIndex r || !r.HasEncounterMark()) { - var match = z.GetMatchRating(pk); - if (match == Match) + var encs = GetValidStaticEncounter(pk, chain, game); + foreach (var z in encs) { - yield return z; - } - else if (match < rating) - { - cache = z; - rating = match; + var match = z.GetMatchRating(pk); + if (match == Match) + { + yield return z; + } + else if (match < rating) + { + cache = z; + rating = match; + } } } + // Wild encounters are more permissive than static encounters. + // Can have encounter marks, can have varied scales/shiny states. foreach (var z in GetValidWildEncounters(pk, chain, game)) { var match = z.GetMatchRating(pk); diff --git a/PKHeX.Core/Legality/Verifiers/MarkVerifier.cs b/PKHeX.Core/Legality/Verifiers/MarkVerifier.cs index 9eeaff51b..cb9294b6d 100644 --- a/PKHeX.Core/Legality/Verifiers/MarkVerifier.cs +++ b/PKHeX.Core/Legality/Verifiers/MarkVerifier.cs @@ -48,7 +48,7 @@ private void VerifyMarksPresent(LegalityAnalysis data, IRibbonIndex m) return; } - bool result = MarkRules.IsMarkValid8(mark, data.Entity, data.EncounterMatch); + bool result = MarkRules.IsEncounterMarkValid(mark, data.Entity, data.EncounterMatch); if (!result) { data.AddLine(GetInvalid(string.Format(LRibbonMarkingFInvalid_0, GetRibbonNameSafe(mark)))); @@ -103,7 +103,7 @@ private void VerifyShedinjaAffixed(LegalityAnalysis data, RibbonIndex affix, PKM var enc = data.EncounterOriginal; if (affix.IsEncounterMark()) { - if (!MarkRules.IsMarkValid8(affix, pk, enc)) + if (!MarkRules.IsEncounterMarkValid(affix, pk, enc)) data.AddLine(GetInvalid(string.Format(LRibbonMarkingAffixedF_0, GetRibbonNameSafe(affix)))); return; } diff --git a/PKHeX.Core/Legality/Verifiers/Ribbons/MarkRules.cs b/PKHeX.Core/Legality/Verifiers/Ribbons/MarkRules.cs index e39a0625a..58c37922c 100644 --- a/PKHeX.Core/Legality/Verifiers/Ribbons/MarkRules.cs +++ b/PKHeX.Core/Legality/Verifiers/Ribbons/MarkRules.cs @@ -8,11 +8,6 @@ namespace PKHeX.Core; /// public static class MarkRules { - /// - /// Checks if the ribbon index is one of the specific SW/SH encounter-only marks. These marks are granted when the encounter spawns in the wild. - /// - public static bool IsEncounterMark(this RibbonIndex m) => (byte)m is >= (int)MarkLunchtime and <= (int)MarkSlump; - /// /// Checks if an encounter-only mark is possible to obtain for the encounter, if not lost via data manipulation. /// @@ -35,39 +30,50 @@ public static bool IsEncounterMarkLost(LegalityAnalysis data) /// /// Checks if a SW/SH mark is valid. /// - public static bool IsMarkValid8(RibbonIndex mark, PKM pk, IEncounterTemplate enc) + public static bool IsEncounterMarkValid(RibbonIndex mark, PKM pk, IEncounterTemplate enc) => enc switch { - return IsEncounterMarkAllowedAny(enc) && IsMarkAllowedSpecific(mark, pk, enc); - } + EncounterSlot8 or EncounterStatic8 { Gift: false, ScriptedNoMarks: false } => IsMarkAllowedSpecific8(mark, pk, enc), + EncounterSlot9 s => IsMarkAllowedSpecific9(mark, s), + _ => false, + }; /// /// Checks if a specific encounter mark is disallowed. /// /// False if mark is disallowed based on specific conditions. - public static bool IsMarkAllowedSpecific(RibbonIndex mark, PKM pk, IEncounterTemplate x) => mark switch + public static bool IsMarkAllowedSpecific8(RibbonIndex mark, PKM pk, IEncounterTemplate x) => mark switch { MarkCurry when !IsMarkAllowedCurry(pk, x) => false, MarkFishing when !IsMarkAllowedFishing(x) => false, MarkMisty when x.Generation == 8 && pk.Met_Level < EncounterArea8.BoostLevel && EncounterArea8.IsBoostedArea60Fog(pk.Met_Location) => false, MarkDestiny => x is EncounterSlot9, // Capture on Birthday - >= MarkCloudy and <= MarkMisty => IsWeatherPermitted(mark, x), + >= MarkCloudy and <= MarkMisty => IsWeatherPermitted8(mark, x), _ => true, }; - private static bool IsWeatherPermitted(RibbonIndex mark, IEncounterTemplate enc) + /// + /// Checks if a specific encounter mark is disallowed. + /// + /// False if mark is disallowed based on specific conditions. + public static bool IsMarkAllowedSpecific9(RibbonIndex mark, EncounterSlot9 x) => mark switch { - var permit = mark.GetWeather8(); + MarkCurry => false, + MarkFishing => false, + MarkDestiny => true, // Capture on Birthday + >= MarkLunchtime and <= MarkDawn => x.CanSpawnAtTime(mark), + >= MarkCloudy and <= MarkMisty => x.CanSpawnInWeather(mark), + _ => true, + }; - // Encounter slots check location weather, while static encounters check weather per encounter. - return enc switch - { - EncounterSlot8 w => IsSlotWeatherPermitted(permit, w), - EncounterStatic8 s => s.Weather.HasFlag(permit), - _ => false, - }; - } + // Encounter slots check location weather, while static encounters check weather per encounter. + private static bool IsWeatherPermitted8(RibbonIndex mark, IEncounterTemplate enc) => enc switch + { + EncounterSlot8 w => IsSlotWeatherPermittedSWSH(mark.GetWeather8(), w), + EncounterStatic8 s => s.Weather.HasFlag(mark.GetWeather8()), + _ => false, + }; - private static bool IsSlotWeatherPermitted(AreaWeather8 permit, EncounterSlot8 s) + private static bool IsSlotWeatherPermittedSWSH(AreaWeather8 permit, EncounterSlot8 s) { var location = s.Location; // If it's not in the main table, it can only have Normal weather. @@ -85,18 +91,7 @@ private static bool IsSlotWeatherPermitted(AreaWeather8 permit, EncounterSlot8 s } /// - /// Checks if any encounter-only mark is available for the . - /// - public static bool IsEncounterMarkAllowedAny(IEncounterTemplate enc) => enc.Generation >= 8 && enc switch - { - // Gen 8 - EncounterSlot8 or EncounterStatic8 { Gift: false, ScriptedNoMarks: false } => true, - EncounterSlot9 => true, - _ => false, - }; - - /// - /// Checks if a mark is valid. + /// Checks if a mark is valid. /// public static bool IsMarkAllowedCurry(PKM pk, IEncounterTemplate enc) { @@ -109,7 +104,7 @@ public static bool IsMarkAllowedCurry(PKM pk, IEncounterTemplate enc) } /// - /// Checks if a mark is valid. + /// Checks if a mark is valid. /// public static bool IsMarkAllowedFishing(IEncounterTemplate enc) { diff --git a/PKHeX.Core/Resources/byte/evolve/evos_sv.pkl b/PKHeX.Core/Resources/byte/evolve/evos_sv.pkl index f860da697..1d3605788 100644 Binary files a/PKHeX.Core/Resources/byte/evolve/evos_sv.pkl and b/PKHeX.Core/Resources/byte/evolve/evos_sv.pkl differ diff --git a/PKHeX.Core/Resources/legality/wild/Gen9/encounter_fixed_paldea.pkl b/PKHeX.Core/Resources/legality/wild/Gen9/encounter_fixed_paldea.pkl index 08b50fcbc..6fec4c9f4 100644 Binary files a/PKHeX.Core/Resources/legality/wild/Gen9/encounter_fixed_paldea.pkl and b/PKHeX.Core/Resources/legality/wild/Gen9/encounter_fixed_paldea.pkl differ diff --git a/PKHeX.Core/Resources/legality/wild/Gen9/encounter_wild_paldea.pkl b/PKHeX.Core/Resources/legality/wild/Gen9/encounter_wild_paldea.pkl index eb1c59691..f03fd3e38 100644 Binary files a/PKHeX.Core/Resources/legality/wild/Gen9/encounter_wild_paldea.pkl and b/PKHeX.Core/Resources/legality/wild/Gen9/encounter_wild_paldea.pkl differ diff --git a/PKHeX.Core/Ribbons/RibbonIndex.cs b/PKHeX.Core/Ribbons/RibbonIndex.cs index 8bfa00874..c2ab907c9 100644 --- a/PKHeX.Core/Ribbons/RibbonIndex.cs +++ b/PKHeX.Core/Ribbons/RibbonIndex.cs @@ -129,7 +129,20 @@ public static class RibbonIndexExtensions { public static bool GetRibbonIndex(this IRibbonIndex x, RibbonIndex r) => x.GetRibbon((int)r); public static void SetRibbonIndex(this IRibbonIndex x, RibbonIndex r, bool value = true) => x.SetRibbon((int)r, value); - public static bool IsMark(this RibbonIndex r) => r is >= MarkLunchtime and <= MarkSlump; + public static bool IsEncounterMark(this RibbonIndex r) => r is >= MarkLunchtime and <= MarkSlump; + + /// + /// Checks if the ribbon index is one of the specific wild encounter-only marks. These marks are granted when the encounter spawns in the wild. + /// + public static bool HasEncounterMark(this IRibbonIndex m) + { + for (int i = (int)MarkLunchtime; i <= (int)MarkSlump; i++) + { + if (m.GetRibbon(i)) + return true; + } + return false; + } public static AreaWeather8 GetWeather8(this RibbonIndex x) => x switch { @@ -147,7 +160,7 @@ public static class RibbonIndexExtensions private enum RibbonIndexGroup : byte { None, - Mark, + EncounterMark, CountMemory, Common3, Common4, @@ -161,8 +174,8 @@ private enum RibbonIndexGroup : byte private static RibbonIndexGroup GetGroup(this RibbonIndex r) { - if (r.IsMark()) - return RibbonIndexGroup.Mark; + if (r.IsEncounterMark()) + return RibbonIndexGroup.EncounterMark; return r switch { ChampionG3 => RibbonIndexGroup.Common3, @@ -249,8 +262,8 @@ public static void Fix(this RibbonIndex r, RibbonVerifierArguments args, bool st var group = r.GetGroup(); switch (group) { - case RibbonIndexGroup.Mark: - r.FixMark(pk, state); + case RibbonIndexGroup.EncounterMark: + r.FixEncounterMark(pk, state); return; case RibbonIndexGroup.CountMemory: if (pk is not IRibbonSetMemory6 m6) @@ -365,7 +378,7 @@ public static void Fix(this RibbonIndex r, RibbonVerifierArguments args, bool st } } - private static void FixMark(this RibbonIndex r, PKM pk, bool state) + private static void FixEncounterMark(this RibbonIndex r, PKM pk, bool state) { if (pk is not IRibbonSetMark8 m) return; diff --git a/Tests/PKHeX.Core.Tests/Legality/LegalityData.cs b/Tests/PKHeX.Core.Tests/Legality/LegalityData.cs index 53f9409ac..c05ea341d 100644 --- a/Tests/PKHeX.Core.Tests/Legality/LegalityData.cs +++ b/Tests/PKHeX.Core.Tests/Legality/LegalityData.cs @@ -36,4 +36,21 @@ public class LegalityData t2.IsLevelUpRequired().Should().BeTrue(); } } + + [Fact] + public void EvolutionsOrderedSV() + { + // SV Crabrawler added a second, UseItem evolution method. Need to be sure it's before the more restrictive level-up method. + var tree = EvolutionTree.Evolves9; + var fEntries = typeof(EvolutionTree).GetFields(BindingFlags.NonPublic | BindingFlags.Instance).First(z => z.Name == "Entries"); + if (fEntries.GetValue(tree) is not IReadOnlyList entries) + throw new ArgumentException(nameof(entries)); + var crab = entries[(int)Species.Crabrawler]; + + var t1 = crab[0].Method; + var t2 = crab[1].Method; + + t1.IsLevelUpRequired().Should().BeFalse(); + t2.IsLevelUpRequired().Should().BeTrue(); + } }