mirror of
https://github.com/kwsch/PKHeX
synced 2024-12-19 00:43:14 +00:00
9166d0eb64
Rewrites a good amount of legality APIs pertaining to: * Legal moves that can be learned * Evolution chains & cross-generation paths * Memory validation with forgotten moves In generation 8, there are 3 separate contexts an entity can exist in: SW/SH, BD/SP, and LA. Not every entity can cross between them, and not every entity from generation 7 can exist in generation 8 (Gogoat, etc). By creating class models representing the restrictions to cross each boundary, we are able to better track and validate data. The old implementation of validating moves was greedy: it would iterate for all generations and evolutions, and build a full list of every move that can be learned, storing it on the heap. Now, we check one game group at a time to see if the entity can learn a move that hasn't yet been validated. End result is an algorithm that requires 0 allocation, and a smaller/quicker search space. The old implementation of storing move parses was inefficient; for each move that was parsed, a new object is created and adjusted depending on the parse. Now, move parse results are `struct` and store the move parse contiguously in memory. End result is faster parsing and 0 memory allocation. * `PersonalTable` objects have been improved with new API methods to check if a species+form can exist in the game. * `IEncounterTemplate` objects have been improved to indicate the `EntityContext` they originate in (similar to `Generation`). * Some APIs have been extended to accept `Span<T>` instead of Array/IEnumerable
212 lines
7.2 KiB
C#
212 lines
7.2 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using static System.Buffers.Binary.BinaryPrimitives;
|
||
|
||
namespace PKHeX.Core;
|
||
|
||
/// <summary>
|
||
/// Generation 3 <see cref="SaveFile"/> object for Pokémon Ruby Sapphire Box saves.
|
||
/// </summary>
|
||
public sealed class SAV3RSBox : SaveFile, IGCSaveFile
|
||
{
|
||
protected internal override string ShortSummary => $"{Version} #{SaveCount:0000}";
|
||
public override string Extension => this.GCExtension();
|
||
public override IPersonalTable Personal => PersonalTable.RS;
|
||
public override IReadOnlyList<ushort> HeldItems => Legal.HeldItems_RS;
|
||
public SAV3GCMemoryCard? MemoryCard { get; init; }
|
||
private readonly bool Japanese;
|
||
|
||
public SAV3RSBox(byte[] data, SAV3GCMemoryCard memCard) : this(data) => MemoryCard = memCard;
|
||
|
||
public SAV3RSBox(bool japanese = false) : base(SaveUtil.SIZE_G3BOX)
|
||
{
|
||
Japanese = japanese;
|
||
Box = 0;
|
||
Blocks = Array.Empty<BlockInfoRSBOX>();
|
||
ClearBoxes();
|
||
}
|
||
|
||
public SAV3RSBox(byte[] data) : base(data)
|
||
{
|
||
Japanese = data[0] == 0x83; // ポケモンボックス R&S
|
||
Blocks = ReadBlocks(data);
|
||
InitializeData();
|
||
}
|
||
|
||
private void InitializeData()
|
||
{
|
||
// Detect active save
|
||
int[] SaveCounts = Array.ConvertAll(Blocks, block => (int)block.SaveCount);
|
||
SaveCount = SaveCounts.Max();
|
||
int ActiveSAV = Array.IndexOf(SaveCounts, SaveCount) / BLOCK_COUNT;
|
||
Blocks = Blocks.Skip(ActiveSAV * BLOCK_COUNT).Take(BLOCK_COUNT).OrderBy(b => b.ID).ToArray();
|
||
|
||
// Set up PC data buffer beyond end of save file.
|
||
Box = Data.Length;
|
||
Array.Resize(ref Data, Data.Length + SIZE_RESERVED); // More than enough empty space.
|
||
|
||
// Copy block to the allocated location
|
||
const int copySize = BLOCK_SIZE - 0x10;
|
||
foreach (var b in Blocks)
|
||
Array.Copy(Data, b.Offset + 0xC, Data, (int) (Box + (b.ID * copySize)), copySize);
|
||
}
|
||
|
||
private static BlockInfoRSBOX[] ReadBlocks(byte[] data)
|
||
{
|
||
var blocks = new BlockInfoRSBOX[2 * BLOCK_COUNT];
|
||
for (int i = 0; i < blocks.Length; i++)
|
||
{
|
||
int offset = BLOCK_SIZE + (i * BLOCK_SIZE);
|
||
blocks[i] = new BlockInfoRSBOX(data, offset);
|
||
}
|
||
|
||
return blocks;
|
||
}
|
||
|
||
private BlockInfoRSBOX[] Blocks;
|
||
private int SaveCount;
|
||
private const int BLOCK_COUNT = 23;
|
||
private const int BLOCK_SIZE = 0x2000;
|
||
private const int SIZE_RESERVED = BLOCK_COUNT * BLOCK_SIZE; // unpacked box data
|
||
|
||
protected override byte[] GetFinalData()
|
||
{
|
||
var newFile = GetInnerData();
|
||
|
||
// Return the gci if Memory Card is not being exported
|
||
if (MemoryCard is null)
|
||
return newFile;
|
||
|
||
MemoryCard.WriteSaveGameData(newFile);
|
||
return MemoryCard.Data;
|
||
}
|
||
|
||
private byte[] GetInnerData()
|
||
{
|
||
// Copy Box data back
|
||
const int copySize = BLOCK_SIZE - 0x10;
|
||
foreach (var b in Blocks)
|
||
Array.Copy(Data, (int) (Box + (b.ID * copySize)), Data, b.Offset + 0xC, copySize);
|
||
|
||
SetChecksums();
|
||
|
||
return GetData(0, Data.Length - SIZE_RESERVED);
|
||
}
|
||
|
||
// Configuration
|
||
protected override SaveFile CloneInternal()
|
||
{
|
||
var data = GetInnerData();
|
||
var sav = MemoryCard is not null ? new SAV3RSBox(data, MemoryCard) : new SAV3RSBox(data);
|
||
return sav;
|
||
}
|
||
|
||
protected override int SIZE_STORED => PokeCrypto.SIZE_3STORED + 4;
|
||
protected override int SIZE_PARTY => PokeCrypto.SIZE_3PARTY; // unused
|
||
public override PKM BlankPKM => new PK3();
|
||
public override Type PKMType => typeof(PK3);
|
||
|
||
public override int MaxMoveID => Legal.MaxMoveID_3;
|
||
public override int MaxSpeciesID => Legal.MaxSpeciesID_3;
|
||
public override int MaxAbilityID => Legal.MaxAbilityID_3;
|
||
public override int MaxItemID => Legal.MaxItemID_3;
|
||
public override int MaxBallID => Legal.MaxBallID_3;
|
||
public override int MaxGameID => Legal.MaxGameID_3;
|
||
|
||
public override int MaxEV => 255;
|
||
public override int Generation => 3;
|
||
public override EntityContext Context => EntityContext.Gen3;
|
||
protected override int GiftCountMax => 1;
|
||
public override int OTLength => 7;
|
||
public override int NickLength => 10;
|
||
public override int MaxMoney => 999999;
|
||
public override bool HasBoxWallpapers => false;
|
||
|
||
public override int BoxCount => 50;
|
||
public override bool HasParty => false;
|
||
public override bool IsPKMPresent(ReadOnlySpan<byte> data) => EntityDetection.IsPresentGBA(data);
|
||
|
||
// Checksums
|
||
protected override void SetChecksums() => Blocks.SetChecksums(Data);
|
||
public override bool ChecksumsValid => Blocks.GetChecksumsValid(Data);
|
||
public override string ChecksumInfo => Blocks.GetChecksumInfo(Data);
|
||
|
||
// Trainer Info
|
||
public override GameVersion Version { get => GameVersion.RSBOX; protected set { } }
|
||
|
||
// Storage
|
||
public override int GetPartyOffset(int slot) => -1;
|
||
public override int GetBoxOffset(int box) => Box + 8 + (SIZE_STORED * box * 30);
|
||
|
||
public override int CurrentBox
|
||
{
|
||
get => Data[Box + 4] * 2;
|
||
set => Data[Box + 4] = (byte)(value / 2);
|
||
}
|
||
|
||
protected override int GetBoxWallpaperOffset(int box)
|
||
{
|
||
// Box Wallpaper is directly after the Box Names
|
||
int offset = Box + 0x1ED19 + (box / 2);
|
||
return offset;
|
||
}
|
||
|
||
public override string GetBoxName(int box)
|
||
{
|
||
// Tweaked for the 1-30/31-60 box showing
|
||
int lo = (30 *(box%2)) + 1;
|
||
int hi = 30*((box % 2) + 1);
|
||
string boxName = $"[{lo:00}-{hi:00}] ";
|
||
box /= 2;
|
||
|
||
int offset = Box + 0x1EC38 + (9 * box);
|
||
if (Data[offset] is 0 or 0xFF)
|
||
boxName += $"BOX {box + 1}";
|
||
boxName += GetString(offset, 9);
|
||
|
||
return boxName;
|
||
}
|
||
|
||
public override void SetBoxName(int box, string value)
|
||
{
|
||
int offset = Box + 0x1EC38 + (9 * box);
|
||
var span = Data.AsSpan(offset, 9);
|
||
if (value == $"BOX {box + 1}")
|
||
{
|
||
span.Clear();
|
||
return;
|
||
}
|
||
SetString(span, value.AsSpan(), 8, StringConverterOption.ClearZero);
|
||
}
|
||
|
||
protected override PKM GetPKM(byte[] data)
|
||
{
|
||
if (data.Length != PokeCrypto.SIZE_3STORED)
|
||
Array.Resize(ref data, PokeCrypto.SIZE_3STORED);
|
||
return new PK3(data);
|
||
}
|
||
|
||
protected override byte[] DecryptPKM(byte[] data)
|
||
{
|
||
if (data.Length != PokeCrypto.SIZE_3STORED)
|
||
Array.Resize(ref data, PokeCrypto.SIZE_3STORED);
|
||
return PokeCrypto.DecryptArray3(data);
|
||
}
|
||
|
||
protected override void SetDex(PKM pk) { /* No Pokedex for this game, do nothing */ }
|
||
|
||
public override void WriteBoxSlot(PKM pk, Span<byte> data, int offset)
|
||
{
|
||
base.WriteBoxSlot(pk, data, offset);
|
||
WriteUInt16LittleEndian(data[(PokeCrypto.SIZE_3STORED)..], (ushort)pk.TID);
|
||
WriteUInt16LittleEndian(data[(PokeCrypto.SIZE_3STORED + 2)..], (ushort)pk.SID);
|
||
}
|
||
|
||
public override string GetString(ReadOnlySpan<byte> data) => StringConverter3.GetString(data, Japanese);
|
||
|
||
public override int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength, StringConverterOption option)
|
||
{
|
||
return StringConverter3.SetString(destBuffer, value, maxLength, Japanese, option);
|
||
}
|
||
}
|