diff --git a/PKHeX.Core/PKM/SK2.cs b/PKHeX.Core/PKM/SK2.cs index a5e6e976d..52c8a9ab0 100644 --- a/PKHeX.Core/PKM/SK2.cs +++ b/PKHeX.Core/PKM/SK2.cs @@ -78,7 +78,7 @@ public bool IsRental } } - // 0x1F + public byte HeldMailID { get => Data[0x1F]; set => Data[0x1F] = value; } public byte PokerusState { get => Data[0x20]; set => Data[0x20] = value; } // Crystal only Caught Data diff --git a/PKHeX.Core/PKM/Strings/StringConverter1.cs b/PKHeX.Core/PKM/Strings/StringConverter1.cs index b5ea3466a..834a53801 100644 --- a/PKHeX.Core/PKM/Strings/StringConverter1.cs +++ b/PKHeX.Core/PKM/Strings/StringConverter1.cs @@ -209,7 +209,7 @@ private static bool TryGetUserFriendlyRemap(in ReadOnlySpan dict, char c, internal const char MNY = '¥'; // Yen internal const char LPO = '@'; // Po internal const char LKE = '#'; // Ke - internal const char LEA = '%'; // é for Box + internal const char LEA = '%'; // é for Box/Mail public const char DOT = '․'; // . for MR.MIME (U+2024, not U+002E) internal const char SPF = ' '; // Full-width space (U+3000) public const char SPH = ' '; // Half-width space @@ -229,7 +229,7 @@ private static bool TryGetUserFriendlyRemap(in ReadOnlySpan dict, char c, '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, NUL, NUL, NUL, NUL, '←', '\'', // D0-DF + 'ì', 'í', 'ñ', 'ò', 'ó', 'ú', 'º', NUL, NUL, NUL, NUL, NUL, NUL, NUL, '←', '\'', // D0-DF '’', LPK, LMN, '-', NUL, NUL, '?', '!', DOT, '&', LEA, '→', '▷', '▶', '▼', '♂', // E0-EF MNY, '×', '.', '/', ',', '♀', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', // F0-FF ]; @@ -242,8 +242,8 @@ private static bool TryGetUserFriendlyRemap(in ReadOnlySpan dict, char c, 'だ', 'ぢ', 'づ', 'で', 'ど', 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 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, 'ぃ', 'ぅ', // 60-6F + '「', '」', '『', '』', '・', '⋯', 'ぁ', 'ぇ', 'ぉ', NUL, NUL, NUL, NUL, NUL, NUL, SPF, // 70-7F 'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ', 'コ', 'サ', 'シ', 'ス', 'セ', 'ソ', 'タ', // 80-8F 'チ', 'ツ', 'テ', 'ト', 'ナ', 'ニ', 'ヌ', 'ネ', 'ノ', 'ハ', 'ヒ', 'フ', 'ホ', 'マ', 'ミ', 'ム', // 90-9F 'メ', 'モ', 'ヤ', 'ユ', 'ヨ', 'ラ', 'ル', 'レ', 'ロ', 'ワ', 'ヲ', 'ン', 'ッ', 'ャ', 'ュ', 'ョ', // A0-AF @@ -251,7 +251,7 @@ private static bool TryGetUserFriendlyRemap(in ReadOnlySpan dict, char c, 'た', 'ち', 'つ', 'て', 'と', 'な', 'に', 'ぬ', 'ね', 'の', 'は', 'ひ', 'ふ', 'ヘ', 'ほ', 'ま', // C0-CF 'み', 'む', 'め', 'も', 'や', 'ゆ', 'よ', 'ら', 'リ', 'る', 'れ', 'ろ', 'わ', 'を', 'ん', 'っ', // D0-DF 'ゃ', 'ゅ', 'ょ', 'ー', '゚', '゙', '?', '!', '。', 'ァ', 'ゥ', 'ェ', NUL, NUL, NUL, '♂', // E0-EF - MNY, NUL, '.', '/', 'ォ', '♀', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', // F0-FF + MNY, '×', '.', '/', 'ォ', '♀', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', // F0-FF ]; #endregion diff --git a/PKHeX.Core/PKM/Strings/StringConverter2.cs b/PKHeX.Core/PKM/Strings/StringConverter2.cs index 0b9ca1af4..8cddd18c6 100644 --- a/PKHeX.Core/PKM/Strings/StringConverter2.cs +++ b/PKHeX.Core/PKM/Strings/StringConverter2.cs @@ -8,9 +8,11 @@ public static class StringConverter2 public const byte TerminatorZero = StringConverter1.TerminatorZero; public const byte TradeOTCode = StringConverter1.TradeOTCode; public const byte SpaceCode = StringConverter1.SpaceCode; + public const byte LineBreakCode = 0x4E; // Mail public const char Terminator = StringConverter1.Terminator; public const char TradeOT = StringConverter1.TradeOT; + public const char LineBreak = '⏎'; // Mail public static bool GetIsJapanese(ReadOnlySpan str) => AllJapanese(str); @@ -175,10 +177,11 @@ private static bool TryGetUserFriendlyRemap(in ReadOnlySpan dict, char c, 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 LEA = StringConverter1.LEA; // é for Box/Mail 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 RET = LineBreak; // Line break for Mail private const char LAP = '’'; // Apostrophe @@ -197,11 +200,11 @@ private static bool TryGetUserFriendlyRemap(in ReadOnlySpan dict, char c, 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 + private const char LI7 = '7'; + private const char LI8 = '8'; + private const char LI9 = '9'; + private const char LIA = 'A'; + private const char LIB = 'B'; /// /// English encoding table with unused indexes merged in from other languages that use them. @@ -213,7 +216,7 @@ private static bool TryGetUserFriendlyRemap(in ReadOnlySpan dict, char c, 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, NUL, RET, 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 @@ -233,7 +236,7 @@ private static bool TryGetUserFriendlyRemap(in ReadOnlySpan dict, char c, 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, NUL, RET, 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 @@ -241,7 +244,7 @@ private static bool TryGetUserFriendlyRemap(in ReadOnlySpan dict, char c, '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 + 'Ä', 'Ö', 'Ü', 'ä', 'ö', 'ü', 'ë', 'ï', 'â', 'ô', 'û', 'ê', 'î', 'Ù', 'Ú', 'á', // 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 @@ -253,7 +256,7 @@ private static bool TryGetUserFriendlyRemap(in ReadOnlySpan dict, char c, 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, NUL, RET, 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 @@ -262,7 +265,7 @@ private static bool TryGetUserFriendlyRemap(in ReadOnlySpan dict, char c, '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 + 'ì', 'í', 'ñ', 'ò', 'ó', 'ú', 'º', 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 ]; @@ -281,16 +284,20 @@ private static void InsertLigature(Span result, char c, bool isAfter) } } + // Control codes used in Randy's mail + private const char NIS = '㋥'; // "に " (particle ni) + private const char NOS = '㋨'; // "の " (particle no) + public static ReadOnlySpan 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, 'バ', 'ビ', 'ブ', 'ボ', NIS, NUL, NUL, // 10-1F + NUL, NUL, NUL, NUL, NUL, NOS, 'が', 'ぎ', 'ぐ', 'げ', 'ご', 'ざ', 'じ', 'ず', 'ぜ', 'ぞ', // 20-2F 'だ', 'ぢ', 'づ', 'で', 'ど', NUL, NUL, NUL, NUL, NUL, 'ば', 'び', 'ぶ', 'ベ', 'ぼ', NUL, // 30-3F - 'パ', 'ピ', 'プ', 'ポ', 'ぱ', 'ぴ', 'ぷ', 'ペ', 'ぽ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 40-4F + 'パ', 'ピ', 'プ', 'ポ', 'ぱ', 'ぴ', 'ぷ', 'ペ', 'ぽ', NUL, NUL, NUL, NUL, NUL, RET, 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 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, 'ぃ', 'ぅ', // 60-6F + '「', '」', '『', '』', '・', '⋯', 'ぁ', 'ぇ', 'ぉ', NUL, NUL, NUL, NUL, NUL, NUL, SPF, // 70-7F 'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ', 'コ', 'サ', 'シ', 'ス', 'セ', 'ソ', 'タ', // 80-8F 'チ', 'ツ', 'テ', 'ト', 'ナ', 'ニ', 'ヌ', 'ネ', 'ノ', 'ハ', 'ヒ', 'フ', 'ホ', 'マ', 'ミ', 'ム', // 90-9F 'メ', 'モ', 'ヤ', 'ユ', 'ヨ', 'ラ', 'ル', 'レ', 'ロ', 'ワ', 'ヲ', 'ン', 'ッ', 'ャ', 'ュ', 'ョ', // A0-AF @@ -298,7 +305,7 @@ private static void InsertLigature(Span result, char c, bool isAfter) 'た', 'ち', 'つ', 'て', 'と', 'な', 'に', 'ぬ', 'ね', 'の', 'は', 'ひ', 'ふ', 'ヘ', 'ほ', 'ま', // C0-CF 'み', 'む', 'め', 'も', 'や', 'ゆ', 'よ', 'ら', 'リ', 'る', 'れ', 'ろ', 'わ', 'を', 'ん', 'っ', // D0-DF 'ゃ', 'ゅ', 'ょ', 'ー', '゚', '゙', '?', '!', '。', 'ァ', 'ゥ', 'ェ', NUL, NUL, NUL, '♂', // E0-EF - MNY, NUL, '.', '/', 'ォ', '♀', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', // F0-FF + MNY, '×', '.', '/', 'ォ', '♀', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', // F0-FF ]; #endregion @@ -330,7 +337,7 @@ public static string InflateLigatures(string result, int language) } private static bool TryGetLigatureIndex(char c, out int index) => -1 != (index = LigatureList.IndexOf(c)); - private static ReadOnlySpan LigatureList => [LI0, LI1, LI2, LI3, LI4, LI5, LI6, LI7, LI8, LI8, LI9, LIA, LIB]; + private static ReadOnlySpan LigatureList => [LI0, LI1, LI2, LI3, LI4, LI5, LI6, LI7, LI8, LI9, LIA, LIB]; private static char GetLigature(int ligatureIndex) => LigatureList[ligatureIndex]; public static int DeflateLigatures(ReadOnlySpan value, Span result, int language) @@ -384,4 +391,69 @@ public static int DeflateLigatures(ReadOnlySpan value, Span result, } return index; } + + /// + /// Converts foreign Mail from the language-unaware encoding used for English Gold/Silver back to its original, language-aware encoding. + /// + /// Encoded data. + /// Mail language. + public static void DecodeMailEnglishGS(Span data, int language) + { + if (language is (int)LanguageID.French or (int)LanguageID.German) + DecodeMailFG(data); + else if (language is (int)LanguageID.Italian or (int)LanguageID.Spanish) + RemapMailIS(data); + } + + /// + /// Converts foreign Mail from its original, language-aware encoding to the language-unaware encoding used for English Gold/Silver. + /// + /// Decoded data. + /// Mail language. + public static void EncodeMailEnglishGS(Span data, int language) + { + if (language is (int)LanguageID.French or (int)LanguageID.German) + EncodeMailFG(data); + else if (language is (int)LanguageID.Italian or (int)LanguageID.Spanish) + RemapMailIS(data); + } + + // Remap 's, swap c' d' j' with unused spaces + // - English: 0xCD-CF (unused spaces), 0xD4-D6 ('s 't 'v), 0xDC (unused space) + // - French/German: 0xCD-CF (unused spaces), 0xD4-D6 (c' d' j'), 0xDC ('s) + private static void DecodeMailFG(Span data) + { + for (int i = 0; i < data.Length; i++) + { + var b = data[i]; + if (b == 0xD4) + data[i] = 0xDC; // 's + else if (b is >= 0xCD and <= 0xCF) + data[i] += 0xD4 - 0xCD; // c' d' j' (shift up) + } + } + + private static void EncodeMailFG(Span data) + { + for (int i = 0; i < data.Length; i++) + { + var b = data[i]; + if (b == 0xDC) + data[i] = 0xD4; // 's + else if (b is >= 0xD4 and <= 0xD6) + data[i] -= 0xD4 - 0xCD; // c' d' j' (shift down) + } + } + + // Swap upper/lower halves of 0xD0-DF + // - English: 0xD0-D7 (ligatures), 0xD8-DF (unused spaces) + // - Italian/Spanish: 0xD0-D7 (accented letters), 0xD8-DF (ligatures) + private static void RemapMailIS(Span data) + { + for (int i = 0; i < data.Length; i++) + { + if ((data[i] & 0xF0) == 0xD0) + data[i] ^= 0x08; + } + } } diff --git a/PKHeX.Core/PKM/Strings/StringConverter2KOR.cs b/PKHeX.Core/PKM/Strings/StringConverter2KOR.cs index 72c7b1459..5c3cf1fe2 100644 --- a/PKHeX.Core/PKM/Strings/StringConverter2KOR.cs +++ b/PKHeX.Core/PKM/Strings/StringConverter2KOR.cs @@ -8,6 +8,10 @@ namespace PKHeX.Core; /// public static class StringConverter2KOR { + // Mail + public const byte LineBreakCode = 0x59; + public const char LineBreak = StringConverter2.LineBreak; + /// /// Checks if any of the characters inside are from the special Korean codepoint pages. /// @@ -453,6 +457,7 @@ public static void LocalizeKOR2(ushort species, ref string nick) // In transporter's code, none of these glyphs are legitimately accessible via keyboard. private const char NUL = NULL; + private const char RET = LineBreak; // Mail, NUL in Transporter private static ReadOnlySpan Table0 => [ NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, @@ -460,7 +465,7 @@ public static void LocalizeKOR2(ushort species, ref string nick) NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, - NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, RET, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, ' ', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', diff --git a/PKHeX.Core/Saves/SAV2Stadium.cs b/PKHeX.Core/Saves/SAV2Stadium.cs index f8286d1db..c71ba37d2 100644 --- a/PKHeX.Core/Saves/SAV2Stadium.cs +++ b/PKHeX.Core/Saves/SAV2Stadium.cs @@ -70,6 +70,12 @@ public SAV2Stadium(bool japanese = false) : base(japanese, SaveUtil.SIZE_G2STAD) ClearBoxes(); } + protected sealed override void SetChecksums() + { + base.SetChecksums(); + SetMailChecksums(); + } + protected override bool GetIsBoxChecksumValid(int box) { var boxOfs = GetBoxOffset(box) - ListHeaderSizeBox; @@ -200,6 +206,27 @@ public static bool IsStadium(ReadOnlySpan data) private static bool IsStadiumJ(ReadOnlySpan data) => StadiumUtil.IsMagicPresentAbsolute(data, BoxStart + BoxSizeJ - ListFooterSize, MAGIC_FOOTER) != StadiumSaveType.None; private static bool IsStadiumU(ReadOnlySpan data) => StadiumUtil.IsMagicPresentAbsolute(data, BoxStart + BoxSizeU - ListFooterSize, MAGIC_FOOTER) != StadiumSaveType.None; + #region Mail Box + public static int MailboxBlockOffset(int language) => language == (int)LanguageID.Japanese ? 0x6530 : 0x62D8; + public static int MailboxHeldBlockOffset(int language) => language == (int)LanguageID.Japanese ? 0x6D6C : 0x6C0E; + public int MailboxBlockSize => 2 + (MailboxMailCount * Mail2.GetMailSize(Language)) + ListFooterSize; + public int MailboxHeldBlockSize => 2 + (MailboxHeldMailCount * Mail2.GetMailSize(Language)) + ListFooterSize; + public const int MailboxMailCount = 50; + public const int MailboxHeldMailCount = 30; + private void SetMailChecksums() + { + var ofs = MailboxBlockOffset(Language); + var size = MailboxBlockSize - 2; + var chk = Checksums.CheckSum16(new ReadOnlySpan(Data, ofs, size)); + WriteUInt16BigEndian(Data.AsSpan(ofs + size), chk); + + var ofsHeld = MailboxHeldBlockOffset(Language); + var sizeHeld = MailboxHeldBlockSize - 2; + var chkHeld = Checksums.CheckSum16(new ReadOnlySpan(Data, ofsHeld, sizeHeld)); + WriteUInt16BigEndian(Data.AsSpan(ofsHeld + sizeHeld), chkHeld); + } + #endregion + private static bool GetIsSwap(ReadOnlySpan data, bool japanese) { var teamSwap = StadiumUtil.IsMagicPresentSwap(data, TeamSize, MAGIC_FOOTER, 1); diff --git a/PKHeX.Core/Saves/SAV_STADIUM.cs b/PKHeX.Core/Saves/SAV_STADIUM.cs index c609acead..a93b1bd8e 100644 --- a/PKHeX.Core/Saves/SAV_STADIUM.cs +++ b/PKHeX.Core/Saves/SAV_STADIUM.cs @@ -53,7 +53,7 @@ protected SAV_STADIUM(bool japanese, [ConstantExpected] int size) : base(size) public sealed override string ChecksumInfo => ChecksumsValid ? "Checksum valid." : "Checksum invalid"; protected abstract void SetBoxChecksum(int box); protected abstract bool GetIsBoxChecksumValid(int box); - protected sealed override void SetChecksums() => SetBoxChecksums(); + protected override void SetChecksums() => SetBoxChecksums(); protected abstract void SetBoxMetadata(int box); protected void SetBoxChecksums() diff --git a/PKHeX.Core/Saves/Substructures/Mail/Mail2.cs b/PKHeX.Core/Saves/Substructures/Mail/Mail2.cs index 018e5e241..2810746ab 100644 --- a/PKHeX.Core/Saves/Substructures/Mail/Mail2.cs +++ b/PKHeX.Core/Saves/Substructures/Mail/Mail2.cs @@ -3,120 +3,221 @@ namespace PKHeX.Core; -// warning: international only public sealed class Mail2 : MailDetail { - private readonly int Language; - private bool Japanese => Language == 1; - private bool Korean => Language == (int)LanguageID.Korean; + private readonly bool EnglishGS; + private readonly bool Japanese; + private readonly bool Korean; // structure: - private const int LINE_LENGTH = 0x10; - private const int MESSAGE_LENGTH = LINE_LENGTH + LINE_LENGTH + 1; // each line has a single char at end - private const int OFS_AUTHOR = MESSAGE_LENGTH; - private const int OFS_AUTHOR_NATION = OFS_AUTHOR + AUTHOR_LENGTH + 1; - private const int OFS_AUTHOR_ID = OFS_AUTHOR_NATION + 2; - private const int OFS_APPEAR = OFS_AUTHOR_ID + 2; - private const int OFS_TYPE = OFS_APPEAR + 1; - private const int SIZE = OFS_TYPE + 1; + private int LINE_LENGTH => Korean ? 0x20 : 0x10; + private int MESSAGE_LENGTH => LINE_LENGTH + 1 + LINE_LENGTH; // line break in the middle + private int OFS_AUTHOR => MESSAGE_LENGTH; + private int OFS_AUTHOR_NATION => OFS_AUTHOR + AUTHOR_LENGTH; // not in Japanese/Korean + private int OFS_AUTHOR_ID => OFS_AUTHOR_NATION + ((Japanese || Korean) ? 0 : 2); + private int OFS_APPEAR => OFS_AUTHOR_ID + 2; + private int OFS_TYPE => OFS_APPEAR + 1; + private int SIZE => OFS_TYPE + 1; + + private const int SIZE_J = 0x2A; + private const int SIZE_U = 0x2F; + private const int SIZE_K = 0x4F; private const int COUNT_PARTY = 6; private const int COUNT_MAILBOX = 10; + private const int COUNT_MAILBOX_STADIUM2 = SAV2Stadium.MailboxMailCount; // 50 + private const int COUNT_PARTY_STADIUM2 = SAV2Stadium.MailboxHeldMailCount; // 30 - private const int AUTHOR_LENGTH = 7; + private int AUTHOR_LENGTH => Japanese ? 5 : (Korean ? 10 : 8); - public Mail2(SAV2 sav, int index) : base(sav.Data.AsSpan(GetMailOffset(index), 0x2F).ToArray(), GetMailOffset(index)) + private byte LineBreakCode => Korean ? StringConverter2KOR.LineBreakCode : StringConverter2.LineBreakCode; + private char LineBreak => Korean ? StringConverter2KOR.LineBreak : StringConverter2.LineBreak; + + public Mail2(SAV2 sav, int index) : base(sav.Data.AsSpan(GetMailOffset(index, GetMailSize(sav.Language)), GetMailSize(sav.Language)).ToArray(), GetMailOffset(index, GetMailSize(sav.Language))) { - Language = sav.Language; + EnglishGS = sav.Version != GameVersion.C && sav.Language == (int)LanguageID.English; + Japanese = sav.Japanese; + Korean = sav.Korean; } - private static int GetMailOffset(int index) + public Mail2(SAV2Stadium sav, int index) : base(sav.Data.AsSpan(GetMailOffsetStadium2(index, GetMailSize(sav.Language)), GetMailSize(sav.Language)).ToArray(), GetMailOffsetStadium2(index, GetMailSize(sav.Language))) + { + EnglishGS = false; + Japanese = sav.Japanese; + Korean = sav.Korean; + } + + public static int GetMailSize(int language) => language switch + { + (int)LanguageID.Japanese => SIZE_J, + (int)LanguageID.Korean => SIZE_K, + _ => SIZE_U, + }; + + #region Offsets + public static int GetMailboxOffset(int language) => 0x600 + (COUNT_PARTY * 2) * GetMailSize(language); + + private static int GetMailOffset(int index, int size) { if (index < COUNT_PARTY) - return GetPartyMailOffset(index); - return GetMailboxMailOffset(index - COUNT_PARTY); + return GetPartyMailOffset(index, size); + return GetMailboxMailOffset(index - COUNT_PARTY, size); } - private static int GetPartyMailOffset(int index) + private static int GetPartyMailOffset(int index, int size) { if ((uint)index >= COUNT_PARTY) throw new ArgumentOutOfRangeException(nameof(index)); - return (index * SIZE) + 0x600; + return (index * size) + 0x600; } - private static int GetMailboxMailOffset(int index) + private static int GetMailboxMailOffset(int index, int size) { if ((uint)index >= COUNT_MAILBOX) throw new ArgumentOutOfRangeException(nameof(index)); - return (index * SIZE) + 0x835; + return (index * size) + (0x600 + (COUNT_PARTY * 2) * size + 1); } + public static int GetMailboxOffsetStadium2(int language) => SAV2Stadium.MailboxBlockOffset(language) + 1; + + private static int GetMailOffsetStadium2(int index, int size) + { + if (index < COUNT_PARTY_STADIUM2) + return GetHeldMailOffsetStadium2(index, size); + return GetMailboxMailOffsetStadium2(index - COUNT_PARTY_STADIUM2, size); + } + + private static int GetMailboxMailOffsetStadium2(int index, int size) + { + if ((uint)index >= COUNT_MAILBOX_STADIUM2) + throw new ArgumentOutOfRangeException(nameof(index)); + return (index * size) + SAV2Stadium.MailboxBlockOffset(size == SIZE_J ? (int)LanguageID.Japanese : (int)LanguageID.English) + 2; + } + + private static int GetHeldMailOffsetStadium2(int index, int size) + { + if ((uint)index >= COUNT_PARTY_STADIUM2) + throw new ArgumentOutOfRangeException(nameof(index)); + return (index * size) + SAV2Stadium.MailboxHeldBlockOffset(size == SIZE_J ? (int)LanguageID.Japanese : (int)LanguageID.English) + 2; + } + #endregion + private string GetString(Span span) { if (Korean) return StringConverter2KOR.GetString(span); - var result = StringConverter2.GetString(span, Language); + if (EnglishGS) + StringConverter2.DecodeMailEnglishGS(span, AuthorLanguage); + var result = StringConverter2.GetString(span, AuthorLanguage); if (!Korean) - result = StringConverter2.InflateLigatures(result, Language); + result = StringConverter2.InflateLigatures(result, AuthorLanguage); return result; } - private void SetString(Span span, ReadOnlySpan value, int maxLength) + private void SetString(Span span, ReadOnlySpan value, int maxLength, StringConverterOption option = StringConverterOption.Clear50) { if (Korean) { - StringConverter2KOR.SetString(span, value, maxLength); + StringConverter2KOR.SetString(span, value, maxLength, option); return; } Span deflated = stackalloc char[maxLength]; - int len = StringConverter2.DeflateLigatures(value, deflated, Language); - StringConverter2.SetString(span, deflated[..len], maxLength, Language); + int len = StringConverter2.DeflateLigatures(value, deflated, AuthorLanguage); + StringConverter2.SetString(span, deflated[..len], maxLength, AuthorLanguage, option); + if (EnglishGS) + StringConverter2.EncodeMailEnglishGS(span, AuthorLanguage); } - public string Line1 + public override string GetMessage(bool isLastLine) { - get => GetString(Data.AsSpan(0, LINE_LENGTH - 1)); - set - { - var span = Data.AsSpan(0, LINE_LENGTH); - SetString(span[..^1], value, LINE_LENGTH - 1); - span[^1] = 0x4E; - } + var span = Data.AsSpan(0, MESSAGE_LENGTH); + var index = span.IndexOf(LineBreakCode); + return index == -1 ? string.Empty : GetString(isLastLine ? span[(index + 1)..] : span[..index]); } - public string Line2 + public override void SetMessage(string line1, string line2, bool userEntered) { - get => GetString(Data.AsSpan(LINE_LENGTH, LINE_LENGTH - 1)); - set + if (IsEmpty == true && line1 == string.Empty && line2 == string.Empty) { - var span = Data.AsSpan(LINE_LENGTH, LINE_LENGTH); - SetString(span[..^1], value, LINE_LENGTH - 1); - span[^1] = 0x4E; + Data.AsSpan(0, MESSAGE_LENGTH).Clear(); + return; } - } - public override string GetMessage(bool isLastLine) => isLastLine ? Line2 : Line1; - public override void SetMessage(string line1, string line2) => (Line1, Line2) = (line1, line2); + if (Korean || !userEntered) + { + // Japanese/international Randy's mail has a line break in different place. + // Korean always puts a line break after the first line, even if it's not full. + var span = Data.AsSpan(0, MESSAGE_LENGTH); + var message = string.Join(LineBreak, line1, line2); + SetString(span, message, MESSAGE_LENGTH, + // Randy's mail can have trash bytes after the end of the message, so don't clear it. + userEntered ? StringConverterOption.Clear50 : StringConverterOption.None); + return; + } + + // Japanese/international user-entered mail always has a line break at index 0x10 + var span1 = Data.AsSpan(0, LINE_LENGTH); + SetString(span1, line1, LINE_LENGTH); + if (line2 != string.Empty) // Pad the first line with spaces if needed + span1.Replace(0x50, 0x7F); + Data[LINE_LENGTH] = LineBreakCode; + var span2 = Data.AsSpan(LINE_LENGTH + 1, LINE_LENGTH); + SetString(span2, line2, LINE_LENGTH); + } public override string AuthorName { - get => GetString(Data.AsSpan(OFS_AUTHOR, AUTHOR_LENGTH + 1)); + get => GetString(Data.AsSpan(OFS_AUTHOR, AUTHOR_LENGTH)); + set => SetString(Data.AsSpan(OFS_AUTHOR, AUTHOR_LENGTH), value, + // Japanese/Korean don't have an extra byte for the terminator. + AUTHOR_LENGTH - ((Japanese || Korean) ? 0 : 1), + // Randy's mail can have trash bytes after the end of the OT, so don't clear it. + UserEntered ? StringConverterOption.Clear50 : StringConverterOption.None); + } + + public override byte AuthorLanguage + { + get + { + if (Japanese) + return (byte)LanguageID.Japanese; + if (Korean) + return (byte)LanguageID.Korean; + return (byte)(Nationality switch + { + 0x0000 => LanguageID.English, + 0x8485 => LanguageID.French, // "EF" + 0x8486 => LanguageID.German, // "EG" + 0x8488 => LanguageID.Italian, // "EI" + 0x8492 => LanguageID.Spanish, // "ES" + _ => LanguageID.English, + }); + } set { - SetString(Data.AsSpan(OFS_AUTHOR, 8), value, AUTHOR_LENGTH); - Nationality = 0; // ?? + if (Japanese || Korean) + return; + Nationality = (LanguageID)value switch + { + LanguageID.English => 0x0000, + LanguageID.French => 0x8485, // "EF" + LanguageID.German => 0x8486, // "EG" + LanguageID.Italian => 0x8488, // "EI" + LanguageID.Spanish => 0x8492, // "ES" + _ => Nationality, // Invalid, don't change. + }; } } public ushort Nationality { get => ReadUInt16BigEndian(Data.AsSpan(OFS_AUTHOR_NATION, 2)); - set => WriteUInt16LittleEndian(Data.AsSpan(OFS_AUTHOR_NATION, 2), value); + set => WriteUInt16BigEndian(Data.AsSpan(OFS_AUTHOR_NATION, 2), value); } public override ushort AuthorTID { - get => ReadUInt16BigEndian(Data.AsSpan(OFS_AUTHOR_ID + 2)); + get => ReadUInt16BigEndian(Data.AsSpan(OFS_AUTHOR_ID, 2)); set => WriteUInt16BigEndian(Data.AsSpan(OFS_AUTHOR_ID, 2), value); } @@ -131,5 +232,27 @@ public override ushort AuthorTID _ => null, }; + public override bool UserEntered + { + get + { + // Blank mail, prepare for writing + if (MailType == 0) + return true; + + // Japanese/international user-entered mail always has a line break at index 0x10 + // Randy's mail instead has it at index 0x0D (Japanese) or 0x0F (international) + if (!Korean) + return Data[LINE_LENGTH] == LineBreakCode; + + // Korean mail can have a line break anywhere, so look at trash bytes instead + // User-entered mail always fills the message buffer with 0x50 + // Randy's mail has trash bytes after the terminator, so check for any trash bytes + var span = Data.AsSpan(0, MESSAGE_LENGTH); + var terminator = span.IndexOf(0x50); + return terminator == -1 || !span[terminator..].ContainsAnyExcept(0x50); + } + } + public override void SetBlank() => Data.AsSpan(0, SIZE).Clear(); } diff --git a/PKHeX.Core/Saves/Substructures/Mail/MailDetail.cs b/PKHeX.Core/Saves/Substructures/Mail/MailDetail.cs index 44d74b206..5acdd7534 100644 --- a/PKHeX.Core/Saves/Substructures/Mail/MailDetail.cs +++ b/PKHeX.Core/Saves/Substructures/Mail/MailDetail.cs @@ -16,7 +16,7 @@ protected MailDetail(byte[] data, int offset = 0) public virtual void CopyTo(PK5 pk5) { } public virtual string GetMessage(bool isLastLine) => string.Empty; public virtual ushort GetMessage(int index1, int index2) => 0; - public virtual void SetMessage(string line1, string line2) { } + public virtual void SetMessage(string line1, string line2, bool userEntered) { } public virtual void SetMessage(int index1, int index2, ushort value) { } public virtual string AuthorName { get; set; } = string.Empty; public virtual ushort AuthorTID { get; set; } @@ -27,5 +27,6 @@ protected MailDetail(byte[] data, int offset = 0) public virtual ushort AppearPKM { get; set; } public virtual int MailType { get; set; } public abstract bool? IsEmpty { get; } // true: empty, false: legal mail, null: illegal mail + public virtual bool UserEntered { get; } public virtual void SetBlank() { } } diff --git a/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs b/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs index 0cfee7ec1..b089bb3fa 100644 --- a/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs +++ b/PKHeX.WinForms/Controls/SAV Editor/SAVEditor.cs @@ -1233,8 +1233,8 @@ private void ToggleViewSubEditors(SaveFile sav) B_OpenChatterEditor.Visible = sav is SAV4 or SAV5; B_OpenSealStickers.Visible = B_Poffins.Visible = sav is SAV8BS; B_OpenApricorn.Visible = sav is SAV4HGSS; - B_OpenRTCEditor.Visible = sav.Generation == 2 || sav is IGen3Hoenn; - B_MailBox.Visible = sav is SAV2 or SAV3 or SAV4 or SAV5; + B_OpenRTCEditor.Visible = (sav.Generation == 2 && sav is not SAV2Stadium) || sav is IGen3Hoenn; + B_MailBox.Visible = sav is SAV2 or SAV2Stadium or SAV3 or SAV4 or SAV5; B_Raids.Visible = sav is SAV8SWSH or SAV9SV; B_RaidsSevenStar.Visible = sav is SAV9SV; diff --git a/PKHeX.WinForms/Subforms/Save Editors/SAV_MailBox.Designer.cs b/PKHeX.WinForms/Subforms/Save Editors/SAV_MailBox.Designer.cs index ae8f1ecbb..86cc911dd 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/SAV_MailBox.Designer.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/SAV_MailBox.Designer.cs @@ -58,6 +58,7 @@ private void InitializeComponent() NUD_Message01 = new System.Windows.Forms.NumericUpDown(); NUD_Message00 = new System.Windows.Forms.NumericUpDown(); GB_Author = new System.Windows.Forms.GroupBox(); + CHK_UserEntered = new System.Windows.Forms.CheckBox(); CB_AuthorVersion = new System.Windows.Forms.ComboBox(); CB_AuthorLang = new System.Windows.Forms.ComboBox(); Label_OTGender = new System.Windows.Forms.Label(); @@ -200,21 +201,21 @@ private void InitializeComponent() // TB_MessageBody21.Location = new System.Drawing.Point(10, 27); TB_MessageBody21.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); - TB_MessageBody21.MaxLength = 16; + TB_MessageBody21.MaxLength = 32; TB_MessageBody21.Name = "TB_MessageBody21"; - TB_MessageBody21.Size = new System.Drawing.Size(186, 23); + TB_MessageBody21.Size = new System.Drawing.Size(216, 23); TB_MessageBody21.TabIndex = 7; - TB_MessageBody21.Text = "MMMMMMMMMMMMMMMM"; + TB_MessageBody21.Text = "0123456789ABCDEF"; // // TB_MessageBody22 // TB_MessageBody22.Location = new System.Drawing.Point(10, 61); TB_MessageBody22.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); - TB_MessageBody22.MaxLength = 16; + TB_MessageBody22.MaxLength = 32; TB_MessageBody22.Name = "TB_MessageBody22"; - TB_MessageBody22.Size = new System.Drawing.Size(186, 23); + TB_MessageBody22.Size = new System.Drawing.Size(216, 23); TB_MessageBody22.TabIndex = 8; - TB_MessageBody22.Text = "MMMMMMMMMMMMMMMM"; + TB_MessageBody22.Text = "0123456789ABCDEF"; // // TB_AuthorName // @@ -271,11 +272,11 @@ private void InitializeComponent() NUD_BoxSize.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left; NUD_BoxSize.Location = new System.Drawing.Point(105, 464); NUD_BoxSize.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); - NUD_BoxSize.Maximum = new decimal(new int[] { 10, 0, 0, 0 }); + NUD_BoxSize.Maximum = new decimal(new int[] { 50, 0, 0, 0 }); NUD_BoxSize.Name = "NUD_BoxSize"; NUD_BoxSize.Size = new System.Drawing.Size(43, 23); NUD_BoxSize.TabIndex = 14; - NUD_BoxSize.Value = new decimal(new int[] { 10, 0, 0, 0 }); + NUD_BoxSize.Value = new decimal(new int[] { 50, 0, 0, 0 }); NUD_BoxSize.ValueChanged += NUD_BoxSize_ValueChanged; // // GB_MessageTB @@ -286,7 +287,7 @@ private void InitializeComponent() GB_MessageTB.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); GB_MessageTB.Name = "GB_MessageTB"; GB_MessageTB.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3); - GB_MessageTB.Size = new System.Drawing.Size(208, 96); + GB_MessageTB.Size = new System.Drawing.Size(238, 96); GB_MessageTB.TabIndex = 15; GB_MessageTB.TabStop = false; GB_MessageTB.Text = "Message"; @@ -447,6 +448,7 @@ private void InitializeComponent() // // GB_Author // + GB_Author.Controls.Add(CHK_UserEntered); GB_Author.Controls.Add(CB_AuthorVersion); GB_Author.Controls.Add(CB_AuthorLang); GB_Author.Controls.Add(Label_OTGender); @@ -462,6 +464,16 @@ private void InitializeComponent() GB_Author.TabStop = false; GB_Author.Text = "Author"; // + // CHK_UserEntered + // + CHK_UserEntered.AutoSize = true; + CHK_UserEntered.Location = new System.Drawing.Point(159, 62); + CHK_UserEntered.Name = "CHK_UserEntered"; + CHK_UserEntered.Size = new System.Drawing.Size(94, 19); + CHK_UserEntered.TabIndex = 61; + CHK_UserEntered.Text = "User-Entered"; + CHK_UserEntered.UseVisualStyleBackColor = true; + // // CB_AuthorVersion // CB_AuthorVersion.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; @@ -1002,5 +1014,6 @@ private void InitializeComponent() private System.Windows.Forms.Button B_PartyDown; private System.Windows.Forms.Button B_BoxDown; private System.Windows.Forms.Button B_BoxUp; + private System.Windows.Forms.CheckBox CHK_UserEntered; } } diff --git a/PKHeX.WinForms/Subforms/Save Editors/SAV_MailBox.cs b/PKHeX.WinForms/Subforms/Save Editors/SAV_MailBox.cs index 792646e65..4fcd2cc29 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/SAV_MailBox.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/SAV_MailBox.cs @@ -47,16 +47,18 @@ public SAV_MailBox(SaveFile sav) AppearPKMs = [CB_AppearPKM1, CB_AppearPKM2, CB_AppearPKM3]; Miscs = [NUD_Misc1, NUD_Misc2, NUD_Misc3]; - NUD_BoxSize.Visible = L_BoxSize.Visible = Generation == 2; + NUD_BoxSize.Visible = L_BoxSize.Visible = CHK_UserEntered.Visible = Generation == 2; GB_MessageTB.Visible = Generation == 2; GB_MessageNUD.Visible = Generation != 2; Messages[0][3].Visible = Messages[1][3].Visible = Messages[2][3].Visible = Generation is 4 or 5; NUD_AuthorSID.Visible = Generation != 2; - Label_OTGender.Visible = CB_AuthorLang.Visible = CB_AuthorVersion.Visible = Generation is 4 or 5; + Label_OTGender.Visible = CB_AuthorVersion.Visible = Generation is 4 or 5; + CB_AuthorLang.Visible = Generation is 2 or 4 or 5; L_AppearPKM.Visible = AppearPKMs[0].Visible = Generation != 5; AppearPKMs[1].Visible = AppearPKMs[2].Visible = Generation == 4; NUD_MessageEnding.Visible = Generation == 5; L_MiscValue.Visible = NUD_Misc1.Visible = NUD_Misc2.Visible = NUD_Misc3.Visible = Generation == 5; + GB_PKM.Visible = B_PartyUp.Enabled = B_PartyDown.Enabled = SAV is not SAV2Stadium; for (int i = p.Count; i < 6; i++) PKMNUDs[i].Visible = PKMLabels[i].Visible = PKMHeldItems[i].Visible = false; @@ -76,9 +78,20 @@ public SAV_MailBox(SaveFile sav) for (int i = 0; i < m.Length; i++) m[i] = new Mail2(sav2, i); - NUD_BoxSize.Value = SAV.Data[0x834]; + NUD_BoxSize.Value = SAV.Data[Mail2.GetMailboxOffset(SAV.Language)]; MailItemID = [0x9E, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD]; PartyBoxCount = 6; + NUD_BoxSize.Maximum = 10; + break; + case SAV2Stadium sav2Stadium: + m = new Mail2[SAV2Stadium.MailboxHeldMailCount + SAV2Stadium.MailboxMailCount]; + for (int i = 0; i < m.Length; i++) + m[i] = new Mail2(sav2Stadium, i); + + NUD_BoxSize.Value = SAV.Data[Mail2.GetMailboxOffsetStadium2(SAV.Language)]; + MailItemID = [0x9E, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD]; + PartyBoxCount = SAV2Stadium.MailboxHeldMailCount; + NUD_BoxSize.Maximum = SAV2Stadium.MailboxMailCount; break; case SAV3 sav3: m = new Mail3[6 + 10]; @@ -138,7 +151,10 @@ public SAV_MailBox(SaveFile sav) CB_AuthorVersion.Items.Clear(); CB_AuthorVersion.InitializeBinding(); CB_AuthorVersion.DataSource = new BindingSource(vers, null); + } + if (Generation is 2 or 4 or 5) + { CB_AuthorLang.Items.Clear(); CB_AuthorLang.InitializeBinding(); CB_AuthorLang.DataSource = new BindingSource(GameInfo.LanguageDataSource(SAV.Generation), null); @@ -217,14 +233,22 @@ private void Save() { case 2: foreach (var n in m) n.CopyTo(SAV); - // duplicate - int ofs = 0x600; - int len = 0x2F * 6; - Array.Copy(SAV.Data, ofs, SAV.Data, ofs + len, len); - ofs += len << 1; - SAV.Data[ofs] = (byte)NUD_BoxSize.Value; - len = (0x2F * 10) + 1; - Array.Copy(SAV.Data, ofs, SAV.Data, ofs + len, len); + if (SAV is SAV2) + { + // duplicate + int ofs = 0x600; + int len = Mail2.GetMailSize(SAV.Language) * 6; + Array.Copy(SAV.Data, ofs, SAV.Data, ofs + len, len); + ofs += len << 1; + SAV.Data[ofs] = (byte)NUD_BoxSize.Value; + len = (Mail2.GetMailSize(SAV.Language) * 10) + 1; + Array.Copy(SAV.Data, ofs, SAV.Data, ofs + len, len); + } + else if (SAV is SAV2Stadium) + { + int ofs = Mail2.GetMailboxOffsetStadium2(SAV.Language); + SAV.Data[ofs] = (byte)NUD_BoxSize.Value; + } break; case 3: foreach (var n in m) n.CopyTo(SAV); @@ -257,7 +281,9 @@ private void TempSave() if (Generation == 2) { mail.AppearPKM = species; - mail.SetMessage(TB_MessageBody21.Text, TB_MessageBody22.Text); + mail.SetMessage(TB_MessageBody21.Text, TB_MessageBody22.Text, CHK_UserEntered.Checked); + // ReSharper disable once ConstantNullCoalescingCondition + mail.AuthorLanguage = (byte)((int?)CB_AuthorLang.SelectedValue ?? (int)LanguageID.English); return; } mail.AuthorSID = (ushort)NUD_AuthorSID.Value; @@ -498,6 +524,9 @@ private void LoadMail() AppearPKMs[0].SelectedValue = (int)species; TB_MessageBody21.Text = mail.GetMessage(false); TB_MessageBody22.Text = mail.GetMessage(true); + CB_AuthorLang.SelectedValue = (int)mail.AuthorLanguage; + CB_AuthorLang.Enabled = CB_AuthorLang.SelectedValue is not (int)LanguageID.Japanese and not (int)LanguageID.Korean; + CHK_UserEntered.Checked = mail.UserEntered; editing = false; return; }