Revise gender symbol remapping

Handles Nidoran's shenanigans as well as more clear method names
- Add normalization for PK7->PK8 (no more 0xE... usage for the gender symbols... maybe more chars?)
- Not sure if Gen3 gamecube encoding needs sanitizing. Who is transferring Nidoran to CXD? :)

Requires some silly usage of Language passing as arguments. Future improvements can be made to revise the half/full encoding determination when setting a string. Probably has issues since we're just doing a naive check without considering nicknames w/ special chars.

Closes #4174
This commit is contained in:
Kurt 2024-05-12 10:47:55 -05:00
parent a33884895f
commit 08ed482555
38 changed files with 338 additions and 162 deletions

View file

@ -424,7 +424,7 @@ public sealed class ShowdownSet : IBattleTemplate
private string GetSpeciesNickname(string specForm)
{
if (Nickname.Length == 0)
if (Nickname.Length == 0 || Nickname == specForm)
return specForm;
bool isNicknamed = SpeciesName.IsNicknamedAnyLanguage(Species, Nickname, Context.Generation());
if (!isNicknamed)

View file

@ -280,7 +280,10 @@ public sealed class NicknameVerifier : Verifier
if (pk.IsNicknamed != flagState)
data.AddLine(GetInvalid(flagState ? LNickFlagEggYes : LNickFlagEggNo, CheckIdentifier.Egg));
ReadOnlySpan<char> nickname = pk.Nickname;
Span<char> nickname = stackalloc char[pk.MaxStringLengthNickname];
int len = pk.LoadString(pk.NicknameTrash, nickname);
nickname = nickname[..len];
if (pk.Format == 2 && !SpeciesName.IsNicknamedAnyLanguage(0, nickname, 2))
data.AddLine(GetValid(LNickMatchLanguageEgg, CheckIdentifier.Egg));
else if (!nickname.SequenceEqual(SpeciesName.GetEggName(pk.Language, Info.Generation)))

View file

@ -76,7 +76,7 @@ public sealed class PCD(byte[] Data)
public override string CardTitle
{
get => StringConverter4.GetString(CardTitleSpan);
set => StringConverter4.SetString(CardTitleSpan, value, TitleLength / 2, StringConverterOption.ClearFF);
set => StringConverter4.SetString(CardTitleSpan, value, TitleLength / 2, 0, StringConverterOption.ClearFF);
}
public ushort CardCompatibility => ReadUInt16LittleEndian(Data.AsSpan(0x14C)); // rest of bytes we don't really care about

View file

@ -57,7 +57,7 @@ public sealed class PGF(byte[] Data) : DataMysteryGift(Data), IRibbonSetEvent3,
public string Nickname
{
get => StringConverter5.GetString(Data.AsSpan(0x1E, 11 * 2));
set => StringConverter5.SetString(Data.AsSpan(0x1E, 11 * 2), value, 11, StringConverterOption.ClearFF);
set => StringConverter5.SetString(Data.AsSpan(0x1E, 11 * 2), value, 11, Language, StringConverterOption.ClearFF);
}
public Nature Nature { get => (Nature)Data[0x34]; set => Data[0x34] = (byte)value; }
@ -83,7 +83,7 @@ public sealed class PGF(byte[] Data) : DataMysteryGift(Data), IRibbonSetEvent3,
public override string OriginalTrainerName
{
get => StringConverter5.GetString(Data.AsSpan(0x4A, 8 * 2));
set => StringConverter5.SetString(Data.AsSpan(0x4A, 8 * 2), value, 8, StringConverterOption.ClearFF);
set => StringConverter5.SetString(Data.AsSpan(0x4A, 8 * 2), value, 8, Language, StringConverterOption.ClearFF);
}
public byte OTGender { get => Data[0x5A]; set => Data[0x5A] = value; }
@ -93,7 +93,7 @@ public sealed class PGF(byte[] Data) : DataMysteryGift(Data), IRibbonSetEvent3,
public override string CardTitle
{
get => StringConverter5.GetString(Data.AsSpan(0x60, 37 * 2));
set => StringConverter5.SetString(Data.AsSpan(0x60, 37 * 2), value, 36, StringConverterOption.ClearZero);
set => StringConverter5.SetString(Data.AsSpan(0x60, 37 * 2), value, 36, Language, StringConverterOption.ClearZero);
}
// Card Attributes

View file

@ -35,7 +35,7 @@ public sealed class PL6(Memory<byte> Raw)
/// <summary>
/// Name of data source
/// </summary>
public string Origin { get => StringConverter6.GetString(Source); set => StringConverter6.SetString(Source, value, 54, StringConverterOption.ClearZero); }
public string Origin { get => StringConverter6.GetString(Source); set => StringConverter6.SetString(Source, value, 54, 0, StringConverterOption.ClearZero); }
// Pokemon transfer flags?
public uint Flags1 { get => ReadUInt32LittleEndian(Data[0x099..]); set => WriteUInt32LittleEndian(Data[0x099..], value); }
@ -109,7 +109,7 @@ public sealed class LinkEntity6(Memory<byte> Raw) : IRibbonSetEvent3, IRibbonSet
public string Nickname
{
get => StringConverter6.GetString(NicknameTrash);
set => StringConverter6.SetString(NicknameTrash, value, 12, StringConverterOption.ClearZero);
set => StringConverter6.SetString(NicknameTrash, value, 12, Language, StringConverterOption.ClearZero);
}
public Nature Nature { get => (Nature)Data[0x38]; set => Data[0x38] = (byte)value; }
@ -141,7 +141,7 @@ public sealed class LinkEntity6(Memory<byte> Raw) : IRibbonSetEvent3, IRibbonSet
public string OT
{
get => StringConverter6.GetString(OriginalTrainerTrash);
set => StringConverter6.SetString(OriginalTrainerTrash, value, 12, StringConverterOption.ClearZero);
set => StringConverter6.SetString(OriginalTrainerTrash, value, 12, Language, StringConverterOption.ClearZero);
}
public int Level { get => Data[0x68]; set => Data[0x68] = (byte)value; }

View file

@ -43,7 +43,7 @@ public sealed class WC6(byte[] Data) : DataMysteryGift(Data), IRibbonSetEvent3,
{
// Max len 36 char, followed by null terminator
get => StringConverter6.GetString(Data.AsSpan(2, 0x4A));
set => StringConverter6.SetString(Data.AsSpan(2, 0x4A), value, 36, StringConverterOption.ClearZero);
set => StringConverter6.SetString(Data.AsSpan(2, 0x4A), value, 36, Language, StringConverterOption.ClearZero);
}
internal uint RawDate
@ -170,7 +170,7 @@ public sealed class WC6(byte[] Data) : DataMysteryGift(Data), IRibbonSetEvent3,
public string Nickname
{
get => StringConverter6.GetString(Data.AsSpan(0x86, 0x1A));
set => StringConverter6.SetString(Data.AsSpan(0x86, 0x1A), value, 12, StringConverterOption.ClearZero);
set => StringConverter6.SetString(Data.AsSpan(0x86, 0x1A), value, 12, Language, StringConverterOption.ClearZero);
}
public Nature Nature { get => (Nature)Data[0xA0]; set => Data[0xA0] = (byte)value; }
@ -199,7 +199,7 @@ public sealed class WC6(byte[] Data) : DataMysteryGift(Data), IRibbonSetEvent3,
public override string OriginalTrainerName
{
get => StringConverter6.GetString(Data.AsSpan(0xB6, 0x1A));
set => StringConverter6.SetString(Data.AsSpan(0xB6, 0x1A), value, 12, StringConverterOption.ClearZero);
set => StringConverter6.SetString(Data.AsSpan(0xB6, 0x1A), value, 12, Language, StringConverterOption.ClearZero);
}
public override byte Level { get => Data[0xD0]; set => Data[0xD0] = value; }

View file

@ -200,7 +200,18 @@ public sealed class BK4 : G4PKM
#endregion
#region Block C
public override string Nickname { get => StringConverter4GC.GetString(NicknameTrash); set => StringConverter4GC.SetString(NicknameTrash, value, 10, StringConverterOption.None); }
public override string Nickname
{
get => StringConverter4GC.GetString(NicknameTrash);
set
{
var language = Language;
CheckKoreanNidoranDPPt(value, ref language);
StringConverter4GC.SetString(NicknameTrash, value, 10, language, StringConverterOption.None);
}
}
// 0x5E unused
public override GameVersion Version { get => (GameVersion)Data[0x5F]; set => Data[0x5F] = (byte)value; }
private byte RIB8 { get => Data[0x60]; set => Data[0x60] = value; } // Sinnoh 3
@ -243,7 +254,7 @@ public sealed class BK4 : G4PKM
#endregion
#region Block D
public override string OriginalTrainerName { get => StringConverter4GC.GetString(OriginalTrainerTrash); set => StringConverter4GC.SetString(OriginalTrainerTrash, value, 7, StringConverterOption.None); }
public override string OriginalTrainerName { get => StringConverter4GC.GetString(OriginalTrainerTrash); set => StringConverter4GC.SetString(OriginalTrainerTrash, value, 7, Language, StringConverterOption.None); }
public override byte EggYear { get => Data[0x78]; set => Data[0x78] = value; }
public override byte EggMonth { get => Data[0x79]; set => Data[0x79] = value; }
public override byte EggDay { get => Data[0x7A]; set => Data[0x7A] = value; }
@ -307,5 +318,5 @@ public sealed class BK4 : G4PKM
public override int LoadString(ReadOnlySpan<byte> data, Span<char> destBuffer)
=> StringConverter4.LoadString(data, destBuffer);
public override int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength, StringConverterOption option)
=> StringConverter4.SetString(destBuffer, value, maxLength, option);
=> StringConverter4.SetString(destBuffer, value, maxLength, Language, option);
}

View file

@ -113,10 +113,17 @@ public sealed class GameDataPK8 : HomeOptional1, IGameDataSide<PK8>, IGigantamax
Ability = (ushort)pk.Ability;
pkh.MarkingValue &= 0b1111_1111_1111;
if (!pk.IsNicknamed)
pkh.Nickname = SpeciesName.GetSpeciesNameImportHOME(pk.Species, pk.Language, 8);
if (FormInfo.IsTotemForm(pk.Species, pk.Form))
pkh.Form = FormInfo.GetTotemBaseForm(pk.Species, pk.Form);
StringConverter8.NormalizeHalfWidth(pkh.OriginalTrainerTrash);
if (pk.IsNicknamed)
{
StringConverter8.NormalizeHalfWidth(pkh.NicknameTrash);
}
else
{
pkh.NicknameTrash.Clear();
var reset = SpeciesName.GetSpeciesNameImportHOME(pk.Species, pk.Language, 8);
pkh.SetString(pkh.NicknameTrash, reset, reset.Length, StringConverterOption.None);
}
}
public PK8 ConvertToPKM(PKH pkh)

View file

@ -183,7 +183,12 @@ public sealed class PK4 : G4PKM
public override string Nickname
{
get => StringConverter4.GetString(NicknameTrash);
set => StringConverter4.SetString(NicknameTrash, value, 10, StringConverterOption.None);
set
{
var language = Language;
CheckKoreanNidoranDPPt(value, ref language);
StringConverter4.SetString(NicknameTrash, value, 10, language, StringConverterOption.None);
}
}
// 0x5E unused
@ -231,7 +236,7 @@ public sealed class PK4 : G4PKM
public override string OriginalTrainerName
{
get => StringConverter4.GetString(OriginalTrainerTrash);
set => StringConverter4.SetString(OriginalTrainerTrash, value, 7, StringConverterOption.None);
set => StringConverter4.SetString(OriginalTrainerTrash, value, 7, Language, StringConverterOption.None);
}
public override byte EggYear { get => Data[0x78]; set => Data[0x78] = value; }
@ -352,8 +357,8 @@ public sealed class PK4 : G4PKM
pk5.Ball = Ball;
// Transfer Nickname and OT Name, update encoding
TransferTrash(NicknameTrash, pk5.NicknameTrash);
TransferTrash(OriginalTrainerTrash, pk5.OriginalTrainerTrash);
TransferTrash(NicknameTrash, pk5.NicknameTrash, Language);
TransferTrash(OriginalTrainerTrash, pk5.OriginalTrainerTrash, Language);
// Fix Level
pk5.MetLevel = pk5.CurrentLevel;
@ -376,12 +381,12 @@ public sealed class PK4 : G4PKM
return pk5;
}
public static void TransferTrash(ReadOnlySpan<byte> src, Span<byte> dest)
public static void TransferTrash(ReadOnlySpan<byte> src, Span<byte> dest, int language)
{
Span<char> temp = stackalloc char[13];
var len = StringConverter4.LoadString(src, temp);
StringConverter345.TransferGlyphs45(temp[..len]);
StringConverter5.SetString(dest, temp[..len], len);
StringConverter5.SetString(dest, temp[..len], len, language);
}
public override string GetString(ReadOnlySpan<byte> data)
@ -389,5 +394,5 @@ public sealed class PK4 : G4PKM
public override int LoadString(ReadOnlySpan<byte> data, Span<char> destBuffer)
=> StringConverter4.LoadString(data, destBuffer);
public override int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength, StringConverterOption option)
=> StringConverter4.SetString(destBuffer, value, maxLength, option);
=> StringConverter4.SetString(destBuffer, value, maxLength, Language, option);
}

View file

@ -191,7 +191,18 @@ public sealed class PK5 : PKM, ISanityChecksum,
#endregion
#region Block C
public override string Nickname { get => StringConverter5.GetString(NicknameTrash); set => StringConverter5.SetString(NicknameTrash, value, 10, StringConverterOption.None); }
public override string Nickname
{
get => StringConverter5.GetString(NicknameTrash);
set
{
var language = Language;
CheckKoreanNidoranDPPt(value, ref language);
StringConverter5.SetString(NicknameTrash, value, 10, language, StringConverterOption.None);
}
}
// 0x5E unused
public override GameVersion Version { get => (GameVersion)Data[0x5F]; set => Data[0x5F] = (byte)value; }
private byte RIB8 { get => Data[0x60]; set => Data[0x60] = value; } // Sinnoh 3
@ -234,7 +245,7 @@ public sealed class PK5 : PKM, ISanityChecksum,
#endregion
#region Block D
public override string OriginalTrainerName { get => StringConverter5.GetString(OriginalTrainerTrash); set => StringConverter5.SetString(OriginalTrainerTrash, value, 7, StringConverterOption.None); }
public override string OriginalTrainerName { get => StringConverter5.GetString(OriginalTrainerTrash); set => StringConverter5.SetString(OriginalTrainerTrash, value, 7, Language, StringConverterOption.None); }
public override byte EggYear { get => Data[0x78]; set => Data[0x78] = value; }
public override byte EggMonth { get => Data[0x79]; set => Data[0x79] = value; }
public override byte EggDay { get => Data[0x7A]; set => Data[0x7A] = value; }
@ -558,5 +569,24 @@ public sealed class PK5 : PKM, ISanityChecksum,
public override int LoadString(ReadOnlySpan<byte> data, Span<char> destBuffer)
=> StringConverter5.LoadString(data, destBuffer);
public override int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength, StringConverterOption option)
=> StringConverter5.SetString(destBuffer, value, maxLength, option);
=> StringConverter5.SetString(destBuffer, value, maxLength, Language, option);
/// <inheritdoc cref="G4PKM.CheckKoreanNidoranDPPt"/>
/// <remarks> Gen4->Gen5 chars transfer without resetting the name. Still relevant even as PK5. </remarks>
private void CheckKoreanNidoranDPPt(ReadOnlySpan<char> value, ref int language)
{
if (language != (int)LanguageID.Korean)
return;
if (IsNicknamed)
return;
if (Version is not (GameVersion.D or GameVersion.P or GameVersion.Pt))
return;
// Full-width gender symbols for not-nicknamed Nidoran in D/P/Pt
// Full/Half is technically legal either way as trainers can reset the nickname in HG/SS, or vice versa for origins.
// Still try to set whichever it originated with. Default would be half, but if it's a Nidoran species name, set full-width.
if (Species == (int)Core.Species.NidoranM && value is "니드런♂")
language = 1; // Use Japanese to force full-width encoding of the gender symbol.
else if (Species == (int)Core.Species.NidoranF && value is "니드런♀")
language = 1;
}
}

View file

@ -227,7 +227,7 @@ public sealed class PK6 : G6PKM, IRibbonSetEvent3, IRibbonSetEvent4, IRibbonSetC
public override string Nickname
{
get => StringConverter6.GetString(NicknameTrash);
set => StringConverter6.SetString(NicknameTrash, value, 12, StringConverterOption.None);
set => StringConverter6.SetString(NicknameTrash, value, 12, Language, StringConverterOption.None);
}
public override ushort Move1
@ -304,7 +304,7 @@ public sealed class PK6 : G6PKM, IRibbonSetEvent3, IRibbonSetEvent4, IRibbonSetC
public override string HandlingTrainerName
{
get => StringConverter6.GetString(HandlingTrainerTrash);
set => StringConverter6.SetString(HandlingTrainerTrash, value, 12, StringConverterOption.None);
set => StringConverter6.SetString(HandlingTrainerTrash, value, 12, Language, StringConverterOption.None);
}
public override byte HandlingTrainerGender { get => Data[0x92]; set => Data[0x92] = value; }
public override byte CurrentHandler { get => Data[0x93]; set => Data[0x93] = value; }
@ -340,7 +340,7 @@ public sealed class PK6 : G6PKM, IRibbonSetEvent3, IRibbonSetEvent4, IRibbonSetC
public override string OriginalTrainerName
{
get => StringConverter6.GetString(OriginalTrainerTrash);
set => StringConverter6.SetString(OriginalTrainerTrash, value, 12, StringConverterOption.None);
set => StringConverter6.SetString(OriginalTrainerTrash, value, 12, Language, StringConverterOption.None);
}
public override byte OriginalTrainerFriendship { get => Data[0xCA]; set => Data[0xCA] = value; }
public byte OriginalTrainerAffection { get => Data[0xCB]; set => Data[0xCB] = value; }
@ -529,5 +529,5 @@ public sealed class PK6 : G6PKM, IRibbonSetEvent3, IRibbonSetEvent4, IRibbonSetC
public override int LoadString(ReadOnlySpan<byte> data, Span<char> destBuffer)
=> StringConverter6.LoadString(data, destBuffer);
public override int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength, StringConverterOption option)
=> StringConverter6.SetString(destBuffer, value, maxLength, option);
=> StringConverter6.SetString(destBuffer, value, maxLength, Language, option);
}

View file

@ -186,7 +186,12 @@ public sealed class RK4 : G4PKM
public override string Nickname
{
get => StringConverter4.GetString(NicknameTrash);
set => StringConverter4.SetString(NicknameTrash, value, 10, StringConverterOption.None);
set
{
var language = Language;
CheckKoreanNidoranDPPt(value, ref language);
StringConverter4.SetString(NicknameTrash, value, 10, language, StringConverterOption.None);
}
}
// 0x5E unused
@ -234,7 +239,7 @@ public sealed class RK4 : G4PKM
public override string OriginalTrainerName
{
get => StringConverter4.GetString(OriginalTrainerTrash);
set => StringConverter4.SetString(OriginalTrainerTrash, value, 7, StringConverterOption.None);
set => StringConverter4.SetString(OriginalTrainerTrash, value, 7, Language, StringConverterOption.None);
}
public override byte EggYear { get => Data[0x78]; set => Data[0x78] = value; }
@ -314,7 +319,7 @@ public sealed class RK4 : G4PKM
public override string HandlingTrainerName
{
get => StringConverter4.GetString(HandlingTrainerTrash);
set => StringConverter4.SetString(HandlingTrainerTrash, value, 7, StringConverterOption.None);
set => StringConverter4.SetString(HandlingTrainerTrash, value, 7, Language, StringConverterOption.None);
}
#endregion
@ -351,5 +356,5 @@ public sealed class RK4 : G4PKM
public override int LoadString(ReadOnlySpan<byte> data, Span<char> destBuffer)
=> StringConverter4.LoadString(data, destBuffer);
public override int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength, StringConverterOption option)
=> StringConverter4.SetString(destBuffer, value, maxLength, option);
=> StringConverter4.SetString(destBuffer, value, maxLength, Language, option);
}

View file

@ -1,6 +1,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
@ -497,6 +498,27 @@ public abstract class G4PKM : PKM, IHandlerUpdate,
// Strings need to change between Little Endian and Big Endian
var s = MemoryMarshal.Cast<byte, ushort>(src);
var d = MemoryMarshal.Cast<byte, ushort>(dest);
System.Buffers.Binary.BinaryPrimitives.ReverseEndianness(s, d);
ReverseEndianness(s, d);
}
/// <summary>
/// Nidoran originating from Korean D/P/Pt games use the Full-width gender symbols instead of the other Gen4-Gen7 games which use Half-width.
/// </summary>
/// <seealso cref="PK5.CheckKoreanNidoranDPPt"/>
protected void CheckKoreanNidoranDPPt(ReadOnlySpan<char> value, ref int language)
{
if (language != (int)LanguageID.Korean)
return;
if (IsNicknamed)
return;
if (Version is not (GameVersion.D or GameVersion.P or GameVersion.Pt))
return;
// Full-width gender symbols for not-nicknamed Nidoran in D/P/Pt
// Full/Half is technically legal either way as trainers can reset the nickname in HG/SS, or vice versa for origins.
// Still try to set whichever it originated with. Default would be half, but if it's a Nidoran species name, set full-width.
if (Species == (int)Core.Species.NidoranM && value is "니드런♂")
language = 1; // Use Japanese to force full-width encoding of the gender symbol.
else if (Species == (int)Core.Species.NidoranF && value is "니드런♀")
language = 1;
}
}

View file

@ -76,30 +76,46 @@ public static class StringConverter
byte generation, bool jp, bool isBigEndian, int language = 0) => generation switch
{
3 when isBigEndian => StringConverter3GC.SetString(destBuffer, value, maxLength, option),
4 when isBigEndian => StringConverter4GC.SetString(destBuffer, value, maxLength, option),
4 when isBigEndian => StringConverter4GC.SetString(destBuffer, value, maxLength, language, 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),
6 => StringConverter6.SetString(destBuffer, value, maxLength, option),
4 => StringConverter4.SetString(destBuffer, value, maxLength, language, option),
5 => StringConverter5.SetString(destBuffer, value, maxLength, language, option),
6 => StringConverter6.SetString(destBuffer, value, maxLength, language, option),
7 => StringConverter7.SetString(destBuffer, value, maxLength, language, option),
8 => StringConverter8.SetString(destBuffer, value, maxLength, option),
9 => StringConverter8.SetString(destBuffer, value, maxLength, option),
_ => throw new ArgumentOutOfRangeException(nameof(generation)),
};
/// <summary>
/// Full-width gender 16-bit char representation.
/// </summary>
public const char FGF = '\u2640'; // '♀'
/// <inheritdoc cref="FGM"/>
public const char FGM = '\u2642'; // '♂'
/// <summary>
/// Half-width gender 16-bit char representation.
/// </summary>
/// <remarks>
/// Exact value is the value when converted to Generation 6 &amp; 7 encoding.
/// Once transferred to the Nintendo Switch era, the value is converted to full-width.
/// </remarks>
public const char HGM = '\uE08E'; // '♂'
/// <inheritdoc cref="HGM"/>
public const char HGF = '\uE08F'; // '♀'
/// <summary>
/// Converts full width to single width
/// </summary>
/// <param name="chr">Input character to sanitize.</param>
internal static char SanitizeChar(char chr) => chr switch
public static char NormalizeGenderSymbol(char chr) => chr switch
{
'\uE08F' => '♀',
'\uE08E' => '♂',
'\u246E' => '♀',
'\u246D' => '♂',
HGM => FGM, // '♂'
HGF => FGF, // '♀'
_ => chr,
};
@ -108,27 +124,10 @@ public static class StringConverter
/// </summary>
/// <param name="chr">Input character to set back to data</param>
/// <param name="fullWidth">Checks if the overall string is full-width</param>
internal static char UnSanitizeChar(char chr, bool fullWidth = false)
public static char UnNormalizeGenderSymbol(char chr, bool fullWidth = false) => fullWidth ? chr : chr switch
{
if (fullWidth) // jp/ko/zh strings
return chr; // keep as full width
return chr switch
{
'\u2640' => '\uE08F',
'\u2642' => '\uE08E',
_ => chr,
};
}
/// <summary>
/// Converts full width to half width when appropriate, for Gen5 and prior.
/// </summary>
/// <param name="chr">Input character to set back to data</param>
internal static char UnSanitizeChar5(char chr) => chr switch
{
'\u2640' => '\u246E',
'\u2642' => '\u246D',
FGM => HGM, // '♂'
FGF => HGF, // '♀'
_ => chr,
};
@ -143,7 +142,7 @@ public static class StringConverter
{
if (c >> 12 is (0 or 0xE))
continue;
if (c is '\u2640' or '\u2642') // ♀♂
if (c is FGF or FGM) // ♀♂
continue;
return true;
}

View file

@ -14,6 +14,11 @@ public static class StringConverter3
private const byte QuoteLeftByte = 0xB1;
private const byte QuoteRightByte = 0xB2;
private const char FGM = '♂';
private const char FGF = '♀';
private const char HGM = StringConverter4Util.HGM; // '♂'
private const char HGF = StringConverter4Util.HGF; // '♀'
/// <summary>
/// Converts a Generation 3 encoded value array to string.
/// </summary>
@ -53,7 +58,7 @@ public static class StringConverter3
}; // Convert to Unicode
if (c == Terminator) // Stop if Terminator/Invalid
break;
c = StringConverter.SanitizeChar(c);
c = StringConverter4Util.NormalizeGenderSymbol(c);
result[i] = c;
}
return i;
@ -84,12 +89,15 @@ public static class StringConverter3
else if (option is StringConverterOption.ClearZero)
buffer.Clear();
var table = (language == (int)LanguageID.Japanese) ? G3_JP : G3_EN;
bool jp = language == (int)LanguageID.Japanese;
var table = jp ? G3_JP : G3_EN;
int i = 0;
for (; i < value.Length; i++)
{
var c = StringConverter.UnSanitizeChar5(value[i]);
if (!TryGetIndex(table, c, language, out var b))
var chr = value[i];
if (!jp)
chr = StringConverter4Util.UnNormalizeGenderSymbol(chr);
if (!TryGetIndex(table, chr, language, out var b))
break;
buffer[i] = b;
}
@ -193,7 +201,7 @@ public static class StringConverter3
'ィ', 'ゥ', 'ェ', 'ォ', 'ャ', 'ュ', 'ョ', 'ガ', 'ギ', 'グ', 'ゲ', 'ゴ', 'ザ', 'ジ', 'ズ', 'ゼ', // 8
'ゾ', 'ダ', 'ヂ', 'ヅ', 'デ', 'ド', 'バ', 'ビ', 'ブ', 'ベ', 'ボ', 'パ', 'ピ', 'プ', 'ペ', 'ポ', // 9
'ッ', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '!', '?', '.', '-', '・',// A
'⑬', '“', '”', '', '', '⑭', '⑮', '$', ',', '⑧', '/', 'A', 'B', 'C', 'D', 'E', // B
'⑬', '“', '”', '', '', HGM, HGF, '$', ',', '⑧', '/', 'A', 'B', 'C', 'D', 'E', // B
'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', // C
'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', // D
'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '►', // E
@ -216,7 +224,7 @@ public static class StringConverter3
'ィ', 'ゥ', 'ェ', 'ォ', 'ャ', 'ュ', 'ョ', 'ガ', 'ギ', 'グ', 'ゲ', 'ゴ', 'ザ', 'ジ', 'ズ', 'ゼ', // 8
'ゾ', 'ダ', 'ヂ', 'ヅ', 'デ', 'ド', 'バ', 'ビ', 'ブ', 'ベ', 'ボ', 'パ', 'ピ', 'プ', 'ペ', 'ポ', // 9
'ッ', '', '', '', '', '', '', '', '', '', '', '', '', '。', 'ー', '・', // A
'…', '『', '』', '「', '」', '♂', '♀', '円', '', '×', '', '', '', '', '', '', // B
'…', '『', '』', '「', '」', FGM, FGF, '円', '', '×', '', '', '', '', '', '', // B
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', // C
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', // D
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '►', // E

View file

@ -31,7 +31,7 @@ public static class StringConverter345
{
Span<char> result = stackalloc char[data.Length];
int count = TransferGlyphs34(data, result, language, maxLength);
StringConverter4.SetString(dest, result[..count], maxLength, StringConverterOption.None);
StringConverter4.SetString(dest, result[..count], maxLength, language, StringConverterOption.None);
}
private static int TransferGlyphs34(ReadOnlySpan<byte> data, Span<char> result, int language, int maxLength)

View file

@ -28,25 +28,27 @@ public static class StringConverter4
public static int LoadString(ReadOnlySpan<byte> data, Span<char> result)
{
int i = 0;
int ctr = 0;
for (; i < data.Length; i += 2)
{
var value = ReadUInt16LittleEndian(data[i..]);
if (value == Terminator)
break;
char chr = (char)ConvertValue2CharG4(value);
chr = StringConverter.SanitizeChar(chr);
result[i/2] = chr;
chr = NormalizeGenderSymbol(chr);
result[ctr++] = chr;
}
return i/2;
return ctr;
}
/// <summary>Gets the bytes for a 4th Generation String</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">Language specific conversion</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,
public static int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength, int language,
StringConverterOption option = StringConverterOption.ClearZero)
{
if (value.Length > maxLength)
@ -55,10 +57,12 @@ public static class StringConverter4
if (option is StringConverterOption.ClearZero)
destBuffer.Clear();
bool isHalfWidth = language == (int)LanguageID.Korean || !StringConverter.GetIsFullWidthString(value);
for (int i = 0; i < value.Length; i++)
{
var chr = value[i];
chr = StringConverter.UnSanitizeChar5(chr);
if (isHalfWidth)
chr = UnNormalizeGenderSymbol(chr);
ushort val = ConvertChar2ValueG4(chr);
WriteUInt16LittleEndian(destBuffer[(i * 2)..], val);
}

View file

@ -31,16 +31,17 @@ public static class StringConverter4GC
public static int LoadString(ReadOnlySpan<byte> data, Span<char> result)
{
int i = 0;
int ctr = 0;
for (; i < data.Length; i += 2)
{
var value = ReadUInt16BigEndian(data[i..]);
if (value == Terminator)
break;
char chr = (char)ConvertValue2CharG4(value);
chr = StringConverter.SanitizeChar(chr);
result[i/2] = chr;
chr = NormalizeGenderSymbol(chr);
result[ctr++] = chr;
}
return i/2;
return ctr;
}
/// <summary>
@ -49,9 +50,10 @@ public static class StringConverter4GC
/// <param name="destBuffer">Span of bytes to write encoded string data</param>
/// <param name="value">String to be converted.</param>
/// <param name="maxLength">Maximum length of string</param>
/// <param name="language">Language specific conversion</param>
/// <param name="option">Buffer pre-formatting option</param>
/// <returns>Byte array containing encoded character data</returns>
public static int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength,
public static int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength, int language,
StringConverterOption option = StringConverterOption.ClearZero)
{
if (value.Length > maxLength)
@ -60,10 +62,12 @@ public static class StringConverter4GC
if (option is StringConverterOption.ClearZero)
destBuffer.Clear();
bool isHalfWidth = language == (int)LanguageID.Korean || !StringConverter.GetIsFullWidthString(value);
for (int i = 0; i < value.Length; i++)
{
var chr = value[i];
chr = StringConverter.UnSanitizeChar5(chr);
if (isHalfWidth)
chr = UnNormalizeGenderSymbol(chr);
ushort val = ConvertChar2ValueG4(chr);
WriteUInt16BigEndian(destBuffer[(i * 2)..], val);
}
@ -95,15 +99,15 @@ public static class StringConverter4GC
public static int LoadStringUnicode(ReadOnlySpan<byte> data, Span<char> result)
{
int i = 0;
int ctr = 0;
for (; i < data.Length; i += 2)
{
char chr = (char)ReadUInt16BigEndian(data[i..]);
if (chr == TerminatorChar)
break;
chr = StringConverter.SanitizeChar(chr);
result[i/2] = chr;
result[ctr++] = chr;
}
return i/2;
return ctr;
}
/// <summary>
@ -126,7 +130,6 @@ public static class StringConverter4GC
for (int i = 0; i < value.Length; i++)
{
var c = value[i];
c = StringConverter.UnSanitizeChar5(c);
WriteUInt16BigEndian(destBuffer[(i * 2)..], c);
}

View file

@ -72,6 +72,37 @@ public static class StringConverter4Util
private const char NUL = (char)StringConverter4.Terminator;
private const char EMP = NUL; // Empty, not available on keyboard.
/// <summary>
/// Half-width gender 16-bit char representation.
/// </summary>
/// <remarks>Exact value is the value when converted to Generation 5's encoding.</remarks>
public const char HGM = '\u246D'; // '♂'
/// <inheritdoc cref="HGM"/>
public const char HGF = '\u246E'; // '♀'
/// <summary>
/// Converts full width to single width from the 0x246D/0x246E characters.
/// </summary>
/// <param name="chr">Input character to sanitize.</param>
public static char NormalizeGenderSymbol(char chr) => chr switch
{
HGM => '♂',
HGF => '♀',
_ => chr,
};
/// <summary>
/// Converts full width to half width when appropriate
/// </summary>
/// <param name="chr">Input character to set back to data</param>
/// <param name="fullWidth">Checks if the overall string is full-width</param>
public static char UnNormalizeGenderSymbol(char chr, bool fullWidth = false) => fullWidth ? chr : chr switch
{
'♂' => HGM,
'♀' => HGF,
_ => chr,
};
public static ReadOnlySpan<char> TableINT =>
[
NUL, ' ', 'ぁ', 'あ', 'ぃ', 'い', 'ぅ', 'う', 'ぇ', 'え', 'ぉ', 'お', 'か', 'が', 'き', 'ぎ', // 000-00F
@ -101,7 +132,7 @@ public static class StringConverter4Util
'á', 'â', 'ã', 'ä', 'å', 'æ', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï', 'ð', // 180-18F
'ñ', 'ò', 'ó', 'ô', 'õ', 'ö', '⑨', 'ø', 'ù', 'ú', 'û', 'ü', 'ý', 'þ', 'ÿ', 'Œ', // 190-19F
'œ', 'Ş', 'ş', 'ª', 'º', '⑩', '⑪', '⑫', '$', '¡', '¿', '!', '?', ',', '.', '⑬', // 1A0-1AF
'・', '/', '', '', '“', '”', '„', '«', '»', '(', ')', '⑭', '⑮', '+', '-', '*', // 1B0-1BF
'・', '/', '', '', '“', '”', '„', '«', '»', '(', ')', HGM, HGF, '+', '-', '*', // 1B0-1BF
'#', '=', '&', '~', ':', ';', '⑯', '⑰', '⑱', '⑲', '⑳', '⑴', '⑵', '⑶', '⑷', '⑸', // 1C0-1CF
'@', '⑹', '%', '⑺', '⑻', '⑼', '⑽', '⑾', '⑿', '⒀', '⒁', '⒂', '⒃', '⒄', ' ', '⒅', // 1D0-1DF
'⒆', '⒇', '⒈', '⒉', '⒊', '⒋', '⒌', '⒍', '°', '_', '_', '⒎', '⒏', // 1E0-1EC*

View file

@ -27,23 +27,25 @@ public static class StringConverter5
public static int LoadString(ReadOnlySpan<byte> data, Span<char> result)
{
int i = 0;
int ctr = 0;
for (; i < data.Length; i += 2)
{
var value = ReadUInt16LittleEndian(data[i..]);
if (value == TerminatorFFFF)
break;
result[i/2] = StringConverter.SanitizeChar((char)value);
result[ctr++] = (char)value;
}
return i/2;
return ctr;
}
/// <summary>Gets the bytes for a Generation 5 string.</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">Language specific conversion</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,
public static int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength, int language,
StringConverterOption option = StringConverterOption.ClearZero)
{
if (value.Length > maxLength)
@ -54,11 +56,13 @@ public static class StringConverter5
else if (option is StringConverterOption.ClearFF)
destBuffer.Fill(0xFF);
bool isHalfWidth = language == (int)LanguageID.Korean || !StringConverter.GetIsFullWidthString(value);
for (int i = 0; i < value.Length; i++)
{
char c = value[i];
ushort val = StringConverter.UnSanitizeChar5(c);
WriteUInt16LittleEndian(destBuffer[(i * 2)..], val);
var chr = value[i];
if (isHalfWidth)
chr = StringConverter4Util.UnNormalizeGenderSymbol(chr);
WriteUInt16LittleEndian(destBuffer[(i * 2)..], chr);
}
int count = value.Length * 2;

View file

@ -28,23 +28,26 @@ public static class StringConverter6
public static int LoadString(ReadOnlySpan<byte> data, Span<char> result)
{
int i = 0;
int ctr = 0;
for (; i < data.Length; i += 2)
{
var value = ReadUInt16LittleEndian(data[i..]);
if (value == TerminatorNull)
break;
result[i/2] = StringConverter.SanitizeChar((char)value);
result[ctr++] = StringConverter.NormalizeGenderSymbol((char)value);
}
return i/2;
return ctr;
}
/// <summary>Gets the bytes for a Generation 6 string.</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">Language specific conversion</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.ClearZero)
{
if (value.Length > maxLength)
@ -53,13 +56,13 @@ public static class StringConverter6
if (option is StringConverterOption.ClearZero)
destBuffer.Clear();
bool isFullWidth = StringConverter.GetIsFullWidthString(value);
bool isHalfWidth = language == (int)LanguageID.Korean || !StringConverter.GetIsFullWidthString(value);
for (int i = 0; i < value.Length; i++)
{
char c = value[i];
if (!isFullWidth)
c = StringConverter.UnSanitizeChar(c);
WriteUInt16LittleEndian(destBuffer[(i * 2)..], c);
var chr = value[i];
if (isHalfWidth)
chr = StringConverter.UnNormalizeGenderSymbol(chr);
WriteUInt16LittleEndian(destBuffer[(i * 2)..], chr);
}
int count = value.Length * 2;

View file

@ -28,24 +28,25 @@ public static class StringConverter7
public static int LoadString(ReadOnlySpan<byte> data, Span<char> result)
{
int i = 0;
int ctr = 0;
for (; i < data.Length; i += 2)
{
var value = ReadUInt16LittleEndian(data[i..]);
var value = (char)ReadUInt16LittleEndian(data[i..]);
if (value == TerminatorNull)
break;
result[i/2] = StringConverter.SanitizeChar((char)value);
}
var span = result[..(i/2)];
StringConverter7ZH.RemapChineseGlyphsBin2String(span);
return i/2;
result[ctr++] = StringConverter7ZH.IsPrivateChar(value)
? StringConverter7ZH.GetUnicodeChar(value)
: StringConverter.NormalizeGenderSymbol(value);
}
return ctr;
}
/// <summary>Gets the bytes for a Generation 7 string.</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">Language specific conversion (Chinese)</param>
/// <param name="language">Language specific conversion</param>
/// <param name="option">Buffer pre-formatting option</param>
/// <param name="chinese">Chinese string remapping should be attempted (only Pokémon names, without Nickname flag set)</param>
/// <returns>Encoded data.</returns>
@ -58,15 +59,15 @@ public static class StringConverter7
if (option is StringConverterOption.ClearZero)
destBuffer.Clear();
bool isFullWidth = StringConverter.GetIsFullWidthString(value);
bool isHalfWidth = language == (int)LanguageID.Korean || !StringConverter.GetIsFullWidthString(value);
for (int i = 0; i < value.Length; i++)
{
char c = value[i];
if (!isFullWidth)
c = StringConverter.UnSanitizeChar(c);
var chr = value[i];
if (isHalfWidth)
chr = StringConverter.UnNormalizeGenderSymbol(chr);
if (chinese)
c = StringConverter7ZH.GetPrivateChar(c, language == (int)LanguageID.ChineseT);
WriteUInt16LittleEndian(destBuffer[(i * 2)..], c);
chr = StringConverter7ZH.GetPrivateChar(chr, language == (int)LanguageID.ChineseT);
WriteUInt16LittleEndian(destBuffer[(i * 2)..], chr);
}
int count = value.Length * 2;

View file

@ -12,8 +12,8 @@ namespace PKHeX.Core;
/// </remarks>
public static class StringConverter7ZH
{
private static bool IsPrivateChar(ushort glyph) => glyph is >= Start and <= End;
private static char GetUnicodeChar(char glyph) => Table[glyph - Start];
internal static bool IsPrivateChar(ushort glyph) => glyph is >= Start and <= End;
internal static char GetUnicodeChar(char glyph) => Table[glyph - Start];
public static bool IsTraditional(ushort glyph) => glyph is (>= StartTraditional and <= EndTraditional) or (>= StartTraditionalUSUM and <= EndTraditionalUSUM);
public static bool IsSimplified(ushort glyph) => glyph is (>= StartSimplified and <= EndSimplified) or (>= StartSimplifiedUSUM and <= EndSimplifiedUSUM);
@ -50,21 +50,6 @@ public static class StringConverter7ZH
return chr;
}
/// <summary>
/// Converts a Generation 7 in-game Chinese string to Unicode string.
/// </summary>
/// <param name="input">In-game Chinese string.</param>
/// <returns>Unicode string.</returns>
internal static void RemapChineseGlyphsBin2String(Span<char> input)
{
for (int i = 0; i < input.Length; i++)
{
var val = input[i];
if (IsPrivateChar(val))
input[i] = GetUnicodeChar(val);
}
}
#region Gen 7 Chinese Character Table
private const ushort Start = 0xE800;

View file

@ -137,4 +137,33 @@ public static class StringConverter8
WriteCharacters(expect, under);
return relevantSection.SequenceEqual(expect);
}
/// <summary>
/// Used when importing a 3DS string into HOME.
/// </summary>
public static void NormalizeHalfWidth(Span<byte> str)
{
if (BitConverter.IsLittleEndian)
{
var u16 = MemoryMarshal.Cast<byte, char>(str);
foreach (ref var c in u16)
{
if (c == TerminatorNull)
return;
c = NormalizeHalfWidth(c);
}
}
// Slower path for Big-Endian runtimes.
for (int i = 0; i < str.Length; i += 2)
{
var data = str[i..];
var c = ReadUInt16LittleEndian(data);
if (c == TerminatorNull)
return;
WriteUInt16LittleEndian(data, NormalizeHalfWidth((char)c));
}
}
private static char NormalizeHalfWidth(char str) => StringConverter.NormalizeGenderSymbol(str);
}

View file

@ -483,7 +483,7 @@ public abstract class SAV4 : SaveFile, IEventFlag37, IDaycareStorage, IDaycareRa
public sealed override int LoadString(ReadOnlySpan<byte> data, Span<char> destBuffer)
=> StringConverter4.LoadString(data, destBuffer);
public sealed override int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength, StringConverterOption option)
=> StringConverter4.SetString(destBuffer, value, maxLength, option);
=> StringConverter4.SetString(destBuffer, value, maxLength, Language, option);
#region Event Flag/Event Work
public bool GetEventFlag(int flagNumber)

View file

@ -122,7 +122,7 @@ public abstract class SAV5 : SaveFile, ISaveBlock5BW, IEventFlagProvider37, IBox
public sealed override int LoadString(ReadOnlySpan<byte> data, Span<char> result)
=> StringConverter5.LoadString(data, result);
public sealed override int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength, StringConverterOption option)
=> StringConverter5.SetString(destBuffer, value, maxLength, option);
=> StringConverter5.SetString(destBuffer, value, maxLength, Language, option);
// DLC
private int CGearSkinInfoOffset => CGearInfoOffset + (this is SAV5B2W2 ? 0x10 : 0) + 0x24;

View file

@ -143,7 +143,7 @@ public abstract class SAV6 : SAV_BEEF, ITrainerStatRecord, ISaveBlock6Core, IReg
public sealed override int LoadString(ReadOnlySpan<byte> data, Span<char> destBuffer)
=> StringConverter6.LoadString(data, destBuffer);
public sealed override int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength, StringConverterOption option)
=> StringConverter6.SetString(destBuffer, value, maxLength, option);
=> StringConverter6.SetString(destBuffer, value, maxLength, Language, option);
public int GetRecord(int recordID) => Records.GetRecord(recordID);
public int GetRecordOffset(int recordID) => Records.GetRecordOffset(recordID);

View file

@ -30,13 +30,13 @@ public sealed class BattleVideo6(byte[] Data) : IBattleVideo
public string Debug1
{
get => StringConverter6.GetString(Data.AsSpan(0x6, 0x1A));
set => StringConverter6.SetString(Data.AsSpan(0x6, 0x1A), value, 12, StringConverterOption.ClearZero);
set => StringConverter6.SetString(Data.AsSpan(0x6, 0x1A), value, 12, 0, StringConverterOption.ClearZero);
}
public string Debug2
{
get => StringConverter6.GetString(Data.AsSpan(0x50, 0x1A));
set => StringConverter6.SetString(Data.AsSpan(0x50, 0x1A), value, 12, StringConverterOption.ClearZero);
set => StringConverter6.SetString(Data.AsSpan(0x50, 0x1A), value, 12, 0, StringConverterOption.ClearZero);
}
public ulong RNGConst1 { get => ReadUInt64LittleEndian(Data.AsSpan(0x1A0)); set => WriteUInt64LittleEndian(Data.AsSpan(0x1A0), value); }
@ -70,7 +70,7 @@ public sealed class BattleVideo6(byte[] Data) : IBattleVideo
{
var span = Data.AsSpan(0xEC + (0x1A * i), 0x1A);
string tr = value[i] == NPC ? string.Empty : value[i];
StringConverter6.SetString(span, tr, 12, StringConverterOption.ClearZero);
StringConverter6.SetString(span, tr, 12, 0, StringConverterOption.ClearZero);
}
}

View file

@ -3,7 +3,7 @@ using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
public sealed class Dendou4(Memory<byte> raw)
public sealed class Dendou4(Memory<byte> raw, int language)
{
private const int SIZE = 0x2AB0;
private const int SIZE_FOOTER = 0x10;
@ -25,7 +25,7 @@ public sealed class Dendou4(Memory<byte> raw)
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual<uint>((uint)index, MaxRecords);
var slice = Data.Slice(index * Dendou4Record.SIZE, Dendou4Record.SIZE);
return new Dendou4Record(slice);
return new Dendou4Record(slice, language);
}
private const int EndDataOffset = MaxRecords * Dendou4Record.SIZE; // 0x2AA8
@ -63,9 +63,14 @@ public readonly ref struct Dendou4Record
// u8 Day
private readonly Span<byte> Data;
private readonly int Language;
// ReSharper disable once ConvertToPrimaryConstructor
public Dendou4Record(Span<byte> data) => Data = data;
public Dendou4Record(Span<byte> data, int language)
{
Data = data;
Language = language;
}
public Dendou4Entity this[int index] => GetEntity(index);
@ -78,7 +83,7 @@ public readonly ref struct Dendou4Record
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual<uint>((uint)index, Count);
var slice = Data.Slice(index * Dendou4Entity.SIZE, Dendou4Entity.SIZE);
return new Dendou4Entity(slice);
return new Dendou4Entity(slice, Language);
}
}
@ -86,9 +91,15 @@ public readonly ref struct Dendou4Entity
{
public const int SIZE = 0x3C;
private readonly Span<byte> Data;
private readonly int Language;
// ReSharper disable once ConvertToPrimaryConstructor
public Dendou4Entity(Span<byte> data) => Data = data;
public Dendou4Entity(Span<byte> data, int language)
{
Data = data;
Language = language;
}
public ushort Species { get => ReadUInt16LittleEndian(Data); set => WriteUInt16LittleEndian(Data, value); }
public byte Level { get => Data[2]; set => Data[2] = value; }
public byte Form { get => Data[3]; set => Data[3] = value; }
@ -100,12 +111,12 @@ public readonly ref struct Dendou4Entity
public string Nickname
{
get => StringConverter4.GetString(NicknameTrash);
set => StringConverter4.SetString(NicknameTrash, value, 10, StringConverterOption.None);
set => StringConverter4.SetString(NicknameTrash, value, 10, Language, StringConverterOption.None);
}
public string OriginalTrainerName
{
get => StringConverter4.GetString(OriginalTrainerTrash);
set => StringConverter4.SetString(OriginalTrainerTrash, value, 7, StringConverterOption.None);
set => StringConverter4.SetString(OriginalTrainerTrash, value, 7, 0, StringConverterOption.None);
}
public ushort Move1 { get => ReadUInt16LittleEndian(Data[0x32..]); set => WriteUInt16LittleEndian(Data[0x32..], value); }

View file

@ -23,9 +23,11 @@ public sealed class RanchTrainerMii(byte[] Data)
// 0x28-29: ??
// 0x2A-2B: ??
private static byte Language => 0;
public string TrainerName
{
get => StringConverter4.GetString(Trainer_Trash);
set => StringConverter4.SetString(Trainer_Trash, value, 7);
set => StringConverter4.SetString(Trainer_Trash, value, 7, Language);
}
}

View file

@ -7,8 +7,14 @@ public readonly ref struct HallFame6Entity
{
public const int SIZE = 0x48;
private readonly Span<byte> Data;
private readonly int Language;
// ReSharper disable once ConvertToPrimaryConstructor
public HallFame6Entity(Span<byte> data) => Data = data;
public HallFame6Entity(Span<byte> data, int language)
{
Data = data;
Language = language;
}
public ushort Species { get => ReadUInt16LittleEndian(Data); set => WriteUInt16LittleEndian(Data, value); }
public ushort HeldItem { get => ReadUInt16LittleEndian(Data[0x02..]); set => WriteUInt16LittleEndian(Data[0x02..], value); }
@ -48,13 +54,13 @@ public readonly ref struct HallFame6Entity
public string Nickname
{
get => StringConverter6.GetString(Nick_Trash);
set => StringConverter6.SetString(Nick_Trash, value, 12, Option);
set => StringConverter6.SetString(Nick_Trash, value, 12, Language, Option);
}
public string OriginalTrainerName
{
get => StringConverter6.GetString(OriginalTrainerTrash);
set => StringConverter6.SetString(OriginalTrainerTrash, value, 12, Option);
set => StringConverter6.SetString(OriginalTrainerTrash, value, 12, Language, Option);
}
}

View file

@ -80,15 +80,17 @@ public class SecretBase6(Memory<byte> raw)
private const int NameLength = (0x1A / 2) - 1; // + terminator
private const int MessageLength = (0x22 / 2) - 1; // + terminator
private static int Language => 0;
public string TrainerName
{
get => StringConverter6.GetString(Data.Slice(0x21A, NameLengthBytes));
set => StringConverter6.SetString(Data.Slice(0x21A, NameLengthBytes), value, NameLength, StringConverterOption.ClearZero);
set => StringConverter6.SetString(Data.Slice(0x21A, NameLengthBytes), value, NameLength, Language, StringConverterOption.ClearZero);
}
private Span<byte> GetMessageSpan(int index) => Data.Slice(0x234 + (MessageLengthBytes * index), MessageLengthBytes);
private string GetMessage(int index) => StringConverter6.GetString(GetMessageSpan(index));
private void SetMessage(int index, ReadOnlySpan<char> value) => StringConverter6.SetString(GetMessageSpan(index), value, MessageLength, StringConverterOption.ClearZero);
private void SetMessage(int index, ReadOnlySpan<char> value) => StringConverter6.SetString(GetMessageSpan(index), value, MessageLength, Language, StringConverterOption.ClearZero);
public string TeamName { get => GetMessage(0); set => SetMessage(0, value); }
public string TeamSlogan { get => GetMessage(1); set => SetMessage(1, value); }

View file

@ -35,8 +35,8 @@ public static class UgItemUtil
}
#region Table
private static readonly IReadOnlyList<UgItemDef> Items = new UgItemDef[]
{
private static readonly IReadOnlyList<UgItemDef> Items =
[
new(000,000,000,000,000), // None
new(001, -1,001, -1, -1), // Red Sphere S
new(002, -1,002, -1, -1), // Blue Sphere S
@ -994,6 +994,6 @@ public static class UgItemUtil
new(954, -1, -1, -1, -1), //
new(955, -1, -1, -1, -1), //
new(956, -1, -1, -1, -1), //
};
];
#endregion
}

View file

@ -44,7 +44,7 @@ public sealed class Mail4 : MailDetail
public override byte AuthorLanguage { get => Data[5]; set => Data[5] = value; }
public override byte AuthorVersion { get => Data[6]; set => Data[6] = value; }
public override int MailType { get => Data[7]; set => Data[7] = (byte)value; }
public override string AuthorName { get => StringConverter4.GetString(Data.AsSpan(8, 0x10)); set => StringConverter4.SetString(Data.AsSpan(8, 0x10), value, 7, StringConverterOption.ClearFF); }
public override string AuthorName { get => StringConverter4.GetString(Data.AsSpan(8, 0x10)); set => StringConverter4.SetString(Data.AsSpan(8, 0x10), value, 7, AuthorLanguage, StringConverterOption.ClearFF); }
public ushort GetAppearSpecies(int index) => ReadUInt16LittleEndian(Data.AsSpan(0x1C - (index * 2)));
public void SetAppearSpecies(int index, ushort value) => WriteUInt16LittleEndian(Data.AsSpan(0x1C - (index * 2)), (ushort)(value == 0 ? 0xFFFF : value));
public override ushort GetMessage(int index1, int index2) => ReadUInt16LittleEndian(Data.AsSpan(0x20 + (((index1 * 4) + index2) * 2)));

View file

@ -40,7 +40,7 @@ public sealed class Mail5 : MailDetail
public override byte AuthorLanguage { get => Data[5]; set => Data[5] = value; }
public override byte AuthorVersion { get => Data[6]; set => Data[6] = value; }
public override int MailType { get => Data[7]; set => Data[7] = (byte)value; }
public override string AuthorName { get => StringConverter5.GetString(Data.AsSpan(8, 0x10)); set => StringConverter5.SetString(Data.AsSpan(8, 0x10), value, 7, StringConverterOption.ClearZero); }
public override string AuthorName { get => StringConverter5.GetString(Data.AsSpan(8, 0x10)); set => StringConverter5.SetString(Data.AsSpan(8, 0x10), value, 7, AuthorLanguage, StringConverterOption.ClearZero); }
public int GetMisc(int index) => ReadUInt16LittleEndian(Data.AsSpan(0x1C - (index * 2)));
public void SetMisc(int index, int value) => WriteUInt16LittleEndian(Data.AsSpan(0x1C - (index * 2)), (ushort)value);
public ushort MessageEnding { get => ReadUInt16LittleEndian(Data.AsSpan(0x1E)); set => WriteUInt16LittleEndian(Data.AsSpan(0x1E), value); }

View file

@ -120,7 +120,7 @@ public partial class SAV_HallOfFame : Form
for (int i = 0; i < 6; i++)
{
var slice = data[(i * HallFame6Entity.SIZE)..];
var entry = new HallFame6Entity(slice);
var entry = new HallFame6Entity(slice, SAV.Language);
if (entry.Species == 0)
continue;
moncount++;
@ -156,7 +156,7 @@ public partial class SAV_HallOfFame : Form
int index = LB_DataEntry.SelectedIndex;
var member = (Convert.ToInt32(NUP_PartyIndex.Value) - 1);
var slice = Fame.GetEntity(index, member);
var entry = new HallFame6Entity(slice);
var entry = new HallFame6Entity(slice, SAV.Language);
CB_Species.SelectedValue = (int)entry.Species;
CB_HeldItem.SelectedValue = (int)entry.HeldItem;
CB_Move1.SelectedValue = (int)entry.Move1;
@ -196,7 +196,7 @@ public partial class SAV_HallOfFame : Form
int member = Convert.ToInt32(NUP_PartyIndex.Value) - 1;
var slice = Fame.GetEntity(index, member);
var entry = new HallFame6Entity(slice)
var entry = new HallFame6Entity(slice, SAV.Language)
{
Species = Convert.ToUInt16(CB_Species.SelectedValue),
HeldItem = Convert.ToUInt16(CB_HeldItem.SelectedValue),

View file

@ -11,7 +11,7 @@ public class StringTests
{
const string name_fabian = "Fabian♂";
var pk = new PK7 { OriginalTrainerName = name_fabian };
Span<byte> byte_fabian =
ReadOnlySpan<byte> byte_fabian =
[
0x46, 0x00, // F
0x61, 0x00, // a
@ -30,7 +30,7 @@ public class StringTests
{
const string name_nidoran = "ニドラン♀";
var pk = new PK7 { Nickname = name_nidoran };
Span<byte> byte_nidoran =
ReadOnlySpan<byte> byte_nidoran =
[
0xCB, 0x30, // ニ
0xC9, 0x30, // ド

View file

@ -7,6 +7,11 @@ namespace PKHeX.Core.Tests.Simulator;
public class ShowdownSetTests
{
static ShowdownSetTests()
{
ParseSettings.Settings.Handler.CheckActiveHandler = false;
}
[Fact]
public void SimulatorGetParse()
{