diff --git a/PKHeX.Core/Saves/SAV5.cs b/PKHeX.Core/Saves/SAV5.cs index 12abe7d48..40e877901 100644 --- a/PKHeX.Core/Saves/SAV5.cs +++ b/PKHeX.Core/Saves/SAV5.cs @@ -213,6 +213,13 @@ protected ushort WriteExtSection(ReadOnlySpan data, int offset, int size, ArgumentOutOfRangeException.ThrowIfNotEqual(data.Length, size); SetData(data, offset); + return RefreshExtSectionFooter(offset, size, count); + } + + private ushort RefreshExtSectionFooter(int offset, int size, ushort count) + { + var data = Data.Slice(offset, size); + // Update Tail Section ushort chk = Checksums.CRC16_CCITT(data); var tail = Data[(offset + size)..]; @@ -292,8 +299,9 @@ public void SetMusical(ReadOnlySpan data, ushort count = 1) public void SetPokeDexSkin(ReadOnlySpan data, ushort count = 1) { WriteExtSection(data, ExtPokeDexSkinOffset, PokeDexSkin5.SIZE, count); + IsAvailablePokedexSkin = true; // checksum might be changed via this, need to refresh footer to be safe + RefreshExtSectionFooter(ExtPokeDexSkinOffset, PokeDexSkin5.SIZE, count); PlayerData.UpdateExtData(ExtDataSectionNote5.PokedexSkin, count); - IsAvailablePokedexSkin = true; } private Span DexSkinFooter => Data.Slice(ExtPokeDexSkinOffset + PokeDexSkin5.SIZE - 4, 4); diff --git a/PKHeX.Core/Saves/Substructures/Gen5/Musical5.cs b/PKHeX.Core/Saves/Substructures/Gen5/Musical5.cs index eb92b2c93..d6108c1cb 100644 --- a/PKHeX.Core/Saves/Substructures/Gen5/Musical5.cs +++ b/PKHeX.Core/Saves/Substructures/Gen5/Musical5.cs @@ -6,12 +6,12 @@ public sealed class Musical5(SAV5 SAV, Memory raw) : SaveBlock(SAV, { public const int MusicalNameMaxLength = 20; - private Span MusicalTrash => Data.Slice(0x208, MusicalNameMaxLength * 2); + private Span MusicalTrash => Data.Slice(0x208, MusicalNameMaxLength * 2 + 2); // require terminator, so +2 for 0xFFFF public string MusicalName { get => StringConverter5.GetString(MusicalTrash); - set => StringConverter5.SetString(MusicalTrash, value, MusicalNameMaxLength, 0, StringConverterOption.ClearZero); + set => StringConverter5.SetString(MusicalTrash, value, MusicalNameMaxLength, SAV.Language, StringConverterOption.ClearZero); } private const int PropOffset = 0x258; diff --git a/PKHeX.WinForms/Subforms/Save Editors/Gen5/SAV_DLC5.cs b/PKHeX.WinForms/Subforms/Save Editors/Gen5/SAV_DLC5.cs index b0586823d..12c2a49c5 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/Gen5/SAV_DLC5.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/Gen5/SAV_DLC5.cs @@ -180,6 +180,38 @@ private static void ExportFile(string extension, string name, ReadOnlySpan File.WriteAllBytes(sfd.FileName, data); } + private static string GetImportedMusicalName(string path) + { + var name = Path.GetFileNameWithoutExtension(path).Trim(); + + var split = name.LastIndexOf(" - ", StringComparison.Ordinal); + if (split >= 0 && split + 3 < name.Length) + name = name[(split + 3)..].Trim(); + + var suffix = name.LastIndexOf(" (", StringComparison.Ordinal); + if (suffix > 0 && name[^1] == ')' && IsLikelyLanguageTag(name[(suffix + 2)..^1])) + name = name[..suffix].TrimEnd(); + + if (name.Length > Musical5.MusicalNameMaxLength) + name = name[..Musical5.MusicalNameMaxLength].TrimEnd(); + + return name; + } + + private static bool IsLikelyLanguageTag(ReadOnlySpan value) + { + if (value.Length is < 2 or > 5) + return false; + + foreach (var c in value) + { + if ((uint)(c - 'A') > 'Z' - 'A') + return false; + } + + return true; + } + private void B_ImportPNGCGear_Click(object sender, EventArgs e) { using var ofd = new OpenFileDialog(); @@ -355,7 +387,7 @@ private void B_MusicalImport_Click(object sender, EventArgs e) var musical = new MusicalShow5(data); SAV.SetMusical(data); if (LastImportedFile is { } name) - SAV.Musical.MusicalName = musical.IsUninitialized ? "" : Path.GetFileNameWithoutExtension(name).Trim(); + SAV.Musical.MusicalName = musical.IsUninitialized ? "" : GetImportedMusicalName(name); } private void B_MusicalExport_Click(object sender, EventArgs e) diff --git a/Tests/PKHeX.Core.Tests/Saves/SAV5DLC.cs b/Tests/PKHeX.Core.Tests/Saves/SAV5DLC.cs new file mode 100644 index 000000000..671f49e0a --- /dev/null +++ b/Tests/PKHeX.Core.Tests/Saves/SAV5DLC.cs @@ -0,0 +1,29 @@ +using FluentAssertions; +using Xunit; +using static System.Buffers.Binary.BinaryPrimitives; + +namespace PKHeX.Core.Tests.Saves; + +public class SAV5DLC +{ + [Fact] + public void SetPokeDexSkin_RecomputesChecksumAfterSettingAvailabilityFlag() + { + var sav = new SAV5B2W2(); + + // Create a random garbage PokeDex skin data with the last 4 bytes reserved for the availability flag and checksum + byte[] data = new byte[PokeDexSkin5.SIZE]; + for (int i = 0; i < data.Length - sizeof(uint); i++) + data[i] = (byte)i; + + sav.SetPokeDexSkin(data); + + sav.IsAvailablePokedexSkin.Should().BeTrue(); + ReadUInt32LittleEndian(sav.PokedexSkinData.Span[^sizeof(uint)..]).Should().Be(1u); + + const int offset = 0x6D800; + var tail = sav.Data[(offset + PokeDexSkin5.SIZE)..]; + ReadUInt16LittleEndian(tail).Should().Be(1); + ReadUInt16LittleEndian(tail[2..]).Should().Be(Checksums.CRC16_CCITT(sav.PokedexSkinData.Span)); + } +}