PKHeX/PKHeX.Core/Saves/SAV3RSBox.cs

217 lines
7.4 KiB
C#
Raw Normal View History

using System;
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, IBoxDetailName, IBoxDetailWallpaper
{
protected internal override string ShortSummary => $"{Version} #{SaveCount:0000}";
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; }
private readonly bool Japanese;
public SAV3RSBox(byte[] data, SAV3GCMemoryCard memCard) : this(data) => MemoryCard = memCard;
2018-09-15 05:37:47 +00:00
public SAV3RSBox(bool japanese = false) : base(SaveUtil.SIZE_G3BOX)
{
Japanese = japanese;
Box = 0;
Blocks = [];
ClearBoxes();
}
public SAV3RSBox(byte[] data) : base(data)
{
Japanese = data[0] == 0x83; // ポケモンボックス
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;
var ordered = Blocks
.Skip(ActiveSAV * BLOCK_COUNT)
.Take(BLOCK_COUNT)
.OrderBy(b => b.ID);
Blocks = [..ordered];
// 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(ReadOnlySpan<byte> data)
{
var blocks = new BlockInfoRSBOX[2 * BLOCK_COUNT];
for (int i = 0; i < blocks.Length; i++)
PKHeX.Core Nullable cleanup (#2401) * Handle some nullable cases Refactor MysteryGift into a second abstract class (backed by a byte array, or fake data) Make some classes have explicit constructors instead of { } initialization * Handle bits more obviously without null * Make SaveFile.BAK explicitly readonly again * merge constructor methods to have readonly fields * Inline some properties * More nullable handling * Rearrange box actions define straightforward classes to not have any null properties * Make extrabyte reference array immutable * Move tooltip creation to designer * Rearrange some logic to reduce nesting * Cache generated fonts * Split mystery gift album purpose * Handle more tooltips * Disallow null setters * Don't capture RNG object, only type enum * Unify learnset objects Now have readonly properties which are never null don't new() empty learnsets (>800 Learnset objects no longer created, total of 2400 objects since we also new() a move & level array) optimize g1/2 reader for early abort case * Access rewrite Initialize blocks in a separate object, and get via that object removes a couple hundred "might be null" warnings since blocks are now readonly getters some block references have been relocated, but interfaces should expose all that's needed put HoF6 controls in a groupbox, and disable * Readonly personal data * IVs non nullable for mystery gift * Explicitly initialize forced encounter moves * Make shadow objects readonly & non-null Put murkrow fix in binary data resource, instead of on startup * Assign dex form fetch on constructor Fixes legality parsing edge cases also handle cxd parse for valid; exit before exception is thrown in FrameGenerator * Remove unnecessary null checks * Keep empty value until init SetPouch sets the value to an actual one during load, but whatever * Readonly team lock data * Readonly locks Put locked encounters at bottom (favor unlocked) * Mail readonly data / offset Rearrange some call flow and pass defaults Add fake classes for SaveDataEditor mocking Always party size, no need to check twice in stat editor use a fake save file as initial data for savedata editor, and for gamedata (wow i found a usage) constrain eventwork editor to struct variable types (uint, int, etc), thus preventing null assignment errors
2019-10-17 01:47:31 +00:00
{
int offset = BLOCK_SIZE + (i * BLOCK_SIZE);
blocks[i] = new BlockInfoRSBOX(data, offset);
PKHeX.Core Nullable cleanup (#2401) * Handle some nullable cases Refactor MysteryGift into a second abstract class (backed by a byte array, or fake data) Make some classes have explicit constructors instead of { } initialization * Handle bits more obviously without null * Make SaveFile.BAK explicitly readonly again * merge constructor methods to have readonly fields * Inline some properties * More nullable handling * Rearrange box actions define straightforward classes to not have any null properties * Make extrabyte reference array immutable * Move tooltip creation to designer * Rearrange some logic to reduce nesting * Cache generated fonts * Split mystery gift album purpose * Handle more tooltips * Disallow null setters * Don't capture RNG object, only type enum * Unify learnset objects Now have readonly properties which are never null don't new() empty learnsets (>800 Learnset objects no longer created, total of 2400 objects since we also new() a move & level array) optimize g1/2 reader for early abort case * Access rewrite Initialize blocks in a separate object, and get via that object removes a couple hundred "might be null" warnings since blocks are now readonly getters some block references have been relocated, but interfaces should expose all that's needed put HoF6 controls in a groupbox, and disable * Readonly personal data * IVs non nullable for mystery gift * Explicitly initialize forced encounter moves * Make shadow objects readonly & non-null Put murkrow fix in binary data resource, instead of on startup * Assign dex form fetch on constructor Fixes legality parsing edge cases also handle cxd parse for valid; exit before exception is thrown in FrameGenerator * Remove unnecessary null checks * Keep empty value until init SetPouch sets the value to an actual one during load, but whatever * Readonly team lock data * Readonly locks Put locked encounters at bottom (favor unlocked) * Mail readonly data / offset Rearrange some call flow and pass defaults Add fake classes for SaveDataEditor mocking Always party size, no need to check twice in stat editor use a fake save file as initial data for savedata editor, and for gamedata (wow i found a usage) constrain eventwork editor to struct variable types (uint, int, etc), thus preventing null assignment errors
2019-10-17 01:47:31 +00:00
}
return blocks;
}
2018-09-15 05:37:47 +00:00
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);
2024-03-30 06:17:18 +00:00
return MemoryCard.Data.ToArray();
}
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 Data[..^SIZE_RESERVED];
}
// Configuration
protected override SAV3RSBox CloneInternal() => new(GetInnerData()) { MemoryCard = MemoryCard };
2018-09-15 05:37:47 +00:00
protected override int SIZE_STORED => PokeCrypto.SIZE_3STORED + 4;
protected override int SIZE_PARTY => PokeCrypto.SIZE_3PARTY; // unused
public override PK3 BlankPKM => new();
public override Type PKMType => typeof(PK3);
public override ushort MaxMoveID => Legal.MaxMoveID_3;
public override ushort 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 GameVersion MaxGameID => Legal.MaxGameID_3;
2023-09-28 05:15:59 +00:00
public override int MaxEV => EffortValues.Max255;
public override byte Generation => 3;
public override EntityContext Context => EntityContext.Gen3;
public override int MaxStringLengthTrainer => 7;
public override int MaxStringLengthNickname => 10;
public override int MaxMoney => 999999;
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; 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);
}
2018-09-15 05:37:47 +00:00
private Span<byte> GetBoxNameSpan(int box)
{
int offset = Box + 0x1EC38 + (9 * box);
return Data.AsSpan(offset, 9);
}
private int GetBoxWallpaperOffset(int box)
{
// Box Wallpaper is directly after the Box Names
return Box + 0x1ED19 + (box / 2);
}
2018-09-15 05:37:47 +00:00
public int GetBoxWallpaper(int box) => Data[GetBoxWallpaperOffset(box)];
public void SetBoxWallpaper(int box, int value) => Data[GetBoxWallpaperOffset(box)] = (byte)value;
public 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;
var span = GetBoxNameSpan(box);
if (span[0] is 0 or 0xFF)
boxName += BoxDetailNameExtensions.GetDefaultBoxNameCaps(box);
else
boxName += GetString(span);
return boxName;
}
2018-09-15 05:37:47 +00:00
public void SetBoxName(int box, ReadOnlySpan<char> value)
{
var span = GetBoxNameSpan(box);
if (value == BoxDetailNameExtensions.GetDefaultBoxNameCaps(box))
{
span.Clear();
return;
}
SetString(span, value, 8, StringConverterOption.ClearZero);
}
2018-09-15 05:37:47 +00:00
protected override PK3 GetPKM(byte[] data)
{
if (data.Length != PokeCrypto.SIZE_3STORED)
Array.Resize(ref data, PokeCrypto.SIZE_3STORED);
return new(data);
}
protected override byte[] DecryptPKM(byte[] data)
{
if (data.Length != PokeCrypto.SIZE_3STORED)
Array.Resize(ref data, PokeCrypto.SIZE_3STORED);
return PokeCrypto.DecryptArray3(data);
}
2018-05-12 15:13:39 +00:00
protected override void SetDex(PKM pk) { /* No Pokédex for this game, do nothing */ }
public override void WriteBoxSlot(PKM pk, Span<byte> data)
{
base.WriteBoxSlot(pk, data);
WriteUInt16LittleEndian(data[(PokeCrypto.SIZE_3STORED)..], pk.TID16);
WriteUInt16LittleEndian(data[(PokeCrypto.SIZE_3STORED + 2)..], pk.SID16);
}
2018-09-15 05:37:47 +00:00
public override string GetString(ReadOnlySpan<byte> data)
=> StringConverter3.GetString(data, Japanese);
public override int LoadString(ReadOnlySpan<byte> data, Span<char> destBuffer)
=> StringConverter3.LoadString(data, destBuffer, Japanese);
public override int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength, StringConverterOption option)
=> StringConverter3.SetString(destBuffer, value, maxLength, Japanese, option);
}