Created
September 18, 2023 13:09
-
-
Save pleonex/635a394e2d616c6f33e162880f873b39 to your computer and use it in GitHub Desktop.
Nintendo DS BMG text format deserializer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright(c) 2023 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. | |
namespace SceneGate.Ekona.Text; | |
using System; | |
using System.Text; | |
using Yarhl.FileFormat; | |
using Yarhl.IO; | |
public class Binary2NitroBinaryMessages : IConverter<IBinary, NitroBinaryMessages> | |
{ | |
private const string MagicStamp = "MESGbmg1"; | |
private DataReader reader = null!; | |
private BinaryBmg bmg = null!; | |
private NitroBinaryMessages model = null!; | |
public NitroBinaryMessages Convert(IBinary source) | |
{ | |
source.Stream.Position = 0; | |
reader = new DataReader(source.Stream); | |
bmg = new BinaryBmg(); | |
model = new NitroBinaryMessages(); | |
ReadHeader(); | |
reader.DefaultEncoding = model.TextEncoding; | |
// Required sections: INF1 and DAT1 | |
FindSections(); | |
if (bmg.Inf1Offset == 0) { | |
throw new FormatException("Missing required section INF1"); | |
} else if (bmg.Dat1Offset == 0) { | |
throw new FormatException("Missing required section DAT1"); | |
} | |
ReadSectionsMetadata(); | |
ReadScriptMessages(); | |
return model; | |
} | |
private void ReadHeader() | |
{ | |
string stamp = reader.ReadString(8, Encoding.ASCII); | |
if (stamp != MagicStamp) { | |
throw new FormatException($"Invalid magic stamp: {stamp}"); | |
} | |
uint fileSize = reader.ReadUInt32(); | |
if (fileSize > reader.Stream.Length) { | |
throw new FormatException( | |
"Stream is not big enough. " + | |
$"Expected {fileSize} bytes, but stream is {reader.Stream.Length} bytes"); | |
} | |
bmg.NumSections = reader.ReadInt32(); | |
int encoding = reader.ReadInt32(); | |
model.TextEncoding = encoding switch { | |
2 => Encoding.Unicode, | |
_ => throw new FormatException($"Invalid encoding ID: {encoding}"), | |
}; | |
// 12 bytes reserved (as far as we know). | |
reader.SkipPadding(0x10); | |
bmg.SectionsOffset = reader.Stream.Position; | |
} | |
private void FindSections() | |
{ | |
reader.Stream.Position = bmg.SectionsOffset; | |
for (int i = 0; i < bmg.NumSections; i++) { | |
string currentId = reader.ReadString(4, Encoding.ASCII); | |
uint sectionSize = reader.ReadUInt32(); | |
switch (currentId) { | |
// Required | |
case "INF1": | |
bmg.Inf1Offset = reader.Stream.Position; | |
break; | |
case "DAT1": | |
bmg.Dat1Offset = reader.Stream.Position; | |
break; | |
// Optional | |
case "MID1": | |
bmg.Mid1Offset = reader.Stream.Position; | |
break; | |
case "STR1": | |
break; // unknown | |
// Deprecated | |
case "FLI1": | |
case "FLW1": | |
break; | |
default: | |
throw new FormatException($"Unknown section: {currentId}"); | |
} | |
reader.Stream.Position += sectionSize - 8; | |
} | |
} | |
private void ReadSectionsMetadata() | |
{ | |
ReadInf1Header(); | |
if (bmg.Mid1Offset != 0) { | |
ReadMid1Header(); | |
} | |
} | |
private void ReadInf1Header() | |
{ | |
reader.Stream.Position = bmg.Inf1Offset; | |
bmg.NumMessages = reader.ReadInt16(); | |
bmg.InfoEntrySize = reader.ReadInt16(); | |
model.Unknown1 = reader.ReadInt16(); | |
model.Unknown2 = reader.ReadInt16(); | |
model.AttributesKind = bmg.InfoEntrySize switch { | |
4 => MessageAttributesKind.None, | |
8 => MessageAttributesKind.Simple, | |
12 => MessageAttributesKind.Full, | |
_ => throw new FormatException($"Invalid entry size: {bmg.InfoEntrySize}"), | |
}; | |
} | |
private void ReadMid1Header() | |
{ | |
reader.Stream.Position = bmg.Mid1Offset; | |
if (reader.ReadUInt16() != bmg.NumMessages) { | |
throw new FormatException($"IDs and messages do not match"); | |
} | |
byte idKind = reader.ReadByte(); | |
if (idKind != 0x10) { | |
throw new FormatException($"Unknown MID section: {idKind}"); | |
} | |
model.IdKind = MessageIdKind.Uid; | |
byte unk1 = reader.ReadByte(); | |
uint unk2 = reader.ReadUInt32(); | |
if (unk1 != 0 || unk2 != 0) { | |
throw new FormatException($"Unknown reserved values: {unk1:X},{unk2:X}"); | |
} | |
} | |
private void ReadScriptMessages() | |
{ | |
reader.Stream.Position = bmg.Inf1DataOffset; | |
for (int i = 0; i < bmg.NumMessages; i++) { | |
var entry = new NitroMessage(); | |
uint offset = reader.ReadUInt32(); | |
if (bmg.InfoEntrySize > 4) { | |
entry.Attribute4_5 = reader.ReadUInt16(); | |
entry.Attribute6_7 = reader.ReadUInt16(); | |
} | |
if (bmg.InfoEntrySize > 8) { | |
entry.Attribute8 = reader.ReadByte(); | |
entry.Attribute9 = reader.ReadByte(); | |
entry.AttributeA = reader.ReadByte(); | |
entry.AttributeB = reader.ReadByte(); | |
} | |
reader.Stream.PushToPosition(bmg.Dat1Offset + offset); | |
entry.Text = ReadScript(); | |
reader.Stream.PopPosition(); | |
if (bmg.Mid1DataOffset != 0) { | |
reader.Stream.PushToPosition(bmg.Mid1DataOffset + (i * 4)); | |
entry.Id = reader.ReadInt32(); | |
reader.Stream.PopPosition(); | |
} | |
model.Add(entry); | |
} | |
} | |
private string ReadScript() | |
{ | |
var builder = new StringBuilder(); | |
// This algoritm only works for UTF-16 | |
ushort codeUnit = reader.ReadUInt16(); | |
while (codeUnit != 0) { | |
if (codeUnit != 0x001A) { | |
// in C# a char is a UTF-16 code unit, no need to further decode. | |
_ = builder.Append((char)codeUnit); | |
} else { | |
ReadScriptFunction(builder); | |
} | |
codeUnit = reader.ReadUInt16(); | |
} | |
return builder.ToString(); | |
} | |
private void ReadScriptFunction(StringBuilder builder) | |
{ | |
byte size = reader.ReadByte(); | |
byte classId = reader.ReadByte(); | |
ushort funcId = reader.ReadUInt16(); | |
int numArgs = (size - 6) / 2; | |
short[] funcArgs = new short[numArgs]; | |
for (int i = 0; i < numArgs; i++) { | |
funcArgs[i] = reader.ReadInt16(); | |
} | |
_ = builder.Append('{') | |
.Append(classId.ToString("X")) | |
.Append("::") | |
.Append(funcId.ToString("X")) | |
.Append('(') | |
.AppendJoin(", ", funcArgs) | |
.Append(")}"); | |
} | |
private sealed class BinaryBmg | |
{ | |
public int NumSections { get; set; } | |
public int NumMessages { get; set; } | |
public int InfoEntrySize { get; set; } | |
public long SectionsOffset { get; set; } | |
public long Inf1Offset { get; set; } | |
public long Inf1DataOffset => Inf1Offset > 0 ? Inf1Offset + 8 : 0; | |
public long Dat1Offset { get; set; } | |
public long Mid1Offset { get; set; } | |
public long Mid1DataOffset => Mid1Offset > 0 ? Mid1Offset + 8 : 0; | |
} | |
} | |
/// <summary> | |
/// File format for .bmg (Binary MessaGes) files found in Nin games. | |
/// </summary> | |
public class NitroBinaryMessages : IFormat | |
{ | |
private readonly List<NitroMessage> messages = new(); | |
public short Unknown1 { get; set; } | |
public short Unknown2 { get; set; } | |
public MessageAttributesKind AttributesKind { get; set; } | |
public MessageIdKind IdKind { get; set; } | |
public IReadOnlyList<NitroMessage> Messages => messages; | |
public Encoding TextEncoding { get; set; } = Encoding.Unicode; | |
public void Add(NitroMessage message) | |
{ | |
if (IdKind == MessageIdKind.Uid && message.Id == -1) { | |
throw new ArgumentException("Message must have ID", nameof(message)); | |
} | |
messages.Add(message); | |
} | |
} | |
/// <summary> | |
/// Message entry of a <see cref="NitroBinaryMessages"/>. | |
/// </summary> | |
public class NitroMessage | |
{ | |
public int Id { get; set; } = -1; | |
public string Text { get; set; } = string.Empty; | |
public ushort Attribute4_5 { get; set; } // unconfirmed | |
public ushort Attribute6_7 { get; set; } | |
public byte Attribute8 { get; set; } // unconfirmed | |
public byte Attribute9 { get; set; } | |
public byte AttributeA { get; set; } // unconfirmed | |
public byte AttributeB { get; set; } | |
} | |
public enum MessageIdKind | |
{ | |
None = 0, | |
Uid, | |
} | |
public enum MessageAttributesKind | |
{ | |
None = 0, | |
Simple, | |
Full, | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment