Refactor: Split Gen1/2 string & pokelist conversion methods (#4251)

* Split Gen1/2 string & pokelist conversion methods

* Refactor pokelist to direct read/write, skip on save if blanked

* Add settings editor for SaveLanguage overrides
This commit is contained in:
Kurt
2024-04-22 15:42:22 -05:00
committed by GitHub
parent 3087519c62
commit 1b294f7c97
36 changed files with 1583 additions and 550 deletions

View File

@@ -237,7 +237,7 @@ private static List<SlotInfoMisc> GetExtraSlots9(SAV9SV sav)
if (sav.Blocks.TryGetBlock(SaveBlockAccessor9SV.KSurpriseTrade, out var surprise))
{
var st = surprise.Data;
var st = surprise.Data.AsMemory();
list.Add(new(st[0x198..], 0) { Type = StorageSlotType.Misc }); // my upload
list.Add(new(st[0x2C..], 1) { Type = StorageSlotType.Misc }); // received from others
}

View File

@@ -72,7 +72,7 @@ private bool IsNicknameValid(PKM pk, ReadOnlySpan<char> nick)
private static bool IsTrainerNameValid(PKM pk)
{
if (pk.Format <= 2)
return pk.OriginalTrainerTrash is [StringConverter12.G1TradeOTCode, StringConverter12.G1TerminatorCode, ..];
return pk.OriginalTrainerTrash is [StringConverter1.TradeOTCode, StringConverter1.TerminatorCode, ..];
var lang = pk.Language;
var expect = StringConverter12Transporter.GetTradeNameGen1(lang);
return pk.OriginalTrainerName == expect;
@@ -125,7 +125,7 @@ public PK1 ConvertToPKM(ITrainerInfo tr, EncounterCriteria criteria)
Type1 = pi.Type1,
Type2 = pi.Type2,
};
pk.OriginalTrainerTrash[0] = StringConverter12.G1TradeOTCode;
pk.OriginalTrainerTrash[0] = StringConverter1.TradeOTCode;
EncounterUtil.SetEncounterMoves(pk, Version, level);
if (EvolveOnTrade)

View File

@@ -305,17 +305,17 @@ private static void VerifyNicknameTrade(LegalityAnalysis data, IEncounterTemplat
private void VerifyG1NicknameWithinBounds(LegalityAnalysis data, ReadOnlySpan<char> str)
{
var pk = data.Entity;
if (StringConverter12.GetIsG1English(str))
if (StringConverter1.GetIsEnglish(str))
{
if (str.Length > 10)
data.AddLine(GetInvalid(LNickLengthLong));
}
else if (StringConverter12.GetIsG1Japanese(str))
else if (StringConverter1.GetIsJapanese(str))
{
if (str.Length > 5)
data.AddLine(GetInvalid(LNickLengthLong));
}
else if (pk.Korean && StringConverter2KOR.GetIsG2Korean(str))
else if (pk.Korean && StringConverter2KOR.GetIsKorean(str))
{
if (str.Length > 5)
data.AddLine(GetInvalid(LNickLengthLong));

View File

@@ -134,21 +134,21 @@ private void VerifyGBOTWithinBounds(LegalityAnalysis data, ReadOnlySpan<char> st
{
if (str.Length > 5)
data.AddLine(GetInvalid(LOTLong));
if (!StringConverter12.GetIsG1Japanese(str))
if (!StringConverter1.GetIsJapanese(str))
data.AddLine(GetInvalid(LG1CharOT));
}
else if (pk.Korean)
{
if (str.Length > 5)
data.AddLine(GetInvalid(LOTLong));
if (!StringConverter2KOR.GetIsG2Korean(str))
if (!StringConverter2KOR.GetIsKorean(str))
data.AddLine(GetInvalid(LG1CharOT));
}
else
{
if (str.Length > 7)
data.AddLine(GetInvalid(LOTLong));
if (!StringConverter12.GetIsG1English(str))
if (!StringConverter1.GetIsEnglish(str))
data.AddLine(GetInvalid(LG1CharOT));
}
}

View File

@@ -19,6 +19,14 @@ public sealed class PK1 : GBPKML, IPersonalType
public PK1(bool jp = false) : base(PokeCrypto.SIZE_1PARTY, jp) { }
public PK1(byte[] decryptedData, bool jp = false) : base(EnsurePartySize(decryptedData), jp) { }
public PK1(ReadOnlySpan<byte> data, ReadOnlySpan<byte> ot, ReadOnlySpan<byte> nick)
: this(ot.Length == StringLengthJapanese)
{
data.CopyTo(Data);
ot.CopyTo(OriginalTrainerTrash);
nick.CopyTo(NicknameTrash);
}
private static byte[] EnsurePartySize(byte[] data)
{
if (data.Length != PokeCrypto.SIZE_1PARTY)
@@ -34,7 +42,7 @@ public override PK1 Clone()
return clone;
}
protected override byte[] Encrypt() => new PokeList1(this).Write();
protected override byte[] Encrypt() => PokeList1.WrapSingle(this);
#region Stored Attributes
public byte SpeciesInternal { get => Data[0]; set => Data[0] = value; } // raw access
@@ -235,8 +243,29 @@ public PK7 ConvertToPK7()
private string GetTransferTrainerName(int lang)
{
if (OriginalTrainerTrash[0] == StringConverter12.G1TradeOTCode) // In-game Trade
if (OriginalTrainerTrash[0] == StringConverter1.TradeOTCode) // In-game Trade
return StringConverter12Transporter.GetTradeNameGen1(lang);
return StringConverter12Transporter.GetString(OriginalTrainerTrash, Japanese);
}
protected override string GetString(ReadOnlySpan<byte> data)
{
return StringConverter1.GetString(data, Japanese);
}
protected override int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength,
StringConverterOption option = StringConverterOption.None)
{
return StringConverter1.SetString(destBuffer, value, maxLength, Japanese, option);
}
/// <summary>
/// Gets a checksum over all the entity's data using a single list to wrap all components.
/// </summary>
public ushort GetSingleListChecksum()
{
Span<byte> tmp = stackalloc byte[PokeList1.GetListLengthSingle(Japanese)];
PokeList1.WrapSingle(this, tmp);
return Checksums.CRC16_CCITT(tmp);
}
}

View File

@@ -8,8 +8,7 @@ public sealed class PK2 : GBPKML, ICaughtData2
{
public override PersonalInfo2 PersonalInfo => PersonalTable.C[Species];
internal const byte EggSpeciesValue = 0xFD;
public override bool Valid => Species is <= Legal.MaxSpeciesID_2 or EggSpeciesValue; // egg
public override bool Valid => Species <= Legal.MaxSpeciesID_2;
public override int SIZE_PARTY => PokeCrypto.SIZE_2PARTY;
public override int SIZE_STORED => PokeCrypto.SIZE_2STORED;
@@ -20,6 +19,14 @@ public sealed class PK2 : GBPKML, ICaughtData2
public PK2(bool jp = false) : base(PokeCrypto.SIZE_2PARTY, jp) { }
public PK2(byte[] decryptedData, bool jp = false) : base(EnsurePartySize(decryptedData), jp) { }
public PK2(ReadOnlySpan<byte> data, ReadOnlySpan<byte> ot, ReadOnlySpan<byte> nick)
: this(ot.Length == StringLengthJapanese)
{
data.CopyTo(Data);
ot.CopyTo(OriginalTrainerTrash);
nick.CopyTo(NicknameTrash);
}
private static byte[] EnsurePartySize(byte[] data)
{
if (data.Length != PokeCrypto.SIZE_2PARTY)
@@ -35,10 +42,11 @@ public override PK2 Clone()
return clone;
}
protected override byte[] Encrypt() => new PokeList2(this).Write();
protected override byte[] Encrypt() => PokeList2.WrapSingle(this);
#region Stored Attributes
public override ushort Species { get => Data[0]; set => Data[0] = (byte)value; }
public byte SpeciesInternal { get => Data[0]; set => Data[0] = value; } // Alias with a different type.
public override int SpriteItem => ItemConverter.GetItemFuture2((byte)HeldItem);
public override int HeldItem { get => Data[0x1]; set => Data[0x1] = (byte)value; }
public override ushort Move1 { get => Data[2]; set => Data[2] = (byte)value; }
@@ -209,7 +217,7 @@ public PK7 ConvertToPK7()
private string GetTransferTrainerName(int lang)
{
if (OriginalTrainerTrash[0] == StringConverter12.G1TradeOTCode) // In-game Trade
if (OriginalTrainerTrash[0] == StringConverter1.TradeOTCode) // In-game Trade
return StringConverter12Transporter.GetTradeNameGen1(lang);
if (Korean)
return OriginalTrainerName;
@@ -250,4 +258,29 @@ public SK2 ConvertToSK2() => new(Japanese)
Nickname = Nickname,
OriginalTrainerName = OriginalTrainerName,
};
protected override string GetString(ReadOnlySpan<byte> data)
{
if (Korean)
return StringConverter2KOR.GetString(data);
return StringConverter2.GetString(data, Language);
}
protected override int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength,
StringConverterOption option = StringConverterOption.None)
{
if (Korean)
return StringConverter2KOR.SetString(destBuffer, value, maxLength, option);
return StringConverter2.SetString(destBuffer, value, maxLength, Language, option);
}
/// <summary>
/// Gets a checksum over all the entity's data using a single list to wrap all components.
/// </summary>
public ushort GetSingleListChecksum()
{
Span<byte> tmp = stackalloc byte[PokeList2.GetListLengthSingle(Japanese)];
PokeList2.WrapSingle(this, tmp);
return Checksums.CRC16_CCITT(tmp);
}
}

View File

@@ -8,7 +8,7 @@ public sealed class SK2 : GBPKM, ICaughtData2
{
public override PersonalInfo2 PersonalInfo => PersonalTable.C[Species];
public override bool Valid => Species <= 252;
public override bool Valid => Species <= Legal.MaxSpeciesID_2;
public override int SIZE_PARTY => PokeCrypto.SIZE_2STADIUM;
public override int SIZE_STORED => PokeCrypto.SIZE_2STADIUM;
@@ -94,13 +94,13 @@ public bool IsRental
public override string Nickname
{
get => StringConverter12.GetString(NicknameTrash, Japanese);
set => StringConverter12.SetString(NicknameTrash, value, StringLength, Japanese, StringConverterOption.None);
get => StringConverter2.GetString(NicknameTrash, Language);
set => StringConverter2.SetString(NicknameTrash, value, StringLength, Language, StringConverterOption.None);
}
public override string OriginalTrainerName
{
get => StringConverter12.GetString(OriginalTrainerTrash, Japanese);
get => StringConverter2.GetString(OriginalTrainerTrash, Language);
set
{
if (IsRental)
@@ -108,7 +108,7 @@ public override string OriginalTrainerName
OriginalTrainerTrash.Clear();
return;
}
StringConverter12.SetString(OriginalTrainerTrash, value, StringLength, Japanese, StringConverterOption.None);
StringConverter2.SetString(OriginalTrainerTrash, value, StringLength, Language, StringConverterOption.None);
}
}
@@ -135,7 +135,7 @@ public override string OriginalTrainerName
protected override void GetNonNickname(int language, Span<byte> data)
{
var name = SpeciesName.GetSpeciesNameGeneration(Species, language, 2);
StringConverter12.SetString(data, name, data.Length, Japanese, StringConverterOption.Clear50);
StringConverter2.SetString(data, name, data.Length, Language, StringConverterOption.Clear50);
}
public override void SetNotNicknamed(int language) => GetNonNickname(language, NicknameTrash);
@@ -184,18 +184,18 @@ public PK2 ConvertToPK2() => new(Japanese)
private static bool IsJapanese(ReadOnlySpan<byte> data)
{
const byte empty = 0;
const byte terminator = StringConverter12.G1TerminatorCode;
const byte terminator = StringConverter2.TerminatorCode;
var ot = data.Slice(0x30, StringLength);
if (ot[6..].ContainsAnyExcept(empty, terminator))
return false;
if (!StringConverter12.GetIsG1Japanese(ot))
if (!StringConverter2.GetIsJapanese(ot))
return false;
var nick = data.Slice(0x24, StringLength);
if (nick[6..].ContainsAnyExcept(empty, terminator))
return false;
if (!StringConverter12.GetIsG1Japanese(nick))
if (!StringConverter2.GetIsJapanese(nick))
return false;
return true;
@@ -203,9 +203,9 @@ private static bool IsJapanese(ReadOnlySpan<byte> data)
private static bool IsEnglish(ReadOnlySpan<byte> data)
{
if (!StringConverter12.GetIsG1English(data.Slice(0x30, StringLength)))
if (!StringConverter2.GetIsEnglish(data.Slice(0x30, StringLength)))
return false;
if (!StringConverter12.GetIsG1English(data.Slice(0x24, StringLength)))
if (!StringConverter2.GetIsEnglish(data.Slice(0x24, StringLength)))
return false;
return true;
}

View File

@@ -74,9 +74,12 @@ public sealed override int Language
return (int)LanguageID.Japanese;
if (Korean)
return (int)LanguageID.Korean;
if (StringConverter12.IsG12German(OriginalTrainerTrash))
if (StringConverter1.IsG12German(OriginalTrainerTrash))
return (int)LanguageID.German; // german
int lang = SpeciesName.GetSpeciesNameLanguage(Species, Nickname, Format);
Span<char> nick = stackalloc char[11];
int len = StringConverter1.LoadString(NicknameTrash, nick, false);
int lang = SpeciesName.GetSpeciesNameLanguage(Species, nick[..len], Format);
if (lang > 0)
return lang;
return 0;

View File

@@ -29,8 +29,8 @@ protected GBPKML([ConstantExpected] int size, bool jp = false) : base(size)
// initialize string buffers
RawOT = new byte[strLen];
RawNickname = new byte[strLen];
OriginalTrainerTrash.Fill(StringConverter12.G1TerminatorCode);
NicknameTrash.Fill(StringConverter12.G1TerminatorCode);
OriginalTrainerTrash.Fill(StringConverter1.TerminatorCode);
NicknameTrash.Fill(StringConverter1.TerminatorCode);
}
protected GBPKML(byte[] data, bool jp = false) : base(data)
@@ -40,8 +40,8 @@ protected GBPKML(byte[] data, bool jp = false) : base(data)
// initialize string buffers
RawOT = new byte[strLen];
RawNickname = new byte[strLen];
OriginalTrainerTrash.Fill(StringConverter12.G1TerminatorCode);
NicknameTrash.Fill(StringConverter12.G1TerminatorCode);
OriginalTrainerTrash.Fill(StringConverter1.TerminatorCode);
NicknameTrash.Fill(StringConverter1.TerminatorCode);
}
public override void SetNotNicknamed(int language) => GetNonNickname(language, RawNickname);
@@ -49,7 +49,7 @@ protected GBPKML(byte[] data, bool jp = false) : base(data)
protected override void GetNonNickname(int language, Span<byte> data)
{
var name = SpeciesName.GetSpeciesNameGeneration(Species, language, Format);
SetString(name, data, data.Length, StringConverterOption.Clear50);
SetString(data, name, data.Length, StringConverterOption.Clear50);
if (Korean)
return;
@@ -61,19 +61,10 @@ protected override void GetNonNickname(int language, Span<byte> data)
}
}
private string GetString(ReadOnlySpan<byte> span)
{
if (Korean)
return StringConverter2KOR.GetString(span);
return StringConverter12.GetString(span, Japanese);
}
protected abstract string GetString(ReadOnlySpan<byte> data);
private int SetString(ReadOnlySpan<char> value, Span<byte> destBuffer, int maxLength, StringConverterOption option = StringConverterOption.None)
{
if (Korean)
return StringConverter2KOR.SetString(destBuffer, value, maxLength, option);
return StringConverter12.SetString(destBuffer, value, maxLength, Japanese, option);
}
protected abstract int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength,
StringConverterOption option = StringConverterOption.None);
public sealed override string Nickname
{
@@ -103,6 +94,6 @@ private void SetStringKeepTerminatorStyle(ReadOnlySpan<char> value, Span<byte> e
// Reset the destination buffer based on the termination style of the existing string.
bool zeroed = exist.Contains<byte>(0);
StringConverterOption converterOption = (zeroed) ? StringConverterOption.ClearZero : StringConverterOption.Clear50;
SetString(value, exist, value.Length, converterOption);
SetString(exist, value, value.Length, converterOption);
}
}

View File

@@ -5,24 +5,266 @@ namespace PKHeX.Core;
/// <summary>
/// List of <see cref="PK1"/> prefixed by a count.
/// </summary>
public sealed class PokeList1 : PokeListGB<PK1>
public static class PokeList1
{
protected override byte GetSpeciesBoxIdentifier(PK1 pk) => SpeciesConverter.GetInternal1(pk.Species);
protected override PK1 GetEntry(byte[] dat, ReadOnlySpan<byte> otname, ReadOnlySpan<byte> nick, bool egg)
// Structure:
// u8 Count of slots filled
// s8[capacity+1] GB Species ID in Slot (-1 = no species)
// pkx[capacity] GB PKM data (no strings)
// str[capacity] Trainer Name
// str[capacity] Nickname
//
// where,
// - str has variable size (jp/int)
// - pkx is different size for Gen1/Gen2
public const byte SlotEmpty = 0xFF;
/// <summary>
/// Gets the species identifier for the slot.
/// </summary>
public static byte GetHeaderIdentifierMark(PK1 pk)
{
var result = new PK1(dat, Japanese); // no eggs
otname.CopyTo(result.OriginalTrainerTrash);
nick.CopyTo(result.NicknameTrash);
return result;
var species = pk.SpeciesInternal;
if (species == 0)
return SlotEmpty;
// Let out-of-bounds species fall through.
return species;
}
protected override int GetEntrySize() => GetEntrySize(IsFormatParty);
/// <summary>
/// Checks if the slot has content present.
/// </summary>
/// <param name="marker">Identifying summary mark from the list header.</param>
/// <returns>True if present.</returns>
private static bool IsPresent(byte marker) => marker is not (0 or SlotEmpty);
public PokeList1(byte[] d, PokeListType c = PokeListType.Single, bool jp = false) : base(d, c, jp) { }
public PokeList1(PokeListType c = PokeListType.Single, bool jp = false) : base(c, jp) { }
public PokeList1(byte[] d) : base(d, PokeListType.Single, d.Length == PokeCrypto.SIZE_1JLIST) { }
public PokeList1(PK1 pk) : base(pk) { }
/// <summary>
/// Counts the number of present slots in the list.
/// </summary>
/// <param name="input">List header</param>
/// <param name="capacity">Count of slots allowed in the list</param>
/// <param name="lerp">Offset jump between slot indexes; used to parse multi-single lists instead of a multi-list.</param>
private static int CountPresent(ReadOnlySpan<byte> input, int capacity, int lerp = 0)
{
var count = 0;
for (int i = 0; i < capacity; i++)
{
var mark = input[(i * lerp) + 1];
if (IsPresent(mark))
count++;
}
return count;
}
private static int GetEntrySize(bool party) => party ? PokeCrypto.SIZE_1PARTY : PokeCrypto.SIZE_1STORED;
public static int GetDataLength(PokeListType c, bool jp) => GetDataSize(c, jp, GetEntrySize(IsCapacityPartyFormat(c)));
private static bool IsJapaneseList(int length) => length == PokeCrypto.SIZE_1JLIST;
private static bool IsJapaneseString(int length) => length == GBPKML.StringLengthJapanese;
public static int GetListLengthSingle(bool jp) => jp ? PokeCrypto.SIZE_1JLIST : PokeCrypto.SIZE_1ULIST;
private static int GetBodyLength(bool party) => party ? PokeCrypto.SIZE_1PARTY : PokeCrypto.SIZE_1STORED;
private static int GetStringLength(bool jp) => jp ? GBPKML.StringLengthJapanese : GBPKML.StringLengthNotJapan;
/// <summary>
/// Checks if the header is sane.
/// </summary>
/// <param name="input">List header</param>
/// <param name="capacity">Count of slots allowed in the list</param>
public static bool IsValidHeader(ReadOnlySpan<byte> input, int capacity)
{
if (input.Length < 2)
return false;
var count = input[0];
if (count > capacity)
return false;
var expectLength = 1 + (capacity + 1);
if (input.Length < expectLength)
return false;
for (int i = 0; i < capacity; i++)
{
var mark = input[1 + i];
bool present = IsPresent(mark);
if (present != (i < count))
return false;
}
return true;
}
/// <summary>
/// Reads an entity from a single-slot list.
/// </summary>
public static PK1 ReadFromSingle(ReadOnlySpan<byte> input)
{
bool jp = IsJapaneseList(input.Length);
var stringLength = GetStringLength(jp);
return ReadFromList(input, stringLength);
}
/// <summary>
/// Reads an entity from a list.
/// </summary>
/// <param name="input">List data</param>
/// <param name="stringLength">Trainer and Nickname string length</param>
/// <param name="capacity">Count of slots allowed in the list</param>
/// <param name="isParty">List stores party stats for each entity</param>
/// <param name="index">Entity index to read</param>
public static PK1 ReadFromList(ReadOnlySpan<byte> input, int stringLength, int capacity = 1, bool isParty = true, int index = 0)
{
var start = 1 + (capacity + 1);
int sizeBody = GetBodyLength(isParty);
var ofsBody = start + (sizeBody * index);
var ofsStr1 = start + (sizeBody * capacity) + (stringLength * index);
var ofsStr2 = ofsStr1 + (capacity * stringLength);
var body = input.Slice(ofsBody, sizeBody);
var stringSection1 = input.Slice(ofsStr1, stringLength);
var stringSection2 = input.Slice(ofsStr2, stringLength);
return new PK1(body, stringSection1, stringSection2);
}
/// <summary>
/// Writes an entity to a list.
/// </summary>
/// <param name="output">List data</param>
/// <param name="pk">Entity to write</param>
/// <param name="capacity">Count of slots allowed in the list</param>
/// <param name="isParty">List stores party stats for each entity</param>
/// <param name="index">Entity index to write</param>
public static void WriteToList(Span<byte> output, PK1 pk, int capacity = 1, bool isParty = true, int index = 0)
{
var start = 1 + (capacity + 1);
var sizeBody = GetBodyLength(isParty);
var stringLength = pk.OriginalTrainerTrash.Length;
var ofsBody = start + (sizeBody * index);
var ofsStr1 = start + (sizeBody * capacity) + (stringLength * index);
var ofsStr2 = ofsStr1 + (capacity * stringLength);
var body = output.Slice(ofsBody, sizeBody);
var stringSection1 = output.Slice(ofsStr1, stringLength);
var stringSection2 = output.Slice(ofsStr2, stringLength);
pk.Data.AsSpan()[..sizeBody].CopyTo(body);
pk.OriginalTrainerTrash.CopyTo(stringSection1);
pk.NicknameTrash.CopyTo(stringSection2);
output[1 + index] = GetHeaderIdentifierMark(pk);
output[0] = (byte)CountPresent(output, capacity);
output[1 + capacity] = SlotEmpty; // cap off the list
}
/// <summary>
/// Unpacks a multi-slot list into many single-slot lists.
/// </summary>
/// <param name="input">Multi-slot list</param>
/// <param name="output">Destination to write each single-slot list</param>
/// <param name="capacity">Count of slots allowed in the list</param>
/// <param name="stringLength">Trainer and Nickname string length</param>
/// <param name="isParty">List stores party stats for each entity</param>
public static void Unpack(ReadOnlySpan<byte> input, Span<byte> output, int stringLength, int capacity, bool isParty)
{
var lengthBody = GetBodyLength(isParty);
var lengthParty = GetBodyLength(true);
var ofsBody = 1 + (capacity + 1);
var ofsStr1 = ofsBody + (capacity * lengthBody);
var ofsStr2 = ofsStr1 + (capacity * stringLength);
for (int i = 0; i < capacity; i++)
{
var species = input[1 + i];
var body = input.Slice(ofsBody, lengthBody);
var stringSection1 = input.Slice(ofsStr1, stringLength);
var stringSection2 = input.Slice(ofsStr2, stringLength);
output[0] = 1;
output[1] = species;
output = output[3..];
body.CopyTo(output);
output = output[lengthParty..];
stringSection1.CopyTo(output);
output = output[stringLength..];
stringSection2.CopyTo(output);
output = output[stringLength..];
ofsBody += lengthBody;
ofsStr1 += stringLength;
ofsStr2 += stringLength;
}
}
/// <summary>
/// Packs many single-slot lists into a multi-slot list.
/// </summary>
/// <param name="input">Source single-slot lists</param>
/// <param name="output">Destination multi-slot list</param>
/// <param name="stringLength">Trainer and Nickname string length</param>
/// <param name="capacity">Count of slots allowed in the list</param>
/// <param name="isParty">List stores party stats for each entity</param>
public static bool MergeSingles(ReadOnlySpan<byte> input, Span<byte> output, int stringLength, int capacity, bool isParty)
{
// Collect the count of set slots
var jp = IsJapaneseString(stringLength);
var size = GetListLengthSingle(jp);
int count = CountPresent(input, capacity, size);
if (count == 0 && !output.ContainsAnyExcept<byte>(0))
return false; // No need to merge if all empty and dest is not initialized.
output[0] = (byte)count; // ensure written list is valid
// Put non-zero species first, then empty slots
var ctr = 0;
var emptyIndex = count;
var lengthBody = GetBodyLength(isParty);
var lengthParty = GetBodyLength(true);
for (int i = 0; i < capacity; i++)
{
var single = input.Slice(i * size, size);
var marker = single[1]; // assume correct, don't look in body data.
var index = IsPresent(marker) ? ctr++ : emptyIndex++;
output[1 + index] = marker;
// Source Properties
var body = single.Slice(3, lengthBody);
var str1 = single.Slice(3 + lengthParty, stringLength);
var str2 = single.Slice(3 + lengthParty + stringLength, stringLength);
// Destination Properties
var ofsStart = 1 + (capacity + 1);
var ofsBody = ofsStart + (index * lengthBody);
var ofsStr1 = ofsStart + (capacity * lengthBody) + (index * stringLength);
var ofsStr2 = ofsStr1 + (capacity * stringLength);
var destBody = output.Slice(ofsBody, lengthBody);
var destStr1 = output.Slice(ofsStr1, stringLength);
var destStr2 = output.Slice(ofsStr2, stringLength);
body.CopyTo(destBody);
str1.CopyTo(destStr1);
str2.CopyTo(destStr2);
}
output[1 + capacity] = SlotEmpty; // cap off the list
return true;
}
/// <inheritdoc cref="WrapSingle(PK1,Span{byte})"/>
public static byte[] WrapSingle(PK1 pk)
{
var length = GetListLengthSingle(pk.Japanese);
var data = new byte[length];
WrapSingle(pk, data);
return data;
}
/// <summary>
/// Wraps a single entity into a single-slot list.
/// </summary>
/// <param name="pk">Entity to wrap</param>
/// <param name="output">Destination to write the single-slot list</param>
public static void WrapSingle(PK1 pk, Span<byte> output) => WriteToList(output, pk);
}

View File

@@ -3,26 +3,272 @@
namespace PKHeX.Core;
/// <summary>
/// List of <see cref="PK1"/> prefixed by a count.
/// List of <see cref="PK2"/> prefixed by a count.
/// </summary>
public sealed class PokeList2 : PokeListGB<PK2>
public static class PokeList2
{
protected override byte GetSpeciesBoxIdentifier(PK2 pk) => pk.IsEgg ? PK2.EggSpeciesValue : (byte)pk.Species;
protected override PK2 GetEntry(byte[] dat, ReadOnlySpan<byte> otname, ReadOnlySpan<byte> nick, bool egg)
// Structure:
// u8 Count of slots filled
// s8[capacity+1] GB Species ID in Slot (-1 = no species)
// pkx[capacity] GB PKM data (no strings)
// str[capacity] Trainer Name
// str[capacity] Nickname
//
// where,
// - str has variable size (jp/int)
// - pkx is different size for Gen1/Gen2
public const byte SlotEmpty = 0xFF;
public const byte SlotEgg = 0xFD;
/// <summary>
/// Gets the species identifier for the slot.
/// </summary>
public static byte GetHeaderIdentifierMark(PK2 pk)
{
var result = new PK2(dat, Japanese) { IsEgg = egg };
otname.CopyTo(result.OriginalTrainerTrash);
nick.CopyTo(result.NicknameTrash);
return result;
var species = pk.SpeciesInternal;
if (species == 0)
return SlotEmpty;
if (pk.IsEgg)
return SlotEgg;
// Let out-of-bounds species fall through.
return species;
}
protected override int GetEntrySize() => GetEntrySize(IsFormatParty);
/// <summary>
/// Checks if the slot has content present.
/// </summary>
/// <param name="marker">Identifying summary mark from the list header.</param>
/// <returns>True if present.</returns>
private static bool IsPresent(byte marker) => marker is not (0 or SlotEmpty);
private static bool IsEgg(byte marker) => marker == SlotEgg;
public PokeList2(byte[] d, PokeListType c = PokeListType.Single, bool jp = false) : base(d, c, jp) { }
public PokeList2(PokeListType c = PokeListType.Single, bool jp = false) : base(c, jp) { }
public PokeList2(byte[] d) : base(d, PokeListType.Single, d.Length == PokeCrypto.SIZE_2JLIST) { }
public PokeList2(PK2 pk) : base(pk) { }
/// <summary>
/// Counts the number of present slots in the list.
/// </summary>
/// <param name="input">List header</param>
/// <param name="capacity">Count of slots allowed in the list</param>
/// <param name="lerp">Offset jump between slot indexes; used to parse multi-single lists instead of a multi-list.</param>
private static int CountPresent(ReadOnlySpan<byte> input, int capacity, int lerp = 0)
{
var nonZeroSpecies = 0;
for (int i = 0; i < capacity; i++)
{
var mark = input[(i * lerp) + 1];
if (IsPresent(mark))
nonZeroSpecies++;
}
return nonZeroSpecies;
}
private static int GetEntrySize(bool party) => party ? PokeCrypto.SIZE_2PARTY : PokeCrypto.SIZE_2STORED;
public static int GetDataLength(PokeListType c, bool jp) => GetDataSize(c, jp, GetEntrySize(IsCapacityPartyFormat(c)));
private static bool IsJapaneseList(int length) => length == PokeCrypto.SIZE_2JLIST;
private static bool IsJapaneseString(int length) => length == GBPKML.StringLengthJapanese;
public static int GetListLengthSingle(bool jp) => jp ? PokeCrypto.SIZE_2JLIST : PokeCrypto.SIZE_2ULIST;
private static int GetBodyLength(bool party) => party ? PokeCrypto.SIZE_2PARTY : PokeCrypto.SIZE_2STORED;
private static int GetStringLength(bool jp) => jp ? GBPKML.StringLengthJapanese : GBPKML.StringLengthNotJapan;
/// <summary>
/// Checks if the header is sane.
/// </summary>
/// <param name="input">List header</param>
/// <param name="capacity">Count of slots allowed in the list</param>
public static bool IsValidHeader(ReadOnlySpan<byte> input, int capacity)
{
if (input.Length < 2)
return false;
var count = input[0];
if (count > capacity)
return false;
var expectLength = 1 + (capacity + 1);
if (input.Length < expectLength)
return false;
for (int i = 0; i < capacity; i++)
{
var mark = input[1 + i];
bool present = IsPresent(mark);
if (present != (i < count))
return false;
}
return true;
}
/// <summary>
/// Reads an entity from a single-slot list.
/// </summary>
public static PK2 ReadFromSingle(ReadOnlySpan<byte> input)
{
var jp = IsJapaneseList(input.Length);
var stringLength = GetStringLength(jp);
return ReadFromList(input, stringLength);
}
/// <summary>
/// Reads an entity from a list.
/// </summary>
/// <param name="input">List data</param>
/// <param name="stringLength">Trainer and Nickname string length</param>
/// <param name="capacity">Count of slots allowed in the list</param>
/// <param name="isParty">List stores party stats for each entity</param>
/// <param name="index">Entity index to read</param>
public static PK2 ReadFromList(ReadOnlySpan<byte> input, int stringLength, int capacity = 1, bool isParty = true, int index = 0)
{
var start = 1 + (capacity + 1);
var sizeBody = GetBodyLength(isParty);
var ofsBody = start + (sizeBody * index);
var ofsStr1 = start + (sizeBody * capacity) + (stringLength * index);
var ofsStr2 = ofsStr1 + (capacity * stringLength);
var body = input.Slice(ofsBody, sizeBody);
var stringSection1 = input.Slice(ofsStr1, stringLength);
var stringSection2 = input.Slice(ofsStr2, stringLength);
return new PK2(body, stringSection1, stringSection2) { IsEgg = IsEgg(input[1 + index]) };
}
/// <summary>
/// Writes an entity to a list.
/// </summary>
/// <param name="output">List data</param>
/// <param name="pk">Entity to write</param>
/// <param name="capacity">Count of slots allowed in the list</param>
/// <param name="isParty">List stores party stats for each entity</param>
/// <param name="index">Entity index to write</param>
public static void WriteToList(Span<byte> output, PK2 pk, int capacity = 1, bool isParty = true, int index = 0)
{
var start = 1 + (capacity + 1);
var sizeBody = GetBodyLength(isParty);
var stringLength = pk.OriginalTrainerTrash.Length;
var ofsBody = start + (sizeBody * index);
var ofsStr1 = start + (sizeBody * capacity) + (stringLength * index);
var ofsStr2 = ofsStr1 + (capacity * stringLength);
var body = output.Slice(ofsBody, sizeBody);
var stringSection1 = output.Slice(ofsStr1, stringLength);
var stringSection2 = output.Slice(ofsStr2, stringLength);
pk.Data.AsSpan()[..sizeBody].CopyTo(body);
pk.OriginalTrainerTrash.CopyTo(stringSection1);
pk.NicknameTrash.CopyTo(stringSection2);
output[1 + index] = GetHeaderIdentifierMark(pk);
output[0] = (byte)CountPresent(output, capacity);
output[1 + capacity] = SlotEmpty; // cap off the list
}
/// <summary>
/// Unpacks a multi-slot list into many single-slot lists.
/// </summary>
/// <param name="input">Multi-slot list</param>
/// <param name="output">Destination to write each single-slot list</param>
/// <param name="stringLength">Trainer and Nickname string length</param>
/// <param name="capacity">Count of slots allowed in the list</param>
/// <param name="isParty">List stores party stats for each entity</param>
public static void Unpack(ReadOnlySpan<byte> input, Span<byte> output, int stringLength, int capacity, bool isParty)
{
var lengthBody = GetBodyLength(isParty);
var lengthParty = GetBodyLength(true);
var ofsBody = 1 + (capacity + 1);
var ofsStr1 = ofsBody + (capacity * lengthBody);
var ofsStr2 = ofsStr1 + (capacity * stringLength);
for (int i = 0; i < capacity; i++)
{
var species = input[1 + i];
var body = input.Slice(ofsBody, lengthBody);
var stringSection1 = input.Slice(ofsStr1, stringLength);
var stringSection2 = input.Slice(ofsStr2, stringLength);
output[0] = 1;
output[1] = species;
output = output[3..];
body.CopyTo(output);
output = output[lengthParty..];
stringSection1.CopyTo(output);
output = output[stringLength..];
stringSection2.CopyTo(output);
output = output[stringLength..];
ofsBody += lengthBody;
ofsStr1 += stringLength;
ofsStr2 += stringLength;
}
}
/// <summary>
/// Packs many single-slot lists into a multi-slot list.
/// </summary>
/// <param name="input">Source single-slot lists</param>
/// <param name="output">Destination multi-slot list</param>
/// <param name="stringLength">Trainer and Nickname string length</param>
/// <param name="capacity">Count of slots allowed in the list</param>
/// <param name="isParty">List stores party stats for each entity</param>
public static bool MergeSingles(ReadOnlySpan<byte> input, Span<byte> output, int stringLength, int capacity, bool isParty)
{
// Collect the count of set slots
var jp = IsJapaneseString(stringLength);
var size = GetListLengthSingle(jp);
int count = CountPresent(input, capacity, size);
if (count == 0 && !output.ContainsAnyExcept<byte>(0))
return false; // No need to merge if all empty and dest is not initialized.
output[0] = (byte)count; // ensure written list is valid
// Put non-zero species first, then empty slots
var ctr = 0;
var emptyIndex = count;
var lengthBody = GetBodyLength(isParty);
var lengthParty = GetBodyLength(true);
for (int i = 0; i < capacity; i++)
{
var single = input.Slice(i * size, size);
var marker = single[1]; // assume correct, don't look in body data.
var index = IsPresent(marker) ? ctr++ : emptyIndex++;
output[1 + index] = marker;
// Source Properties
var body = single.Slice(3, lengthBody);
var str1 = single.Slice(3 + lengthParty, stringLength);
var str2 = single.Slice(3 + lengthParty + stringLength, stringLength);
// Destination Properties
var ofsStart = 1 + (capacity + 1);
var ofsBody = ofsStart + (index * lengthBody);
var ofsStr1 = ofsStart + (capacity * lengthBody) + (index * stringLength);
var ofsStr2 = ofsStr1 + (capacity * stringLength);
var destBody = output.Slice(ofsBody, lengthBody);
var destStr1 = output.Slice(ofsStr1, stringLength);
var destStr2 = output.Slice(ofsStr2, stringLength);
body.CopyTo(destBody);
str1.CopyTo(destStr1);
str2.CopyTo(destStr2);
}
output[1 + capacity] = SlotEmpty; // cap off the list
return true;
}
/// <inheritdoc cref="WrapSingle(PK2,Span{byte})"/>
public static byte[] WrapSingle(PK2 pk)
{
var length = GetListLengthSingle(pk.Japanese);
var data = new byte[length];
WrapSingle(pk, data);
return data;
}
/// <summary>
/// Wraps a single entity into a single-slot list.
/// </summary>
/// <param name="pk">Entity to wrap</param>
/// <param name="output">Destination to write the single-slot list</param>
public static void WrapSingle(PK2 pk, Span<byte> output) => WriteToList(output, pk);
}

View File

@@ -1,161 +0,0 @@
using System;
namespace PKHeX.Core;
/// <summary>
/// List of <see cref="T"/> prefixed by a count.
/// </summary>
/// <typeparam name="T"><see cref="PKM"/> type that inherits from <see cref="GBPKML"/>.</typeparam>
public abstract class PokeListGB<T> where T : GBPKML
{
// Structure:
// u8 Count of slots filled
// s8[capacity+1] GB Species ID in Slot (-1 = no species)
// pkx[capacity] GB PKM data (no strings)
// str[capacity] Trainer Name
// str[capacity] Nickname
//
// where,
// - str has variable size (jp/int)
// - pkx is different size for Gen1/Gen2
private readonly int StringLength;
private readonly byte[] Data;
private readonly byte Capacity;
private readonly int EntrySize;
protected readonly bool Japanese;
public readonly T[] Pokemon;
/// <summary>
/// Count of slots filled in the list
/// </summary>
public byte Count { get => Data[0]; private set => Data[0] = value > Capacity ? Capacity : value; }
private const int SLOT_NONE = byte.MaxValue;
protected PokeListGB(byte[]? d, PokeListType c = PokeListType.Single, bool jp = false)
{
Japanese = jp;
Capacity = (byte)c;
EntrySize = GetEntrySize();
StringLength = GetStringLength(jp);
byte[] data = d ?? GetEmptyList(c, jp);
var dataSize = 1 + 1 + (Capacity * (EntrySize + 1 + (2 * StringLength)));
Array.Resize(ref data, dataSize);
Data = data;
Pokemon = Read();
}
protected PokeListGB(PokeListType c = PokeListType.Single, bool jp = false)
: this(null, c, jp) => Count = 1;
protected PokeListGB(T pk)
: this(PokeListType.Single, pk.Japanese)
{
this[0] = pk;
Count = 1;
}
private byte[] GetEmptyList(PokeListType c, bool jp = false)
{
int capacity = (byte)c;
int size_intro = capacity + 1;
int size_pkm = GetEntrySize() * capacity;
int size_str = 2 * GetStringLength(jp) * capacity;
// first byte: count (0)
var result = new byte[1 + size_intro + size_pkm + size_str];
// species present in slot: none
for (int i = 1; i <= size_intro; i++)
result[i] = SLOT_NONE;
// fill string buffers with terminators
for (int i = 1 + size_intro + size_pkm; i < result.Length; i++)
result[i] = StringConverter12.G1TerminatorCode; // terminator
return result;
}
private int GetOffsetPKMData(int base_ofs, int index) => base_ofs + (EntrySize * index);
private int GetOffsetPKMOT(int base_ofs, int index) => GetOffsetPKMData(base_ofs, Capacity) + (StringLength * index);
private int GetOffsetPKMNickname(int base_ofs, int index) => GetOffsetPKMOT(base_ofs, Capacity) + (StringLength * index);
private static int GetStringLength(bool jp) => jp ? GBPKML.StringLengthJapanese : GBPKML.StringLengthNotJapan;
protected bool IsFormatParty => IsCapacityPartyFormat((PokeListType)Capacity);
protected static bool IsCapacityPartyFormat(PokeListType Capacity) => Capacity is PokeListType.Single or PokeListType.Party;
protected static int GetDataSize(PokeListType c, bool jp, int entrySize)
{
var entryLength = 1 + entrySize + (2 * GetStringLength(jp));
return 2 + ((byte)c * entryLength);
}
protected abstract int GetEntrySize();
protected abstract byte GetSpeciesBoxIdentifier(T pk);
protected abstract T GetEntry(byte[] dat, ReadOnlySpan<byte> otname, ReadOnlySpan<byte> nick, bool egg);
public T this[int i]
{
get
{
if ((uint)i > Capacity)
throw new ArgumentOutOfRangeException($"Invalid {nameof(PokeListGB<T>)} Access: {i}");
return Pokemon[i];
}
set => Pokemon[i] = (T)value.Clone();
}
private T[] Read()
{
var arr = new T[Capacity];
int base_ofs = 2 + Capacity;
for (int i = 0; i < Capacity; i++)
arr[i] = GetEntry(base_ofs, i);
return arr;
}
public byte[] Write()
{
// Rebuild GB Species ID table
int count = Array.FindIndex(Pokemon, static pk => pk.Species == 0);
Count = count < 0 ? Capacity : (byte)count;
int base_ofs = 1 + 1 + Capacity;
for (int i = 0; i < Count; i++)
{
Data[1 + i] = GetSpeciesBoxIdentifier(Pokemon[i]);
SetEntry(base_ofs, i);
}
Data[1 + Count] = SLOT_NONE;
return Data;
}
private T GetEntry(int base_ofs, int index)
{
int pkOfs = GetOffsetPKMData(base_ofs, index);
int otOfs = GetOffsetPKMOT(base_ofs, index);
int nkOfs = GetOffsetPKMNickname(base_ofs, index);
var dat = Data.AsSpan(pkOfs, EntrySize).ToArray();
var otname = Data.AsSpan(otOfs, StringLength);
var nick = Data.AsSpan(nkOfs, StringLength);
return GetEntry(dat, otname, nick, Data[1 + index] == 0xFD);
}
private void SetEntry(int base_ofs, int index)
{
int pkOfs = GetOffsetPKMData(base_ofs, index);
int otOfs = GetOffsetPKMOT(base_ofs, index);
int nkOfs = GetOffsetPKMNickname(base_ofs, index);
var pk = Pokemon[index];
Array.Copy(pk.Data, 0, Data, pkOfs, EntrySize);
pk.OriginalTrainerTrash.CopyTo(Data.AsSpan(otOfs));
pk.NicknameTrash.CopyTo(Data.AsSpan(nkOfs));
}
}

View File

@@ -1,23 +0,0 @@
namespace PKHeX.Core;
/// <summary>
/// Type of <see cref="PokeListGB{T}"/> that is stored.
/// </summary>
/// <remarks>Acts as a magic capacity value; there aren't any other List sizes used by the games.</remarks>
public enum PokeListType : byte
{
/// <summary> Invalid type. </summary>
None = 0,
/// <summary> A single <see cref="GBPKM"/> is contained in this list. </summary>
Single = 1,
/// <summary> A full party of <see cref="GBPKM"/> is contained in this list. </summary>
Party = 6,
/// <summary> A full box of <see cref="GBPKM"/> (International format) is contained in this list. </summary>
Stored = 20,
/// <summary> A full box of <see cref="GBPKM"/> (Japanese) is contained in this list. </summary>
StoredJP = 30,
}

View File

@@ -21,7 +21,8 @@ public static class StringConverter
3 when isBigEndian => StringConverter3GC.GetString(data),
4 when isBigEndian => StringConverter4GC.GetString(data),
1 or 2 => StringConverter12.GetString(data, jp),
1 => StringConverter1.GetString(data, jp),
2 => StringConverter2.GetString(data, language),
3 => StringConverter3.GetString(data, language),
4 => StringConverter4.GetString(data),
5 => StringConverter5.GetString(data),
@@ -47,7 +48,8 @@ public static class StringConverter
3 when isBigEndian => StringConverter3GC.LoadString(data, result),
4 when isBigEndian => StringConverter4GC.LoadString(data, result),
1 or 2 => StringConverter12.LoadString(data, result, jp),
1 => StringConverter1.LoadString(data, result, jp),
2 => StringConverter2.LoadString(data, result, language),
3 => StringConverter3.LoadString(data, result, language),
4 => StringConverter4.LoadString(data, result),
5 => StringConverter5.LoadString(data, result),
@@ -76,7 +78,8 @@ public static class StringConverter
3 when isBigEndian => StringConverter3GC.SetString(destBuffer, value, maxLength, option),
4 when isBigEndian => StringConverter4GC.SetString(destBuffer, value, maxLength, option),
1 or 2 => StringConverter12.SetString(destBuffer, value, maxLength, jp, option),
1 => StringConverter1.SetString(destBuffer, value, maxLength, jp, option),
2 => StringConverter2.SetString(destBuffer, value, maxLength, language, option),
3 => StringConverter3.SetString(destBuffer, value, maxLength, language, option),
4 => StringConverter4.SetString(destBuffer, value, maxLength, option),
5 => StringConverter5.SetString(destBuffer, value, maxLength, option),

View File

@@ -3,11 +3,20 @@
namespace PKHeX.Core;
/// <summary>
/// Logic for converting a <see cref="string"/> for Generation 1 &amp; 2 games.
/// Logic for converting a <see cref="string"/> for Generation 1.
/// </summary>
public static class StringConverter12
/// <remarks>Slight differences when compared to <seealso cref="StringConverter2"/>.</remarks>
public static class StringConverter1
{
public static bool GetIsG1Japanese(ReadOnlySpan<char> str) => AllJapanese(str);
public const byte TerminatorCode = 0x50;
public const byte TerminatorZero = 0x00;
public const byte TradeOTCode = 0x5D;
public const byte SpaceCode = 0x7F;
public const char Terminator = '\0';
public const char TradeOT = '*';
public static bool GetIsJapanese(ReadOnlySpan<char> str) => AllJapanese(str);
private static bool AllJapanese(ReadOnlySpan<char> str)
{
@@ -20,28 +29,21 @@ private static bool AllJapanese(ReadOnlySpan<char> str)
static bool IsJapanese(char c) => c is >= '\u3000' and <= '\u30FC';
}
public static bool GetIsG1English(ReadOnlySpan<char> str) => !GetIsG1Japanese(str);
public static bool GetIsG1Japanese(ReadOnlySpan<byte> raw) => AllCharsInTable(raw, TableJP);
public static bool GetIsG1English(ReadOnlySpan<byte> raw) => AllCharsInTable(raw, TableEN);
public static bool GetIsEnglish(ReadOnlySpan<char> str) => !GetIsJapanese(str);
public static bool GetIsJapanese(ReadOnlySpan<byte> raw) => AllCharsInTable(raw, TableJP);
public static bool GetIsEnglish(ReadOnlySpan<byte> raw) => AllCharsInTable(raw, TableEN);
private static bool AllCharsInTable(ReadOnlySpan<byte> data, ReadOnlySpan<char> table)
{
foreach (var c in data)
{
var b = table[c];
if (b == G1Terminator && c is not (G1TerminatorCode or G1TerminatorZero))
if (b == Terminator && c is not (TerminatorCode or TerminatorZero))
return false;
}
return true;
}
public const byte G1TerminatorCode = 0x50;
public const byte G1TerminatorZero = 0x00;
public const char G1Terminator = '\0';
public const byte G1TradeOTCode = 0x5D;
public const char G1TradeOT = '*';
public const byte G1SpaceCode = 0x7F;
/// <summary>
/// Checks if the input byte array is definitely of German origin (any ÄÖÜäöü)
/// </summary>
@@ -104,9 +106,9 @@ public static int LoadString(ReadOnlySpan<byte> data, Span<char> result, bool jp
{
if (data.Length == 0)
return 0;
if (data[0] == G1TradeOTCode) // In-game Trade
if (data[0] == TradeOTCode) // In-game Trade
{
result[0] = G1TradeOT;
result[0] = TradeOT;
return 1;
}
@@ -116,7 +118,7 @@ public static int LoadString(ReadOnlySpan<byte> data, Span<char> result, bool jp
{
var value = data[i];
var c = dict[value];
if (c == G1Terminator) // Stop if Terminator
if (c == Terminator) // Stop if Terminator
break;
result[i] = c;
}
@@ -138,16 +140,16 @@ public static int LoadString(ReadOnlySpan<byte> data, Span<char> result, bool jp
if (option is StringConverterOption.ClearZero)
destBuffer.Clear();
else if (option is StringConverterOption.Clear50)
destBuffer.Fill(G1TerminatorCode);
destBuffer.Fill(TerminatorCode);
else if (option is StringConverterOption.Clear7F)
destBuffer.Fill(G1SpaceCode);
destBuffer.Fill(SpaceCode);
if (value.Length == 0)
return 0;
if (value[0] == G1TradeOT) // Handle "[TRAINER]"
if (value[0] == TradeOT) // Handle "[TRAINER]"
{
destBuffer[0] = G1TradeOTCode;
destBuffer[1] = G1TerminatorCode;
destBuffer[0] = TradeOTCode;
destBuffer[1] = TerminatorCode;
return 2;
}
@@ -166,7 +168,7 @@ public static int LoadString(ReadOnlySpan<byte> data, Span<char> result, bool jp
int count = i;
if (count == destBuffer.Length)
return count;
destBuffer[count] = G1TerminatorCode;
destBuffer[count] = TerminatorCode;
return count + 1;
}
@@ -181,7 +183,7 @@ private static bool TryGetIndex(in ReadOnlySpan<char> dict, char c, out byte res
}
// べ (U+3079), ぺ (U+307A), へ (U+3078), and り (U+308A)
private const string Hiragana = "べぺへり";
internal const string Hiragana = "べぺへり";
/// <summary>
/// Tries to remap the user input to a valid character.
@@ -198,18 +200,18 @@ private static bool TryGetUserFriendlyRemap(in ReadOnlySpan<char> dict, char c,
return false;
}
#region Gen 1/2 Character Tables
#region Gen 1 Character Tables
private const char NUL = G1Terminator;
private const char TOT = G1TradeOT;
private const char LPK = '{'; // Pk
private const char LMN = '}'; // Mn
private const char MNY = '¥'; // Yen
private const char LPO = '@'; // Po
private const char LKE = '#'; // Ke
private const char LEA = '%'; // é for Box
internal const char NUL = Terminator;
internal const char TOT = TradeOT;
internal const char LPK = '{'; // Pk
internal const char LMN = '}'; // Mn
internal const char MNY = '¥'; // Yen
internal const char LPO = '@'; // Po
internal const char LKE = '#'; // Ke
internal const char LEA = '%'; // é for Box
public const char DOT = '․'; // . for MR.MIME (U+2024, not U+002E)
private const char SPF = ' '; // Full-width space (U+3000)
internal const char SPF = ' '; // Full-width space (U+3000)
public const char SPH = ' '; // Half-width space
public static ReadOnlySpan<char> TableEN =>

View File

@@ -0,0 +1,387 @@
using System;
namespace PKHeX.Core;
public static class StringConverter2
{
public const byte TerminatorCode = StringConverter1.TerminatorCode;
public const byte TerminatorZero = StringConverter1.TerminatorZero;
public const byte TradeOTCode = StringConverter1.TradeOTCode;
public const byte SpaceCode = StringConverter1.SpaceCode;
public const char Terminator = StringConverter1.Terminator;
public const char TradeOT = StringConverter1.TradeOT;
public static bool GetIsJapanese(ReadOnlySpan<char> str) => AllJapanese(str);
private static bool AllJapanese(ReadOnlySpan<char> str)
{
foreach (var x in str)
{
if (!IsJapanese(x))
return false;
}
return true;
static bool IsJapanese(char c) => c is >= '\u3000' and <= '\u30FC';
}
public static bool GetIsEnglish(ReadOnlySpan<char> str) => !GetIsJapanese(str);
public static bool GetIsJapanese(ReadOnlySpan<byte> raw) => AllCharsInTable(raw, TableJP);
public static bool GetIsEnglish(ReadOnlySpan<byte> raw) => AllCharsInTable(raw, TableEN);
private static bool AllCharsInTable(ReadOnlySpan<byte> data, ReadOnlySpan<char> table)
{
foreach (var c in data)
{
var b = table[c];
if (b == Terminator && c is not (TerminatorCode or TerminatorZero))
return false;
}
return true;
}
/// <summary>
/// Converts Generation 1 encoded data into a string.
/// </summary>
/// <param name="data">Encoded data.</param>
/// <param name="language">Data source language.</param>
/// <returns>Decoded string.</returns>
public static string GetString(ReadOnlySpan<byte> data, int language)
{
Span<char> result = stackalloc char[data.Length];
int length = LoadString(data, result, language);
return new string(result[..length]);
}
/// <inheritdoc cref="GetString(ReadOnlySpan{byte},int)"/>
/// <param name="data">Encoded data</param>
/// <param name="result">Decoded character result buffer</param>
/// <param name="language">Data source language.</param>
/// <returns>Character count loaded.</returns>
public static int LoadString(ReadOnlySpan<byte> data, Span<char> result, int language)
{
if (data.Length == 0)
return 0;
if (data[0] == TradeOTCode) // In-game Trade
{
result[0] = TradeOT;
return 1;
}
int i = 0;
var dict = GetDict(language);
for (; i < data.Length; i++)
{
var value = data[i];
var c = dict[value];
if (c == Terminator) // Stop if Terminator
break;
result[i] = c;
}
return i;
}
private static ReadOnlySpan<char> GetDict(int language) => language switch
{
1 => TableJP,
3 or 5 => TableFRE,
4 or 7 => TableITA,
_ => TableEN,
};
/// <summary>
/// Converts a string to Generation 1 encoded data.
/// </summary>
/// <param name="destBuffer">Span of bytes to write encoded string data</param>
/// <param name="value">Decoded string.</param>
/// <param name="maxLength">Maximum length of the input <see cref="value"/></param>
/// <param name="language">Data source language.</param>
/// <param name="option">Buffer pre-formatting option</param>
/// <returns>Encoded data.</returns>
public static int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength, int language,
StringConverterOption option = StringConverterOption.Clear50)
{
ConditionBuffer(destBuffer, option);
if (value.Length == 0)
return 0;
if (value[0] == TradeOT) // Handle "[TRAINER]"
{
destBuffer[0] = TradeOTCode;
destBuffer[1] = TerminatorCode;
return 2;
}
if (value.Length > maxLength)
value = value[..maxLength]; // Hard cap
var dict = GetDict(language);
int i = 0;
for (; i < value.Length; i++)
{
if (!TryGetIndex(dict, value[i], out var index))
break;
destBuffer[i] = index;
}
int count = i;
if (count == destBuffer.Length)
return count;
destBuffer[count] = TerminatorCode;
return count + 1;
}
private static void ConditionBuffer(Span<byte> destBuffer, StringConverterOption option)
{
if (option is StringConverterOption.ClearZero)
destBuffer.Clear();
else if (option is StringConverterOption.Clear50)
destBuffer.Fill(TerminatorCode);
else if (option is StringConverterOption.Clear7F)
destBuffer.Fill(SpaceCode);
}
private static bool TryGetIndex(in ReadOnlySpan<char> dict, char c, out byte result)
{
var index = dict.IndexOf(c);
if (index == -1)
return TryGetUserFriendlyRemap(dict, c, out result);
// \0 shouldn't really be user-entered, but just in case
result = (byte)index;
return index != default;
}
/// <summary>
/// Tries to remap the user input to a valid character.
/// </summary>
/// <remarks><seealso cref="StringConverter1.TryGetUserFriendlyRemap"/></remarks>
private static bool TryGetUserFriendlyRemap(in ReadOnlySpan<char> dict, char c, out byte result)
{
if (StringConverter1.Hiragana.Contains(c))
{
int index = dict.IndexOf((char)(c + (char)0x60));
result = (byte)index;
return true; // Valid Hiragana will always be found if it's in the table
}
result = default;
return false;
}
#region Gen 2 Character Tables
private const char NUL = StringConverter1.NUL;
private const char TOT = StringConverter1.TOT;
private const char LPK = StringConverter1.LPK; // Pk
private const char LMN = StringConverter1.LMN; // Mn
private const char MNY = StringConverter1.MNY; // Yen
private const char LPO = StringConverter1.LPO; // Po
private const char LKE = StringConverter1.LKE; // Ke
private const char LEA = StringConverter1.LEA; // é for Box
private const char DOT = StringConverter1.DOT; // . for MR.MIME (U+2024, not U+002E)
private const char SPF = StringConverter1.SPF; // Full-width space (U+3000)
private const char SPH = StringConverter1.SPH; // Half-width space
private const char LAP = '’'; // Apostrophe
// 'd 'l 'm 'r 's 't 'v
// All are apostrophe-before.
private const string LigatureENG = "dlmrstv";
// c' d' j' l' m' n' p' s' 's t' u' y'
// All are apostrophe-after besides index 8 ('s)
private const string LigatureFRE = "cdjlmnpsstuy";
private const char LI0 = '0'; // 'd
private const char LI1 = '1'; // 'l
private const char LI2 = '2'; // 'm
private const char LI3 = '3'; // 'r
private const char LI4 = '4'; // 's
private const char LI5 = '5'; // 't
private const char LI6 = '6'; // 'v
private const char LI7 = '7'; // 'd
private const char LI8 = '8'; // 'l
private const char LI9 = '9'; // 'm
private const char LIA = 'A'; // 'r
private const char LIB = 'B'; // 's
/// <summary>
/// English encoding table with unused indexes merged in from other languages that use them.
/// <see cref="StringConverter1.TableEN"/>
/// </summary>
public static ReadOnlySpan<char> TableEN =>
[
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 00-0F
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 10-1F
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 20-2F
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 30-3F
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 40-4F
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, TOT, NUL, NUL, // 50-5F
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 60-6F
LPO, LKE, '“', '”', NUL, '…', NUL, NUL, NUL, '┌', '─', '┐', '│', '└', '┘', SPH, // 70-7F
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 80-8F
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '(', ')', ':', ';', '[', ']', // 90-9F
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', // A0-AF
'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'à', 'è', 'é', 'ù', 'À', 'Á', // B0-BF
'Ä', 'Ö', 'Ü', 'ä', 'ö', 'ü', 'È', 'É', 'Ì', 'Í', 'Ñ', 'Ò', 'Ó', 'Ù', 'Ú', 'á', // C0-CF
LI0, LI1, LI2, LI3, LI4, LI5, LI6, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, '←', // D0-DF
LAP, LPK, LMN, '-', '+', NUL, '?', '!', DOT, '&', LEA, '→', '▷', '▶', '▼', '♂', // E0-EF
MNY, '×', '.', '/', ',', '♀', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', // F0-FF
];
public static ReadOnlySpan<char> TableFRE => // Also German
[
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 00-0F
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 10-1F
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 20-2F
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 30-3F
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 40-4F
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, TOT, NUL, NUL, // 50-5F
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 60-6F
LPO, LKE, '“', '”', NUL, '…', NUL, NUL, NUL, '┌', '─', '┐', '│', '└', '┘', SPH, // 70-7F
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 80-8F
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '(', ')', ':', ';', '[', ']', // 90-9F
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', // A0-AF
'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'à', 'è', 'é', 'ù', 'ß', 'ç', // B0-BF
'Ä', 'Ö', 'Ü', 'ä', 'ö', 'ü', 'È', 'É', 'Ì', 'Í', 'Ñ', 'Ò', 'Ó', 'Ù', 'Ú', 'á', // C0-CF
NUL, NUL, NUL, NUL, LI0, LI1, LI2, LI3, LI4, LI5, LI6, LI7, LI8, LI9, LIA, LIB, // D0-DF
LAP, LPK, LMN, '-', '+', NUL, '?', '!', DOT, '&', LEA, '→', '▷', '▶', '▼', '♂', // E0-EF
MNY, '×', '.', '/', ',', '♀', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', // F0-FF
];
public static ReadOnlySpan<char> TableITA => // Also Spanish
[
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 00-0F
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 10-1F
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 20-2F
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 30-3F
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 40-4F
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, TOT, NUL, NUL, // 50-5F
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 60-6F
LPO, LKE, '“', '”', NUL, '…', NUL, NUL, NUL, '┌', '─', '┐', '│', '└', '┘', SPH, // 70-7F
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 80-8F
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '(', ')', ':', ';', '[', ']', // 90-9F
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', // A0-AF
'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'à', 'è', 'é', 'ù', 'À', 'Á', // B0-BF
'Ä', 'Ö', 'Ü', 'ä', 'ö', 'ü', 'È', 'É', 'Ì', 'Í', 'Ñ', 'Ò', 'Ó', 'Ù', 'Ú', 'á', // C0-CF
'ì', 'í', 'ñ', 'ò', 'ó', 'ú', NUL, NUL, LI0, LI1, LI2, LI3, LI4, LI5, LI6, NUL, // D0-DF
LAP, LPK, LMN, '-', '¿', '¡', '?', '!', DOT, '&', LEA, '→', '▷', '▶', '▼', '♂', // E0-EF
MNY, '×', '.', '/', ',', '♀', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', // F0-FF
];
private static void InsertLigature(Span<char> result, char c, bool isAfter)
{
if (isAfter)
{
result[0] = c;
result[1] = LAP;
}
else
{
result[0] = LAP;
result[1] = c;
}
}
public static ReadOnlySpan<char> TableJP =>
[
NUL, NUL, NUL, NUL, NUL, 'ガ', 'ギ', 'グ', 'ゲ', 'ゴ', 'ザ', 'ジ', 'ズ', 'ゼ', 'ゾ', 'ダ', // 00-0F
'ヂ', 'ヅ', 'デ', 'ド', NUL, NUL, NUL, NUL, NUL, 'バ', 'ビ', 'ブ', 'ボ', NUL, NUL, NUL, // 10-1F
NUL, NUL, NUL, NUL, NUL, NUL, 'が', 'ぎ', 'ぐ', 'げ', 'ご', 'ざ', 'じ', 'ず', 'ぜ', 'ぞ', // 20-2F
'だ', 'ぢ', 'づ', 'で', 'ど', NUL, NUL, NUL, NUL, NUL, 'ば', 'び', 'ぶ', 'ベ', 'ぼ', NUL, // 30-3F
'パ', 'ピ', 'プ', 'ポ', 'ぱ', 'ぴ', 'ぷ', 'ペ', 'ぽ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 40-4F
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, TOT, NUL, NUL, // 50-5F
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 60-6F
'「', '」', '『', '』', '・', '⋯', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, SPF, // 70-7F
'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ', 'コ', 'サ', 'シ', 'ス', 'セ', 'ソ', 'タ', // 80-8F
'チ', 'ツ', 'テ', 'ト', 'ナ', 'ニ', 'ヌ', 'ネ', 'ノ', 'ハ', 'ヒ', 'フ', 'ホ', 'マ', 'ミ', 'ム', // 90-9F
'メ', 'モ', 'ヤ', 'ユ', 'ヨ', 'ラ', 'ル', 'レ', 'ロ', 'ワ', 'ヲ', 'ン', 'ッ', 'ャ', 'ュ', 'ョ', // A0-AF
'ィ', 'あ', 'い', 'う', 'え', 'お', 'か', 'き', 'く', 'け', 'こ', 'さ', 'し', 'す', 'せ', 'そ', // B0-BF
'た', 'ち', 'つ', 'て', 'と', 'な', 'に', 'ぬ', 'ね', 'の', 'は', 'ひ', 'ふ', 'ヘ', 'ほ', 'ま', // C0-CF
'み', 'む', 'め', 'も', 'や', 'ゆ', 'よ', 'ら', 'リ', 'る', 'れ', 'ろ', 'わ', 'を', 'ん', 'っ', // D0-DF
'ゃ', 'ゅ', 'ょ', 'ー', '゚', '゙', '?', '!', '。', 'ァ', 'ゥ', 'ェ', NUL, NUL, NUL, '♂', // E0-EF
MNY, NUL, '.', '/', 'ォ', '♀', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', // F0-FF
];
#endregion
public static string InflateLigatures(string result, int language)
{
if (language is (int)LanguageID.Japanese or (int)LanguageID.Korean)
return result; // No ligatures in JPN/KOR
bool after = language is (int)LanguageID.French or (int)LanguageID.German;
var ligatures = after ? LigatureFRE : LigatureENG;
Span<char> inflated = stackalloc char[result.Length * 2]; // worst case is double length
int index = 0;
foreach (var c in result)
{
if (!TryGetLigatureIndex(c, out var i) || i >= ligatures.Length)
{
inflated[index++] = c;
continue;
}
var ligature = ligatures[i];
InsertLigature(inflated[index..], ligature, after && i != 8);
index += 2;
}
if (index == result.Length)
return result; // Nothing changed.
return new string(inflated[..index]);
}
private static bool TryGetLigatureIndex(char c, out int index) => -1 != (index = LigatureList.IndexOf(c));
private static ReadOnlySpan<char> LigatureList => [LI0, LI1, LI2, LI3, LI4, LI5, LI6, LI7, LI8, LI8, LI9, LIA, LIB];
private static char GetLigature(int ligatureIndex) => LigatureList[ligatureIndex];
public static int DeflateLigatures(ReadOnlySpan<char> value, Span<char> result, int language)
{
if (language is (int)LanguageID.Japanese or (int)LanguageID.Korean)
{
value.CopyTo(result);
return value.Length; // No ligatures in JPN/KOR
}
bool after = language is (int)LanguageID.French or (int)LanguageID.German;
var ligatures = after ? LigatureFRE : LigatureENG;
int index = 0;
for (var i = 0; i < value.Length; i++)
{
char c = value[i];
if (c is not (LAP or '\''))
{
if (index == result.Length)
return index; // Overflow (shouldn't happen for correctly-written strings)
result[index++] = c;
continue;
}
if (after && index != 0)
{
ref var prev = ref result[index - 1];
var ligatureIndex = ligatures.IndexOf(prev);
if (ligatureIndex != -1)
{
prev = ligatures[ligatureIndex];
continue;
}
}
if (index == result.Length)
return index; // Overflow (shouldn't happen for correctly-written strings)
if (i < value.Length - 1)
{
var next = value[i + 1];
var ligatureIndex = ligatures.IndexOf(next);
if (ligatureIndex != -1)
{
result[index++] = GetLigature(ligatureIndex);
i++;
continue;
}
}
result[index++] = c;
}
return index;
}
}

View File

@@ -1,6 +1,5 @@
using System;
using static PKHeX.Core.StringConverter12;
using static PKHeX.Core.StringConverter1;
namespace PKHeX.Core;
@@ -12,7 +11,7 @@ public static class StringConverter2KOR
/// <summary>
/// Checks if any of the characters inside <see cref="str"/> are from the special Korean codepoint pages.
/// </summary>
public static bool GetIsG2Korean(ReadOnlySpan<char> str)
public static bool GetIsKorean(ReadOnlySpan<char> str)
{
foreach (var c in str)
{
@@ -42,9 +41,9 @@ public static int LoadString(ReadOnlySpan<byte> data, Span<char> result)
{
if (data.Length == 0)
return 0;
if (data[0] == G1TradeOTCode) // In-game Trade
if (data[0] == TradeOTCode) // In-game Trade
{
result[0] = G1TradeOT;
result[0] = TradeOT;
return 1;
}
@@ -70,7 +69,7 @@ public static int LoadString(ReadOnlySpan<byte> data, Span<char> result)
}
var c = table[value];
if (c == G1Terminator) // Stop if Terminator
if (c == Terminator) // Stop if Terminator
break;
result[ctr++] = c;
}
@@ -91,15 +90,15 @@ public static int LoadString(ReadOnlySpan<byte> data, Span<char> result)
if (option is StringConverterOption.ClearZero)
destBuffer.Clear();
else if (option is StringConverterOption.Clear50)
destBuffer.Fill(G1TerminatorCode);
destBuffer.Fill(TerminatorCode);
if (value.Length == 0)
return 0;
// Can't get Gen1 In-Game trades with this char, but handle it anyway since the game can handle it.
if (value[0] == G1TradeOT) // Handle "[TRAINER]"
// Korean games can't trade for Gen1 In-Game trades with this char, but handle it anyway since the game can handle it.
if (value[0] == TradeOT) // Handle "[TRAINER]"
{
destBuffer[0] = G1TradeOTCode;
destBuffer[1] = G1TerminatorCode;
destBuffer[0] = TradeOTCode;
destBuffer[1] = TerminatorCode;
return 2;
}
@@ -108,7 +107,7 @@ public static int LoadString(ReadOnlySpan<byte> data, Span<char> result)
int ctr = LoadCharacters(destBuffer, value);
if (ctr < value.Length)
destBuffer[ctr++] = G1TerminatorCode;
destBuffer[ctr++] = TerminatorCode;
return ctr;
}
@@ -128,7 +127,7 @@ private static int LoadCharacters(Span<byte> destBuffer, ReadOnlySpan<char> valu
else
{
var index = Table0.IndexOf(c);
if (index is -1 or G1TerminatorCode)
if (index is -1 or TerminatorCode)
break;
if (ctr + 1 > destBuffer.Length)
break; // adding 1 character will overflow requested buffer cap
@@ -177,7 +176,7 @@ public static void LocalizeKOR2(ushort species, ref string nick)
}
#region Gen 2 Korean Character Tables
private const char NULL = G1Terminator;
private const char NULL = Terminator;
private const byte TableInvalid = 0;
private const byte TableMin = 1;
private const byte TableMax = 11;

View File

@@ -1,5 +1,5 @@
using System;
using static PKHeX.Core.StringConverter12;
using static PKHeX.Core.StringConverter1;
namespace PKHeX.Core;
@@ -21,5 +21,5 @@ public static int GetStringLength(ReadOnlySpan<byte> buffer)
/// <param name="buffer">Backing buffer of the string.</param>
/// <returns>Index of the terminator, or -1 if not found.</returns>
public static int FindTerminatorIndex(ReadOnlySpan<byte> buffer)
=> buffer.IndexOfAny(G1TerminatorZero, G1TerminatorCode);
=> buffer.IndexOfAny(TerminatorZero, TerminatorCode);
}

View File

@@ -49,13 +49,13 @@ private static string GetRegular(PKM pk)
private static string GetGBPKM(GBPKM gb)
{
ReadOnlySpan<byte> raw = gb switch
var checksum = gb switch
{
PK1 pk1 => new PokeList1(pk1).Write(),
PK2 pk2 => new PokeList2(pk2).Write(),
_ => gb.Data,
PK1 pk1 => pk1.GetSingleListChecksum(),
PK2 pk2 => pk2.GetSingleListChecksum(),
_ => Checksums.CRC16_CCITT(gb.Data),
};
var checksum = Checksums.CRC16_CCITT(raw);
var form = gb.Form != 0 ? $"-{gb.Form:00}" : string.Empty;
var star = gb.IsShiny ? " ★" : string.Empty;
return $"{gb.Species:0000}{form}{star} - {gb.Nickname} - {checksum:X4}";

View File

@@ -117,8 +117,8 @@ private static bool IsFormatReally9(PK8 pk)
private static PKM? GetFromBytes(byte[] data, EntityFormatDetected format, EntityContext prefer) => format switch
{
FormatPK1 => new PokeList1(data)[0],
FormatPK2 => new PokeList2(data)[0],
FormatPK1 => PokeList1.ReadFromSingle(data),
FormatPK2 => PokeList2.ReadFromSingle(data),
FormatSK2 => new SK2(data),
FormatPK3 => new PK3(data),
FormatCK3 => new CK3(data),

View File

@@ -154,11 +154,11 @@ private static string GetSpeciesName1234(ushort species, int language, byte gene
// The only Gen1/2 species with a space is Mr. Mime; different period and no space.
if (species == (int)Species.MrMime)
{
int indexSpace = result.IndexOf(StringConverter12.SPH);
int indexSpace = result.IndexOf(StringConverter1.SPH);
if (indexSpace > 0)
{
// Gen1/2 uses a different period for MR.MIME than user input.
result[indexSpace - 1] = StringConverter12.DOT;
result[indexSpace - 1] = StringConverter1.DOT;
// Shift down. Strings have at most 1 occurrence of a space.
result[(indexSpace + 1)..].CopyTo(result[indexSpace..]);

View File

@@ -7,7 +7,7 @@ namespace PKHeX.Core;
/// <summary>
/// Generation 1 <see cref="SaveFile"/> object.
/// </summary>
public sealed class SAV1 : SaveFile, ILangDeviantSave, IEventFlagArray, IEventWorkArray<byte>, IBoxDetailName, IDaycareStorage
public sealed class SAV1 : SaveFile, ILangDeviantSave, IEventFlagArray, IEventWorkArray<byte>, IBoxDetailNameRead, IDaycareStorage
{
protected internal override string ShortSummary => $"{OT} ({Version}) - {PlayTimeString}";
public override string Extension => ".sav";
@@ -17,7 +17,14 @@ public sealed class SAV1 : SaveFile, ILangDeviantSave, IEventFlagArray, IEventWo
public string SaveRevisionString => (Japanese ? "J" : "U") + (IsVirtualConsole ? "VC" : "GB");
public bool Japanese { get; }
public bool Korean => false;
public override int Language => Japanese ? 1 : -1;
public override int Language { get; set; }
private readonly byte[] Reserved = new byte[SIZE_RESERVED];
private const int SIZE_RESERVED = 0x8000; // unpacked box data
protected override Span<byte> BoxBuffer => Reserved;
protected override Span<byte> PartyBuffer => Reserved;
private readonly SAV1Offsets Offsets;
public override PersonalTable1 Personal { get; }
@@ -29,19 +36,21 @@ public sealed class SAV1 : SaveFile, ILangDeviantSave, IEventFlagArray, IEventWo
return gen is 1 or 2;
});
public SAV1(GameVersion version = GameVersion.RBY, bool japanese = false) : base(SaveUtil.SIZE_G1RAW)
public SAV1(GameVersion version = GameVersion.RBY, LanguageID language = LanguageID.English) : base(SaveUtil.SIZE_G1RAW)
{
Version = version;
Japanese = japanese;
Japanese = language == LanguageID.Japanese;
Language = (int)language;
Offsets = Japanese ? SAV1Offsets.JPN : SAV1Offsets.INT;
Personal = version == GameVersion.YW ? PersonalTable.Y : PersonalTable.RB;
Initialize(version);
ClearBoxes();
}
public SAV1(byte[] data, GameVersion versionOverride = GameVersion.Any) : base(data)
public SAV1(byte[] data, GameVersion versionOverride = GameVersion.Any, LanguageID language = LanguageID.English) : base(data)
{
Japanese = SaveUtil.GetIsG1SAVJ(Data);
Language = (int)language;
Offsets = Japanese ? SAV1Offsets.JPN : SAV1Offsets.INT;
Version = versionOverride != GameVersion.Any ? versionOverride : SaveUtil.GetIsG1SAV(data);
@@ -63,98 +72,48 @@ private void Initialize(GameVersion versionOverride)
Version = Data[Offsets.PikaFriendship] != 0 ? GameVersion.YW : GameVersion.RB;
}
Box = Data.Length;
Array.Resize(ref Data, Data.Length + SIZE_RESERVED);
Box = 0;
Party = GetPartyOffset(0);
// Stash boxes after the save file's end.
int stored = SIZE_STOREDBOX;
var capacity = Japanese ? PokeListType.StoredJP : PokeListType.Stored;
int baseDest = Data.Length - SIZE_RESERVED;
for (int box = 0; box < BoxCount; box++)
int stored = SIZE_BOX_LIST;
var capacity = BoxSlotCount;
for (int i = 0; i < BoxCount; i++)
{
int boxOfs = GetBoxRawDataOffset(box);
UnpackBox(boxOfs, baseDest, stored, box, capacity);
int ofs = GetBoxRawDataOffset(i);
var src = Data.AsSpan(ofs, stored);
var dest = BoxBuffer[(i * SIZE_BOX_AS_SINGLES)..];
PokeList1.Unpack(src, dest, StringLength, capacity, false);
}
if ((uint)CurrentBox < BoxCount)
// Don't treat the CurrentBox segment as valid; Stadium ignores it and will de-synchronize it.
// The main box data segment is the truth, the CurrentBox copy is not always up-to-date.
// When exporting an updated save file in this program, be nice and re-synchronize.
// Stash party immediately after.
{
// overwrite previously cached box data.
UnpackBox(Offsets.CurrentBox, baseDest, stored, CurrentBox, capacity);
var ofs = Offsets.Party;
var src = Data.AsSpan(ofs, SIZE_PARTY_LIST);
var dest = PartyBuffer[GetPartyOffset(0)..];
PokeList1.Unpack(src, dest, StringLength, 6, false);
}
var party = Data.AsSpan(Offsets.Party, SIZE_STOREDPARTY).ToArray();
var partyPL = new PokeList1(party, PokeListType.Party, Japanese);
for (int i = 0; i < partyPL.Pokemon.Length; i++)
{
var dest = GetPartyOffset(i);
var pkDat = i < partyPL.Count
? new PokeList1(partyPL[i]).Write()
: new byte[PokeList1.GetDataLength(PokeListType.Single, Japanese)];
pkDat.CopyTo(Data, dest);
}
Span<byte> rawDC = stackalloc byte[0x38];
Data.AsSpan(Offsets.Daycare, rawDC.Length).CopyTo(rawDC);
byte[] TempDaycare = new byte[PokeList1.GetDataLength(PokeListType.Single, Japanese)];
ReadOnlySpan<byte> rawDC = Data.AsSpan(Offsets.Daycare, 0x38);
var daycareOffset = DaycareOffset = GetPartyOffset(6);
var TempDaycare = BoxBuffer[daycareOffset..];
TempDaycare[0] = rawDC[0];
rawDC.Slice(1, StringLength).CopyTo(TempDaycare.AsSpan(2 + 1 + PokeCrypto.SIZE_1PARTY + StringLength));
rawDC.Slice(1 + StringLength, StringLength).CopyTo(TempDaycare.AsSpan(2 + 1 + PokeCrypto.SIZE_1PARTY));
rawDC.Slice(1 + (2 * StringLength), PokeCrypto.SIZE_1STORED).CopyTo(TempDaycare.AsSpan(2 + 1));
var dcDest = TempDaycare[3..];
PokeList1 daycareList = new(TempDaycare, PokeListType.Single, Japanese);
daycareList.Write().CopyTo(Data, GetPartyOffset(7));
DaycareOffset = GetPartyOffset(7);
// Enable Pokedex editing
// OT, Nickname, and Core data (rather than the reverse...)
rawDC.Slice(1, StringLength).CopyTo(dcDest[(PokeCrypto.SIZE_1PARTY + StringLength)..]);
rawDC.Slice(1 + StringLength, StringLength).CopyTo(dcDest[PokeCrypto.SIZE_1PARTY..]);
rawDC.Slice(1 + (2 * StringLength), PokeCrypto.SIZE_1STORED).CopyTo(dcDest);
}
private int DaycareOffset = -1;
public override bool HasPokeDex => true;
private void UnpackBox(int srcOfs, int destOfs, int boxSize, int boxIndex, PokeListType boxCapacity)
{
var boxData = Data.AsSpan(srcOfs, boxSize).ToArray();
var boxDest = destOfs + (boxIndex * SIZE_BOX);
var boxPL = new PokeList1(boxData, boxCapacity, Japanese);
for (int i = 0; i < boxPL.Pokemon.Length; i++)
{
var slotOfs = boxDest + (i * SIZE_STORED);
var slotData = Data.AsSpan(slotOfs, SIZE_STORED);
if (i < boxPL.Count)
new PokeList1(boxPL[i]).Write().CopyTo(slotData);
else
slotData.Clear();
}
}
private void PackBox(int boxDest, int boxIndex, PokeListType boxCapacity)
{
var boxPL = new PokeList1(boxCapacity, Japanese);
int slot = 0;
for (int i = 0; i < boxPL.Pokemon.Length; i++)
{
var slotOfs = boxDest + (i * SIZE_STORED);
var slotData = Data.AsSpan(slotOfs, SIZE_STORED);
PK1 pk = GetPKM(slotData.ToArray());
if (pk.Species > 0)
boxPL[slot++] = pk;
}
// copy to box location
var boxData = boxPL.Write();
int boxSrc = GetBoxRawDataOffset(boxIndex);
SetData(Data.AsSpan(boxSrc), boxData);
// copy to active loc if current box
if (boxIndex == CurrentBox)
SetData(Data.AsSpan(Offsets.CurrentBox), boxData);
}
private const int SIZE_RESERVED = 0x8000; // unpacked box data
private readonly SAV1Offsets Offsets;
// Event Flags
public int EventFlagCount => 0xA00; // 320 * 8
public bool GetEventFlag(int flagNumber)
@@ -178,24 +137,27 @@ public void SetEventFlag(int flagNumber, bool value)
protected override byte[] GetFinalData()
{
var capacity = Japanese ? PokeListType.StoredJP : PokeListType.Stored;
for (int box = 0; box < BoxCount; box++)
int boxListLength = SIZE_BOX_LIST;
var boxSlotCount = BoxSlotCount;
for (int i = 0; i < BoxCount; i++)
{
var boxOfs = GetBoxOffset(box);
PackBox(boxOfs, box, capacity);
int ofs = GetBoxRawDataOffset(i);
var dest = Data.AsSpan(ofs, boxListLength);
var src = BoxBuffer.Slice(i * SIZE_BOX_AS_SINGLES, SIZE_BOX_AS_SINGLES);
bool written = PokeList1.MergeSingles(src, dest, StringLength, boxSlotCount, false);
if (written && i == CurrentBox)
dest.CopyTo(Data.AsSpan(Offsets.CurrentBox));
}
var partyPL = new PokeList1(PokeListType.Party, Japanese);
int pSlot = 0;
for (int i = 0; i < 6; i++)
// Write Party
{
var ofs = GetPartyOffset(i);
var data = Data.AsSpan(ofs, SIZE_STORED).ToArray();
PK1 partyPK = GetPKM(data);
if (partyPK.Species > 0)
partyPL[pSlot++] = partyPK;
int ofs = Offsets.Party;
var dest = Data.AsSpan(ofs, SIZE_PARTY_LIST);
var src = PartyBuffer[GetPartyOffset(0)..];
PokeList1.MergeSingles(src, dest, StringLength, 6, true);
}
partyPL.Write().CopyTo(Data, Offsets.Party);
// Daycare is read-only, but in case it ever becomes editable, copy it back in.
Span<byte> rawDC = Data.AsSpan(GetDaycareSlotOffset(index: 0), SIZE_STORED);
@@ -207,24 +169,24 @@ protected override byte[] GetFinalData()
dc.CopyTo(Data.AsSpan(Offsets.Daycare));
SetChecksums();
return Data[..^SIZE_RESERVED];
return Data;
}
private int GetBoxRawDataOffset(int box)
{
if (box < BoxCount / 2)
return 0x4000 + (box * SIZE_STOREDBOX);
return 0x6000 + ((box - (BoxCount / 2)) * SIZE_STOREDBOX);
return 0x4000 + (box * SIZE_BOX_LIST);
return 0x6000 + ((box - (BoxCount / 2)) * SIZE_BOX_LIST);
}
// Configuration
protected override SAV1 CloneInternal() => new(Write(), Version);
protected override SAV1 CloneInternal() => new(Write(), Version) { Language = Language };
protected override int SIZE_STORED => Japanese ? PokeCrypto.SIZE_1JLIST : PokeCrypto.SIZE_1ULIST;
protected override int SIZE_PARTY => Japanese ? PokeCrypto.SIZE_1JLIST : PokeCrypto.SIZE_1ULIST;
private int SIZE_BOX => BoxSlotCount*SIZE_STORED;
private int SIZE_STOREDBOX => PokeList1.GetDataLength(Japanese ? PokeListType.StoredJP : PokeListType.Stored, Japanese);
private int SIZE_STOREDPARTY => PokeList1.GetDataLength(PokeListType.Party, Japanese);
private int SIZE_BOX_AS_SINGLES => BoxSlotCount * SIZE_STORED;
private int SIZE_BOX_LIST => (((StringLength * 2) + PokeCrypto.SIZE_1STORED + 1) * BoxSlotCount) + 2;
private int SIZE_PARTY_LIST => (((StringLength * 2) + PokeCrypto.SIZE_1PARTY + 1) * 6) + 2;
public override PK1 BlankPKM => new(Japanese);
public override Type PKMType => typeof(PK1);
@@ -473,8 +435,8 @@ public override int PartyCount
protected set => Data[Offsets.Party] = (byte)value;
}
public override int GetBoxOffset(int box) => Data.Length - SIZE_RESERVED + (box * SIZE_BOX);
public override int GetPartyOffset(int slot) => Data.Length - SIZE_RESERVED + (BoxCount * SIZE_BOX) + (slot * SIZE_STORED);
public override int GetBoxOffset(int box) => box * SIZE_BOX_AS_SINGLES;
public override int GetPartyOffset(int slot) => (BoxCount * SIZE_BOX_AS_SINGLES) + (slot * SIZE_STORED);
public override int CurrentBox
{
@@ -495,15 +457,10 @@ public string GetBoxName(int box)
return BoxDetailNameExtensions.GetDefaultBoxName(box);
}
public void SetBoxName(int box, ReadOnlySpan<char> value)
{
// Don't allow for custom box names
}
protected override PK1 GetPKM(byte[] data)
{
if (data.Length == SIZE_STORED)
return new PokeList1(data, PokeListType.Single, Japanese)[0];
return PokeList1.ReadFromList(data, StringLength);
return new(data);
}
@@ -590,10 +547,10 @@ public bool[] EventSpawnFlags
}
}
public override string GetString(ReadOnlySpan<byte> data) => StringConverter12.GetString(data, Japanese);
public override string GetString(ReadOnlySpan<byte> data) => StringConverter1.GetString(data, Japanese);
public override int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength, StringConverterOption option)
{
return StringConverter12.SetString(destBuffer, value, maxLength, Japanese, option);
return StringConverter1.SetString(destBuffer, value, maxLength, Japanese, option);
}
}

View File

@@ -17,7 +17,14 @@ public sealed class SAV2 : SaveFile, ILangDeviantSave, IEventFlagArray, IEventWo
public string SaveRevisionString => (Japanese ? "J" : !Korean ? "U" : "K") + (IsVirtualConsole ? "VC" : "GB");
public bool Japanese { get; }
public bool Korean { get; }
public override int Language => Japanese ? 1 : Korean ? (int)LanguageID.Korean : -1;
public override int Language { get; set; }
private readonly byte[] Reserved = new byte[0x8000];
protected override Span<byte> BoxBuffer => Reserved;
protected override Span<byte> PartyBuffer => Reserved;
private readonly SAV2Offsets Offsets;
public override PersonalTable2 Personal { get; }
public override ReadOnlySpan<ushort> HeldItems => Legal.HeldItems_GSC;
@@ -30,18 +37,22 @@ public sealed class SAV2 : SaveFile, ILangDeviantSave, IEventFlagArray, IEventWo
return gen is 1 or 2;
});
public SAV2(GameVersion version = GameVersion.C, LanguageID lang = LanguageID.English) : base(SaveUtil.SIZE_G2RAW_J)
public SAV2(GameVersion version = GameVersion.C, LanguageID language = LanguageID.English) : base(SaveUtil.SIZE_G2RAW_J)
{
Version = version;
switch (lang)
switch (language)
{
case LanguageID.Japanese:
Japanese = true;
Language = 1;
break;
case LanguageID.Korean:
Korean = true;
Language = (int)LanguageID.Korean;
break;
default: // otherwise, both false
Language = -1;
break;
// otherwise, both false
}
Offsets = new SAV2Offsets(this);
Personal = Version == GameVersion.GS ? PersonalTable.GS : PersonalTable.C;
@@ -55,6 +66,7 @@ public SAV2(byte[] data, GameVersion versionOverride = GameVersion.Any) : base(d
Japanese = SaveUtil.GetIsG2SAVJ(Data) != GameVersion.Invalid;
if (Version != GameVersion.C && !Japanese)
Korean = SaveUtil.GetIsG2SAVK(Data) != GameVersion.Invalid;
Language = Japanese ? 1 : Korean ? (int)LanguageID.Korean : -1;
Offsets = new SAV2Offsets(this);
Personal = Version == GameVersion.GS ? PersonalTable.GS : PersonalTable.C;
@@ -64,51 +76,30 @@ public SAV2(byte[] data, GameVersion versionOverride = GameVersion.Any) : base(d
private void Initialize()
{
Box = Data.Length;
Array.Resize(ref Data, Data.Length + SIZE_RESERVED);
Party = GetPartyOffset(0);
// 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;
int stored = SIZE_BOX_LIST;
var capacity = BoxSlotCount;
for (int i = 0; i < BoxCount; i++)
{
int ofs = GetBoxRawDataOffset(i, splitAtIndex);
var box = Data.AsSpan(ofs, stored).ToArray();
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 src = Data.AsSpan(ofs, stored);
var dest = BoxBuffer[(i * SIZE_BOX_AS_SINGLES)..];
PokeList2.Unpack(src, dest, StringLength, capacity, false);
}
var current = Data.AsSpan(Offsets.CurrentBox, stored).ToArray();
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);
}
// Don't treat the CurrentBox segment as valid; Stadium ignores it and will de-synchronize it.
// The main box data segment is the truth, the CurrentBox copy is not always up-to-date.
// When exporting an updated save file in this program, be nice and re-synchronize.
var party = Data.AsSpan(Offsets.Party, SIZE_STOREDPARTY).ToArray();
var partyPL = new PokeList2(party, PokeListType.Party, Japanese);
for (int i = 0; i < partyPL.Pokemon.Length; i++)
// Stash party immediately after.
{
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);
var ofs = Offsets.Party;
var src = Data.AsSpan(ofs, SIZE_PARTY_LIST);
var dest = PartyBuffer[GetPartyOffset(0)..];
PokeList2.Unpack(src, dest, StringLength, 6, false);
}
if (Offsets.Daycare >= 0)
@@ -117,7 +108,6 @@ private void Initialize()
offset++;
var pk1 = ReadPKMFromOffset(offset); // parent 1
var daycare1 = new PokeList2(pk1);
offset += (StringLength * 2) + 0x20; // nick/ot/pk
offset++;
//byte steps = Data[offset];
@@ -125,15 +115,13 @@ private void Initialize()
//byte BreedMotherOrNonDitto = Data[offset];
offset++;
var pk2 = ReadPKMFromOffset(offset); // parent 2
var daycare2 = new PokeList2(pk2);
offset += (StringLength * 2) + PokeCrypto.SIZE_2STORED; // nick/ot/pk
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)));
PokeList2.WriteToList(PartyBuffer[GetPartyOffset(6 + (0 * 2))..], pk1, 1, true);
PokeList2.WriteToList(PartyBuffer[GetPartyOffset(6 + (1 * 2))..], pk2, 1, true);
PokeList2.WriteToList(PartyBuffer[GetPartyOffset(6 + (2 * 2))..], pk3, 1, true);
}
}
@@ -158,49 +146,37 @@ private PK2 ReadPKMFromOffset(int offset)
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));
return 0x4000 + (i * (SIZE_BOX_LIST + 2));
return 0x6000 + ((i - splitAtIndex) * (SIZE_BOX_LIST + 2));
}
protected override byte[] GetFinalData()
{
int splitAtIndex = (Japanese ? 6 : 7);
int boxListLength = SIZE_BOX_LIST;
var boxSlotCount = BoxSlotCount;
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++)
{
var ofs = GetBoxOffset(i) + (j * SIZE_STORED);
var data = Data.AsSpan(ofs, SIZE_STORED).ToArray();
PK2 boxPK = GetPKM(data);
if (boxPK.Species > 0)
boxPL[slot++] = boxPK;
}
int ofs = GetBoxRawDataOffset(i, splitAtIndex);
var dest = Data.AsSpan(ofs, boxListLength);
var src = BoxBuffer.Slice(i * SIZE_BOX_AS_SINGLES, SIZE_BOX_AS_SINGLES);
int src = GetBoxRawDataOffset(i, splitAtIndex);
boxPL.Write().CopyTo(Data, src);
if (i == CurrentBox)
boxPL.Write().CopyTo(Data, Offsets.CurrentBox);
bool written = PokeList2.MergeSingles(src, dest, StringLength, boxSlotCount, false);
if (written && i == CurrentBox)
dest.CopyTo(Data.AsSpan(Offsets.CurrentBox));
}
var partyPL = new PokeList2(PokeListType.Party, Japanese);
int pSlot = 0;
for (int i = 0; i < 6; i++)
// Write Party
{
var ofs = GetPartyOffset(i);
var data = Data.AsSpan(ofs, SIZE_STORED).ToArray();
PK2 partyPK = GetPKM(data);
if (partyPK.Species > 0)
partyPL[pSlot++] = partyPK;
int ofs = Offsets.Party;
var dest = Data.AsSpan(ofs, SIZE_PARTY_LIST);
var src = PartyBuffer[GetPartyOffset(0)..];
PokeList2.MergeSingles(src, dest, StringLength, 6, true);
}
partyPL.Write().CopyTo(Data, Offsets.Party);
SetChecksums();
if (Japanese)
@@ -246,20 +222,20 @@ protected override byte[] GetFinalData()
break;
}
}
return Data[..^SIZE_RESERVED];
return Data;
}
// Configuration
protected override SAV2 CloneInternal() => new(Write(), Version);
protected override SAV2 CloneInternal() => new(Write(), Version) { Language = Language };
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 PK2 BlankPKM => new(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);
private int SIZE_BOX_AS_SINGLES => BoxSlotCount*SIZE_STORED;
private int SIZE_BOX_LIST => (((StringLength * 2) + PokeCrypto.SIZE_2STORED + 1) * BoxSlotCount) + 2;
private int SIZE_PARTY_LIST => (((StringLength * 2) + PokeCrypto.SIZE_2PARTY + 1) * 6) + 2;
public override ushort MaxMoveID => Legal.MaxMoveID_2;
public override ushort MaxSpeciesID => Legal.MaxSpeciesID_2;
@@ -583,8 +559,8 @@ private int GetDaycareOffset(int index)
public int DaycareSlotCount => 2;
private int GetDaycareSlotOffset(int slot) => GetPartyOffset(7 + (slot * 2));
public Memory<byte> GetDaycareSlot(int slot) => Data.AsMemory(GetDaycareSlotOffset(slot), SIZE_STORED);
public Memory<byte> GetDaycareEgg() => Data.AsMemory(GetDaycareSlotOffset(2), SIZE_STORED);
public Memory<byte> GetDaycareSlot(int slot) => Reserved.AsMemory(GetDaycareSlotOffset(slot), SIZE_STORED);
public Memory<byte> GetDaycareEgg() => Reserved.AsMemory(GetDaycareSlotOffset(2), SIZE_STORED);
public bool IsDaycareOccupied(int slot) => (DaycareFlagByte(slot) & 1) != 0;
public void SetDaycareOccupied(int slot, bool occupied)
@@ -606,12 +582,12 @@ public override int PartyCount
public override int GetBoxOffset(int box)
{
return Data.Length - SIZE_RESERVED + (box * SIZE_BOX);
return (box * SIZE_BOX_AS_SINGLES);
}
public override int GetPartyOffset(int slot)
{
return Data.Length - SIZE_RESERVED + (BoxCount * SIZE_BOX) + (slot * SIZE_STORED);
return (BoxCount * SIZE_BOX_AS_SINGLES) + (slot * SIZE_STORED);
}
public override int CurrentBox
@@ -626,7 +602,13 @@ public bool CurrentBoxChanged
set => Data[Offsets.CurrentBoxIndex] = (byte)((Data[Offsets.CurrentBoxIndex] & 0x7F) | (byte)(value ? 0x80 : 0));
}
public string GetBoxName(int box) => GetString(GetBoxNameSpan(box));
public string GetBoxName(int box)
{
var result = GetString(GetBoxNameSpan(box));
if (!Korean)
result = StringConverter2.InflateLigatures(result, Language);
return result;
}
private Span<byte> GetBoxNameSpan(int box)
{
@@ -637,13 +619,16 @@ private Span<byte> GetBoxNameSpan(int box)
public void SetBoxName(int box, ReadOnlySpan<char> value)
{
var span = GetBoxNameSpan(box);
SetString(span, value, 8, StringConverterOption.Clear50);
const int maxLen = 8;
Span<char> deflated = stackalloc char[maxLen];
int len = StringConverter2.DeflateLigatures(value, deflated, Language);
SetString(span, deflated[..len], maxLen, StringConverterOption.Clear50);
}
protected override PK2 GetPKM(byte[] data)
{
if (data.Length == SIZE_STORED)
return new PokeList2(data, PokeListType.Single, Japanese)[0];
return PokeList2.ReadFromList(data, StringLength);
return new(data);
}
@@ -790,7 +775,7 @@ private ushort GetResetKey()
foreach (var b in Data.AsSpan(Offsets.Money, 3))
result += b;
var tr = Data.AsSpan(Offsets.Trainer1, 7);
var end = tr[2..].IndexOf(StringConverter12.G1TerminatorCode);
var end = tr[2..].IndexOf(StringConverter1.TerminatorCode);
if (end >= 0)
tr = tr[..(end + 2)];
foreach (var b in tr)
@@ -813,14 +798,14 @@ public override string GetString(ReadOnlySpan<byte> data)
{
if (Korean)
return StringConverter2KOR.GetString(data);
return StringConverter12.GetString(data, Japanese);
return StringConverter2.GetString(data, Language);
}
public override int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength, StringConverterOption option)
{
if (Korean)
return StringConverter2KOR.SetString(destBuffer, value, maxLength, option);
return StringConverter12.SetString(destBuffer, value, maxLength, Japanese, option);
return StringConverter2.SetString(destBuffer, value, maxLength, Language, option);
}
public bool IsGBMobileAvailable => Japanese && Version == GameVersion.C;

View File

@@ -93,16 +93,18 @@ protected override void SetBoxMetadata(int box)
}
var boxOfs = bdata - ListHeaderSizeBox;
if (Data[boxOfs] == 0)
var slice = Data.AsSpan(boxOfs, ListHeaderSizeBox);
if (slice[0] == 0)
{
Data[boxOfs] = 1;
Data[boxOfs + 1] = (byte)count;
Data[boxOfs + 4] = StringConverter12.G1TerminatorCode;
StringConverter12.SetString(Data.AsSpan(boxOfs + 0x10, 4), "1234", 4, Japanese, StringConverterOption.None);
slice[0] = 1;
slice[1] = (byte)count;
slice[4] = StringConverter2.TerminatorCode;
for (int i = 0; i < 4; i++)
slice[0x10 + i] = (byte)(0xF6 + i); // 1234
}
else
{
Data[boxOfs + 1] = (byte)count;
slice[1] = (byte)count;
}
}

View File

@@ -40,7 +40,7 @@ public abstract class SAV3 : SaveFile, ILangDeviantSave, IEventFlag37, IBoxDetai
public readonly byte[] Storage = new byte[9 * SIZE_SECTOR_USED]; // [0x83D0]
private readonly int ActiveSlot;
public sealed override int Language { get => Japanese ? (int)LanguageID.Japanese : (int)LanguageID.English; set { } }
public sealed override int Language { get; set; }
protected SAV3(bool japanese) => Japanese = japanese;

View File

@@ -11,7 +11,7 @@ namespace PKHeX.Core;
public sealed class SAV3E : SAV3, IGen3Hoenn, IGen3Joyful, IGen3Wonder, IDaycareRandomState<uint>
{
// Configuration
protected override SAV3E CloneInternal() => new(Write());
protected override SAV3E CloneInternal() => new(Write()) { Language = Language };
public override GameVersion Version { get => GameVersion.E; set { } }
public override PersonalTable3 Personal => PersonalTable.E;

View File

@@ -10,7 +10,7 @@ namespace PKHeX.Core;
public sealed class SAV3FRLG : SAV3, IGen3Joyful, IGen3Wonder, IDaycareRandomState<ushort>
{
// Configuration
protected override SAV3FRLG CloneInternal() => new(Write());
protected override SAV3FRLG CloneInternal() => new(Write()) { Language = Language };
public override GameVersion Version { get; set; } = GameVersion.FR; // allow mutation
private PersonalTable3 _personal = PersonalTable.FR;
public override PersonalTable3 Personal => _personal;

View File

@@ -11,8 +11,8 @@ namespace PKHeX.Core;
public sealed class SAV3RS : SAV3, IGen3Hoenn, IDaycareRandomState<ushort>
{
// Configuration
protected override SAV3RS CloneInternal() => new(Write());
public override GameVersion Version { get => GameVersion.RS; set { } }
protected override SAV3RS CloneInternal() => new(Write()) { Language = Language };
public override GameVersion Version { get; set; } = GameVersion.RS; // allow mutation
public override PersonalTable3 Personal => PersonalTable.RS;
public override int EventFlagCount => 8 * 288;

View File

@@ -93,11 +93,11 @@ public virtual SlotGroup[] GetRegisteredTeams()
return result;
}
public sealed override string GetString(ReadOnlySpan<byte> data) => StringConverter12.GetString(data, Japanese);
public sealed override string GetString(ReadOnlySpan<byte> data) => StringConverter1.GetString(data, Japanese);
public sealed override int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength, StringConverterOption option)
{
return StringConverter12.SetString(destBuffer, value, maxLength, Japanese, option);
return StringConverter1.SetString(destBuffer, value, maxLength, Japanese, option);
}
/// <summary>

View File

@@ -6,9 +6,9 @@ namespace PKHeX.Core;
// warning: international only
public sealed class Mail2 : MailDetail
{
private readonly bool US;
private bool Japanese => !US;
private bool Korean => !US && !Japanese;
private readonly int Language;
private bool Japanese => Language == 1;
private bool Korean => Language == (int)LanguageID.Korean;
// structure:
private const int LINE_LENGTH = 0x10;
@@ -27,7 +27,7 @@ public sealed class Mail2 : MailDetail
public Mail2(SAV2 sav, int index) : base(sav.Data.AsSpan(GetMailOffset(index), 0x2F).ToArray(), GetMailOffset(index))
{
US = sav is { Japanese: false, Korean: false };
Language = sav.Language;
}
private static int GetMailOffset(int index)
@@ -51,19 +51,26 @@ private static int GetMailboxMailOffset(int index)
return (index * SIZE) + 0x835;
}
private string GetString(Span<byte> asSpan)
private string GetString(Span<byte> span)
{
if (Korean)
return StringConverter2KOR.GetString(asSpan);
return StringConverter12.GetString(asSpan, Japanese);
return StringConverter2KOR.GetString(span);
var result = StringConverter2.GetString(span, Language);
if (!Korean)
result = StringConverter2.InflateLigatures(result, Language);
return result;
}
private void SetString(Span<byte> asSpan, ReadOnlySpan<char> value, int maxLength)
private void SetString(Span<byte> span, ReadOnlySpan<char> value, int maxLength)
{
if (Korean)
StringConverter2KOR.SetString(asSpan, value, maxLength);
else
StringConverter12.SetString(asSpan, value, maxLength, Japanese);
{
StringConverter2KOR.SetString(span, value, maxLength);
return;
}
Span<char> deflated = stackalloc char[maxLength];
int len = StringConverter2.DeflateLigatures(value, deflated, Language);
StringConverter2.SetString(span, deflated[..len], maxLength, Language);
}
public string Line1

View File

@@ -0,0 +1,279 @@
using System;
using static PKHeX.Core.LanguageID;
using static PKHeX.Core.GameVersion;
namespace PKHeX.Core;
/// <summary>
/// Logic to detect and update the language and version of a save file.
/// </summary>
/// <remarks>Useful for save files that do not have a language or version saved in the data, or impossible to infer without file names.</remarks>
public static class SaveLanguage
{
public static LanguageID OverrideLanguageGen1 { get; set; } = English;
public static GameVersion OverrideVersionGen1 { get; set; } = RB;
public static LanguageID OverrideLanguageGen2 { get; set; } = English;
public static GameVersion OverrideVersionGen2 { get; set; } = GS;
public static LanguageID OverrideLanguageGen3RS { get; set; } = English;
public static GameVersion OverrideVersionGen3RS { get; set; } = RS;
public static LanguageID OverrideLanguageGen3FRLG { get; set; } = English;
public static GameVersion OverrideVersionGen3FRLG { get; set; } = FRLG;
/// <summary>
/// Tries to infer the language and version of the save file.
/// </summary>
/// <param name="sav">Save file to infer from</param>
/// <param name="result">Result of the inference. Only use if the result is true.</param>
/// <returns>True if the language and version could be inferred, false otherwise</returns>
public static bool TryGetResult(SaveFile sav, out SaveLanguageResult result) => result = sav switch
{
SAV1 s1 => s1.InferFrom(),
SAV2 s2 => s2.InferFrom(),
SAV3 s3 => s3.InferFrom(),
_ => default,
};
/// <summary>
/// Updates the language and version of the save file if possible.
/// </summary>
/// <param name="sav">Save file to update</param>
/// <returns>True if the language and version were updated, false otherwise</returns>
public static bool TryRevise(SaveFile sav)
{
if (!TryGetResult(sav, out var result))
return false;
sav.Language = (int)result.Language;
sav.Version = result.Version;
if (sav is SAV3FRLG s3)
s3.ResetPersonal(result.Version);
if (sav.Metadata.FilePath is { } path)
sav.Metadata.SetExtraInfo(path);
return true;
}
/// <summary>
/// Infer the language and version of the save file based on the file name.
/// </summary>
public static SaveLanguageResult InferFrom(this SAV1 sav)
{
if (sav.Metadata.FileName is not { } x)
return GetFallback(sav);
var result = InferFrom1(x);
if (result != default && sav.Japanese == (result.Language == Japanese))
return result;
return GetFallback(sav);
}
/// <inheritdoc cref="InferFrom(SAV1)"/>
public static SaveLanguageResult InferFrom(this SAV2 sav)
{
if (sav.Metadata.FileName is not { } x)
return GetFallback(sav);
var result = InferFrom2(x);
if (result != default && (sav.Japanese == (result.Language == Japanese)) && (sav.Korean == (result.Language == Korean)))
return result;
return GetFallback(sav);
}
/// <inheritdoc cref="InferFrom(SAV1)"/>
public static SaveLanguageResult InferFrom(this SAV3 sav)
{
if (sav.Metadata.FileName is not { } x)
return GetFallback(sav);
var result = InferFrom3(x);
if (result != default && sav.Japanese == (result.Language == Japanese))
return result;
return GetFallback(sav);
}
private static bool Contains(ReadOnlySpan<char> span, ReadOnlySpan<char> value)
=> span.Contains(value, StringComparison.OrdinalIgnoreCase);
/// <inheritdoc cref="InferFrom(SAV1)"/>
public static SaveLanguageResult InferFrom1(ReadOnlySpan<char> name)
{
// Red
if (Contains(name, "red")) return (English, RD);
if (Contains(name, "roug")) return (French, RD);
if (Contains(name, "rot")) return (German, RD);
if (Contains(name, "ross")) return (Italian, RD);
if (Contains(name, "rojo")) return (Spanish, RD);
// Blue
if (Contains(name, "blue")) return (English, GN);
if (Contains(name, "bleu")) return (French, GN);
if (Contains(name, "blau")) return (German, GN);
if (Contains(name, "blu")) return (Italian, GN);
if (Contains(name, "azul")) return (Spanish, GN);
// Yellow
if (Contains(name, "yell")) return (English, YW);
if (Contains(name, "jaun")) return (French, YW);
if (Contains(name, "gelb")) return (German, YW);
if (Contains(name, "gial")) return (Italian, YW);
if (Contains(name, "amar")) return (Spanish, YW);
// Japanese
if (Contains(name, "赤")) return (Japanese, RD);
if (Contains(name, "緑")) return (Japanese, GN);
if (Contains(name, "青")) return (Japanese, BU);
if (Contains(name, "ピ")) return (Japanese, YW);
if (Contains(name, "黄")) return (Japanese, YW);
if (Contains(name, "aka")) return (Japanese, RD);
if (Contains(name, "ao")) return (Japanese, BU);
if (Contains(name, "mid")) return (Japanese, GN);
if (Contains(name, "pika")) return (Japanese, YW);
return default;
}
/// <inheritdoc cref="InferFrom(SAV1)"/>
public static SaveLanguageResult InferFrom2(ReadOnlySpan<char> name)
{
// Gold
if (Contains(name, "golde")) return (German, GD);
if (Contains(name, "gold")) return (English, GD);
if (Contains(name, "e oro")) return (Italian, GD);
if (Contains(name, "oro")) return (Spanish, GD);
if (Contains(name, "n or")) return (French, GD);
if (Contains(name, "金")) return (Japanese, GD);
if (Contains(name, "금")) return (Korean, GD);
// Silver
if (Contains(name, "silv")) return (English, SI);
if (Contains(name, "silb")) return (German, SI);
if (Contains(name, "plat")) return (Spanish, SI);
if (Contains(name, "e arg")) return (Italian, SI);
if (Contains(name, "arge")) return (French, SI);
if (Contains(name, "銀")) return (Japanese, SI);
if (Contains(name, "은")) return (Korean, SI);
// Crystal
if (Contains(name, "cry")) return (English, C);
if (Contains(name, "kri")) return (German, C);
if (Contains(name, "cristall")) return (Italian, C);
if (Contains(name, "on cri")) return (French, C);
if (Contains(name, "ón crist")) return (Spanish, C);
if (Contains(name, "クリ")) return (Japanese, C);
if (Contains(name, "kin")) return (Japanese, GD);
if (Contains(name, "gin")) return (Japanese, SI);
if (Contains(name, "geum")) return (Korean, GD);
if (Contains(name, "eun")) return (Korean, SI);
return default;
}
/// <inheritdoc cref="InferFrom(SAV1)"/>
public static SaveLanguageResult InferFrom3(ReadOnlySpan<char> name)
{
// FireRed
if (Contains(name, "fir")) return (English, FR);
if (Contains(name, "feu")) return (French, FR);
if (Contains(name, "feuer")) return (German, FR);
if (Contains(name, "fuoco")) return (Italian, FR);
if (Contains(name, "fueg")) return (Spanish, FR);
if (Contains(name, "ファイアレッド")) return (Japanese, FR);
// LeafGreen
if (Contains(name, "lea")) return (English, LG);
if (Contains(name, "vert")) return (French, LG);
if (Contains(name, "blatt")) return (German, LG);
if (Contains(name, "fogli")) return (Italian, LG);
if (Contains(name, "hoja")) return (Spanish, LG);
if (Contains(name, "リーフグリーン")) return (Japanese, LG);
// Ruby
if (Contains(name, "ruby")) return (English, R);
if (Contains(name, "rubí")) return (Spanish, R);
if (Contains(name, "rubino")) return (Italian, R);
if (Contains(name, "rubis")) return (French, R);
if (Contains(name, "rubin")) return (German, R);
if (Contains(name, "ルビー")) return (Japanese, R);
// Sapphire
if (Contains(name, "sapp")) return (English, S);
if (Contains(name, "saphir")) return (French, S);
if (Contains(name, "safir")) return (German, S);
if (Contains(name, "zafir")) return (Spanish, S);
if (Contains(name, "zaffiro")) return (Italian, S);
if (Contains(name, "サファ")) return (Japanese, S);
// Emerald
if (Contains(name, "esm")) return (Spanish, E);
if (Contains(name, "smar")) return (German, E);
if (Contains(name, "smer")) return (Italian, E);
if (Contains(name, "éme") || Contains(name, "n eme")) return (French, E);
if (Contains(name, "emer")) return (English, E);
if (Contains(name, "エメ")) return (Japanese, E);
// Codes
if (Contains(name, "axpj")) return (Japanese, S);
if (Contains(name, "axp")) return (OverrideLanguageGen3RS, S);
if (Contains(name, "axvj")) return (Japanese, R);
if (Contains(name, "axv")) return (OverrideLanguageGen3RS, R);
if (Contains(name, "bpej")) return (Japanese, E);
if (Contains(name, "bpe")) return (OverrideLanguageGen3RS, E);
if (Contains(name, "bprj")) return (Japanese, FR);
if (Contains(name, "bpr")) return (OverrideLanguageGen3FRLG, FR);
if (Contains(name, "bpgj")) return (Japanese, LG);
if (Contains(name, "bpg")) return (OverrideLanguageGen3FRLG, LG);
return default;
}
/// <summary>
/// Gets a safe fallback language and version for the save file.
/// </summary>
private static SaveLanguageResult GetFallback(SAV1 sav)
{
bool jp = sav.Japanese;
bool yw = sav.Yellow;
var lang = jp ? Japanese : OverrideLanguageGen1;
var ver = yw ? YW : OverrideVersionGen1;
return (lang, ver);
}
/// <inheritdoc cref="GetFallback(SAV1)"/>"/>
private static SaveLanguageResult GetFallback(SAV2 sav)
{
bool jp = sav.Japanese;
bool kor = sav.Korean;
var lang = jp ? Japanese : kor ? Korean : OverrideLanguageGen2;
var ver = sav.Version == C ? C : OverrideVersionGen2;
return (lang, ver);
}
/// <inheritdoc cref="GetFallback(SAV1)"/>"/>
private static SaveLanguageResult GetFallback(SAV3 sav)
{
bool jp = sav.Japanese;
if (sav is SAV3FRLG)
return (jp ? Japanese : OverrideLanguageGen3FRLG, OverrideVersionGen3FRLG);
var lang = jp ? Japanese : OverrideLanguageGen3RS;
var ver = sav.Version == E ? E : OverrideVersionGen3RS;
return (lang, ver);
}
}
/// <summary>
/// Result of the language and version inference.
/// </summary>
/// <param name="Language">Language ID for the save file.</param>
/// <param name="Version">Version ID for the save file.</param>
public readonly record struct SaveLanguageResult(LanguageID Language, GameVersion Version)
{
public static implicit operator bool(SaveLanguageResult r) => r != default;
public static implicit operator SaveLanguageResult((LanguageID Language, GameVersion Version) r) => new(r.Language, r.Version);
}

View File

@@ -653,7 +653,12 @@ private static GameVersion GetIsG9SAV(ReadOnlySpan<byte> data)
{
var data = File.ReadAllBytes(path);
var sav = GetVariantSAV(data, path);
sav?.Metadata.SetExtraInfo(path);
if (sav is null)
return null;
sav.Metadata.SetExtraInfo(path);
if (sav.Generation <= 3)
SaveLanguage.TryRevise(sav);
return sav;
}
catch (Exception ex)
@@ -846,12 +851,12 @@ public static SaveFile GetBlankSAV(GameVersion game, string trainerName, Languag
/// <returns>Blank save file from the requested game, null if no game exists for that <see cref="GameVersion"/>.</returns>
private static SaveFile GetBlankSAV(GameVersion game, LanguageID language) => game switch
{
RD or BU or GN or YW or RBY => new SAV1(version: game, japanese: language == LanguageID.Japanese || game == BU),
RD or BU or GN or YW or RBY => new SAV1(version: game, game == BU ? LanguageID.Japanese : language),
StadiumJ => new SAV1StadiumJ(),
Stadium => new SAV1Stadium(language == LanguageID.Japanese),
GD or SI or GS => new SAV2(version: GS, lang: language),
C or GSC => new SAV2(version: C, lang: language),
GD or SI or GS => new SAV2(version: GS, language: language),
C or GSC => new SAV2(version: C, language: language),
Stadium2 => new SAV2Stadium(language == LanguageID.Japanese),
R or S or RS => new SAV3RS(language == LanguageID.Japanese),

View File

@@ -426,6 +426,7 @@ private void ReloadProgramSettings(PKHeXSettings settings)
PKME_Tabs.HideSecretValues = C_SAV.HideSecretDetails = settings.Privacy.HideSecretDetails;
WinFormsUtil.DetectSaveFileOnFileOpen = settings.Startup.TryDetectRecentSave;
SelectablePictureBox.FocusBorderDeflate = GenderToggle.FocusBorderDeflate = settings.Display.FocusBorderDeflate;
settings.SaveLanguage.Apply();
var converter = settings.Converter;
EntityConverter.AllowIncompatibleConversion = converter.AllowIncompatibleConversion;
@@ -912,6 +913,9 @@ private static bool CheckLoadPath(string path)
private static bool SanityCheckSAV(ref SaveFile sav)
{
if (sav.Generation <= 3)
SaveLanguage.TryRevise(sav);
if (sav.State.Exportable && sav is SAV3 s3)
{
if (ModifierKeys == Keys.Control || s3.IsCorruptPokedexFF())
@@ -935,7 +939,7 @@ private static bool SanityCheckSAV(ref SaveFile sav)
s.Metadata.SetExtraInfo(origin);
sav = s;
}
else if (s3 is SAV3FRLG frlg) // IndeterminateSubVersion
else if (s3 is SAV3FRLG frlg && !frlg.Version.IsValidSavedVersion()) // IndeterminateSubVersion
{
string fr = GameInfo.GetVersionName(GameVersion.FR);
string lg = GameInfo.GetVersionName(GameVersion.LG);

View File

@@ -28,6 +28,7 @@ public sealed class PKHeXSettings
public SetImportSettings Import { get; set; } = new();
public SlotWriteSettings SlotWrite { get; set; } = new();
public PrivacySettings Privacy { get; set; } = new();
public SaveLanguageSettings SaveLanguage { get; set; } = new();
// UI Tweaks
public DisplaySettings Display { get; set; } = new();
@@ -463,6 +464,47 @@ public sealed class PrivacySettings
public bool HideSecretDetails { get; set; }
}
public sealed class SaveLanguageSettings
{
[LocalizedDescription("Gen1: If unable to detect a language or version for a save file, use these instead.")]
public LangVersion OverrideGen1 { get; set; } = new();
[LocalizedDescription("Gen2: If unable to detect a language or version for a save file, use these instead.")]
public LangVersion OverrideGen2 { get; set; } = new();
[LocalizedDescription("Gen3 R/S: If unable to detect a language or version for a save file, use these instead.")]
public LangVersion OverrideGen3RS { get; set; } = new();
[LocalizedDescription("Gen3 FR/LG: If unable to detect a language or version for a save file, use these instead.")]
public LangVersion OverrideGen3FRLG { get; set; } = new();
[TypeConverter(typeof(ExpandableObjectConverter))]
public sealed record LangVersion
{
public LanguageID Language { get; set; } = LanguageID.English;
public GameVersion Version { get; set; }
}
public void Apply()
{
SaveLanguage.OverrideLanguageGen1 = OverrideGen1.Language;
if (GameVersion.RBY.Contains(OverrideGen1.Version))
SaveLanguage.OverrideVersionGen1 = OverrideGen1.Version;
SaveLanguage.OverrideLanguageGen2 = OverrideGen2.Language;
if (GameVersion.GS.Contains(OverrideGen2.Version))
SaveLanguage.OverrideVersionGen2 = OverrideGen2.Version;
SaveLanguage.OverrideLanguageGen3RS = OverrideGen3RS.Language;
if (GameVersion.RS.Contains(OverrideGen3RS.Version))
SaveLanguage.OverrideVersionGen3RS = OverrideGen3RS.Version;
SaveLanguage.OverrideLanguageGen3FRLG = OverrideGen3FRLG.Language;
if (GameVersion.FRLG.Contains(OverrideGen3FRLG.Version))
SaveLanguage.OverrideVersionGen3FRLG = OverrideGen3FRLG.Version;
}
}
public sealed class BulkAnalysisSettings : IBulkAnalysisSettings
{
[LocalizedDescription("Checks the save file data and Current Handler state to determine if the Pokémon's Current Handler does not match the expected value.")]

View File

@@ -27,6 +27,7 @@ public SAV_BoxLayout(SaveFile sav, int box)
LB_BoxSelect.SelectedIndex = box;
TB_BoxName.MaxLength = SAV.Generation switch
{
2 when SAV is SAV2 { Japanese: false, Korean: false } => 8 * 2,
6 or 7 => 14,
>= 8 => 16,
_ => 8,

View File

@@ -79,7 +79,7 @@ public static void Encode45(ushort g4, char g5)
public static void ConvertStringVC(string g12, string g7)
{
Span<byte> b12 = stackalloc byte[g12.Length];
var len = StringConverter12.SetString(b12, g12, g12.Length, true);
var len = StringConverter1.SetString(b12, g12, g12.Length, true);
var result = StringConverter12Transporter.GetString(b12[..len], true);
result.Should().Be(g7);
}
@@ -96,9 +96,9 @@ public static void ConvertStringG1(Species species, string expect)
// Ensure the API converts it back and forth correctly.
Span<byte> convert = stackalloc byte[expect.Length + 1];
var len = StringConverter12.SetString(convert, name, name.Length, jp);
var len = StringConverter1.SetString(convert, name, name.Length, jp);
len.Should().Be(expect.Length + 1);
var gen1Name = StringConverter12.GetString(convert, jp);
var gen1Name = StringConverter1.GetString(convert, jp);
gen1Name.Should().Be(expect);
// Truncated name transferred with Virtual Console rules isn't the same as the Generation 7 name.