Misc tweaks

Seal some classes
Use derived pkm class for template->pk* moves (pk3/pa8)
Revise inventory9 to better handle empty slots
Cache legality for Summary report grid (does this make it faster?? seems to open instantly); add shift-quit to skip prompt
Fix handling for level range on encountercriteria passing level range for gen3/4 encounter slot method1/etc lead checks
This commit is contained in:
Kurt
2025-05-07 23:06:40 -05:00
parent 61b38d1aa0
commit 275f5fb5df
15 changed files with 117 additions and 68 deletions

View File

@@ -41,7 +41,10 @@ public class EntitySummary : IFatefulEncounterReadOnly // do NOT seal, allow inh
public string OT => Entity.OriginalTrainerName;
public string Version => Get(Strings.gamelist, (int)Entity.Version);
public string OTLang => ((LanguageID)Entity.Language).ToString();
public string Legal { get { var la = new LegalityAnalysis(Entity); return la.Parsed ? la.Valid.ToString() : "-"; } }
public string Legal => Legality.Parsed ? Legality.Valid.ToString() : "-";
public string EncounterType => Legality.EncounterMatch.LongName;
private LegalityAnalysis Legality { get; }
#region Extraneous
public string EC => Entity.EncryptionConstant.ToString("X8");
@@ -113,6 +116,7 @@ protected EntitySummary(PKM pk, GameStrings strings)
Entity = pk;
Strings = strings;
Stats = Entity.GetStats(Entity.PersonalInfo);
Legality = new LegalityAnalysis(Entity);
}
/// <summary>

View File

@@ -92,7 +92,7 @@ private void SetPINGA(PK3 pk, EncounterCriteria criteria, PersonalInfo3 pi)
}
}
protected virtual void SetEncounterMoves(PKM pk) => EncounterUtil.SetEncounterMoves(pk, Version, LevelMin);
protected virtual void SetEncounterMoves(PK3 pk) => EncounterUtil.SetEncounterMoves(pk, Version, LevelMin);
#endregion
#region Matching

View File

@@ -9,5 +9,5 @@ namespace PKHeX.Core;
internal sealed record EncounterSlot3Swarm(EncounterArea3 Parent, ushort Species, byte LevelMin, byte LevelMax, byte SlotNumber, Moveset Moves)
: EncounterSlot3(Parent, Species, 0, LevelMin, LevelMax, SlotNumber, 0, 0, 0, 0), IMoveset
{
protected override void SetEncounterMoves(PKM pk) => pk.SetMoves(Moves);
protected override void SetEncounterMoves(PK3 pk) => pk.SetMoves(Moves);
}

View File

@@ -117,16 +117,15 @@ private OverworldParam8a GetParams(PersonalInfo8LA pi) => new()
_ => 0,
});
private void SetEncounterMoves(PKM pk, int level)
private void SetEncounterMoves(PA8 pk, int level)
{
var pa8 = (PA8)pk;
Span<ushort> moves = stackalloc ushort[4];
var (learn, mastery) = GetLevelUpInfo();
LoadInitialMoveset(pa8, moves, learn, level);
LoadInitialMoveset(pk, moves, learn, level);
pk.SetMoves(moves);
pa8.SetEncounterMasteryFlags(moves, mastery, level);
if (pa8.AlphaMove != 0)
pa8.SetMasteryFlagMove(pa8.AlphaMove);
pk.SetEncounterMasteryFlags(moves, mastery, level);
if (pk.AlphaMove != 0)
pk.SetMasteryFlagMove(pk.AlphaMove);
}
public void LoadInitialMoveset(PA8 pa8, Span<ushort> moves, Learnset learn, int level)

View File

@@ -604,7 +604,7 @@ private static bool IsSlotValid<T>(T enc, uint u16SlotRand)
private static bool IsOriginalLevelValid(byte min, byte max, byte format, uint level)
{
if (format == Format)
if (format == Format && min > 1)
return level == min; // Met Level matches
return LevelRangeExtensions.IsLevelWithinRange((int)level, min, max);
}

View File

@@ -472,7 +472,7 @@ private static bool IsSlotValid<T>(T enc, uint u16SlotRand)
private static bool IsOriginalLevelValid(byte min, byte max, byte format, uint level)
{
if (format == Format)
if (format == Format && min > 1)
return level == min; // Met Level matches
return LevelRangeExtensions.IsLevelWithinRange((int)level, min, max);
}

View File

@@ -499,7 +499,7 @@ private static bool IsSlotValid<T>(T enc, uint u16SlotRand)
private static bool IsOriginalLevelValid(byte min, byte max, byte format, uint level)
{
if (format == Format)
if (format == Format && min > 1)
return level == min; // Met Level matches
return LevelRangeExtensions.IsLevelWithinRange((int)level, min, max);
}

View File

@@ -3,7 +3,7 @@
namespace PKHeX.Core;
public class BattleTest5(Memory<byte> Raw)
public sealed class BattleTest5(Memory<byte> Raw)
{
public const int SIZE = 0x5C8;
public const string Extension = "bt5";

View File

@@ -3,7 +3,7 @@
namespace PKHeX.Core;
public class PokestarMovie5(Memory<byte> Raw)
public sealed class PokestarMovie5(Memory<byte> Raw)
{
public const int SIZE = 0x4B4;
public const string Extension = "psm5";

View File

@@ -3,7 +3,7 @@
namespace PKHeX.Core;
public class Record5(SAV5 SAV, Memory<byte> raw) : SaveBlock<SAV5>(SAV, raw)
public sealed class Record5(SAV5 SAV, Memory<byte> raw) : SaveBlock<SAV5>(SAV, raw)
{
public const int Max32 = 999_999_999;
public const ushort Max16 = 65535;

View File

@@ -3,7 +3,7 @@
namespace PKHeX.Core;
public class WorldTournament5(Memory<byte> Raw)
public sealed class WorldTournament5(Memory<byte> Raw)
{
public const int SIZE = 0x1214;
public const string Extension = "pwt";

View File

@@ -13,7 +13,17 @@ public sealed class MyItem9(SAV9SV sav, SCBlock block) : MyItem(sav, block.Raw)
private Span<byte> GetItemSpan(ushort itemIndex) => InventoryPouch9.GetItemSpan(Data, itemIndex);
public void DeleteItem(ushort itemIndex) => InventoryItem9.Clear(GetItemSpan(itemIndex));
public uint DefaultInitPouch => System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(Data); // Item 0
/// <summary>
/// Deletes the item at the requested <paramref name="itemIndex"/>.
/// </summary>
/// <remarks>
/// Copies item 0 to the requested item index, effectively deleting it.
/// Item 0 should always be un-tarnished, so this is a safe operation.
/// <see cref="InventoryItem9.PouchInvalid"/> for remarks on Pouch type quirks. This aims to retain consistency within the block.
/// </remarks>
public void DeleteItem(ushort itemIndex) => GetItemSpan(0).CopyTo(GetItemSpan(itemIndex));
public InventoryItem9 GetItem(ushort itemIndex) => InventoryItem9.Read(itemIndex, GetItemSpan(itemIndex));
public uint GetItemQuantity(ushort itemIndex) => InventoryItem9.GetItemCount(GetItemSpan(itemIndex));
@@ -21,7 +31,7 @@ public sealed class MyItem9(SAV9SV sav, SCBlock block) : MyItem(sav, block.Raw)
public void SetItemQuantity(ushort itemIndex, int quantity)
{
var pouch = GetPouchIndex(GetType(itemIndex));
if (pouch == InventoryItem9.PouchNone)
if (pouch == InventoryItem9.PouchInvalid)
{
DeleteItem(itemIndex); // don't allow setting items that don't exist
return;
@@ -30,6 +40,7 @@ public void SetItemQuantity(ushort itemIndex, int quantity)
var item = InventoryItem9.Read(itemIndex, span);
item.Count = quantity;
item.Pouch = GetPouchIndex(GetType(itemIndex));
item.IsObtained = true;
item.Write(span);
}
@@ -79,6 +90,20 @@ private void CleanIllegalSlots()
}
}
public void ResetToDefault()
{
var block = Data;
var defaultPouch = DefaultInitPouch;
ResetToDefault(block, defaultPouch);
}
public static void ResetToDefault(Span<byte> block, uint defaultPouch)
{
block.Clear();
for (int i = 0; i < block.Length; i += InventoryItem9.SIZE)
System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(block[i..], defaultPouch);
}
private static InventoryPouch9 MakePouch(InventoryType type)
{
var info = ItemStorage9SV.Instance;
@@ -98,6 +123,6 @@ private static InventoryPouch9 MakePouch(InventoryType type)
InventoryType.Treasure => InventoryItem9.PouchTreasure,
InventoryType.Ingredients => InventoryItem9.PouchPicnic,
InventoryType.Candy => InventoryItem9.PouchMaterial,
_ => InventoryItem9.PouchNone,
_ => InventoryItem9.PouchInvalid,
};
}

View File

@@ -7,7 +7,6 @@ public sealed record InventoryItem9 : InventoryItem, IItemFavorite, IItemNewFlag
{
public const int SIZE = 0x10;
public const uint PouchNone = 0xFFFFFFFF;
public const uint PouchMedicine = 0;
public const uint PouchBall = 1;
public const uint PouchBattle = 2;
@@ -20,31 +19,38 @@ public sealed record InventoryItem9 : InventoryItem, IItemFavorite, IItemNewFlag
public const uint PouchMaterial = 9;
public const uint PouchRecipe = 10;
/// <summary>
/// Invalid pouch ID.
/// </summary>
/// <remarks>
/// Only used internally to this program, not stored in the data.
/// The game did have behavior that initialized to this value on early patches, but was later changed to initialize to 0.
/// Therefore, we cannot use this value to set. Technically it can be used to determine if the game was started on an early patch.
/// </remarks>
public const uint PouchInvalid = 0xFFFFFFFF;
public uint Pouch { get; set; }
public uint Flags { get; set; }
public uint Padding { get; set; }
public bool IsNew { get => (Flags & 0x1) != 0; set => Flags = (Flags & ~0x1u) | (value ? 0x1u : 0x0u); } // red dot
public bool IsFavorite { get => (Flags & 0x2) != 0; set => Flags = (Flags & ~0x2u) | (value ? 0x2u : 0x0u); }
public bool IsUpdated { get => (Flags & 0x4) != 0; set => Flags = (Flags & ~0x4u) | (value ? 0x4u : 0x0u); } // always true if pouch is set
public bool IsObtained { get => (Flags & 0x4) != 0; set => Flags = (Flags & ~0x4u) | (value ? 0x4u : 0x0u); } // Has Been Obtained At Least Once
public override string ToString() => $"{Index:000} x{Count}{(IsNew ? "*" : "")}{(IsFavorite ? "F" : "")} - {Flags:X8}";
public override string ToString()
{
if (IsObtained)
return $"{Index:0000} Empty";
return $"{Index:0000} x{Count}{(IsNew ? "*" : "")}{(IsFavorite ? "F" : "")} - {Flags:X8}";
}
public override void Clear()
{
Index = Count = 0;
Pouch = 0;
Count = 0;
Flags = Padding = 0;
IsFavorite = false;
IsUpdated = false;
IsNew = false;
Pouch = PouchNone;
}
/// <summary>
/// Indicates if the item has been acquired by the player.
/// </summary>
public bool IsValidPouch => Pouch != PouchNone;
public static InventoryItem9 Read(ushort index, ReadOnlySpan<byte> data) => new()
{
Index = index,
@@ -56,8 +62,6 @@ public static InventoryItem9 Read(ushort index, ReadOnlySpan<byte> data) => new(
public void Write(Span<byte> data)
{
IsUpdated = Pouch != PouchNone;
// Index is not saved.
WriteUInt32LittleEndian(data, Pouch);
WriteUInt32LittleEndian(data[4..], (uint)Count);
@@ -65,16 +69,13 @@ public void Write(Span<byte> data)
WriteUInt32LittleEndian(data[12..], Padding);
}
public static void Clear(Span<byte> data) => WriteUInt128LittleEndian(data, PouchNone);
public static uint GetItemCount(Span<byte> data) => ReadUInt32LittleEndian(data[4..]);
public override void SetNewDetails(int count)
{
base.SetNewDetails(count);
if (IsValidPouch)
return;
IsNew = true;
IsUpdated = true;
IsObtained = true;
IsFavorite = false;
}

View File

@@ -11,6 +11,8 @@ public sealed class InventoryPouch9(InventoryType type, IItemStorage info, int m
public uint PouchIndex { get; set; } = pouch;
public override InventoryItem9 GetEmpty(int itemID = 0, int count = 0) => new() { Index = itemID, Count = count, IsNew = true };
public static int GetItemOffset(ushort index) => InventoryItem9.SIZE * index;
public static Span<byte> GetItemSpan(Span<byte> block, ushort index) => block[GetItemOffset(index)..];
public override void GetPouch(ReadOnlySpan<byte> data)
{
@@ -19,71 +21,87 @@ public override void GetPouch(ReadOnlySpan<byte> data)
int ctr = 0;
foreach (var index in LegalItems)
{
var item = GetItem(data, index);
if (!item.IsValidPouch)
continue;
items[ctr++] = item;
}
while (ctr != LegalItems.Length)
items[ctr++] = new InventoryItem9();
items[ctr++] = GetItem(data, index);
Items = items;
}
public static InventoryItem9 GetItem(ReadOnlySpan<byte> data, ushort itemID)
public static InventoryItem9 GetItem(ReadOnlySpan<byte> block, ushort itemID)
{
var ofs = GetItemOffset(itemID);
return InventoryItem9.Read(itemID, data[ofs..]);
return InventoryItem9.Read(itemID, block[ofs..]);
}
public override void SetPouch(Span<byte> data)
public override void SetPouch(Span<byte> block)
{
HashSet<ushort> processed = [];
// Write all the item slots still present in the pouch. Keep track of the item IDs processed.
var items = (InventoryItem9[])Items;
var processed = new HashSet<ushort>(items.Length);
var LegalItems = Info.GetItems(Type);
var legal = Info.GetItems(Type);
foreach (var item in items)
{
var index = (ushort)item.Index;
if (index == 0)
continue;
if (!LegalItems.Contains(index))
if (!legal.Contains(index))
{
Debug.WriteLine($"Invalid Item ID returned within this pouch: {index}");
continue;
}
if (SetNew && item.Index != 0)
{
var original = GetItem(data, (ushort)item.Index);
item.IsNew |= !original.IsValidPouch;
}
item.Pouch = PouchIndex;
var ofs = GetItemOffset(index);
item.Write(data[ofs..]);
var span = GetItemSpan(block, index);
var original = InventoryItem9.Read(index, span);
EnsureFlagsConsistent(item, original);
// In the event of duplicates, we just overwrite what was previously written by a prior duplicate.
// Don't care if we've already processed this item, just write it again.
item.Write(span);
processed.Add(index);
}
// For all the items that were not present in the pouch, clear the data for them.
foreach (var index in LegalItems)
foreach (var index in legal)
{
if (processed.Contains(index))
continue;
ClearItem(data, index);
SetQuantityZero(block, index);
}
}
public static int GetItemOffset(ushort index) => InventoryItem9.SIZE * index;
public static Span<byte> GetItemSpan(Span<byte> data, ushort index) => data[GetItemOffset(index)..];
public static void ClearItem(Span<byte> data, ushort index) => InventoryItem9.Clear(GetItemSpan(data, index));
private void EnsureFlagsConsistent(InventoryItem9 item, InventoryItem9 original)
{
if (item.Count != 0)
{
// Ensure the flag is set; 0->X and Y->Z
item.IsObtained = true;
if (!original.IsObtained && SetNew)
item.IsNew = true;
}
else
{
if (!item.IsObtained)
{
item.IsNew = item.IsFavorite = false;
if (item.Pouch is not (0 or uint.MaxValue))
item.Pouch = 0;
}
}
if (item.IsObtained)
item.Pouch = PouchIndex; // ensure the pouch is set
}
public static void SetQuantityZero(Span<byte> block, ushort index)
{
var span = GetItemSpan(block, index);
var exist = InventoryItem9.Read(index, span);
if (exist.Count == 0)
return;
exist.Count = 0;
exist.IsObtained = true;
exist.Write(span);
}
public static int GetSuggestedCount(InventoryType t, int item, int requestVal)
{

View File

@@ -161,6 +161,8 @@ private void Data_Sorted(object sender, EventArgs e)
private void PromptSaveCSV(object sender, FormClosingEventArgs e)
{
if (ModifierKeys.HasFlag(Keys.Shift))
return;
if (WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgReportExportCSV) != DialogResult.Yes)
return;
using var savecsv = new SaveFileDialog();