From 5e2b51b536cac87a9ef87efa5028013462daec97 Mon Sep 17 00:00:00 2001 From: Venomalia Date: Tue, 6 Sep 2022 00:39:24 +0200 Subject: [PATCH] add Retro Studio .PAK format works with Metroid Prime and Donkey Kong Country Returns --- .../Properties/AssemblyInfo.cs | 4 +- lib/AuroraLip/Archives/Formats/PAK_Retro.cs | 150 ++++++++++++ .../Archives/Formats/PAK_RetroWii.cs | 220 ++++++++++++++++++ lib/AuroraLip/AuroraLip.csproj | 2 + lib/AuroraLip/Common/FormatDictionary_List.cs | 7 +- 5 files changed, 378 insertions(+), 5 deletions(-) create mode 100644 lib/AuroraLip/Archives/Formats/PAK_Retro.cs create mode 100644 lib/AuroraLip/Archives/Formats/PAK_RetroWii.cs diff --git a/TextureExtraction tool/Properties/AssemblyInfo.cs b/TextureExtraction tool/Properties/AssemblyInfo.cs index b6630415..528517c3 100644 --- a/TextureExtraction tool/Properties/AssemblyInfo.cs +++ b/TextureExtraction tool/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ // Sie können alle Werte angeben oder Standardwerte für die Build- und Revisionsnummern verwenden, // indem Sie "*" wie unten gezeigt eingeben: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.8.5.0")] -[assembly: AssemblyFileVersion("0.8.5.0")] +[assembly: AssemblyVersion("0.8.9.0")] +[assembly: AssemblyFileVersion("0.8.9.0")] diff --git a/lib/AuroraLip/Archives/Formats/PAK_Retro.cs b/lib/AuroraLip/Archives/Formats/PAK_Retro.cs new file mode 100644 index 00000000..fb515d0b --- /dev/null +++ b/lib/AuroraLip/Archives/Formats/PAK_Retro.cs @@ -0,0 +1,150 @@ +using AuroraLip.Common; +using AuroraLip.Compression; +using AuroraLip.Compression.Formats; +using System; +using System.Collections.Generic; +using System.IO; + +namespace AuroraLip.Archives.Formats +{ + /// + /// The .pak format used in Metroid Prime and Metroid Prime 2 + /// + // base https://www.metroid2002.com/retromodding/wiki/PAK_(Metroid_Prime)#Header + public class PAK_Retro : Archive, IFileAccess + { + public bool CanRead => true; + + public bool CanWrite => false; + + public static string Extension => ".pak"; + + public bool IsMatch(Stream stream, in string extension = "") + => Matcher(stream, extension); + + public static bool Matcher(Stream stream, in string extension = "") + => extension.ToLower().Equals(Extension) && stream.ReadUInt16(Endian.Big) == 3 && stream.ReadUInt16(Endian.Big) == 5 && stream.ReadUInt32(Endian.Big) == 0; + + protected override void Read(Stream stream) + { + //Header + ushort VersionMajor = stream.ReadUInt16(Endian.Big); + ushort VersionMinor = stream.ReadUInt16(Endian.Big); + uint Padding = stream.ReadUInt32(Endian.Big); + if (VersionMajor != 3 && VersionMinor != 5 && Padding != 0) + throw new InvalidIdentifierException($"{VersionMajor},{VersionMinor},{Padding}"); + + //NameTabel + uint Sections = stream.ReadUInt32(Endian.Big); + Dictionary NameTable = new Dictionary(); + for (int i = 0; i < Sections; i++) + { + NameEntry entry = new NameEntry(stream); + if (!NameTable.ContainsKey(entry.ID)) + { + NameTable.Add(entry.ID, entry); + } + else + { + Events.NotificationEvent?.Invoke(NotificationType.Warning, $"{nameof(PAK_Retro)},{entry.ID} is already in name table. string:{entry.Name}."); + } + } + + //ResourceTable + Sections = stream.ReadUInt32(Endian.Big); + List AssetTable = new List(); + for (int i = 0; i < Sections; i++) + { + AssetEntry entry = new AssetEntry(stream); + AssetTable.Add(entry); + } + + //DataTable + Root = new ArchiveDirectory() { OwnerArchive = this }; + foreach (AssetEntry entry in AssetTable) + { + string name; + if (NameTable.TryGetValue(entry.ID, out NameEntry nameEntry)) + { + name = $"{entry.ID}_{nameEntry.Name}.{nameEntry.Type}"; + } + else + { + name = $"{entry.ID}.{entry.Type}"; + } + if (Root.Items.ContainsKey(name)) continue; + + if (entry.Compressed) + { + stream.Seek(entry.Offset, SeekOrigin.Begin); + uint DeSize = stream.ReadUInt32(Endian.Big); + Stream es = new SubStream(stream, entry.Size - 4); + //prime 1 = Zlip & prime 2 = LZO1X-999 + if (Compression.IsMatch(es)) + { + es.Seek(0, SeekOrigin.Begin); + ZLib zLib = new ZLib(); + es = zLib.Decompress(es.Read((int)es.Length), (int)DeSize); + } + else + { + es.Seek(0, SeekOrigin.Begin); + Events.NotificationEvent?.Invoke(NotificationType.Warning, $"{nameof(PAK_Retro)},{entry.ID} LZO is not supported."); + name += ".LZO"; + + } + ArchiveFile Sub = new ArchiveFile() { Parent = Root, Name = name, FileData = es }; + Root.Items.Add(Sub.Name, Sub); + } + else + { + Root.AddArchiveFile(stream, entry.Size, entry.Offset, name); + } + } + } + + protected override void Write(Stream ArchiveFile) + { + throw new NotImplementedException(); + } + + private class NameEntry + { + public string Type; + + public uint ID; + + public string Name; + + public NameEntry(Stream stream) + { + Type = stream.ReadString(4); + ID = stream.ReadUInt32(Endian.Big); + int Length = (int)stream.ReadUInt32(Endian.Big); + Name = stream.ReadString(Length); + } + } + + private class AssetEntry + { + public string Type; + + public uint ID; + + public bool Compressed; + + public uint Size; + + public uint Offset; + + public AssetEntry(Stream stream) + { + Compressed = stream.ReadUInt32(Endian.Big) == 1; + Type = stream.ReadString(4); + ID = stream.ReadUInt32(Endian.Big); + Size = stream.ReadUInt32(Endian.Big); + Offset = stream.ReadUInt32(Endian.Big); + } + } + } +} diff --git a/lib/AuroraLip/Archives/Formats/PAK_RetroWii.cs b/lib/AuroraLip/Archives/Formats/PAK_RetroWii.cs new file mode 100644 index 00000000..51265e17 --- /dev/null +++ b/lib/AuroraLip/Archives/Formats/PAK_RetroWii.cs @@ -0,0 +1,220 @@ +using AuroraLip.Common; +using AuroraLip.Compression; +using AuroraLip.Compression.Formats; +using System; +using System.Collections.Generic; +using System.IO; + +namespace AuroraLip.Archives.Formats +{ + /// + /// The .pak format used in Metroid Prime 3 and Donkey Kong Country Returns + /// + // base https://www.metroid2002.com/retromodding/wiki/PAK_(Metroid_Prime_3)#Header + public class PAK_RetroWii : Archive, IFileAccess + { + public bool CanRead => true; + + public bool CanWrite => false; + + public static string Extension => ".pak"; + + public bool IsMatch(Stream stream, in string extension = "") + => Matcher(stream, extension); + + public static bool Matcher(Stream stream, in string extension = "") + => extension.ToLower().Equals(Extension) && stream.ReadUInt32(Endian.Big) == 2; + + + protected override void Read(Stream stream) + { + //Header + uint Version = stream.ReadUInt32(Endian.Big); + uint HeaderSize = stream.ReadUInt32(Endian.Big); + byte[] MD5hash = stream.Read(16); + + stream.Seek(HeaderSize, SeekOrigin.Begin); + + + //Table of Contents + uint Sections = stream.ReadUInt32(Endian.Big); + + uint STRG_SectionSize = 0, RSHD_SectionSize = 0, DATA_SectionSize = 0; + for (int i = 0; i < Sections; i++) + { + switch (stream.ReadString(4)) + { + case "STRG": + STRG_SectionSize = stream.ReadUInt32(Endian.Big); + break; + case "RSHD": + RSHD_SectionSize = stream.ReadUInt32(Endian.Big); + break; + case "DATA": + DATA_SectionSize = stream.ReadUInt32(Endian.Big); + break; + default: + throw new Exception("unknown section"); + } + } + + //NameTabel + stream.Seek(128, SeekOrigin.Begin); + + Sections = stream.ReadUInt32(Endian.Big); + Dictionary NameTable = new Dictionary(); + for (int i = 0; i < Sections; i++) + { + NameEntry entry = new NameEntry(stream); + if (!NameTable.ContainsKey(entry.ID)) + { + NameTable.Add(entry.ID, entry); + } + else + { + Events.NotificationEvent?.Invoke(NotificationType.Warning, $"{nameof(PAK_RetroWii)},{entry.ID} is already in name table. string:{entry.Name}."); + } + } + + //ResourceTable + stream.Seek(128 + STRG_SectionSize, SeekOrigin.Begin); + + Sections = stream.ReadUInt32(Endian.Big); + List AssetTable = new List(); + for (int i = 0; i < Sections; i++) + { + AssetEntry entry = new AssetEntry(stream); + AssetTable.Add(entry); + } + + //DataTable + long DATAStart = 128 + STRG_SectionSize + RSHD_SectionSize; + stream.Seek(DATAStart, SeekOrigin.Begin); + + Root = new ArchiveDirectory() { OwnerArchive = this }; + foreach (AssetEntry entry in AssetTable) + { + string name; + if (NameTable.TryGetValue(entry.ID, out NameEntry nameEntry)) + { + name = $"{entry.ID}_{nameEntry.Name}.{nameEntry.Type}"; + } + else + { + name = $"{entry.ID}.{entry.Type}"; + } + if (Root.Items.ContainsKey(name)) continue; + + if (entry.Compressed) + { + stream.Seek(entry.Offset + DATAStart, SeekOrigin.Begin); + string Type = stream.ReadString(4); + if (Type != "CMPD") + { + Events.NotificationEvent?.Invoke(NotificationType.Warning, $"{nameof(PAK_Retro)},{name} type:{Type} is not known."); + } + uint blocks = stream.ReadUInt32(Endian.Big); + + CMPDEntry[] CMPD = new CMPDEntry[blocks]; + for (int i = 0; i < blocks; i++) + { + CMPD[i] = new CMPDEntry(stream); + } + + //DKCR = Zlip & prime 3 = LZO1X-999 + Stream MS = new MemoryStream(); + for (int i = 0; i < CMPD.Length; i++) + { + if (CMPD[i].DeSize == (int)CMPD[i].CoSize) + { + MS.Write(stream.Read(CMPD[i].DeSize), 0, (int)CMPD[i].DeSize); + continue; + } + Stream es = new SubStream(stream, (int)CMPD[i].CoSize); + if (Compression.IsMatch(es)) + { + es.Seek(0, SeekOrigin.Begin); + ZLib zLib = new ZLib(); + MS.Write(zLib.Decompress(es.Read((int)es.Length), (int)CMPD[i].DeSize).ToArray(), 0, (int)CMPD[i].DeSize); + } + else + { + es.Seek(0, SeekOrigin.Begin); + Events.NotificationEvent?.Invoke(NotificationType.Warning, $"{nameof(PAK_Retro)},{entry.ID} LZO is not supported."); + name += ".LZO"; + MS = es; + + } + } + ArchiveFile Sub = new ArchiveFile() { Parent = Root, Name = name, FileData = MS }; + Root.Items.Add(Sub.Name, Sub); + } + else + { + Root.AddArchiveFile(stream, entry.Size, entry.Offset + DATAStart, name); + } + } + } + + protected override void Write(Stream ArchiveFile) + { + throw new NotImplementedException(); + } + + private class CMPDEntry + { + public byte Flag; + + public UInt24 CoSize; + + public int DeSize; + + public CMPDEntry(Stream stream) + { + Flag = (byte)stream.ReadByte(); + CoSize = stream.ReadUInt24(Endian.Big); + DeSize = stream.ReadInt32(Endian.Big); + } + } + + private class NameEntry + { + + public string Name; + + public string Type; + + public ulong ID; + + public NameEntry(Stream stream) + { + Name = stream.ReadString(); + Type = stream.ReadString(4); + ID = stream.ReadUInt64(Endian.Big); + } + } + + private class AssetEntry + { + public string Type; + + public ulong ID; + + public bool Compressed; + + public uint Size; + + public uint Offset; + + public AssetEntry(Stream stream) + { + Compressed = stream.ReadUInt32(Endian.Big) == 1; + Type = stream.ReadString(4); + ID = stream.ReadUInt64(Endian.Big); + Size = stream.ReadUInt32(Endian.Big); + Offset = stream.ReadUInt32(Endian.Big); + } + } + + } +} diff --git a/lib/AuroraLip/AuroraLip.csproj b/lib/AuroraLip/AuroraLip.csproj index a30fba02..0e3f1581 100644 --- a/lib/AuroraLip/AuroraLip.csproj +++ b/lib/AuroraLip/AuroraLip.csproj @@ -77,6 +77,8 @@ + + diff --git a/lib/AuroraLip/Common/FormatDictionary_List.cs b/lib/AuroraLip/Common/FormatDictionary_List.cs index f1ac5ae2..0ff4e229 100644 --- a/lib/AuroraLip/Common/FormatDictionary_List.cs +++ b/lib/AuroraLip/Common/FormatDictionary_List.cs @@ -1,6 +1,5 @@ using AuroraLip.Archives.Formats; using AuroraLip.Compression.Formats; -using AuroraLip.Archives.Formats; using AuroraLip.Texture.Formats; using System.IO; @@ -118,7 +117,8 @@ public static partial class FormatDictionary #region Second party developer //Retro Studios - new FormatInfo(".PAK", FormatType.Archive, "Retro Archive", Retro_), //GC https://www.metroid2002.com/retromodding/wiki/PAK_(Metroid_Prime)#Header Wii https://www.metroid2002.com/retfromodding/wiki/PAK_(Metroid_Prime_3) + new FormatInfo(".PAK", FormatType.Archive, "Retro Archive", Retro_){ Class = typeof(PAK_Retro), IsMatch = PAK_Retro.Matcher }, //GC https://www.metroid2002.com/retromodding/wiki/PAK_(Metroid_Prime)#Header + new FormatInfo(".PAK", FormatType.Archive, "Retro Wii Archive", Retro_){ Class = typeof(PAK_RetroWii), IsMatch = PAK_RetroWii.Matcher }, //Wii https://www.metroid2002.com/retfromodding/wiki/PAK_(Metroid_Prime_3)#Header new FormatInfo(".TXTR", FormatType.Texture, "Retro Texture", Retro_){ Class = typeof(TXTR) }, new FormatInfo(".AGSC", FormatType.Audio, "Retro sound effect", Retro_), // https://www.metroid2002.com/retromodding/wiki/AGSC_(File_Format) new FormatInfo(".CSMP", FormatType.Audio, "Retro Audio", Retro_), // https://www.metroid2002.com/retromodding/wiki/CSMP_(File_Format) @@ -128,6 +128,7 @@ public static partial class FormatDictionary new FormatInfo(".RULE", "RULE", FormatType.Parameter, "Retro Studios Rule Set", Retro_), new FormatInfo(".SCAN", "SCAN", FormatType.Else, "Metroid Scan", Retro_), new FormatInfo(".FONT", "FONT", FormatType.Font, "Retro Font", Retro_), + new FormatInfo(".MLVL", "Þ¯º¾", FormatType.Font, "Retro World Data", Retro_), new FormatInfo(".ANIM", FormatType.Animation, "Retro animation", Retro_), new FormatInfo(".CSKR", FormatType.Parameter, "Retro Skin Rules", Retro_), new FormatInfo(".STRG", FormatType.Text, "Retro String Table", Retro_), @@ -334,7 +335,7 @@ public static bool ADX_Matcher(Stream stream, in string extension = "") if (stream.ReadByte() != 128 || stream.ReadByte() != 0) return false; ushort CopyrightOffset = stream.ReadUInt16(Endian.Big); - stream.Seek(CopyrightOffset-2, SeekOrigin.Begin); + stream.Seek(CopyrightOffset - 2, SeekOrigin.Begin); return stream.MatchString("(c)CRI"); } }