Add HoneyTree API

This commit is contained in:
Kurt 2023-09-28 16:40:23 -07:00
parent 963eaaaa40
commit 96e74a05d0
18 changed files with 214 additions and 66 deletions

View file

@ -4,6 +4,9 @@ namespace PKHeX.Core;
internal static class Locations8b
{
public static bool IsUnderground(ushort location) => location is (>= 508 and <= 617);
public static bool IsMarsh(ushort location) => location is (>= 219 and <= 224);
public static ReadOnlySpan<ushort> Met0 => new ushort[]
{
000, 001, 002, 003, 004, 005, 006, 007, 008, 009,

View file

@ -194,14 +194,17 @@ public sealed record EncounterCriteria : IFixedNature, IFixedGender, IFixedAbili
/// Applies random IVs without any correlation.
/// </summary>
/// <param name="pk">Entity to mutate.</param>
public void SetRandomIVs(PKM pk)
public void SetRandomIVs(PKM pk) => SetRandomIVs(pk, Util.Rand);
/// <inheritdoc cref="SetRandomIVs(PKM)"/>
public void SetRandomIVs(PKM pk, Random rnd)
{
pk.IV_HP = IV_HP != RandomIV ? IV_HP : Util.Rand.Next(32);
pk.IV_ATK = IV_ATK != RandomIV ? IV_ATK : Util.Rand.Next(32);
pk.IV_DEF = IV_DEF != RandomIV ? IV_DEF : Util.Rand.Next(32);
pk.IV_SPA = IV_SPA != RandomIV ? IV_SPA : Util.Rand.Next(32);
pk.IV_SPD = IV_SPD != RandomIV ? IV_SPD : Util.Rand.Next(32);
pk.IV_SPE = IV_SPE != RandomIV ? IV_SPE : Util.Rand.Next(32);
pk.IV_HP = IV_HP != RandomIV ? IV_HP : rnd.Next(32);
pk.IV_ATK = IV_ATK != RandomIV ? IV_ATK : rnd.Next(32);
pk.IV_DEF = IV_DEF != RandomIV ? IV_DEF : rnd.Next(32);
pk.IV_SPA = IV_SPA != RandomIV ? IV_SPA : rnd.Next(32);
pk.IV_SPD = IV_SPD != RandomIV ? IV_SPD : rnd.Next(32);
pk.IV_SPE = IV_SPE != RandomIV ? IV_SPE : rnd.Next(32);
}
/// <summary>

View file

@ -73,16 +73,17 @@ public sealed record EncounterArea4 : IEncounterArea<EncounterSlot4>, ISlotRNGTy
{
// We didn't encode the honey tree index to the encounter slot resource.
// Check if any of the slot's location doesn't match any of the groupC trees' area location ID.
var trees = SAV4Sinnoh.CalculateMunchlaxTrees(pk.TID16, pk.SID16);
Span<byte> trees = stackalloc byte[4];
HoneyTreeUtil.CalculateMunchlaxTrees(pk.ID32, trees);
return IsMunchlaxTree(trees, location);
}
private static bool IsMunchlaxTree(in MunchlaxTreeSet4 trees, ushort location)
private static bool IsMunchlaxTree(ReadOnlySpan<byte> trees, ushort location)
{
return LocationID_HoneyTree[trees.Tree1] == location
|| LocationID_HoneyTree[trees.Tree2] == location
|| LocationID_HoneyTree[trees.Tree3] == location
|| LocationID_HoneyTree[trees.Tree4] == location;
return LocationID_HoneyTree[trees[0]] == location
|| LocationID_HoneyTree[trees[1]] == location
|| LocationID_HoneyTree[trees[2]] == location
|| LocationID_HoneyTree[trees[3]] == location;
}
private static ReadOnlySpan<byte> LocationID_HoneyTree => new byte[]

View file

@ -142,7 +142,7 @@ public abstract record EncounterStatic8Nest<T>(GameVersion Version)
if (pk is IRibbonSetMark8 { HasMarkEncounter8: true })
return false;
if (pk.Species == (int)Core.Species.Shedinja && pk is IRibbonSetAffixed { AffixedRibbon: >= (int)RibbonIndex.MarkLunchtime })
if (pk.Species == (int)Core.Species.Shedinja && pk is IRibbonSetAffixed { AffixedRibbon: >= (int)RibbonIndex.MarkLunchtime and <= (int)RibbonIndex.MarkSlump })
return false;
if (!IsMatchEggLocation(pk))

View file

@ -88,16 +88,17 @@ public sealed record EncounterArea8b : IEncounterArea<EncounterSlot8b>, IAreaLoc
{
// We didn't encode the honey tree index to the encounter slot resource.
// Check if any of the slot's location doesn't match any of the groupC trees' area location ID.
var trees = SAV4Sinnoh.CalculateMunchlaxTrees(pk.TID16, pk.SID16);
Span<byte> trees = stackalloc byte[4];
HoneyTreeUtil.CalculateMunchlaxTrees(pk.ID32, trees);
return IsMunchlaxTree(trees, location);
}
private static bool IsMunchlaxTree(in MunchlaxTreeSet4 trees, ushort location)
private static bool IsMunchlaxTree(ReadOnlySpan<byte> trees, ushort location)
{
return LocationID_HoneyTree[trees.Tree1] == location
|| LocationID_HoneyTree[trees.Tree2] == location
|| LocationID_HoneyTree[trees.Tree3] == location
|| LocationID_HoneyTree[trees.Tree4] == location;
return LocationID_HoneyTree[trees[0]] == location
|| LocationID_HoneyTree[trees[1]] == location
|| LocationID_HoneyTree[trees[2]] == location
|| LocationID_HoneyTree[trees[3]] == location;
}
private static ReadOnlySpan<ushort> LocationID_HoneyTree => new ushort[]

View file

@ -14,8 +14,8 @@ public sealed record EncounterSlot8b(EncounterArea8b Parent, ushort Species, byt
public Shiny Shiny => Shiny.Random;
public bool IsShiny => false;
public int EggLocation => 0;
public bool IsUnderground => Parent.Location is (>= 508 and <= 617);
public bool IsMarsh => Parent.Location is (>= 219 and <= 224);
public bool IsUnderground => Locations8b.IsUnderground(Parent.Location);
public bool IsMarsh => Locations8b.IsMarsh(Parent.Location);
public Ball FixedBall => GetRequiredBall();
private Ball GetRequiredBall(Ball fallback = Ball.None) => IsMarsh ? Ball.Safari : fallback;

View file

@ -84,6 +84,17 @@ public static class LCRNG
[MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint Prev8(uint seed) => (seed * rMult8) + rAdd8;
[MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint Prev9(uint seed) => (seed * rMult9) + rAdd9;
/// <summary>
/// Gets the next 16 bits of the next RNG seed.
/// </summary>
/// <param name="seed">Seed to advance one step.</param>
/// <param name="result">Next seed, top 16 bits.</param>
public static void Next16(ref uint seed, out int result)
{
seed = Next(seed);
result = (int)(seed >> 16);
}
/// <summary>
/// Advances the RNG seed to the next state value a specified amount of times.
/// </summary>

View file

@ -16,7 +16,7 @@ public sealed class MarkVerifier : Verifier
if (pk is not IRibbonIndex m)
return;
if (!MarkRules.IsEncounterMarkAllowed(data)) // Shedinja doesn't copy Ribbons or Marks
if (!MarkRules.IsEncounterMarkAllowed(data.EncounterOriginal, data.Entity)) // Shedinja doesn't copy Ribbons or Marks
VerifyNoMarksPresent(data, m);
else
VerifyMarksPresent(data, m);
@ -93,7 +93,7 @@ public sealed class MarkVerifier : Verifier
if (m is not PKM pk)
return;
if (MarkRules.IsEncounterMarkLost(data))
if (MarkRules.IsEncounterMarkLost(data.EncounterOriginal, data.Entity))
{
VerifyShedinjaAffixed(data, affix, pk, m);
return;

View file

@ -11,20 +11,20 @@ public static class MarkRules
/// <summary>
/// Checks if an encounter-only mark is possible to obtain for the encounter, if not lost via data manipulation.
/// </summary>
public static bool IsEncounterMarkAllowed(LegalityAnalysis data)
public static bool IsEncounterMarkAllowed(IEncounterTemplate enc, PKM pk)
{
if (IsEncounterMarkLost(data))
if (IsEncounterMarkLost(enc, pk))
return false;
return data.Info.EncounterOriginal.Context is EntityContext.Gen8 or EntityContext.Gen9;
return enc.Context is EntityContext.Gen8 or EntityContext.Gen9;
}
/// <summary>
/// Checks if original marks and ribbons are lost via data manipulation.
/// </summary>
public static bool IsEncounterMarkLost(LegalityAnalysis data)
public static bool IsEncounterMarkLost(IEncounterTemplate enc, PKM pk)
{
// Nincada -> Shedinja loses all ribbons and marks, but does not purge any Affixed Ribbon value.
return data.EncounterOriginal.Species is (int)Species.Nincada && data.Entity.Species == (int)Species.Shedinja;
return enc.Species is (int)Species.Nincada && pk.Species == (int)Species.Shedinja;
}
/// <summary>

View file

@ -3,7 +3,7 @@ using static PKHeX.Core.RibbonIndex;
namespace PKHeX.Core;
/// <summary>
/// Parsing logic for <see cref="IRibbonSetCommon8"/>.
/// Parsing logic for <see cref="IRibbonSetMark9"/>.
/// </summary>
public static class RibbonVerifierMark9
{

View file

@ -224,7 +224,7 @@ public sealed class PKH : PKM, IHandlerLanguage, IFormArgument, IHomeTrack, IBat
#region Maximums
public override int MaxIV => 31;
public override int MaxEV => 252;
public override int MaxEV => EffortValues.Max252;
public override int MaxStringLengthOT => 12;
public override int MaxStringLengthNickname => 12;
public override ushort MaxMoveID => Legal.MaxMoveID_8a;

View file

@ -21,7 +21,7 @@ public abstract class G3PKM : PKM, IRibbonSetEvent3, IRibbonSetCommon3, IRibbonS
public sealed override int MaxBallID => Legal.MaxBallID_3;
public sealed override int MaxGameID => Legal.MaxGameID_3;
public sealed override int MaxIV => 31;
public sealed override int MaxEV => 255;
public sealed override int MaxEV => EffortValues.Max255;
public sealed override int MaxStringLengthOT => 7;
public sealed override int MaxStringLengthNickname => 10;

View file

@ -100,7 +100,7 @@ public abstract class G6PKM : PKM, ISanityChecksum
// Maximums
public sealed override int MaxIV => 31;
public sealed override int MaxEV => 252;
public sealed override int MaxEV => EffortValues.Max252;
public sealed override int MaxStringLengthOT => 12;
public sealed override int MaxStringLengthNickname => 12;
}

View file

@ -0,0 +1,152 @@
using System;
using static System.Buffers.Binary.BinaryPrimitives;
using static PKHeX.Core.HoneyTreeSlotGroup;
namespace PKHeX.Core;
/// <summary>
/// Utility class for calculating honey trees for <see cref="GameVersion.DPPt"/> and <see cref="GameVersion.BDSP"/>.
/// </summary>
public static class HoneyTreeUtil
{
/// <summary>
/// Number of possible honey trees that exist in the game.
/// </summary>
private const byte HoneyTreeCount = 21;
/// <summary>
/// Populates the given span with the 4 possible honey trees for the given ID.
/// </summary>
/// <param name="id">The 32-bit Trainer ID to calculate the trees for.</param>
/// <param name="result">Result span that will be populated with the 4 possible trees.</param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public static void CalculateMunchlaxTrees(uint id, Span<byte> result)
{
if (result.Length != sizeof(uint))
throw new ArgumentOutOfRangeException(nameof(result));
WriteUInt32BigEndian(result, id);
foreach (ref var b in result)
b %= HoneyTreeCount;
AdjustOverlap(result);
}
private static void AdjustOverlap(Span<byte> result)
{
// If a tree is the same as any previous one, increment it and reset to 0 if it overflows the max.
// Yields 3-4 unique trees per ID. 1935328924d => {10,6,9,10}
// The original intent was likely to have 4 rare trees per save file; if so, the implementation was flawed.
// (the inner loop should have restarted if it detected a duplicate, instead of continuing to the next tree)
for (int i = 1; i < result.Length; i++)
{
ref var b = ref result[i];
for (int j = 0; j < i; j++)
{
if (b != result[j])
continue;
b++;
if (b >= HoneyTreeCount)
b = 0;
}
}
}
// There aren't any RNG considerations for Munchlax/etc encounter slot legality.
// The RNG calls for populating the tree are disjointed from the RNG calls for generating the Entity upon encounter.
// Group -> Slot -> Shakes
/// <summary>
/// Calculates the honey tree result for the given seed when the tree elapses.
/// </summary>
/// <param name="seed">Current RNG state seed of the game.</param>
/// <param name="isMunchlaxTree">If the tree is a "rare" tree based on <see cref="CalculateMunchlaxTrees"/>.</param>"/>
public static (HoneyTreeSlotGroup Group, int Slot, int Shakes) GetHoneyTreeResult(uint seed, bool isMunchlaxTree)
{
LCRNG.Next16(ref seed, out var randGroup);
LCRNG.Next16(ref seed, out var randSlot);
LCRNG.Next16(ref seed, out var randShakes);
var group = GetHoneyTreeGroup(randGroup, isMunchlaxTree);
var slot = GetHoneyTreeSlotIndex(randSlot);
var shakes = GetShakeCount(randShakes, group);
return (group, slot, shakes);
}
/// <summary>
/// Indicates which slot group rarity inhabits a honey tree.
/// </summary>
public static HoneyTreeSlotGroup GetHoneyTreeGroup(int rnd, bool isMunchlaxTree) => isMunchlaxTree switch
{
true => rnd switch
{
0 => Munchlax, // 1%
< 10 => None, // 9% fail
< 30 => Common, // 20%
_ => Rare, // 70%
},
_ => rnd switch
{
< 10 => None, // 10% fail
< 30 => Rare, // 20%
_ => Common, // 70%
},
};
/// <summary>
/// Indicates which slot index of the group inhabits in the honey tree.
/// </summary>
public static int GetHoneyTreeSlotIndex(int rnd) => rnd switch
{
< 05 => 5, // 5%
< 10 => 4, // 5%
< 20 => 3, // 10%
< 40 => 2, // 20%
< 60 => 1, // 20%
_ => 0, // 20%
};
/// <summary>
/// Indicates how many times the tree shakes before the encounter.
/// </summary>
public static int GetShakeCount(int rnd, HoneyTreeSlotGroup group) => group switch
{
Common => rnd switch
{
< 19 => 2, // 19%
< 79 => 1, // 60%
< 99 => 0, // 20%
_ => 3, // 1%
},
Rare => rnd switch
{
< 75 => 2, // 75%
< 95 => 1, // 20%
95 => 0, // 1%
_ => 3, // 4%
},
Munchlax => rnd switch
{
< 5 => 2, // 5%
5 => 1, // 1%
6 => 0, // 1%
_ => 3, // 93%
},
_ => 0,
};
}
/// <summary>
/// Indicates which slot group rarity inhabits a honey tree.
/// </summary>
public enum HoneyTreeSlotGroup : byte
{
/// <summary> No species inhabits the tree. </summary>
None,
/// <summary> Common species inhabit the tree. </summary>
Common,
/// <summary> Rare species inhabit the tree. </summary>
Rare,
/// <summary> Munchlax inhabits the tree. </summary>
Munchlax,
}

View file

@ -12,7 +12,7 @@ public sealed class SAV2Stadium : SAV_STADIUM
public override string SaveRevisionString => Japanese ? "J" : "U";
public override PersonalTable2 Personal => PersonalTable.C;
public override int MaxEV => ushort.MaxValue;
public override int MaxEV => EffortValues.Max12;
public override ReadOnlySpan<ushort> HeldItems => Legal.HeldItems_GSC;
public override GameVersion Version { get; protected set; } = GameVersion.Stadium2;

View file

@ -77,7 +77,7 @@ public abstract class SAV4 : SaveFile, IEventFlag37
public sealed override Type PKMType => typeof(PK4);
public sealed override int BoxCount => 18;
public sealed override int MaxEV => 255;
public sealed override int MaxEV => EffortValues.Max255;
public sealed override int Generation => 4;
public override EntityContext Context => EntityContext.Gen4;
public int EventFlagCount => 0xB60; // 2912

View file

@ -125,25 +125,6 @@ public abstract class SAV4Sinnoh : SAV4
public HoneyTreeValue GetHoneyTree(int index) => new(GetHoneyTreeSpan(index).ToArray());
public void SetHoneyTree(HoneyTreeValue tree, int index) => SetData(GetHoneyTreeSpan(index), tree.Data);
public MunchlaxTreeSet4 GetMunchlaxTrees() => CalculateMunchlaxTrees(TID16, SID16);
public static MunchlaxTreeSet4 CalculateMunchlaxTrees(ushort tid, ushort sid)
{
int A = (tid >> 8) % 21;
int B = (tid & 0x00FF) % 21;
int C = (sid >> 8) % 21;
int D = (sid & 0x00FF) % 21;
if (A == B) B = (B + 1) % 21;
if (A == C) C = (C + 1) % 21;
if (B == C) C = (C + 1) % 21;
if (A == D) D = (D + 1) % 21;
if (B == D) D = (D + 1) % 21;
if (C == D) D = (D + 1) % 21;
return new(A, B, C, D);
}
#endregion
public int OFS_PoffinCase { get; protected set; }
@ -227,8 +208,3 @@ public enum PoketchApp
Stopwatch,
Alarm_Clock,
}
public readonly record struct MunchlaxTreeSet4(int Tree1, int Tree2, int Tree3, int Tree4)
{
public bool Contains(int tree) => tree == Tree1 || tree == Tree2 || tree == Tree3 || tree == Tree4;
}

View file

@ -16,20 +16,21 @@ public partial class SAV_HoneyTree : Form
SAV = (SAV4Sinnoh)(Origin = sav).Clone();
// Get Munchlax tree for this savegame in screen
MunchlaxTrees = SAV.GetMunchlaxTrees();
MunchlaxTrees = new byte[4];
HoneyTreeUtil.CalculateMunchlaxTrees(SAV.ID32, MunchlaxTrees);
const string sep = "- ";
var names = CB_TreeList.Items;
L_Tree0.Text = string.Join(Environment.NewLine,
sep + names[MunchlaxTrees.Tree1],
sep + names[MunchlaxTrees.Tree2],
sep + names[MunchlaxTrees.Tree3],
sep + names[MunchlaxTrees.Tree4]);
sep + names[MunchlaxTrees[0]],
sep + names[MunchlaxTrees[1]],
sep + names[MunchlaxTrees[2]],
sep + names[MunchlaxTrees[3]]);
CB_TreeList.SelectedIndex = 0;
}
private readonly MunchlaxTreeSet4 MunchlaxTrees;
private readonly byte[] MunchlaxTrees;
private int entry;
private bool loading;
private HoneyTreeValue? Tree;
@ -45,7 +46,7 @@ public partial class SAV_HoneyTree : Form
if (loading)
return;
if (species == (int)Species.Munchlax && !MunchlaxTrees.Contains(CB_TreeList.SelectedIndex))
if (species == (int)Species.Munchlax && !MunchlaxTrees.AsSpan().Contains((byte)CB_TreeList.SelectedIndex))
WinFormsUtil.Alert("Catching Munchlax in this tree will make it illegal for this savegame's TID16/SID16 combination.");
}