mirror of
https://github.com/kwsch/PKHeX.git
synced 2026-10-01 08:16:38 -05:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -3,11 +3,20 @@
|
||||
namespace PKHeX.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Logic for converting a <see cref="string"/> for Generation 1 & 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 =>
|
||||
387
PKHeX.Core/PKM/Strings/StringConverter2.cs
Normal file
387
PKHeX.Core/PKM/Strings/StringConverter2.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}";
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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..]);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
279
PKHeX.Core/Saves/Util/SaveLanguage.cs
Normal file
279
PKHeX.Core/Saves/Util/SaveLanguage.cs
Normal 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);
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.")]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user