From ebc0de6d51b681f2120af06f90621f59e91b699b Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Sun, 13 Mar 2022 13:12:34 +0100 Subject: [PATCH 1/2] :sparkles: Implement writing of ROM header --- .../Containers/Rom/Binary2RomHeaderTests.cs | 48 +++++++-- src/Ekona/Containers/Rom/Binary2RomHeader.cs | 3 +- src/Ekona/Containers/Rom/RomHeader2Binary.cs | 102 ++++++++++++++++++ src/Ekona/Containers/Rom/RomSectionInfo.cs | 9 ++ 4 files changed, 154 insertions(+), 8 deletions(-) create mode 100644 src/Ekona/Containers/Rom/RomHeader2Binary.cs diff --git a/src/Ekona.Tests/Containers/Rom/Binary2RomHeaderTests.cs b/src/Ekona.Tests/Containers/Rom/Binary2RomHeaderTests.cs index fe28704..32045a6 100644 --- a/src/Ekona.Tests/Containers/Rom/Binary2RomHeaderTests.cs +++ b/src/Ekona.Tests/Containers/Rom/Binary2RomHeaderTests.cs @@ -17,7 +17,7 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using FluentAssertions; @@ -25,13 +25,14 @@ using SceneGate.Ekona.Containers.Rom; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; +using Yarhl.FileFormat; using Yarhl.FileSystem; using Yarhl.IO; -namespace SceneGate.Ekona.Tests.Containers.Rom +namespace SceneGate.Ekona.Tests.Containers.Rom { - [TestFixture] - public class Binary2RomHeaderTests + [TestFixture] + public class Binary2RomHeaderTests { public static IEnumerable GetFiles() { @@ -63,6 +64,39 @@ public void DeserializeMatchInfo(string infoPath, string headerPath) node.GetFormatAs().Should().BeEquivalentTo( expected, opts => opts.Excluding(p => p.CopyrightLogo)); - } - } -} + } + + [TestCaseSource(nameof(GetFiles))] + [Ignore("It requires to implement DSi fields #9")] + public void TwoWaysIdenticalStream(string infoPath, string headerPath) + { + TestDataBase.IgnoreIfFileDoesNotExist(headerPath); + + using Node node = NodeFactory.FromFile(headerPath, FileOpenMode.Read); + + var header = (RomHeader)ConvertFormat.With(node.Format!); + var generatedStream = (BinaryFormat)ConvertFormat.With(header); + + var originalStream = new DataStream(node.Stream!, 0, header.SectionInfo.HeaderSize); + originalStream.Length.Should().Be(generatedStream.Stream.Length); + originalStream.Compare(generatedStream.Stream).Should().BeTrue(); + } + + [TestCaseSource(nameof(GetFiles))] + public void ThreeWaysIdenticalObjects(string infoPath, string headerPath) + { + TestDataBase.IgnoreIfFileDoesNotExist(headerPath); + + using Node node = NodeFactory.FromFile(headerPath, FileOpenMode.Read); + + var originalHeader = (RomHeader)ConvertFormat.With(node.Format!); + var generatedStream = (BinaryFormat)ConvertFormat.With(originalHeader); + var generatedHeader = (RomHeader)ConvertFormat.With(generatedStream); + + // Ignore ChecksumHeader as we are not generating identical headers due to DSi flags yet (#9). + generatedHeader.Should().BeEquivalentTo( + originalHeader, + opts => opts.Excluding(p => p.ProgramInfo.ChecksumHeader)); + } + } +} diff --git a/src/Ekona/Containers/Rom/Binary2RomHeader.cs b/src/Ekona/Containers/Rom/Binary2RomHeader.cs index e31236f..e8891a2 100644 --- a/src/Ekona/Containers/Rom/Binary2RomHeader.cs +++ b/src/Ekona/Containers/Rom/Binary2RomHeader.cs @@ -83,8 +83,9 @@ public RomHeader Convert(IBinary source) header.ProgramInfo.SecureDisable = reader.ReadUInt64(); header.SectionInfo.RomSize = reader.ReadUInt32(); header.SectionInfo.HeaderSize = reader.ReadUInt32(); + header.SectionInfo.Unknown88 = reader.ReadUInt32(); - source.Stream.Position += 0x38; + source.Stream.Position += 0x34; header.CopyrightLogo = reader.ReadBytes(156); header.ProgramInfo.ChecksumLogo = reader.ValidateCrc16(0xC0, 0x9C); header.ProgramInfo.ChecksumHeader = reader.ValidateCrc16(0x00, 0x15E); diff --git a/src/Ekona/Containers/Rom/RomHeader2Binary.cs b/src/Ekona/Containers/Rom/RomHeader2Binary.cs new file mode 100644 index 0000000..0d0a78d --- /dev/null +++ b/src/Ekona/Containers/Rom/RomHeader2Binary.cs @@ -0,0 +1,102 @@ +// Copyright(c) 2021 SceneGate +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +using System; +using Yarhl.FileFormat; +using Yarhl.IO; + +namespace SceneGate.Ekona.Containers.Rom; + +/// +/// Converter for ROM header object into binary stream (serialization). +/// +public class RomHeader2Binary : IConverter +{ + /// + /// Serialize a ROM header object into a binary stream. + /// + /// The header to convert. + /// The new binary stream. + /// The argument is null. + public BinaryFormat Convert(RomHeader source) + { + if (source is null) + throw new ArgumentNullException(nameof(source)); + + var binary = new BinaryFormat(); + var writer = new DataWriter(binary.Stream); + + writer.Write(source.ProgramInfo.GameTitle, 12, nullTerminator: false); + writer.Write(source.ProgramInfo.GameCode, 4, nullTerminator: false); + writer.Write(source.ProgramInfo.MakerCode, 2, nullTerminator: false); + writer.Write(source.ProgramInfo.UnitCode); + writer.Write(source.ProgramInfo.EncryptionSeed); + double relativeSize = (double)source.ProgramInfo.CartridgeSize / RomInfo.MinimumCartridgeSize; + byte power2Size = (byte)Math.Ceiling(Math.Log2(relativeSize)); + writer.Write(power2Size); + + writer.WriteTimes(0, 7); // reserved + writer.Write(source.ProgramInfo.DsiFlags); + writer.Write(source.ProgramInfo.Region); + writer.Write(source.ProgramInfo.Version); + writer.Write(source.ProgramInfo.AutoStartFlag); + + writer.Write(source.SectionInfo.Arm9Offset); + writer.Write(source.ProgramInfo.Arm9EntryAddress); + writer.Write(source.ProgramInfo.Arm9RamAddress); + writer.Write(source.SectionInfo.Arm9Size); + writer.Write(source.SectionInfo.Arm7Offset); + writer.Write(source.ProgramInfo.Arm7EntryAddress); + writer.Write(source.ProgramInfo.Arm7RamAddress); + writer.Write(source.SectionInfo.Arm7Size); + writer.Write(source.SectionInfo.FntOffset); + writer.Write(source.SectionInfo.FntSize); + writer.Write(source.SectionInfo.FatOffset); + writer.Write(source.SectionInfo.FatSize); + writer.Write(source.SectionInfo.Overlay9TableOffset); + writer.Write(source.SectionInfo.Overlay9TableSize); + writer.Write(source.SectionInfo.Overlay7TableOffset); + writer.Write(source.SectionInfo.Overlay7TableSize); + + writer.Write(source.ProgramInfo.FlagsRead); + writer.Write(source.ProgramInfo.FlagsInit); + writer.Write(source.SectionInfo.BannerOffset); + writer.Write(source.ProgramInfo.ChecksumSecureArea.Expected); + writer.Write(source.ProgramInfo.SecureAreaDelay); + writer.Write(source.ProgramInfo.Arm9Autoload); + writer.Write(source.ProgramInfo.Arm7Autoload); + writer.Write(source.ProgramInfo.SecureDisable); + writer.Write(source.SectionInfo.RomSize); + writer.Write(source.SectionInfo.HeaderSize); + writer.Write(source.SectionInfo.Unknown88); + + writer.WriteTimes(0, 0x34); + writer.Write(source.CopyrightLogo); + writer.Write(source.ProgramInfo.ChecksumLogo.Expected); + writer.Write(source.ProgramInfo.ChecksumHeader.Expected); + + writer.Write(source.ProgramInfo.DebugRomOffset); + writer.Write(source.ProgramInfo.DebugSize); + writer.Write(source.ProgramInfo.DebugRamAddress); + + writer.WriteUntilLength(0, source.SectionInfo.HeaderSize); + + return binary; + } +} diff --git a/src/Ekona/Containers/Rom/RomSectionInfo.cs b/src/Ekona/Containers/Rom/RomSectionInfo.cs index b3e4de9..33095f2 100644 --- a/src/Ekona/Containers/Rom/RomSectionInfo.cs +++ b/src/Ekona/Containers/Rom/RomSectionInfo.cs @@ -98,5 +98,14 @@ public class RomSectionInfo /// Gets or sets the size of the header. /// public uint HeaderSize { get; set; } + + /// + /// Gets or sets an unknown value at 0x88. + /// + /// + /// In DS games it looks like an offset pointing to the SDK information + /// inside the arm9.bin. + /// + public uint Unknown88 { get; set; } } } From 8bcca5ba5403d427e26ed42fb7a20383ee7bb8f6 Mon Sep 17 00:00:00 2001 From: Benito Palacios Sanchez Date: Sun, 13 Mar 2022 22:42:40 +0100 Subject: [PATCH 2/2] :sparkles: Implement serialization of banner --- .../Containers/Rom/Binary2BannerTests.cs | 40 ++++- src/Ekona/Containers/Rom/Banner2Binary.cs | 161 ++++++++++++++++++ src/Ekona/Containers/Rom/Binary2Banner.cs | 1 + src/Ekona/InternalExtensions.cs | 17 ++ 4 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 src/Ekona/Containers/Rom/Banner2Binary.cs diff --git a/src/Ekona.Tests/Containers/Rom/Binary2BannerTests.cs b/src/Ekona.Tests/Containers/Rom/Binary2BannerTests.cs index 9fc66e8..5fe5807 100644 --- a/src/Ekona.Tests/Containers/Rom/Binary2BannerTests.cs +++ b/src/Ekona.Tests/Containers/Rom/Binary2BannerTests.cs @@ -27,6 +27,7 @@ using Texim.Images; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; +using Yarhl.FileFormat; using Yarhl.FileSystem; using Yarhl.IO; @@ -44,7 +45,7 @@ public static IEnumerable GetFiles() Path.Combine(basePath, data), Path.Combine(basePath, data + ".yml"), Path.Combine(basePath, data + ".png")) - .SetName($"({data})")); + .SetName($"{{m}}({data})")); } [TestCaseSource(nameof(GetFiles))] @@ -103,5 +104,42 @@ public void DeserializeMatchInfo(string bannerPath, string infoPath, string icon actual.KoreanTitle.Should().Be(expected.KoreanTitle); } } + + [TestCaseSource(nameof(GetFiles))] + [Ignore("Requires implementing the animations")] + public void TwoWaysIdenticalStream(string bannerPath, string infoPath, string iconPath) + { + TestDataBase.IgnoreIfFileDoesNotExist(bannerPath); + + using Node node = NodeFactory.FromFile(bannerPath, FileOpenMode.Read); + + var banner = (NodeContainerFormat)ConvertFormat.With(node.Format!); + var generatedStream = (BinaryFormat)ConvertFormat.With(banner); + + node.Stream.Length.Should().Be(generatedStream.Stream.Length); + node.Stream.Compare(generatedStream.Stream).Should().BeTrue(); + } + + [TestCaseSource(nameof(GetFiles))] + public void ThreeWaysIdenticalObjects(string bannerPath, string infoPath, string iconPath) + { + TestDataBase.IgnoreIfFileDoesNotExist(bannerPath); + + using Node node = NodeFactory.FromFile(bannerPath, FileOpenMode.Read); + + var originalNode = (NodeContainerFormat)ConvertFormat.With(node.Format!); + var originalBanner = originalNode.Root.Children["info"].GetFormatAs(); + var originalIcon = originalNode.Root.Children["icon"].GetFormatAs(); + + var generatedStream = (BinaryFormat)ConvertFormat.With(originalNode); + + var generatedNode = (NodeContainerFormat)ConvertFormat.With(generatedStream); + var generatedBanner = generatedNode.Root.Children["info"].GetFormatAs(); + var generatedIcon = generatedNode.Root.Children["icon"].GetFormatAs(); + + // TODO: Implement icon animations + generatedBanner.Should().BeEquivalentTo(originalBanner, opts => opts.Excluding(f => f.ChecksumAnimatedIcon)); + generatedIcon.Should().BeEquivalentTo(originalIcon); + } } } diff --git a/src/Ekona/Containers/Rom/Banner2Binary.cs b/src/Ekona/Containers/Rom/Banner2Binary.cs new file mode 100644 index 0000000..80f2e38 --- /dev/null +++ b/src/Ekona/Containers/Rom/Banner2Binary.cs @@ -0,0 +1,161 @@ +// Copyright(c) 2022 SceneGate +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +using System; +using System.Text; +using Texim.Colors; +using Texim.Images; +using Texim.Pixels; +using Yarhl.FileFormat; +using Yarhl.FileSystem; +using Yarhl.IO; + +namespace SceneGate.Ekona.Containers.Rom; + +/// +/// Convert a container with banner information into a binary stream. +/// +/// +/// Supported versions: 0.1, 0.2, 0.3 and 1.3 (except animated icons). +/// The input container expects to have: +/// +/// /infoProgram banner content with Banner format. +/// /iconProgram icon with IndexedPaletteImage format. +/// +/// +public class Banner2Binary : IConverter +{ + /// + /// Write a container banner into a binary format. + /// + /// Banner to serialize into binary format. + /// The new serialized binary. + public BinaryFormat Convert(NodeContainerFormat source) + { + if (source is null) + throw new ArgumentNullException(nameof(source)); + + Banner banner = GetFormatSafe(source.Root, "info"); + IndexedPaletteImage icon = GetFormatSafe(source.Root, "icon"); + + var binary = new BinaryFormat(); + var writer = new DataWriter(binary.Stream) { + DefaultEncoding = Encoding.Unicode, + }; + + // Write empty header, as we need the data to generate checksum + writer.WriteTimes(0, 0x20); + + WriteIcon(writer, icon); + WriteTitles(writer, banner); + + if (banner.Version.Major > 0) { + WriteAnimatedIcon(writer); + } + + writer.Stream.Position = 0; + WriteHeader(writer, banner); + + return binary; + } + + private static T GetFormatSafe(Node root, string childName) + where T : class, IFormat + { + Node child = root.Children[childName] ?? throw new FormatException($"Missing child '{childName}'"); + return child.GetFormatAs() + ?? throw new FormatException($"Child '{childName}' has not the expected format: {typeof(T).Name}"); + } + + private static void WriteHeader(DataWriter writer, Banner banner) + { + writer.Write((byte)banner.Version.Minor); + writer.Write((byte)banner.Version.Major); + + writer.WriteComputedCrc16(0x20, 0x820); + + if (banner.Version.Minor > 1) { + writer.WriteComputedCrc16(0x20, 0x920); + } else { + writer.Write((ushort)0x00); + } + + if (banner.Version.Minor > 2) { + writer.WriteComputedCrc16(0x20, 0xA20); + } else { + writer.Write((ushort)0x00); + } + + if (banner.Version.Major > 0) { + writer.WriteComputedCrc16(0x1240, 0x1180); + } else { + writer.Write((ushort)0x00); + } + + writer.WriteTimes(0, 0x16); // reserved + } + + private static void WriteIcon(DataWriter writer, IndexedPaletteImage icon) + { + var swizzling = new TileSwizzling(icon.Width); + var pixels = swizzling.Swizzle(icon.Pixels); + writer.Write(pixels); + + if (icon.Palettes.Count != 1) { + throw new FormatException("Invalid number of palettes for icon, expected 1"); + } + + writer.Write(icon.Palettes[0].Colors); + } + + private static void WriteTitles(DataWriter writer, Banner banner) + { + writer.Write(banner.JapaneseTitle, 0x100); + writer.Write(banner.EnglishTitle, 0x100); + writer.Write(banner.FrenchTitle, 0x100); + writer.Write(banner.GermanTitle, 0x100); + writer.Write(banner.ItalianTitle, 0x100); + writer.Write(banner.SpanishTitle, 0x100); + + if (banner.Version.Minor > 1) { + writer.Write(banner.ChineseTitle, 0x100); + } else { + writer.WriteTimes(0xFF, 0x100); + } + + if (banner.Version.Minor > 2) { + writer.Write(banner.KoreanTitle, 0x100); + } else { + writer.WriteTimes(0xFF, 0xC0); + writer.WriteTimes(0x00, 0x40); + } + + // reserved for future titles + writer.WriteTimes(0, 0x800); + } + + private static void WriteAnimatedIcon(DataWriter writer) + { + // TODO: implement properly + writer.WriteTimes(0, 0x1000); // 8 bitmaps + writer.WriteTimes(0, 0x100); // 8 palettes + writer.WriteTimes(0, 0x80); // animation sequence + writer.WriteTimes(0xFF, 0x40); // padding + } +} diff --git a/src/Ekona/Containers/Rom/Binary2Banner.cs b/src/Ekona/Containers/Rom/Binary2Banner.cs index aa5d374..d76dda9 100644 --- a/src/Ekona/Containers/Rom/Binary2Banner.cs +++ b/src/Ekona/Containers/Rom/Binary2Banner.cs @@ -78,6 +78,7 @@ public static int GetSize(Stream stream) /// The new container with the banner. public NodeContainerFormat Convert(IBinary source) { + source.Stream.Position = 0; var reader = new DataReader(source.Stream) { DefaultEncoding = Encoding.Unicode, }; diff --git a/src/Ekona/InternalExtensions.cs b/src/Ekona/InternalExtensions.cs index bfd1719..c5bdbc6 100644 --- a/src/Ekona/InternalExtensions.cs +++ b/src/Ekona/InternalExtensions.cs @@ -30,5 +30,22 @@ public static ChecksumInfo ValidateCrc16(this DataReader reader, long of Actual = actual, }; } + + /// + /// Compute a CRC16 over the specific substream and write the result. + /// + /// Write to write the result and get the stream. + /// Offset of the segment to calculate the CRC. + /// The length to calculate the CRC. + public static void WriteComputedCrc16(this DataWriter writer, long offset, long length) + { + using DataStream segment = new DataStream(writer.Stream, offset, length); + + ICRC crc = CRCFactory.Instance.Create(CRCConfig.MODBUS); + IHashValue hash = crc.ComputeHash(segment); + ushort actual = (ushort)(hash.Hash[0] | (hash.Hash[1] << 8)); + + writer.Write(actual); + } } }