Add Misty Mark recognition & weather bleed for slots (#4519)

Closes #4036
Weather now handled upstream in pkNX, with bleed applied to individual spawn points, serialized to consumable legality binary.
Misty marks thanks to @Lusamine and her discord helpers -- also utilized https://github.com/kwsch/MistyMarkVisualize to help visualize and filter entity dumps => spawn-position.
This commit is contained in:
Kurt 2025-07-05 00:08:29 -05:00 committed by GitHub
parent 607901dda1
commit 7864907f81
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 34 additions and 112 deletions

View File

@ -8,18 +8,17 @@ namespace PKHeX.Core;
/// Encounter Conditions for <see cref="GameVersion.SV"/>
/// </summary>
[Flags]
public enum AreaWeather9 : ushort
public enum AreaWeather9 : byte
{
None,
Normal = 1,
Overcast = 1 << 1,
Raining = 1 << 2,
Thunderstorm = 1 << 3,
Intense_Sun = 1 << 4,
Mist = 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,
@ -40,6 +39,7 @@ public static class AreaWeather9Extensions
MarkSnowy => (weather & Snowing) != 0,
MarkBlizzard => (weather & Snowstorm) != 0,
MarkSandstorm => (weather & Sandstorm) != 0,
MarkMisty => (weather & Mist) != 0,
_ => false,
};
}

View File

@ -48,8 +48,9 @@ private EncounterSlot9[] ReadSlots(ReadOnlySpan<byte> areaData)
var min = slot[4];
var max = slot[5];
var time = slot[6];
var weather = (AreaWeather9)slot[7];
result[i] = new EncounterSlot9(this, species, form, min, max, gender, time);
result[i] = new EncounterSlot9(this, species, form, min, max, gender, time, weather);
}
return result;
}

View File

@ -26,6 +26,7 @@ public sealed record EncounterOutbreak9
public required byte LevelMax { get; init; }
public required byte Gender { get; init; }
public required RibbonIndex Ribbon { get; init; }
public AreaWeather9 Weather { get; init; }
public required byte MetBase { get; init; }
public required bool IsForcedScaleRange { get; init; }
public required byte ScaleMin { get; init; }
@ -33,7 +34,7 @@ public sealed record EncounterOutbreak9
public required bool IsShiny { get; init; }
public required UInt128 MetFlags { get; init; }
private const int SIZE = 0x14 + 8;
private const int SIZE = 0xC + 16;
public static EncounterOutbreak9[] GetArray(ReadOnlySpan<byte> data)
{
@ -53,13 +54,15 @@ private static EncounterOutbreak9 ReadEncounter(ReadOnlySpan<byte> data) => new(
LevelMin = data[0x04],
LevelMax = data[0x05],
Ribbon = (RibbonIndex)data[0x06],
MetBase = data[0x07],
Weather = (AreaWeather9)data[0x07],
IsForcedScaleRange = data[0x08] != 0,
ScaleMin = data[0x09],
ScaleMax = data[0x0A],
IsShiny = data[0x0B] != 0,
MetFlags = ReadUInt128LittleEndian(data[0x0C..]),
MetBase = data[0x0C],
MetFlags = ReadUInt128LittleEndian(data[0x0C..]) >> 8,
};
public string Name => "Distribution Outbreak Encounter";
@ -226,6 +229,12 @@ private EncounterMatchRating IsMatchDeferred(PKM pk)
}
}
if (pk is IRibbonSetMark8 m)
{
if (m.HasWeatherMark(out var weather) && !CanSpawnInWeather(weather))
return EncounterMatchRating.DeferredErrors;
}
return EncounterMatchRating.Match;
}
@ -237,4 +246,6 @@ private bool IsMatchPartial(PKM pk)
}
#endregion
public bool CanSpawnInWeather(RibbonIndex mark) => Weather.IsMarkCompatible(mark);
}

View File

@ -1,11 +1,9 @@
using static PKHeX.Core.AreaWeather9;
namespace PKHeX.Core;
/// <summary>
/// Encounter Slot found in <see cref="GameVersion.SV"/>.
/// </summary>
public sealed record EncounterSlot9(EncounterArea9 Parent, ushort Species, byte Form, byte LevelMin, byte LevelMax, byte Gender, byte Time)
public sealed record EncounterSlot9(EncounterArea9 Parent, ushort Species, byte Form, byte LevelMin, byte LevelMax, byte Gender, byte Time, AreaWeather9 Weather)
: IEncounterable, IEncounterMatch, IEncounterConvertible<PK9>, IEncounterFormRandom, IFixedGender
{
public byte Generation => 9;
@ -33,92 +31,7 @@ public sealed record EncounterSlot9(EncounterArea9 Parent, ushort Species, byte
};
public bool CanSpawnAtTime(RibbonIndex mark) => (Time & (1 << GetTime(mark))) == 0;
public bool CanSpawnInWeather(RibbonIndex mark)
{
var loc = (byte)Location;
return CanSpawnInWeather(mark, loc);
}
public static bool CanSpawnInWeather(RibbonIndex mark, byte loc)
{
var weather = GetWeather(loc);
return weather.IsMarkCompatible(mark);
}
/// <summary>
/// Location IDs matched with possible weather types. Unlisted locations may only have Normal weather.
/// </summary>
public static AreaWeather9 GetWeather(byte location) => location switch
{
006 => Standard, // South Province (Area One)
010 => Standard, // Pokémon League
012 => Standard, // South Province (Area Two)
014 => Standard, // South Province (Area Four)
016 => Standard, // South Province (Area Six)
018 => Standard, // South Province (Area Five)
020 => Standard, // South Province (Area Three)
022 => Standard, // West Province (Area One)
024 => Sand, // Asado Desert
026 => Standard, // West Province (Area Two)
028 => Standard, // West Province (Area Three)
030 => Standard, // Tagtree Thicket
032 => Standard, // East Province (Area Three)
034 => Standard, // East Province (Area One)
036 => Standard, // East Province (Area Two)
038 => Snow, // Glaseado Mountain (1)
040 => Standard, // Casseroya Lake
044 => Standard, // North Province (Area Three)
046 => Standard, // North Province (Area One)
048 => Standard, // North Province (Area Two)
050 => Standard, // Great Crater of Paldea
056 => Standard, // South Paldean Sea
058 => Standard, // West Paldean Sea
060 => Standard, // East Paldean Sea
062 => Standard, // North Paldean Sea
064 => Inside, // Inlet Grotto
067 => Inside, // Alfornada Cavern
069 => Standard | Inside | Snow | Snow,// Dalizapa Passage (Near Medali, Tunnels, Near Pokémon Center, Near Zapico)
070 => Standard, // Poco Path
080 => Standard, // Cabo Poco
109 => Standard, // Socarrat Trail
124 => Inside, // Area Zero (5)
132 => Standard, // Kitakami Road
134 => Standard, // Mossui Town
136 => Standard, // Apple Hills
138 => Standard, // Loyalty Plaza
140 => Standard, // Revelers Road
142 => Standard, // Kitakami Hall
144 => Standard, // Oni Mountain
146 => Standard, // Dreaded Den
148 => Standard, // Onis Maw
150 => Standard, // Oni Mountain
152 => Standard, // Crystal Pool
154 => Standard, // Crystal Pool
156 => Standard, // Wistful Fields
158 => Standard, // Mossfell Confluence
160 => Standard, // Fellhorn Gorge
162 => Standard, // Paradise Barrens
164 => Standard, // Kitakami Wilds
166 => Standard, // Timeless Woods
168 => Standard, // Infernal Pass
170 => Standard, // Chilling Waterhead
174 => Standard, // Savanna Biome
176 => Standard, // Coastal Biome
178 => Standard, // Canyon Biome
180 => Snow, // Polar Biome
182 => Standard, // Central Plaza
184 => Standard, // Savanna Plaza
186 => Standard, // Coastal Plaza
188 => Standard, // Canyon Plaza
190 => Standard, // Polar Plaza
192 => Inside, // Chargestone Cavern
194 => Inside, // Torchlit Labyrinth
_ => None,
};
public bool CanSpawnInWeather(RibbonIndex mark) => Weather.IsMarkCompatible(mark);
#region Generating
@ -240,10 +153,7 @@ public EncounterMatchRating GetMatchRating(PKM pk)
// Some encounters can cross over into non-snow, and their encounter match might not cross back over to snow.
// Imagine a venn diagram, one circle is Desert, the other is Snow. The met location is in the middle, so both satisfy.
// But if we pick the Desert circle, it's wrong, and we need to defer to the other.
// Might need to add other deferral cases or maybe defer everything with a crossover location.
if (m.RibbonMarkSnowy && !CanSpawnInWeather(RibbonIndex.MarkSnowy))
return EncounterMatchRating.DeferredErrors;
if (m.RibbonMarkBlizzard && !CanSpawnInWeather(RibbonIndex.MarkBlizzard))
if (m.HasWeatherMark(out var weather) && !CanSpawnInWeather(weather))
return EncounterMatchRating.DeferredErrors;
}

View File

@ -35,7 +35,7 @@ public static bool IsEncounterMarkLost(IEncounterTemplate enc, PKM pk)
EncounterSlot8 or EncounterStatic8 { Gift: false, ScriptedNoMarks: false } => IsMarkAllowedSpecific8(mark, pk, enc),
EncounterSlot9 s => IsMarkAllowedSpecific9(mark, s),
EncounterStatic9 s => IsMarkAllowedSpecific9(mark, s),
EncounterOutbreak9 o when o.Ribbon == mark || IsMarkAllowedSpecific9(mark, pk) => true, // not guaranteed ribbon/mark
EncounterOutbreak9 o when o.Ribbon == mark || IsMarkAllowedSpecific9(mark, o) => true, // not guaranteed ribbon/mark
WC9 wc9 => wc9.GetRibbonIndex(mark),
_ => false,
};
@ -44,13 +44,13 @@ public static bool IsEncounterMarkLost(IEncounterTemplate enc, PKM pk)
/// Checks if a specific encounter mark is disallowed.
/// </summary>
/// <returns>False if mark is disallowed based on specific conditions.</returns>
public static bool IsMarkAllowedSpecific8(RibbonIndex mark, PKM pk, IEncounterTemplate x) => mark switch
public static bool IsMarkAllowedSpecific8(RibbonIndex mark, PKM pk, IEncounterTemplate enc) => mark switch
{
MarkCurry when !IsMarkAllowedCurry(pk, x) => false,
MarkFishing when !IsMarkAllowedFishing(x) => false,
MarkMisty when x.Generation == 8 && pk.MetLevel < EncounterArea8.BoostLevel && EncounterArea8.IsBoostedArea60Fog(pk.MetLocation) => false,
MarkDestiny => x is EncounterSlot9, // Capture on Birthday
>= MarkCloudy and <= MarkMisty => IsWeatherPermitted8(mark, x),
MarkCurry when !IsMarkAllowedCurry(pk, enc) => false,
MarkFishing when !IsMarkAllowedFishing(enc) => false,
MarkMisty when enc.Generation == 8 && pk.MetLevel < EncounterArea8.BoostLevel && EncounterArea8.IsBoostedArea60Fog(pk.MetLocation) => false,
MarkDestiny => enc is EncounterSlot9, // Capture on Birthday
>= MarkCloudy and <= MarkMisty => IsWeatherPermitted8(mark, enc),
_ => true,
};
@ -58,13 +58,13 @@ public static bool IsEncounterMarkLost(IEncounterTemplate enc, PKM pk)
/// Checks if a specific encounter mark is disallowed.
/// </summary>
/// <returns>False if mark is disallowed based on specific conditions.</returns>
public static bool IsMarkAllowedSpecific9(RibbonIndex mark, EncounterSlot9 x) => mark switch
public static bool IsMarkAllowedSpecific9(RibbonIndex mark, EncounterSlot9 enc) => mark switch
{
MarkCurry => false,
MarkFishing => false,
MarkDestiny => true, // Capture on Birthday
>= MarkLunchtime and <= MarkDawn => x.CanSpawnAtTime(mark),
>= MarkCloudy and <= MarkMisty => x.CanSpawnInWeather(mark),
>= MarkLunchtime and <= MarkDawn => enc.CanSpawnAtTime(mark),
>= MarkCloudy and <= MarkMisty => enc.CanSpawnInWeather(mark),
_ => true,
};
@ -73,13 +73,13 @@ public static bool IsEncounterMarkLost(IEncounterTemplate enc, PKM pk)
/// </summary>
/// <returns>False if mark is disallowed based on specific conditions.</returns>
/// <remarks>ONLY USE FOR <see cref="EncounterOutbreak9"/></remarks>
public static bool IsMarkAllowedSpecific9(RibbonIndex mark, PKM pk) => mark switch
public static bool IsMarkAllowedSpecific9(RibbonIndex mark, EncounterOutbreak9 enc) => mark switch
{
MarkCurry => false,
MarkFishing => false,
MarkDestiny => true, // Capture on Birthday
>= MarkLunchtime and <= MarkDawn => true, // no time restrictions
>= MarkCloudy and <= MarkMisty => pk is PK8 || EncounterSlot9.CanSpawnInWeather(mark, (byte)pk.MetLocation),
>= MarkCloudy and <= MarkMisty => enc.CanSpawnInWeather(mark),
_ => true,
};