PKHeX/PKHeX.Core/Saves/SAV2.cs

769 lines
28 KiB
C#
Raw Normal View History

using System;
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
using System.Collections.Generic;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core
{
/// <summary>
/// Generation 2 <see cref="SaveFile"/> object.
/// </summary>
public sealed class SAV2 : SaveFile, ILangDeviantSave
{
protected internal override string ShortSummary => $"{OT} ({Version}) - {PlayTimeString}";
public override string Extension => ".sav";
public bool IsVirtualConsole => State.Exportable && Metadata.FileName is { } s && s.StartsWith("sav") && s.Contains(".dat"); // default to GB-Era for non-exportable
public int SaveRevision => Japanese ? 0 : !Korean ? 1 : 2;
public string SaveRevisionString => (Japanese ? "J" : !Korean ? "U" : "K") + (IsVirtualConsole ? "VC" : "GB");
public bool Japanese { get; }
public bool Korean { get; }
2018-09-15 05:37:47 +00:00
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
public override PersonalTable Personal { get; }
public override IReadOnlyList<ushort> HeldItems => Legal.HeldItems_GSC;
2021-08-06 05:39:38 +00:00
public override IReadOnlyList<string> PKMExtensions => Array.FindAll(PKM.Extensions, f =>
{
int gen = f[^1] - 0x30;
if (Korean)
return gen == 2;
return gen is 1 or 2;
2021-08-06 05:39:38 +00:00
});
2019-07-21 19:30:21 +00:00
public SAV2(GameVersion version = GameVersion.C, LanguageID lang = LanguageID.English) : base(SaveUtil.SIZE_G2RAW_J)
{
2019-07-21 19:30:21 +00:00
Version = version;
switch (lang)
{
case LanguageID.Japanese:
Japanese = true;
break;
case LanguageID.Korean:
Korean = true;
break;
// otherwise, both false
}
Offsets = new SAV2Offsets(this);
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
Personal = Version == GameVersion.GS ? PersonalTable.GS : PersonalTable.C;
Initialize();
ClearBoxes();
}
public SAV2(byte[] data, GameVersion versionOverride = GameVersion.Any) : base(data)
{
Version = versionOverride != GameVersion.Any ? versionOverride : SaveUtil.GetIsG2SAV(Data);
Japanese = SaveUtil.GetIsG2SAVJ(Data) != GameVersion.Invalid;
if (Version != GameVersion.C && !Japanese)
Korean = SaveUtil.GetIsG2SAVK(Data) != GameVersion.Invalid;
Offsets = new SAV2Offsets(this);
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
Personal = Version == GameVersion.GS ? PersonalTable.GS : PersonalTable.C;
Initialize();
}
private void Initialize()
{
Box = Data.Length;
Array.Resize(ref Data, Data.Length + SIZE_RESERVED);
Party = GetPartyOffset(0);
2018-05-12 15:13:39 +00:00
// Stash boxes after the save file's end.
int splitAtIndex = (Japanese ? 6 : 7);
int stored = SIZE_STOREDBOX;
int baseDest = Data.Length - SIZE_RESERVED;
var capacity = Japanese ? PokeListType.StoredJP : PokeListType.Stored;
for (int i = 0; i < BoxCount; i++)
{
int ofs = GetBoxRawDataOffset(i, splitAtIndex);
var box = GetData(ofs, stored);
var boxDest = baseDest + (i * SIZE_BOX);
var boxPL = new PokeList2(box, capacity, Japanese);
for (int j = 0; j < boxPL.Pokemon.Length; j++)
{
var dest = boxDest + (j * SIZE_STORED);
var pkDat = (j < boxPL.Count)
? new PokeList2(boxPL[j]).Write()
: new byte[PokeList2.GetDataLength(PokeListType.Single, Japanese)];
pkDat.CopyTo(Data, dest);
}
}
var current = GetData(Offsets.CurrentBox, stored);
var curBoxPL = new PokeList2(current, capacity, Japanese);
var curDest = baseDest + (CurrentBox * SIZE_BOX);
for (int i = 0; i < curBoxPL.Pokemon.Length; i++)
{
var dest = curDest + (i * SIZE_STORED);
var pkDat = i < curBoxPL.Count
? new PokeList2(curBoxPL[i]).Write()
: new byte[PokeList2.GetDataLength(PokeListType.Single, Japanese)];
pkDat.CopyTo(Data, dest);
}
var party = GetData(Offsets.Party, SIZE_STOREDPARTY);
var partyPL = new PokeList2(party, PokeListType.Party, Japanese);
for (int i = 0; i < partyPL.Pokemon.Length; i++)
{
var dest = GetPartyOffset(i);
var pkDat = i < partyPL.Count
? new PokeList2(partyPL[i]).Write()
: new byte[PokeList2.GetDataLength(PokeListType.Single, Japanese)];
pkDat.CopyTo(Data, dest);
}
if (Offsets.Daycare >= 0)
{
int offset = Offsets.Daycare;
DaycareFlags[0] = Data[offset];
offset++;
var pk1 = ReadPKMFromOffset(offset); // parent 1
var daycare1 = new PokeList2(pk1);
2018-09-15 05:37:47 +00:00
offset += (StringLength * 2) + 0x20; // nick/ot/pkm
DaycareFlags[1] = Data[offset];
offset++;
//byte steps = Data[offset];
offset++;
//byte BreedMotherOrNonDitto = Data[offset];
offset++;
var pk2 = ReadPKMFromOffset(offset); // parent 2
var daycare2 = new PokeList2(pk2);
offset += (StringLength * 2) + PokeCrypto.SIZE_2STORED; // nick/ot/pkm
var pk3 = ReadPKMFromOffset(offset); // egg!
pk3.IsEgg = true;
var daycare3 = new PokeList2(pk3);
daycare1.Write().CopyTo(Data, GetPartyOffset(7 + (0 * 2)));
daycare2.Write().CopyTo(Data, GetPartyOffset(7 + (1 * 2)));
daycare3.Write().CopyTo(Data, GetPartyOffset(7 + (2 * 2)));
DaycareOffset = Offsets.Daycare;
}
// Enable Pokedex editing
PokeDex = 0;
EventFlag = Offsets.EventFlag;
EventConst = Offsets.EventConst;
}
private PK2 ReadPKMFromOffset(int offset)
{
var stringLength = StringLength;
var span = Data.AsSpan(offset);
var pkData = span.Slice(stringLength * 2, PokeCrypto.SIZE_2STORED).ToArray();
var pk = new PK2(pkData, jp: Japanese);
var nick = span[..stringLength];
var ot = span.Slice(stringLength, stringLength);
nick.CopyTo(pk.RawNickname);
ot.CopyTo(pk.RawOT);
return pk;
}
private const int SIZE_RESERVED = 0x8000; // unpacked box data
private readonly SAV2Offsets Offsets;
private int GetBoxRawDataOffset(int i, int splitAtIndex)
{
if (i < splitAtIndex)
return 0x4000 + (i * (SIZE_STOREDBOX + 2));
return 0x6000 + ((i - splitAtIndex) * (SIZE_STOREDBOX + 2));
}
protected override byte[] GetFinalData()
{
int splitAtIndex = (Japanese ? 6 : 7);
for (int i = 0; i < BoxCount; i++)
{
var boxPL = new PokeList2(Japanese ? PokeListType.StoredJP : PokeListType.Stored, Japanese);
int slot = 0;
for (int j = 0; j < boxPL.Pokemon.Length; j++)
{
2018-09-15 05:37:47 +00:00
PK2 boxPK = (PK2) GetPKM(GetData(GetBoxOffset(i) + (j * SIZE_STORED), SIZE_STORED));
if (boxPK.Species > 0)
boxPL[slot++] = boxPK;
}
int src = GetBoxRawDataOffset(i, splitAtIndex);
boxPL.Write().CopyTo(Data, src);
if (i == CurrentBox)
boxPL.Write().CopyTo(Data, Offsets.CurrentBox);
}
var partyPL = new PokeList2(PokeListType.Party, Japanese);
int pSlot = 0;
for (int i = 0; i < 6; i++)
{
PK2 partyPK = (PK2)GetPKM(GetData(GetPartyOffset(i), SIZE_STORED));
if (partyPK.Species > 0)
partyPL[pSlot++] = partyPK;
}
partyPL.Write().CopyTo(Data, Offsets.Party);
SetChecksums();
if (Japanese)
{
switch (Version)
{
case GameVersion.GS: Data.AsSpan(Offsets.Trainer1, 0xC83).CopyTo(Data.AsSpan(0x7209)); break;
case GameVersion.C: Data.AsSpan(Offsets.Trainer1, 0xADA).CopyTo(Data.AsSpan(0x7209)); break;
}
}
else if (Korean)
{
// Calculate oddball checksum
ushort sum = 0;
Span<(ushort, ushort)> offsetpairs = stackalloc (ushort,ushort)[]
{
(0x106B, 0x1533),
(0x1534, 0x1A12),
(0x1A13, 0x1C38),
(0x3DD8, 0x3F79),
(0x7E39, 0x7E6A),
};
foreach (var p in offsetpairs)
2018-09-15 05:37:47 +00:00
{
for (int i = p.Item1; i < p.Item2; i++)
sum += Data[i];
2018-09-15 05:37:47 +00:00
}
WriteUInt16LittleEndian(Data.AsSpan(0x7E6B), sum);
}
else
2016-09-04 06:15:09 +00:00
{
switch (Version)
{
case GameVersion.GS:
Array.Copy(Data, 0x2009, Data, 0x15C7, 0x222F - 0x2009);
Array.Copy(Data, 0x222F, Data, 0x3D69, 0x23D9 - 0x222F);
Array.Copy(Data, 0x23D9, Data, 0x0C6B, 0x2856 - 0x23D9);
Array.Copy(Data, 0x2856, Data, 0x7E39, 0x288A - 0x2856);
Array.Copy(Data, 0x288A, Data, 0x10E8, 0x2D69 - 0x288A);
break;
case GameVersion.C:
Array.Copy(Data, 0x2009, Data, 0x1209, 0xB7A);
break;
}
2016-09-04 06:15:09 +00:00
}
byte[] outData = new byte[Data.Length - SIZE_RESERVED];
Array.Copy(Data, outData, outData.Length);
return outData;
}
// Configuration
protected override SaveFile CloneInternal() => new SAV2(Write(), Version);
protected override int SIZE_STORED => Japanese ? PokeCrypto.SIZE_2JLIST : PokeCrypto.SIZE_2ULIST;
protected override int SIZE_PARTY => Japanese ? PokeCrypto.SIZE_2JLIST : PokeCrypto.SIZE_2ULIST;
public override PKM BlankPKM => new PK2(jp: Japanese);
public override Type PKMType => typeof(PK2);
private int SIZE_BOX => BoxSlotCount*SIZE_STORED;
private int SIZE_STOREDBOX => PokeList2.GetDataLength(Japanese ? PokeListType.StoredJP : PokeListType.Stored, Japanese);
private int SIZE_STOREDPARTY => PokeList2.GetDataLength(PokeListType.Party, Japanese);
public override int MaxMoveID => Legal.MaxMoveID_2;
public override int MaxSpeciesID => Legal.MaxSpeciesID_2;
public override int MaxAbilityID => Legal.MaxAbilityID_2;
public override int MaxItemID => Legal.MaxItemID_2;
public override int MaxBallID => 0; // unused
public override int MaxGameID => 99; // unused
public override int MaxMoney => 999999;
public override int MaxCoins => 9999;
public override bool IsPKMPresent(ReadOnlySpan<byte> data) => PKX.IsPKMPresentGB(data);
protected override int EventConstMax => 0x100;
protected override int EventFlagMax => 2000;
public override int BoxCount => Japanese ? 9 : 14;
public override int MaxEV => 65535;
public override int MaxIV => 15;
public override int Generation => 2;
protected override int GiftCountMax => 0;
public override int OTLength => Japanese || Korean ? 5 : 7;
public override int NickLength => Japanese || Korean ? 5 : 10;
public override int BoxSlotCount => Japanese ? 30 : 20;
public override bool HasParty => true;
2017-12-27 23:52:29 +00:00
public override bool HasNamableBoxes => true;
private int StringLength => Japanese ? GBPKML.StringLengthJapanese : GBPKML.StringLengthNotJapan;
2018-05-12 15:13:39 +00:00
// Checksums
private ushort GetChecksum()
{
ushort sum = 0;
for (int i = Offsets.Trainer1; i <= Offsets.AccumulatedChecksumEnd; i++)
sum += Data[i];
return sum;
}
2018-09-15 05:37:47 +00:00
protected override void SetChecksums()
{
ushort accum = GetChecksum();
WriteUInt16LittleEndian(Data.AsSpan(Offsets.OverallChecksumPosition), accum);
WriteUInt16LittleEndian(Data.AsSpan(Offsets.OverallChecksumPosition2), accum);
}
2018-09-15 05:37:47 +00:00
public override bool ChecksumsValid
{
get
{
ushort accum = GetChecksum();
ushort actual = ReadUInt16LittleEndian(Data.AsSpan(Offsets.OverallChecksumPosition));
return accum == actual;
}
}
2016-09-04 06:54:11 +00:00
public override string ChecksumInfo => ChecksumsValid ? "Checksum valid." : "Checksum invalid";
// Trainer Info
public override GameVersion Version { get; protected set; }
public override string OT
{
get => GetString(Offsets.Trainer1 + 2, (Korean ? 2 : 1) * OTLength);
set => SetString(Data.AsSpan(Offsets.Trainer1 + 2, (Korean ? 2 : 1) * OTLength), value.AsSpan(), 8, StringConverterOption.Clear50);
}
2018-09-15 05:37:47 +00:00
public Span<byte> OT_Trash
{
get => Data.AsSpan(Offsets.Trainer1 + 2, StringLength);
set { if (value.Length == StringLength) value.CopyTo(Data.AsSpan(Offsets.Trainer1 + 2)); }
}
2018-09-15 05:37:47 +00:00
public string Rival
{
get => GetString(Offsets.Rival, (Korean ? 2 : 1) * OTLength);
set => SetString(Data.AsSpan(Offsets.Rival, (Korean ? 2 : 1) * OTLength), value.AsSpan(), 8, StringConverterOption.Clear50);
}
public Span<byte> Rival_Trash
{
get => Data.AsSpan(Offsets.Rival, StringLength);
set { if (value.Length == StringLength) value.CopyTo(Data.AsSpan(Offsets.Rival)); }
}
public override int Gender
{
get => Version == GameVersion.C ? Data[Offsets.Gender] : 0;
set
{
if (Version != GameVersion.C)
return;
Data[Offsets.Gender] = (byte) value;
Data[Offsets.Palette] = (byte) value;
}
}
2018-09-15 05:37:47 +00:00
public override int TID
{
get => ReadUInt16BigEndian(Data.AsSpan(Offsets.Trainer1));
set => WriteUInt16BigEndian(Data.AsSpan(Offsets.Trainer1), (ushort)value);
}
2018-09-15 05:37:47 +00:00
public override int SID { get => 0; set { } }
2018-09-15 05:37:47 +00:00
public override int PlayedHours
{
get => ReadUInt16BigEndian(Data.AsSpan(Offsets.TimePlayed));
set => WriteUInt16BigEndian(Data.AsSpan(Offsets.TimePlayed), (ushort)value);
}
2018-09-15 05:37:47 +00:00
public override int PlayedMinutes
{
get => Data[Offsets.TimePlayed + 2];
set => Data[Offsets.TimePlayed + 2] = (byte)value;
}
2018-09-15 05:37:47 +00:00
public override int PlayedSeconds
{
get => Data[Offsets.TimePlayed + 3];
set => Data[Offsets.TimePlayed + 3] = (byte)value;
}
public int Badges
{
get => ReadUInt16LittleEndian(Data.AsSpan(Offsets.JohtoBadges));
set { if (value < 0) return; WriteUInt16LittleEndian(Data.AsSpan(Offsets.JohtoBadges), (ushort)value); }
}
2018-09-15 05:37:47 +00:00
private byte Options
{
get => Data[Offsets.Options];
set => Data[Offsets.Options] = value;
}
2018-09-15 05:37:47 +00:00
public bool BattleEffects
{
get => (Options & 0x80) == 0;
set => Options = (byte)((Options & 0x7F) | (value ? 0 : 0x80));
}
2018-09-15 05:37:47 +00:00
public bool BattleStyleSwitch
{
get => (Options & 0x40) == 0;
set => Options = (byte)((Options & 0xBF) | (value ? 0 : 0x40));
}
2018-09-15 05:37:47 +00:00
public int Sound
{
get => (Options & 0x30) >> 4;
set => Options = (byte)((Options & 0xCF) | ((value != 0 ? 2 : 0) << 4)); // Stereo 2, Mono 0
}
2018-09-15 05:37:47 +00:00
public int TextSpeed
{
2017-12-27 23:52:29 +00:00
get => Options & 0x7;
set => Options = (byte)((Options & 0xF8) | (value & 7));
}
2018-09-15 05:37:47 +00:00
public bool SaveFileExists
{
get => Data[Offsets.Options + 1] == 1;
set => Data[Offsets.Options + 1] = value ? (byte)1 : (byte)0;
}
public int TextBoxFrame // 3bits
{
get => Data[Offsets.Options + 2] & 0b0000_0111;
set => Data[Offsets.Options + 2] = (byte)((Data[Offsets.Options + 2] & 0b1111_1000) | (value & 0b0000_0111));
}
public int TextBoxFlags { get => Data[Offsets.Options + 3]; set => Data[Offsets.Options + 3] = (byte)value; }
public bool TextBoxFrameDelay1 // bit 0
{
get => (TextBoxFlags & 0x01) == 0x01;
set => TextBoxFlags = (TextBoxFlags & ~0x01) | (value ? 0x01 : 0);
}
public bool TextBoxFrameDelayNone // bit 4
{
get => (TextBoxFlags & 0x10) == 0x10;
set => TextBoxFlags = (TextBoxFlags & ~0x10) | (value ? 0x10 : 0);
}
public byte GBPrinterBrightness { get => Data[Offsets.Options + 4]; set => Data[Offsets.Options + 4] = value; }
public bool MenuAccountOn
{
get => Data[Offsets.Options + 5] == 1;
set => Data[Offsets.Options + 5] = value ? (byte)1 : (byte)0;
}
// 3 bytes
public override uint Money
{
get => ReadUInt32BigEndian(Data.AsSpan(Offsets.Money)) >> 8;
set
{
var clamp = (uint)Math.Min(value, MaxMoney);
var toWrite = (clamp << 8) | Data[Offsets.Money + 3];
WriteUInt32BigEndian(Data.AsSpan(Offsets.Money), toWrite);
}
}
2018-09-15 05:37:47 +00:00
public uint Coin
{
get => ReadUInt16BigEndian(Data.AsSpan(Offsets.Money + 7));
set
{
var clamped = (ushort)Math.Min(value, MaxCoins);
WriteUInt16BigEndian(Data.AsSpan(Offsets.Money + 7), clamped);
}
}
public byte BlueCardPoints
{
get
{
int ofs = Offsets.BlueCardPoints;
if (ofs == -1)
return 0;
return Data[ofs];
}
set
{
int ofs = Offsets.BlueCardPoints;
if (ofs == -1)
return;
Data[ofs] = value;
}
}
private static ushort[] LegalItems => Legal.Pouch_Items_GSC;
2019-11-16 21:15:09 +00:00
private ushort[] LegalKeyItems => Version == GameVersion.C? Legal.Pouch_Key_C : Legal.Pouch_Key_GS;
private static ushort[] LegalBalls => Legal.Pouch_Ball_GSC;
private static ushort[] LegalTMHMs => Legal.Pouch_TMHM_GSC;
2018-09-15 05:37:47 +00:00
public override IReadOnlyList<InventoryPouch> Inventory
{
get
{
InventoryPouch[] pouch =
{
new InventoryPouchGB(InventoryType.TMHMs, LegalTMHMs, 99, Offsets.PouchTMHM, 57),
new InventoryPouchGB(InventoryType.Items, LegalItems, 99, Offsets.PouchItem, 20),
new InventoryPouchGB(InventoryType.KeyItems, LegalKeyItems, 99, Offsets.PouchKey, 26),
new InventoryPouchGB(InventoryType.Balls, LegalBalls, 99, Offsets.PouchBall, 12),
2021-08-20 20:49:20 +00:00
new InventoryPouchGB(InventoryType.PCItems, ArrayUtil.ConcatAll(LegalItems, LegalKeyItems, LegalBalls, LegalTMHMs), 99, Offsets.PouchPC, 50),
};
2019-03-30 23:10:14 +00:00
return pouch.LoadAll(Data);
}
2019-03-30 23:10:14 +00:00
set => value.SaveAll(Data);
}
private readonly byte[] DaycareFlags = new byte[2];
2018-09-15 05:37:47 +00:00
public override int GetDaycareSlotOffset(int loc, int slot) => GetPartyOffset(7 + (slot * 2));
public override uint? GetDaycareEXP(int loc, int slot) => null;
public override bool? IsDaycareOccupied(int loc, int slot) => (DaycareFlags[slot] & 1) != 0;
public override void SetDaycareEXP(int loc, int slot, uint EXP) { }
public override void SetDaycareOccupied(int loc, int slot, bool occupied) { }
// Storage
public override int PartyCount
{
get => Data[Offsets.Party]; protected set => Data[Offsets.Party] = (byte)value;
}
2018-09-15 05:37:47 +00:00
public override int GetBoxOffset(int box)
{
2018-09-15 05:37:47 +00:00
return Data.Length - SIZE_RESERVED + (box * SIZE_BOX);
}
2018-09-15 05:37:47 +00:00
public override int GetPartyOffset(int slot)
{
2018-09-15 05:37:47 +00:00
return Data.Length - SIZE_RESERVED + (BoxCount * SIZE_BOX) + (slot * SIZE_STORED);
}
2018-09-15 05:37:47 +00:00
public override int CurrentBox
{
get => Data[Offsets.CurrentBoxIndex] & 0x7F; set => Data[Offsets.CurrentBoxIndex] = (byte)((Data[Offsets.OtherCurrentBox] & 0x80) | (value & 0x7F));
}
public override string GetBoxName(int box)
{
int len = Korean ? 17 : 9;
2018-09-15 05:37:47 +00:00
return GetString(Offsets.BoxNames + (box * len), len);
}
2018-09-15 05:37:47 +00:00
public override void SetBoxName(int box, string value)
{
2017-12-27 23:52:29 +00:00
int len = Korean ? 17 : 9;
var span = Data.AsSpan(Offsets.BoxNames + (box * len), len);
SetString(span, value.AsSpan(), 8, StringConverterOption.Clear50);
}
2019-03-30 02:43:33 +00:00
protected override PKM GetPKM(byte[] data)
{
if (data.Length == SIZE_STORED)
return new PokeList2(data, PokeListType.Single, Japanese)[0];
return new PK2(data);
}
2018-09-15 05:37:47 +00:00
2019-03-30 02:43:33 +00:00
protected override byte[] DecryptPKM(byte[] data)
{
return data;
}
// Pokédex
protected override void SetDex(PKM pkm)
{
int species = pkm.Species;
2021-06-05 00:29:55 +00:00
if (species is 0 or > Legal.MaxSpeciesID_2)
return;
if (pkm.IsEgg)
return;
SetCaught(pkm.Species, true);
SetSeen(pkm.Species, true);
}
2018-09-15 05:37:47 +00:00
private void SetUnownFormFlags()
{
// Give all Unown caught to prevent a crash on pokedex view
for (int i = 1; i <= 26; i++)
Data[Offsets.PokedexSeen + 0x1F + i] = (byte)i;
if (UnownFirstSeen == 0) // Invalid
UnownFirstSeen = 1; // A
}
2018-09-15 05:37:47 +00:00
/// <summary>
/// Toggles the availability of Unown letter groups in the Wild
/// </summary>
/// <remarks>
/// Max value of 0x0F, 4 bitflags
/// 1 lsh 0: A, B, C, D, E, F, G, H, I, J, K
/// 1 lsh 1: L, M, N, O, P, Q, R
/// 1 lsh 2: S, T, U, V, W
/// 1 lsh 3: X, Y, Z
/// </remarks>
public int UnownUnlocked
{
get => Data[Offsets.PokedexSeen + 0x1F + 27];
set => Data[Offsets.PokedexSeen + 0x1F + 27] = (byte)value;
}
2018-09-15 05:37:47 +00:00
/// <summary>
/// Unlocks all Unown letters/forms in the wild.
/// </summary>
public void UnownUnlockAll() => UnownUnlocked = 0x0F; // all 4 bitflags
2018-09-15 05:37:47 +00:00
/// <summary>
/// Flag that determines if Unown Letters are available in the wild: A, B, C, D, E, F, G, H, I, J, K
/// </summary>
public bool UnownUnlocked0
{
get => (UnownUnlocked & 1 << 0) == 1 << 0;
2021-05-18 20:12:43 +00:00
set => UnownUnlocked = (UnownUnlocked & ~(1 << 0)) | ((value ? 1 : 0) << 0);
}
2018-09-15 05:37:47 +00:00
/// <summary>
/// Flag that determines if Unown Letters are available in the wild: L, M, N, O, P, Q, R
/// </summary>
public bool UnownUnlocked1
{
get => (UnownUnlocked & 1 << 1) == 1 << 1;
2021-05-18 20:12:43 +00:00
set => UnownUnlocked = (UnownUnlocked & ~(1 << 1)) | ((value ? 1 : 0) << 1);
}
2018-09-15 05:37:47 +00:00
/// <summary>
/// Flag that determines if Unown Letters are available in the wild: S, T, U, V, W
/// </summary>
public bool UnownUnlocked2
{
get => (UnownUnlocked & 1 << 2) == 1 << 2;
2021-05-18 20:12:43 +00:00
set => UnownUnlocked = (UnownUnlocked & ~(1 << 2)) | ((value ? 1 : 0) << 2);
}
2018-09-15 05:37:47 +00:00
/// <summary>
/// Flag that determines if Unown Letters are available in the wild: X, Y, Z
/// </summary>
public bool UnownUnlocked3
{
get => (UnownUnlocked & 1 << 3) == 1 << 3;
2021-05-18 20:12:43 +00:00
set => UnownUnlocked = (UnownUnlocked & ~(1 << 3)) | ((value ? 1 : 0) << 3);
}
2018-09-15 05:37:47 +00:00
/// <summary>
/// Chooses which Unown sprite to show in the regular Pokédex View
/// </summary>
public int UnownFirstSeen
{
get => Data[Offsets.PokedexSeen + 0x1F + 28];
set => Data[Offsets.PokedexSeen + 0x1F + 28] = (byte)value;
}
2018-09-15 05:37:47 +00:00
public override bool GetSeen(int species) => GetDexFlag(Offsets.PokedexSeen, species);
public override bool GetCaught(int species) => GetDexFlag(Offsets.PokedexCaught, species);
public override void SetSeen(int species, bool seen) => SetDexFlag(Offsets.PokedexSeen, species, seen);
public override void SetCaught(int species, bool caught)
{
SetDexFlag(Offsets.PokedexCaught, species, caught);
if (caught && species == (int)Species.Unown)
SetUnownFormFlags();
}
private bool GetDexFlag(int region, int species)
{
int bit = species - 1;
int ofs = bit >> 3;
return GetFlag(region + ofs, bit & 7);
}
2018-09-15 05:37:47 +00:00
private void SetDexFlag(int region, int species, bool value)
{
int bit = species - 1;
int ofs = bit >> 3;
SetFlag(region + ofs, bit & 7, value);
}
/// <summary>All Event Constant values for the save file</summary>
/// <remarks>These are all bytes</remarks>
public override ushort[] GetEventConsts()
{
ushort[] Constants = new ushort[EventConstMax];
for (int i = 0; i < Constants.Length; i++)
Constants[i] = Data[EventConst + i];
return Constants;
}
/// <summary>All Event Constant values for the save file</summary>
/// <remarks>These are all bytes</remarks>
public override void SetEventConsts(ReadOnlySpan<ushort> value)
{
if (value.Length != EventConstMax)
return;
for (int i = 0; i < value.Length; i++)
Data[EventConst + i] = Math.Min(byte.MaxValue, (byte)value[i]);
}
2017-09-23 23:03:59 +00:00
// Misc
public ushort ResetKey => GetResetKey();
2018-09-15 05:37:47 +00:00
2017-09-23 23:03:59 +00:00
private ushort GetResetKey()
{
var value = (TID >> 8) + (TID & 0xFF) + ((Money >> 16) & 0xFF) + ((Money >> 8) & 0xFF) + (Money & 0xFF);
2021-08-06 05:39:38 +00:00
var ot = Data.AsSpan(Offsets.Trainer1 + 2, 5);
var sum = 0;
foreach (var b in ot)
{
if (b == StringConverter12.G1TerminatorCode)
break;
sum += b;
}
return (ushort)(value + sum);
2017-09-23 23:03:59 +00:00
}
2018-09-15 05:37:47 +00:00
/// <summary>
/// Sets the "Time Not Set" flag to the RTC Flag list.
/// </summary>
public void ResetRTC() => Data[Offsets.RTCFlags] |= 0x80;
public void UnlockAllDecorations()
{
for (int i = 676; i <= 721; i++)
SetEventFlag(i, true);
}
2017-09-23 23:03:59 +00:00
public override string GetString(ReadOnlySpan<byte> data)
{
if (Korean)
return StringConverter2KOR.GetString(data);
return StringConverter12.GetString(data, Japanese);
}
2018-09-15 05:37:47 +00:00
public override int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength, StringConverterOption option)
{
if (Korean)
return StringConverter2KOR.SetString(value, destBuffer, maxLength, option);
return StringConverter12.SetString(destBuffer, value, maxLength, Japanese, option);
}
public bool IsGBMobileAvailable => Japanese && Version == GameVersion.C;
public bool IsGBMobileEnabled => Japanese && Enum.IsDefined(typeof(GBMobileCableColor), GBMobileCable);
public GBMobileCableColor GBMobileCable
{
get => (GBMobileCableColor) Data[0xE800];
set
{
Data[0xE800] = (byte)value;
Data[0x9000] = (byte)(0xFF - value);
}
}
}
public enum GBMobileCableColor : byte
{
None = 0,
Blue = 1,
Yellow = 2,
Green = 3,
Red = 4,
Debug = 0x81,
}
}