Advertisement
Moolah60

MSBT

Feb 1st, 2022
1,425
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 18.14 KB | None | 0 0
  1. using System.Runtime.InteropServices;
  2. using System.Text;
  3.  
  4. namespace NLItemParamDump
  5. {
  6.     public sealed class MSBT
  7.     {
  8.         public enum MSBTEncoding : byte
  9.         {
  10.             UTF8 = 0,
  11.             Unicode = 1,
  12.             UTF32 = 3
  13.         }
  14.  
  15.         [StructLayout(LayoutKind.Explicit, CharSet = CharSet.Ansi, Pack = 1, Size = 32)]
  16.         public unsafe struct MSBTHeader
  17.         {
  18.             public const string MSBTMagic = "MsgStdBn";
  19.  
  20.             [FieldOffset(0x00)]
  21.             public fixed byte Magic[8];
  22.  
  23.             [FieldOffset(0x08)]
  24.             public ushort ByteOrderMark;
  25.  
  26.             [FieldOffset(0x0A)]
  27.             public ushort Padding0A; // Verify this.
  28.  
  29.             [FieldOffset(0x0C)]
  30.             public MSBTEncoding Encoding;
  31.  
  32.             [FieldOffset(0x0D)]
  33.             public byte Version; // Unknown. Verify this.
  34.  
  35.             [FieldOffset(0x0E)]
  36.             public ushort SectionCount; // Verify this.
  37.  
  38.             [FieldOffset(0x10)]
  39.             public ushort Padding10; // Verify this.
  40.  
  41.             [FieldOffset(0x12)]
  42.             public uint FileSize;
  43.  
  44.             // Check 0x14 - 0x1F.
  45.  
  46.             public string GetMagic()
  47.             {
  48.                 char[] buf = new char[8];
  49.                 for (int i = 0; i < 8; i++)
  50.                 {
  51.                     buf[i] = (char)Magic[i];
  52.                 }
  53.                 return new string(buf);
  54.             }
  55.         }
  56.  
  57.         [StructLayout(LayoutKind.Explicit, CharSet = CharSet.Ansi, Pack = 1, Size = 16)]
  58.         public unsafe struct MSBTSectionHeader
  59.         {
  60.             [FieldOffset(0x0)]
  61.             public fixed byte Magic[4];
  62.  
  63.             [FieldOffset(0x4)]
  64.             public uint Size;
  65.  
  66.             public string GetMagic()
  67.             {
  68.                 char[] buf = new char[4];
  69.                 for (int i = 0; i < 4; i++)
  70.                 {
  71.                     buf[i] = (char)Magic[i];
  72.                 }
  73.                 return new string(buf);
  74.             }
  75.         }
  76.  
  77.         public static bool IsMSBT(in byte[] buffer) => Encoding.ASCII.GetString(buffer, 0, 8) == MSBTHeader.MSBTMagic;
  78.  
  79.         private Encoding? _Encoding;
  80.  
  81.         public MSBTHeader Header { get; private set; }
  82.         public Endianness Endianness { get; private set; }
  83.         public string? Name { get; private set; }
  84.         public List<string> Messages => GetSection<TXTSection>()?.Entries.Select(bArr => Encoding.GetString(bArr)).ToList() ?? new List<string>();
  85.         public List<byte[]> RawMessageData => GetSection<TXTSection>()?.Entries ?? new List<byte[]>();
  86.  
  87.         private readonly List<MSBTSection> Sections = new();
  88.  
  89.         public Encoding Encoding { get => _Encoding ?? Encoding.Default; private set => _Encoding = value; }
  90.  
  91.         public int GetMaximumMessageID()
  92.         {
  93.             if (HasSection<NLISection>())
  94.                 return (int)GetSection<NLISection>()!.Ids.Max(o => o.Value);
  95.             else if (HasSection<TXTSection>())
  96.                 return GetSection<TXTSection>()!.Entries.Count - 1;
  97.             else
  98.                 return -1;
  99.         }
  100.  
  101.         public string? GetMessage(int messageId)
  102.         {
  103.             if (messageId < 0) return null;
  104.             if (HasSection<TXTSection>())
  105.             {
  106.                 var txt = GetSection<TXTSection>();
  107.                 if (txt!.Entries.Count > messageId) return txt!.GetMessage(messageId, Encoding);
  108.             }
  109.  
  110.             return null;
  111.         }
  112.  
  113.         public string? GetStrippedMessage(int messageId)
  114.         {
  115.             if (messageId < 0) return null;
  116.             if (HasSection<TXTSection>())
  117.             {
  118.                 var txt = GetSection<TXTSection>()!;
  119.                 if (txt.Entries.Count > messageId) return txt.GetStrippedMessage(messageId, Encoding);
  120.             }
  121.  
  122.             return null;
  123.         }
  124.  
  125.         public string? GetMessage(string label)
  126.         {
  127.             if (HasSection<LBLSection>() && HasSection<TXTSection>())
  128.             {
  129.                 var lbl = GetSection<LBLSection>()!;
  130.                 var txt = GetSection<TXTSection>()!;
  131.                 int idx = (int?)lbl.Labels.FirstOrDefault(o => o.Name == label)?.Index ?? -1;
  132.                 if (idx == -1 || idx >= txt.EntryCount) return null;
  133.                 return txt.GetMessage(idx, Encoding);
  134.             }
  135.             return null;
  136.         }
  137.  
  138.         public string? GetStrippedMessage(string label)
  139.         {
  140.             if (HasSection<LBLSection>() && HasSection<TXTSection>())
  141.             {
  142.                 var lbl = GetSection<LBLSection>()!;
  143.                 var txt = GetSection<TXTSection>()!;
  144.                 int idx = (int?)lbl.Labels.FirstOrDefault(o => o.Name == label)?.Index ?? -1;
  145.                 if (idx == -1 || idx >= txt.EntryCount) return null;
  146.                 return txt.GetStrippedMessage(idx, Encoding);
  147.             }
  148.             return null;
  149.         }
  150.  
  151.         public string[]? GetMessages()
  152.         {
  153.             if (HasSection<TXTSection>())
  154.                 return GetSection<TXTSection>()!.Entries.Select(bArr => Encoding.GetString(bArr)).ToArray();
  155.  
  156.             return null;
  157.         }
  158.  
  159.         public string[]? GetLabels()
  160.         {
  161.             if (HasSection<LBLSection>())
  162.                 return GetSection<LBLSection>()!.Labels.Select(o => o.Name).ToArray();
  163.             return null;
  164.         }
  165.  
  166.         public bool SetMessages(string[] messages)
  167.         {
  168.             if (messages == null || HasSection<TXTSection>() == false) return false;
  169.  
  170.             var txt = GetSection<TXTSection>()!;
  171.             txt.Entries = messages.Select(s => Encoding.GetBytes(s)).ToList();
  172.             txt.EntryCount = (uint)messages.Length;
  173.  
  174.             // TODO: Labels?
  175.             return true;
  176.         }
  177.  
  178.         public bool UpdateMessage(int messageId, string message)
  179.         {
  180.             if (messageId < 0 || HasSection<TXTSection>() == false) return false;
  181.  
  182.             // TODO: Do we want this? How would we handle an overlap where the message id is the same as an index in the TXT section?
  183.             if (HasSection<NLISection>())
  184.             {
  185.                 var nli = GetSection<NLISection>();
  186.                 if (nli != null && nli.Ids.ContainsValue((uint)messageId))
  187.                     messageId = (int)nli.Ids.First(o => o.Value == (uint)messageId).Key;
  188.             }
  189.  
  190.             var txt = GetSection<TXTSection>();
  191.             if (txt == null || messageId >= txt.Entries.Count) return false;
  192.             txt.Entries[messageId] = Encoding.GetBytes(message);
  193.  
  194.             return true;
  195.         }
  196.  
  197.         private bool TryDetermineEndianness(Stream stream)
  198.         {
  199.             // Default to little-endian.
  200.             using (var reader = new EndianReader(stream, Endianness.Little, true))
  201.             {
  202.                 var header = reader.ReadStruct<MSBTHeader>();
  203.                 if (header.ByteOrderMark == 0xFEFF)
  204.                 {
  205.                     Header = header;
  206.                     Endianness = Endianness.Little;
  207.                     return true;
  208.                 }
  209.             }
  210.  
  211.             // Now try big-endian.
  212.             stream.Seek(0, SeekOrigin.Begin);
  213.             using (var reader = new EndianReader(stream, Endianness.Big, true))
  214.             {
  215.                 var header = reader.ReadStruct<MSBTHeader>();
  216.                 if (header.ByteOrderMark == 0xFEFF)
  217.                 {
  218.                     Header = header;
  219.                     Endianness = Endianness.Big;
  220.                     return true;
  221.                 }
  222.             }
  223.  
  224.             return false; // Byte order mark is invalid.
  225.         }
  226.  
  227.         private bool ValidateHeader(Stream stream)
  228.         {
  229.             if (Header.GetMagic() != MSBTHeader.MSBTMagic) return false;
  230.             if (Header.ByteOrderMark != 0xFEFF) return false;
  231.             if (Header.FileSize != stream.Length) return false;
  232.  
  233.             return true;
  234.         }
  235.  
  236.         private bool HasSection<T>() where T : MSBTSection => Sections.Any(s => s is T);
  237.         private T? GetSection<T>() where T : MSBTSection => Sections.First(s => s is T) as T;
  238.  
  239.         private uint GetTextHash(string text, uint numLabelGroups) => text.Aggregate(0u, (current, c) => current * 0x492 + c) % numLabelGroups;
  240.  
  241.         private void LoadSections(EndianReader reader)
  242.         {
  243.             reader.BaseStream.Seek(0x20, SeekOrigin.Begin);
  244.             while (reader.BaseStream.Position < reader.BaseStream.Length)
  245.             {
  246.                 switch (reader.PeekString(4))
  247.                 {
  248.                     case "LBL1":
  249.                         Sections.Add(new LBLSection(reader));
  250.                         break;
  251.                     case "ATR1":
  252.                         Sections.Add(new ATRSection(reader));
  253.                         break;
  254.                     case "NLI1":
  255.                         Sections.Add(new NLISection(reader));
  256.                         break;
  257.                     case "ATO1":
  258.                         Sections.Add(new ATOSection(reader));
  259.                         break;
  260.                     case "TSY1":
  261.                         Sections.Add(new TSYSection(reader));
  262.                         break;
  263.                     case "TXT2":
  264.                         Sections.Add(new TXTSection(reader));
  265.                         break;
  266.                 }
  267.             }
  268.         }
  269.  
  270.         public bool Load(in byte[] data)
  271.         {
  272.             using MemoryStream ms = new(data);
  273.             return Load(ms);
  274.         }
  275.  
  276.         public bool Load(Stream stream)
  277.         {
  278.             // Sanity checks first.
  279.             if (stream.Length < 32) throw new ArgumentException("The input stream cannot be smaller than the size of an MSBT header (32 bytes)!");
  280.             if (TryDetermineEndianness(stream) == false) throw new ArgumentException("The header's byte order mark was invalid! It must be FFFE or FEFF!");
  281.             if (ValidateHeader(stream) == false) throw new ArgumentException("The header failed to pass validation checks!");
  282.  
  283.             Name = (stream is FileStream fs) ? Path.GetFileNameWithoutExtension(fs.Name) : "msbt";
  284.  
  285.             // Set encoding based on encoding value in header & endianness.
  286.             Encoding = Header.Encoding switch
  287.             {
  288.                 MSBTEncoding.UTF8 => Encoding.UTF8,
  289.                 MSBTEncoding.Unicode => Endianness == Endianness.Big ? Encoding.BigEndianUnicode : Encoding.Unicode,
  290.                 MSBTEncoding.UTF32 => Encoding.UTF32,
  291.                 _ => throw new InvalidOperationException("This MSBT file's encoding format is unknown. Please open an issue on the GitHub page!"),
  292.             };
  293.  
  294.             // Load the MSBT sections.
  295.             try
  296.             {
  297.                 using var reader = new EndianReader(stream, Endianness);
  298.                 LoadSections(reader);
  299.             }
  300.             catch
  301.             {
  302.                 return false;
  303.             }
  304.  
  305.             if (Sections.Count != Header.SectionCount) throw new ArgumentException("The header's section count does not match the real section count!");
  306.             return true;
  307.         }
  308.  
  309.         public bool Save(string path)
  310.         {
  311.             throw new NotImplementedException();
  312.         }
  313.  
  314.         public void Dump(string path)
  315.         {
  316.             if (!HasSection<TXTSection>() || Name == null) return;
  317.  
  318.             string dir = Path.Combine(path, Name);
  319.             Directory.CreateDirectory(dir);
  320.             TXTSection? textData = GetSection<TXTSection>();
  321.             LBLSection? labels = HasSection<LBLSection>() ? GetSection<LBLSection>() : null;
  322.             if (textData == null || labels == null) return;
  323.             for (int i = 0; i < textData.EntryCount; i++)
  324.             {
  325.                 string name = labels?.Labels.First(l => l.Index == i).Name ?? i.ToString();
  326.                 File.WriteAllText(Path.Combine(dir, name), textData.GetMessage(i, Encoding));
  327.             }
  328.         }
  329.  
  330.         private abstract class MSBTSection
  331.         {
  332.             public MSBTSectionHeader Header;
  333.             public MSBTSection(EndianReader reader) => Header = reader.ReadStruct<MSBTSectionHeader>();
  334.         }
  335.  
  336.         private sealed class LBLSection : MSBTSection
  337.         {
  338.             [StructLayout(LayoutKind.Explicit, Pack = 1, Size = 8)]
  339.             public struct Group
  340.             {
  341.                 [FieldOffset(0)]
  342.                 public uint LabelCount;
  343.                 [FieldOffset(4)]
  344.                 public uint Offset;
  345.             }
  346.  
  347.             public sealed class Label
  348.             {
  349.                 public string Name;
  350.                 public uint Index;
  351.                 public uint Checksum;
  352.  
  353.                 public Label(string name, uint index, uint checksum)
  354.                 {
  355.                     Name = name;
  356.                     Index = index;
  357.                     Checksum = checksum;
  358.                 }
  359.             }
  360.  
  361.             public uint GroupCount;
  362.             public List<Group> Groups;
  363.             public List<Label> Labels;
  364.  
  365.             public LBLSection(EndianReader reader) : base(reader)
  366.             {
  367.                 Groups = new List<Group>();
  368.                 Labels = new List<Label>();
  369.  
  370.                 GroupCount = reader.ReadUInt32();
  371.  
  372.                 // Load groups
  373.                 for (var i = 0; i < GroupCount; i++)
  374.                     Groups.Add(reader.ReadStruct<Group>());
  375.  
  376.                 // Load labels
  377.                 for (var x = 0; x < Groups.Count; x++)
  378.                     for (var i = 0; i < Groups[x].LabelCount; i++)
  379.                         Labels.Add(new Label(Encoding.ASCII.GetString(reader.ReadBytes(reader.ReadByte())), reader.ReadUInt32(), (uint)x));
  380.  
  381.                 reader.Align(16);
  382.             }
  383.         }
  384.  
  385.         private sealed class NLISection : MSBTSection
  386.         {
  387.             public uint IdCount;
  388.             public Dictionary<uint, uint> Ids;
  389.  
  390.             public NLISection(EndianReader reader) : base(reader)
  391.             {
  392.                 Ids = new Dictionary<uint, uint>();
  393.                 IdCount = reader.ReadUInt32();
  394.  
  395.                 for (var i = 0; i < IdCount; i++)
  396.                 {
  397.                     var id = reader.ReadUInt32();
  398.                     var index = reader.ReadUInt32();
  399.                     Ids.Add(index, id);
  400.                 }
  401.  
  402.                 reader.Align(16);
  403.             }
  404.         }
  405.  
  406.         private sealed class ATOSection : MSBTSection
  407.         {
  408.             // TODO: ATO1 section needs to be researched.
  409.             public byte[] Data;
  410.  
  411.             public ATOSection(EndianReader reader) : base(reader)
  412.             {
  413.                 Data = reader.ReadBytes((int)Header.Size);
  414.                 reader.Align(16);
  415.             }
  416.         }
  417.  
  418.         private sealed class ATRSection : MSBTSection
  419.         {
  420.             public uint AttributeCount;
  421.             public uint AttributeSize;
  422.  
  423.             public List<byte[]> Attributes;
  424.  
  425.             public ATRSection(EndianReader reader) : base(reader)
  426.             {
  427.                 Attributes = new List<byte[]>();
  428.  
  429.                 AttributeCount = reader.ReadUInt32();
  430.                 AttributeSize = reader.ReadUInt32();
  431.  
  432.                 for (var i = 0; i < AttributeCount; i++)
  433.                     Attributes.Add(reader.ReadBytes((int)AttributeSize));
  434.  
  435.                 reader.Align(16);
  436.             }
  437.         }
  438.  
  439.         private sealed class TSYSection : MSBTSection
  440.         {
  441.             // TODO: TSY1 section needs to be researched.
  442.             public uint[] Data;
  443.  
  444.             public TSYSection(EndianReader reader) : base(reader)
  445.             {
  446.                 Data = new uint[Header.Size / sizeof(uint)];
  447.                 for (int i = 0; i < Data.Length; i++)
  448.                 {
  449.                     Data[i] = reader.ReadUInt32();
  450.                 }
  451.  
  452.                 reader.Align(16);
  453.             }
  454.         }
  455.  
  456.         private sealed class TXTSection : MSBTSection
  457.         {
  458.             public uint EntryCount;
  459.  
  460.             public List<byte[]> Entries;
  461.  
  462.             public TXTSection(EndianReader reader) : base(reader)
  463.             {
  464.                 Entries = new List<byte[]>();
  465.                 EntryCount = reader.ReadUInt32();
  466.  
  467.                 // Load string offsets.
  468.                 var offsets = new uint[EntryCount];
  469.                 for (var i = 0; i < EntryCount; i++)
  470.                     offsets[i] = reader.ReadUInt32();
  471.  
  472.                 for (var i = 0; i < EntryCount; i++)
  473.                 {
  474.                     uint size;
  475.                     if (i == EntryCount - 1)
  476.                         size = Header.Size - offsets[i];
  477.                     else
  478.                         size = offsets[i + 1] - offsets[i];
  479.  
  480.                     Entries.Add(reader.ReadBytes((int)size));
  481.                 }
  482.  
  483.                 reader.Align(16);
  484.             }
  485.  
  486.             public string? GetMessage(int idx, Encoding encoding)
  487.             {
  488.                 return idx > -1 && idx < Entries.Count ? encoding.GetString(Entries[idx]) : null;
  489.             }
  490.  
  491.             public byte[]? GetRawMessage(int idx)
  492.             {
  493.                 return idx > -1 && idx < Entries.Count ? Entries[idx] : null;
  494.             }
  495.  
  496.             public byte[]? GetRawMessageStripped(int idx)
  497.             {
  498.                 byte[]? data = GetRawMessage(idx);
  499.                 if (data == null) return null;
  500.                 List<byte> stripped = new(data.Length);
  501.                 for (int i = 0; i < data.Length; i += 2)
  502.                 {
  503.                     if (data[i + 1] == 0 && (data[i] == 0x0E || data[i] == 0x0F))
  504.                     {
  505.                         int size = 4 * sizeof(ushort) + BitConverter.ToUInt16(data, i + 3 * sizeof(ushort)); /* u16 cmd, u16 group, u16 index, u16 arg_size, u8 args[] */
  506.                         i += size - 2;
  507.                     }
  508.                     else
  509.                     {
  510.                         stripped.Add(data[i]);
  511.                         stripped.Add(data[i + 1]);
  512.                     }
  513.                 }
  514.                 return stripped.ToArray();
  515.             }
  516.  
  517.             public string? GetStrippedMessage(int idx, Encoding encoding)
  518.             {
  519.                 byte[]? data = GetRawMessageStripped(idx);
  520.                 return data == null ? null : encoding.GetString(data).Replace("\0", "");
  521.             }
  522.         }
  523.     }
  524. }
  525.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement