Misc tweaks

Overworld8a: acknowledge Nature request
8U/8N: remove unnecessary auto-mint (not applied anywhere else)
Nature: use extension properties, use the `IsFixed` check throughout codebase
Wallpaper: add PLA default to pasture (not-obvious prior behavior was removed in refactor).
Tests: fix ck3 file with OT trash bytes (now cleared)
Tests: fix pk3 file with OT trash bytes now passes (added 1 trash pattern, future work)
Trash3: initial stubs for default OT trash recognition (one included to pass above ^)
This commit is contained in:
Kurt 2026-03-18 01:17:17 -05:00
parent b1dbc6a82b
commit 3c1e7bdc6c
30 changed files with 123 additions and 52 deletions

View File

@ -342,12 +342,12 @@ private bool ParseLineNature(ReadOnlySpan<char> input, ReadOnlySpan<string> natu
return false;
var nature = (Nature)index;
if (!nature.IsFixed())
if (!nature.IsFixed)
{
LogError(NatureUnrecognized, input);
return false;
}
if (Nature != Nature.Random && Nature != nature)
if (Nature.IsFixed && Nature != nature)
{
LogError(NatureAlreadySpecified, input);
return false;
@ -627,7 +627,7 @@ private void AddEVs(List<string> result, in BattleTemplateExportSettings setting
BattleTemplateToken.EVsAppendNature => GetStringStatsNatureAmp(EVs, 0, nameEVs, Nature),
_ => GetStringStats(EVs, 0, nameEVs),
};
if (token is BattleTemplateToken.EVsAppendNature && Nature.IsFixed())
if (token is BattleTemplateToken.EVsAppendNature && Nature.IsFixed)
line += $" ({settings.Localization.Strings.natures[(int)Nature]})";
result.Add(cfg.Push(BattleTemplateToken.EVs, line));
}
@ -1081,7 +1081,7 @@ private bool ParseLineEVs(ReadOnlySpan<char> line, BattleTemplateLocalization lo
return false; // invalid line
}
if (Nature != Nature.Random) // specified in a separate Nature line
if (Nature.IsFixed) // specified in a separate Nature line
LogError(NatureEffortAmpAlreadySpecified, natureName);
else
Nature = (Nature)natureIndex;
@ -1100,7 +1100,7 @@ private bool ParseLineEVs(ReadOnlySpan<char> line, BattleTemplateLocalization lo
result.TreatAmpsAsSpeedNotLast();
var ampNature = AdjustNature(result.Plus, result.Minus);
success &= ampNature;
if (ampNature && currentNature != Nature.Random && currentNature != Nature)
if (ampNature && currentNature.IsFixed && currentNature != Nature)
{
LogError(NatureEffortAmpConflictNature);
Nature = currentNature; // revert to original

View File

@ -137,7 +137,7 @@ public bool SetUnshiny()
/// <param name="nature">Desired <see cref="PKM.Nature"/> value to set.</param>
public void SetNature(Nature nature)
{
if (!nature.IsFixed())
if (!nature.IsFixed)
nature = 0; // default valid
var format = pk.Format;

View File

@ -55,7 +55,7 @@ public static class NatureUtil
/// Checks if the provided <see cref="value"/> is a valid stored <see cref="Nature"/> value.
/// </summary>
/// <returns>True if value is an actual nature.</returns>
public bool IsFixed() => value < Nature.Random;
public bool IsFixed => value != Nature.Random;
/// <summary>
/// Checks if the provided <see cref="value"/> is a possible mint nature.
@ -63,12 +63,12 @@ public static class NatureUtil
/// <remarks>
/// The only valid mint natures are those which have a stat amp applied, or neutral nature being Serious.
/// </remarks>
public bool IsMint() => (value.IsFixed() && (byte)value % 6 != 0) || value == Nature.Serious;
public bool IsMint => (value.IsFixed && (byte)value % 6 != 0) || value == Nature.Serious;
/// <summary>
/// Checks if the provided <see cref="value"/> is a neutral nature which has no stat amps applied.
/// </summary>
public bool IsNeutral() => value.IsFixed() && (byte)value % 6 == 0;
public bool IsNeutral => value.IsFixed && (byte)value % 6 == 0;
/// <summary>
/// Converts the provided <see cref="value"/> to a neutral nature.

View File

@ -112,7 +112,7 @@ public EncounterCriteria()
/// Determines whether a specific Nature is specified in the criteria or if complex nature mutations are allowed.
/// </summary>
/// <returns>><see langword="true"/> if a Nature is specified or complex nature mutations are allowed; otherwise, <see langword="false"/>.</returns>
public bool IsSpecifiedNature() => Nature != Nature.Random || Mutations.IsComplexNature();
public bool IsSpecifiedNature() => Nature.IsFixed || Mutations.IsComplexNature();
/// <summary>
/// Determines whether a level range is specified in the criteria.
@ -191,7 +191,7 @@ public int GetCountSpecifiedIVs() => Convert.ToInt32(IV_HP != RandomIV)
public bool IsSatisfiedNature(Nature nature)
{
if (Mutations.HasFlag(AllowOnlyNeutralNature))
return nature.IsNeutral();
return nature.IsNeutral;
if (Nature == Nature.Random)
return true;
return nature == Nature || Mutations.HasFlag(CanMintNature);
@ -300,7 +300,7 @@ public Nature GetNature(Nature encValue)
/// </summary>
public Nature GetNature()
{
if (Nature != Nature.Random)
if (Nature.IsFixed)
return Nature;
var result = (Nature)Util.Rand.Next(25);
if (Mutations.HasFlag(AllowOnlyNeutralNature))

View File

@ -155,7 +155,7 @@ private bool IsMatchNatureGenderShiny(PKM pk)
return false;
if (Gender != FixedGenderUtil.GenderRandom && Gender != pk.Gender)
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
return true;
}

View File

@ -153,7 +153,7 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo)
return false;
if (IVs.IsSpecified && !Legal.GetIsFixedIVSequenceValidSkipRand(IVs, pk))
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
if (FlawlessIVCount != 0 && pk.FlawlessIVCount < FlawlessIVCount)
return false;

View File

@ -167,7 +167,7 @@ private bool IsMatchNatureGenderShiny(PKM pk)
return false;
if (Gender != pk.Gender)
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
return true;
}

View File

@ -145,7 +145,7 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo)
return false;
if (IVs.IsSpecified && !Legal.GetIsFixedIVSequenceValidSkipRand(IVs, pk))
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
if (FlawlessIVCount != 0 && pk.FlawlessIVCount < FlawlessIVCount)
return false;

View File

@ -166,7 +166,7 @@ private bool IsMatchNatureGenderShiny(PKM pk)
return false;
if (Gender != pk.Gender)
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
return true;
}

View File

@ -122,8 +122,6 @@ protected virtual void SetPINGA(PK8 pk, in EncounterCriteria criteria, PersonalI
}
FinishCorrelation(pk, seed);
if (criteria.IsSpecifiedNature() && criteria.Nature != pk.Nature && criteria.Nature.IsMint())
pk.StatNature = criteria.Nature;
}
protected GenerateParam8 GetParam() => GetParam(Info);

View File

@ -78,8 +78,6 @@ protected override void SetPINGA(PK8 pk, in EncounterCriteria criteria, Personal
}
FinishCorrelation(pk, seed);
if (criteria.IsSpecifiedNature() && criteria.Nature != pk.Nature && criteria.Nature.IsMint())
pk.StatNature = criteria.Nature;
if (criteria.Shiny.IsShiny())
pk.PID = ShinyUtil.GetShinyPID(pk.TID16, pk.SID16, pk.PID, ShinyXor);
}

View File

@ -209,7 +209,7 @@ private bool IsMatchNatureGenderShiny(PKM pk)
{
if (!Shiny.IsValid(pk))
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
if (Gender != FixedGenderUtil.GenderRandom && pk.Gender != Gender)
return false;

View File

@ -154,7 +154,7 @@ private void SetPINGA(PK9 pk, in EncounterCriteria criteria, PersonalInfo9SV pi)
if (Gender != FixedGenderUtil.GenderRandom)
pk.Gender = Gender;
if (Nature != Nature.Random)
if (Nature.IsFixed)
pk.Nature = pk.StatNature = Nature;
}
#endregion
@ -178,7 +178,7 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo)
return false;
if (TeraType != GemType.Random && pk is ITeraType t && !Tera9RNG.IsMatchTeraType(TeraType, Species, Form, (byte)t.TeraTypeOriginal))
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
return true;

View File

@ -158,7 +158,7 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo)
return false;
if (FlawlessIVCount != 0 && pk.FlawlessIVCount < FlawlessIVCount)
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
if (pk is IAlphaReadOnly a && a.IsAlpha != IsAlpha)
return false;

View File

@ -135,7 +135,7 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo)
return false;
if (FlawlessIVCount != 0 && pk.FlawlessIVCount < FlawlessIVCount)
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
if (pk is IAlphaReadOnly a && a.IsAlpha != IsAlpha)
return false;

View File

@ -155,7 +155,7 @@ private bool IsMatchNatureGenderShiny(PKM pk)
return false;
if (pk.Gender != Gender)
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
return true;
}
@ -192,7 +192,7 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo)
return false;
if (FlawlessIVCount != 0 && pk.FlawlessIVCount < FlawlessIVCount)
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
if (pk is IAlphaReadOnly a && a.IsAlpha != IsAlpha)
return false;

View File

@ -104,7 +104,7 @@ public static bool GenerateData(PA9 pk, in GenerateParam9a enc, in EncounterCrit
return false;
pk.Gender = gender;
var nature = enc.Nature != Nature.Random ? enc.Nature
var nature = enc.Nature.IsFixed ? enc.Nature
: (Nature)rand.NextInt(25);
// Compromise on Nature -- some are fixed, some are random. If the request wants a specific nature, just mint it.
@ -264,7 +264,7 @@ private static bool IsMatchIVsAndFollowing(PKM pk, in GenerateParam9a enc, Xoros
if (pk.Gender != gender)
return false;
var nature = enc.Nature != Nature.Random ? enc.Nature
var nature = enc.Nature.IsFixed ? enc.Nature
: (Nature)rand.NextInt(25);
if (pk.Nature != nature)
return false;

View File

@ -13,5 +13,5 @@ public interface IFixedNature
/// <summary>
/// Indicates if the nature is a single value (not random).
/// </summary>
bool IsFixedNature => Nature != Nature.Random;
bool IsFixedNature => Nature.IsFixed;
}

View File

@ -114,7 +114,7 @@ public static bool Verify(PKM pk, ulong seed, Span<int> ivs, in GenerateParam8 p
break;
}
var nature = param.Nature != Nature.Random ? param.Nature
var nature = param.Nature.IsFixed ? param.Nature
: param.Species == (int)Species.Toxtricity
? ToxtricityUtil.GetRandomNature(ref rng, pk.Form)
: (Nature)rng.NextInt(25);
@ -236,7 +236,7 @@ public static bool TryApply(PK8 pk, ulong seed, Span<int> ivs, in GenerateParam8
return false;
pk.Gender = gender;
var nature = param.Nature != Nature.Random ? param.Nature
var nature = param.Nature.IsFixed ? param.Nature
: param.Species == (int)Species.Toxtricity
? ToxtricityUtil.GetRandomNature(ref rng, pk.Form)
: (Nature)rng.NextInt(25);

View File

@ -172,6 +172,8 @@ public static bool TryApplyFromSeed(PA8 pk, in EncounterCriteria criteria, in Ov
pk.Gender = gender;
var nature = (Nature)rand.NextInt(25);
if (criteria.IsSpecifiedNature() && !criteria.IsSatisfiedNature(nature))
return false;
pk.Nature = pk.StatNature = nature;
var (height, weight) = para.IsAlpha
@ -179,16 +181,10 @@ public static bool TryApplyFromSeed(PA8 pk, in EncounterCriteria criteria, in Ov
: ((byte)(rand.NextInt(0x81) + rand.NextInt(0x80)),
(byte)(rand.NextInt(0x81) + rand.NextInt(0x80)));
if (pk is IScaledSize s)
{
s.HeightScalar = height;
s.WeightScalar = weight;
if (pk is IScaledSizeValue a)
{
a.ResetHeight();
a.ResetWeight();
}
}
pk.HeightScalar = height;
pk.WeightScalar = weight;
pk.ResetHeight();
pk.ResetWeight();
return true;
}

View File

@ -121,7 +121,7 @@ public static bool GenerateData(PK9 pk, in GenerateParam9 enc, in EncounterCrite
return false;
pk.Gender = gender;
var nature = enc.Nature != Nature.Random ? enc.Nature : enc.Species == (int)Species.Toxtricity
var nature = enc.Nature.IsFixed ? enc.Nature : enc.Species == (int)Species.Toxtricity
? ToxtricityUtil.GetRandomNature(ref rand, pk.Form)
: (Nature)rand.NextInt(25);
@ -205,7 +205,7 @@ public static bool IsMatch(PKM pk, in GenerateParam9 enc, in ulong seed)
if (pk.Gender != gender)
return false;
var nature = enc.Nature != Nature.Random ? enc.Nature : enc.Species == (int)Species.Toxtricity
var nature = enc.Nature.IsFixed ? enc.Nature : enc.Species == (int)Species.Toxtricity
? ToxtricityUtil.GetRandomNature(ref rand, pk.Form)
: (Nature)rand.NextInt(25);
if (pk.Nature != nature)

View File

@ -122,7 +122,10 @@ private static void VerifyTrashINT(LegalityAnalysis data, PK3 pk)
var trash = pk.OriginalTrainerTrash;
// OT name from save file is copied byte-for-byte. All 8 bytes are initialized to FF on new game.
if (!TrashByteRules3.IsTerminatedFFZero(trash, 7))
data.AddLine(GetInvalid(Trainer, TrashBytesMissingTerminatorFinal));
{
if (!TrashByteRules3.IsTrashPatternDefaultTrainer(trash, pk.Version, (LanguageID)pk.Language))
data.AddLine(GetInvalid(Trainer, TrashBytesMissingTerminatorFinal));
}
// Nickname can be all FF's (nicknamed) or whatever random garbage is in the buffer before filling. Unsure if we can reliably check this, but it should be "dirty" usually.
// If it is clean, flag as fishy.
FlagIsNicknameClean(data, pk);
@ -214,4 +217,61 @@ public static bool IsTerminatedFFZero(ReadOnlySpan<byte> data, int preFill = 0)
}
return !data[first..].ContainsAnyExcept<byte>(0);
}
// TRASH BYTES: New Game Default OTs
// Default OT names in International (not JPN) Gen3 mainline games memcpy 7 chars and FF from the "default OT name" table, regardless of strlen.
// Copied strings therefore contain trash from the next string entry that is encoded into the rom's string table.
// Below is a list of possible (version, language, trash) default OTs, as initialized by the game. An `*` is used to denote the terminator.
/// <summary>
/// Checks if the specified trash byte pattern matches a default trainer name pattern for the given game <see cref="version"/> and <see cref="language"/>.
/// </summary>
/// <remarks>Default trainer names in certain Generation 3 Pokémon games may include trailing bytes ("trash") due to how names are stored in the game's ROM.
/// This method checks if the provided pattern matches any of these known default patterns for the specified version and language.
/// </remarks>
public static bool IsTrashPatternDefaultTrainer(ReadOnlySpan<byte> trash, GameVersion version, LanguageID language) => version switch
{
GameVersion.R or GameVersion.S => IsTrashPatternDefaultTrainerRS(trash, language),
GameVersion.E => IsTrashPatternDefaultTrainerE(trash, language),
GameVersion.FR => IsTrashPatternDefaultTrainerFR(trash, language),
GameVersion.LG => IsTrashPatternDefaultTrainerLG(trash, language),
_ => false,
};
/// <inheritdoc cref="IsTrashPatternDefaultTrainer"/>
/// <remarks>Default OT names present in <see cref="GameVersion.R"/> and <see cref="GameVersion.S"/> based on the language of the game.</remarks>
public static bool IsTrashPatternDefaultTrainerRS(ReadOnlySpan<byte> trash, LanguageID language) => language switch
{
// TODO
LanguageID.English => trash switch
{
[0xCD, 0xBB, 0xCC, 0xBB, 0xFF, 0xCE, 0xDC] => true, // SARA*Th
_ => false,
},
_ => false,
};
/// <inheritdoc cref="IsTrashPatternDefaultTrainer"/>
/// <remarks>Default OT names present in <see cref="GameVersion.E"/> based on the language of the game.</remarks>
public static bool IsTrashPatternDefaultTrainerE(ReadOnlySpan<byte> trash, LanguageID language) => language switch
{
// TODO
_ => false,
};
/// <inheritdoc cref="IsTrashPatternDefaultTrainer"/>
/// <remarks>Default OT names present in <see cref="GameVersion.FR"/> based on the language of the game.</remarks>
public static bool IsTrashPatternDefaultTrainerFR(ReadOnlySpan<byte> trash, LanguageID language) => language switch
{
// TODO
_ => false,
};
/// <inheritdoc cref="IsTrashPatternDefaultTrainer"/>
/// <remarks>Default OT names present in <see cref="GameVersion.LG"/> based on the language of the game.</remarks>
public static bool IsTrashPatternDefaultTrainerLG(ReadOnlySpan<byte> trash, LanguageID language) => language switch
{
// TODO
_ => false,
};
}

View File

@ -17,7 +17,7 @@ internal static void VerifyStatNature(LegalityAnalysis data, PKM pk)
return;
// Must be a valid mint nature.
if (!statNature.IsMint())
if (!statNature.IsMint)
data.AddLine(Get(Invalid, Misc, StatNatureInvalid));
}

View File

@ -24,7 +24,7 @@ public override void Verify(LegalityAnalysis data)
if (pk.PID == 0)
data.AddLine(Get(Severity.Fishy, PIDZero));
if (!pk.Nature.IsFixed()) // out of range
if (!pk.Nature.IsFixed) // out of range
data.AddLine(GetInvalid(PIDNatureMismatch));
if (data.Info.EncounterMatch is IEncounterEgg egg)
VerifyEggPID(data, pk, egg);

View File

@ -626,7 +626,7 @@ public override bool IsMatchExact(PKM pk, EvoCriteria evo)
if (MetLevel != 0 && MetLevel != pk.MetLevel) return false;
if ((Ball == 0 ? 4 : Ball) != pk.Ball) return false;
if (OTGender < 2 && OTGender != pk.OriginalTrainerGender) return false;
if (Nature != Nature.Random && pk.Nature != Nature) return false;
if (Nature.IsFixed && pk.Nature != Nature) return false;
if (Gender != 3 && Gender != pk.Gender) return false;
if (pk is IScaledSize s)

View File

@ -168,7 +168,7 @@ private bool SearchSimple(PKM pk)
return false;
if (Ability > -1 && pk.Ability != Ability)
return false;
if (Nature.IsFixed() && pk.StatNature != Nature)
if (Nature.IsFixed && pk.StatNature != Nature)
return false;
if (Item > -1 && pk.HeldItem != Item)
return false;

View File

@ -6,13 +6,21 @@ namespace PKHeX.Core;
/// <summary>
/// Generation 3 <see cref="SaveFile"/> object for Pokémon Colosseum saves.
/// </summary>
public sealed class SAV3Colosseum : SaveFile, IGCSaveFile, IBoxDetailName, IDaycareStorage, IDaycareExperience, IGCRegion
public sealed class SAV3Colosseum : SaveFile, IGCSaveFile, IBoxDetailName, IDaycareStorage, IDaycareExperience, IGCRegion, ISaveFileRevision
{
protected internal override string ShortSummary => $"{OT} ({Version}) - {PlayTimeString}";
public override string Extension => this.GCExtension();
public override PersonalTable3 Personal => PersonalTable.RS;
public override ReadOnlySpan<ushort> HeldItems => Legal.HeldItems_RS;
public SAV3GCMemoryCard? MemoryCard { get; init; }
public int SaveRevision => 0;
public string SaveRevisionString => OriginalRegion switch
{
GCRegion.NTSC_J => "-J",
GCRegion.NTSC_U => "-U",
GCRegion.PAL => "-PAL",
_ => "-?",
};
private readonly Memory<byte> Container;
protected override Span<byte> BoxBuffer => Data;

View File

@ -7,9 +7,18 @@ namespace PKHeX.Core;
/// <summary>
/// Generation 3 <see cref="SaveFile"/> object for Pokémon XD saves.
/// </summary>
public sealed class SAV3XD : SaveFile, IGCSaveFile, IBoxDetailName, IDaycareStorage, IDaycareExperience, IGCRegion
public sealed class SAV3XD : SaveFile, IGCSaveFile, IBoxDetailName, IDaycareStorage, IDaycareExperience, IGCRegion, ISaveFileRevision
{
protected internal override string ShortSummary => $"{OT} ({Version}) {PlayTimeString}";
public int SaveRevision => 0;
public string SaveRevisionString => OriginalRegion switch
{
GCRegion.NTSC_J => "-J",
GCRegion.NTSC_U => "-U",
GCRegion.PAL => "-PAL",
_ => "-?",
};
public override string Extension => this.GCExtension();
public SAV3GCMemoryCard? MemoryCard { get; init; }

View File

@ -28,6 +28,8 @@ private static Bitmap GetWallpaper(SaveFile sav, int box)
// City box wallpaper for Lumiose City
if (sav is SAV9ZA)
return Resources.box_wp02bdsp;
if (sav is SAV8LA) // pasture for PLA
return Resources.box_wp01bdsp;
int wallpaper = wp.GetBoxWallpaper(box);
string s = GetWallpaperResourceName(sav.Version, wallpaper);