Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Implement binary writing of rom header and banner #14

Merged
merged 2 commits into from
Mar 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion src/Ekona.Tests/Containers/Rom/Binary2BannerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
using Texim.Images;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using Yarhl.FileFormat;
using Yarhl.FileSystem;
using Yarhl.IO;

Expand All @@ -44,7 +45,7 @@ public static IEnumerable<TestCaseData> GetFiles()
Path.Combine(basePath, data),
Path.Combine(basePath, data + ".yml"),
Path.Combine(basePath, data + ".png"))
.SetName($"({data})"));
.SetName($"{{m}}({data})"));
}

[TestCaseSource(nameof(GetFiles))]
Expand Down Expand Up @@ -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<Binary2Banner>(node.Format!);
var generatedStream = (BinaryFormat)ConvertFormat.With<Banner2Binary>(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<Binary2Banner>(node.Format!);
var originalBanner = originalNode.Root.Children["info"].GetFormatAs<Banner>();
var originalIcon = originalNode.Root.Children["icon"].GetFormatAs<IndexedPaletteImage>();

var generatedStream = (BinaryFormat)ConvertFormat.With<Banner2Binary>(originalNode);

var generatedNode = (NodeContainerFormat)ConvertFormat.With<Binary2Banner>(generatedStream);
var generatedBanner = generatedNode.Root.Children["info"].GetFormatAs<Banner>();
var generatedIcon = generatedNode.Root.Children["icon"].GetFormatAs<IndexedPaletteImage>();

// TODO: Implement icon animations
generatedBanner.Should().BeEquivalentTo(originalBanner, opts => opts.Excluding(f => f.ChecksumAnimatedIcon));
generatedIcon.Should().BeEquivalentTo(originalIcon);
}
}
}
48 changes: 41 additions & 7 deletions src/Ekona.Tests/Containers/Rom/Binary2RomHeaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,22 @@
// 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;
using NUnit.Framework;
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<TestCaseData> GetFiles()
{
Expand Down Expand Up @@ -63,6 +64,39 @@ public void DeserializeMatchInfo(string infoPath, string headerPath)
node.GetFormatAs<RomHeader>().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<Binary2RomHeader>(node.Format!);
var generatedStream = (BinaryFormat)ConvertFormat.With<RomHeader2Binary>(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<Binary2RomHeader>(node.Format!);
var generatedStream = (BinaryFormat)ConvertFormat.With<RomHeader2Binary>(originalHeader);
var generatedHeader = (RomHeader)ConvertFormat.With<Binary2RomHeader>(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));
}
}
}
161 changes: 161 additions & 0 deletions src/Ekona/Containers/Rom/Banner2Binary.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Convert a container with banner information into a binary stream.
/// </summary>
/// <remarks>
/// <para>Supported versions: 0.1, 0.2, 0.3 and 1.3 (except animated icons).</para>
/// <para>The input container expects to have:</para>
/// <list type="table">
/// <item><term>/info</term><description>Program banner content with Banner format.</description></item>
/// <item><term>/icon</term><description>Program icon with IndexedPaletteImage format.</description></item>
/// </list>
/// </remarks>
public class Banner2Binary : IConverter<NodeContainerFormat, BinaryFormat>
{
/// <summary>
/// Write a container banner into a binary format.
/// </summary>
/// <param name="source">Banner to serialize into binary format.</param>
/// <returns>The new serialized binary.</returns>
public BinaryFormat Convert(NodeContainerFormat source)
{
if (source is null)
throw new ArgumentNullException(nameof(source));

Banner banner = GetFormatSafe<Banner>(source.Root, "info");
IndexedPaletteImage icon = GetFormatSafe<IndexedPaletteImage>(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<T>(Node root, string childName)
where T : class, IFormat
{
Node child = root.Children[childName] ?? throw new FormatException($"Missing child '{childName}'");
return child.GetFormatAs<T>()
?? 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<IndexedPixel>(icon.Width);
var pixels = swizzling.Swizzle(icon.Pixels);
writer.Write<Indexed4Bpp>(pixels);

if (icon.Palettes.Count != 1) {
throw new FormatException("Invalid number of palettes for icon, expected 1");
}

writer.Write<Bgr555>(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
}
}
1 change: 1 addition & 0 deletions src/Ekona/Containers/Rom/Binary2Banner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public static int GetSize(Stream stream)
/// <returns>The new container with the banner.</returns>
public NodeContainerFormat Convert(IBinary source)
{
source.Stream.Position = 0;
var reader = new DataReader(source.Stream) {
DefaultEncoding = Encoding.Unicode,
};
Expand Down
3 changes: 2 additions & 1 deletion src/Ekona/Containers/Rom/Binary2RomHeader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading