diff --git a/ArcFormats/TopCat/ArcTCD3.cs b/ArcFormats/TopCat/ArcTCD3.cs index 37f5fc5d..c361dfaf 100644 --- a/ArcFormats/TopCat/ArcTCD3.cs +++ b/ArcFormats/TopCat/ArcTCD3.cs @@ -34,12 +34,16 @@ namespace GameRes.Formats.TopCat { internal class TcdSection { + public string Extension; public uint DataSize; public uint IndexOffset; public int DirCount; public int DirNameLength; public int FileCount; public int FileNameLength; + + public int DirNamesSize; + public int FileNamesSize; } internal struct TcdDirEntry @@ -49,29 +53,9 @@ namespace GameRes.Formats.TopCat public int FirstIndex; } - internal class TcdEntry : AutoEntry + internal class TcdEntry : Entry { public int Index; - - public TcdEntry (int index, string name, ArcView file, long offset) - : base (name, () => DetectFileType (file, offset)) - { - Index = index; - Offset = offset; - } - - static readonly Lazy SpdcFormat = new Lazy (() => ImageFormat.FindByTag ("SPD")); - - private static IResource DetectFileType (ArcView file, long offset) - { - uint signature = file.View.ReadUInt32 (offset); - byte spdc_key = (byte)(signature - 'S'); - if ('P' == (((signature >> 8) - spdc_key) & 0xFF) && - 'D' == (((signature >> 16) - spdc_key) & 0xFF) && - 'C' == (((signature >> 24) - spdc_key) & 0xFF)) - return SpdcFormat.Value; - return AutoEntry.DetectFileType (signature); - } } internal class TcdArchive : ArcFile @@ -93,7 +77,7 @@ namespace GameRes.Formats.TopCat [Export(typeof(ArchiveFormat))] public class TcdOpener : ArchiveFormat { - public override string Tag { get { return "TCD3"; } } + public override string Tag { get { return "TCD"; } } public override string Description { get { return "TopCat data archive"; } } public override uint Signature { get { return 0x33444354; } } // 'TCD3' public override bool IsHierarchic { get { return true; } } @@ -101,7 +85,7 @@ namespace GameRes.Formats.TopCat public TcdOpener () { - Extensions = new string[] { "tcd" }; + Signatures = new uint[] { 0x32444354, 0x33444354 }; // 'TCD2', 'TCD3' } public static Dictionary KnownKeys = new Dictionary(); @@ -114,133 +98,71 @@ namespace GameRes.Formats.TopCat public override ArcFile TryOpen (ArcView file) { - int count = file.View.ReadInt32 (4); - if (!IsSaneCount (count)) - return null; - - uint current_offset = 8; - var sections = new List (5); - for (int i = 0; i < 5; ++i, current_offset += 0x20) + int version = file.View.ReadByte (3) - '0'; + TcdIndexReader reader; + if (2 == version) + reader = new TcdReaderV2 (file); + else + reader = new TcdReaderV3 (file); + using (reader) { - uint index_offset = file.View.ReadUInt32 (current_offset+4); - if (0 == index_offset) - continue; - var section = new TcdSection - { - IndexOffset = index_offset, - DataSize = file.View.ReadUInt32 (current_offset), - DirCount = file.View.ReadInt32 (current_offset+8), - DirNameLength = file.View.ReadInt32 (current_offset+0x0C), - FileCount = file.View.ReadInt32 (current_offset+0x10), - FileNameLength = file.View.ReadInt32 (current_offset+0x14), - }; - sections.Add (section); - } - - var list = new List (count); - foreach (var section in sections) - { - current_offset = section.IndexOffset; - uint dir_size = (uint)(section.DirCount * section.DirNameLength); - var dir_names = new byte[dir_size]; - if (dir_size != file.View.Read (current_offset, dir_names, 0, dir_size)) + var dir = reader.ReadIndex(); + if (null == dir) return null; - current_offset += dir_size; - DecryptNames (dir_names, section.DirNameLength); - - var dirs = new TcdDirEntry[section.DirCount]; - for (int i = 0; i < dirs.Length; ++i) - { - dirs[i].FileCount = file.View.ReadInt32 (current_offset); - dirs[i].NamesOffset = file.View.ReadInt32 (current_offset+4); - dirs[i].FirstIndex = file.View.ReadInt32 (current_offset+8); - current_offset += 0x10; - } - - uint entries_size = (uint)(section.FileCount * section.FileNameLength); - var file_names = new byte[entries_size]; - if (entries_size != file.View.Read (current_offset, file_names, 0, entries_size)) - return null; - current_offset += entries_size; - DecryptNames (file_names, section.FileNameLength); - - var offsets = new uint[section.FileCount + 1]; - for (int i = 0; i < offsets.Length; ++i) - { - offsets[i] = file.View.ReadUInt32 (current_offset); - current_offset += 4; - } - - int dir_name_offset = 0; - foreach (var dir in dirs) - { - string dir_name = Binary.GetCString (dir_names, dir_name_offset, section.DirNameLength); - dir_name_offset += section.DirNameLength; - int index = dir.FirstIndex; - int name_offset = dir.NamesOffset; - for (int i = 0; i < dir.FileCount; ++i) - { - string name = Binary.GetCString (file_names, name_offset, section.FileNameLength); - name_offset += section.FileNameLength; - name = dir_name + '\\' + name; - var entry = new TcdEntry (index, name, file, offsets[index]); - entry.Size = offsets[index+1] - offsets[index]; - ++index; - list.Add (entry); - } - } + return new TcdArchive (file, this, dir); } - return new TcdArchive (file, this, list); - } - - private void DecryptNames (byte[] buffer, int name_length) - { - byte key = buffer[name_length-1]; - for (int i = 0; i < buffer.Length; ++i) - buffer[i] -= key; } public override Stream OpenEntry (ArcFile arc, Entry entry) { var tcde = entry as TcdEntry; var tcda = arc as TcdArchive; - if (null == tcde || null == tcda || entry.Size <= 0x14) + if (null == tcde || null == tcda) return arc.File.CreateStream (entry.Offset, entry.Size); - int signature = arc.File.View.ReadInt32 (entry.Offset); - if (0x43445053 == signature) // 'SPDC' - return arc.File.CreateStream (entry.Offset, entry.Size); - if (0x5367674F == signature) // 'OggS' + if (entry.Name.EndsWith (".SPD", StringComparison.InvariantCultureIgnoreCase)) + return OpenSpdc (tcda, tcde); + if (entry.Name.EndsWith (".OGG", StringComparison.InvariantCultureIgnoreCase)) return RestoreOggStream (arc, entry); + if (entry.Name.EndsWith (".TSF", StringComparison.InvariantCultureIgnoreCase) || + entry.Name.EndsWith (".TCT", StringComparison.InvariantCultureIgnoreCase)) + return OpenScript (tcda, tcde); + return arc.File.CreateStream (entry.Offset, entry.Size); + } - var header = new byte[0x14]; - arc.File.View.Read (entry.Offset, header, 0, 0x14); + Stream OpenSpdc (TcdArchive arc, TcdEntry entry) + { + int signature = arc.File.View.ReadInt32 (entry.Offset); + if (0x43445053 == signature || entry.Size <= 0x14) // 'SPDC' + return arc.File.CreateStream (entry.Offset, entry.Size); + + var header = arc.File.View.ReadBytes (entry.Offset, 0x14); byte header_key = (byte)(header[0x12] + header[0x10]); header[0] -= header_key; header[1] -= header_key; header[2] -= header_key; header[3] -= header_key; - bool spdc_entry = Binary.AsciiEqual (header, "SPDC"); + bool spdc_entry = Binary.AsciiEqual (header, "SPD") && (header[3] == 'C' || header[3] == '8'); if (!spdc_entry) { LittleEndian.Pack (signature, header, 0); - if (null == tcda.Key) + if (null == arc.Key) { - foreach (var key in KnownKeys.Values) + foreach (var key in TcdOpener.KnownKeys.Values) { - int first = signature + key * (tcde.Index + 3); + int first = signature + key * (entry.Index + 3); if (0x43445053 == first) // 'SPDC' { - tcda.Key = key; + arc.Key = key; spdc_entry = true; break; } } } - else if (0x43445053 == signature + tcda.Key.Value * (tcde.Index + 3)) + else if (0x43445053 == (signature + arc.Key.Value * (entry.Index + 3))) { spdc_entry = true; } - if (spdc_entry && 0 != tcda.Key.Value) + if (spdc_entry && 0 != arc.Key.Value) { unsafe { @@ -248,21 +170,18 @@ namespace GameRes.Formats.TopCat { int* dw = (int*)raw; for (int i = 0; i < 5; ++i) - dw[i] += tcda.Key.Value * (tcde.Index + 3 + i); + dw[i] += arc.Key.Value * (entry.Index + 3 + i); } } } - - if (!spdc_entry && entry.Name.StartsWith ("TXT\\", StringComparison.InvariantCultureIgnoreCase) - && signature > 0 && signature < 0x01000000) - return OpenScript (tcda, tcde, signature); } var rest = arc.File.CreateStream (entry.Offset+0x14, entry.Size-0x14); return new PrefixStream (header, rest); } - Stream OpenScript (TcdArchive arc, TcdEntry entry, int unpacked_size) + Stream OpenScript (TcdArchive arc, TcdEntry entry) { + int unpacked_size = arc.File.View.ReadInt32 (entry.Offset); byte[] data = new byte[unpacked_size]; using (var input = arc.File.CreateStream (entry.Offset+4, entry.Size-4)) UnpackLz (input, data); @@ -336,4 +255,185 @@ namespace GameRes.Formats.TopCat return new MemoryStream (data); } } + + internal abstract class TcdIndexReader : IDisposable + { + BinaryReader m_input; + int m_section_count; + + public int Count { get; private set; } + protected BinaryReader Input { get { return m_input; } } + + protected TcdIndexReader (ArcView file, int section_count) + { + Count = file.View.ReadInt32 (4); + var input = file.CreateStream(); + m_input = new BinaryReader (input); + m_section_count = section_count; + } + + protected string[] Extensions = { ".TCT", ".TSF", ".SPD", ".OGG", ".WAV" }; + + public List ReadIndex () + { + if (!ArchiveFormat.IsSaneCount (Count)) + return null; + + var sections = ReadSections (m_section_count); + var list = new List (Count); + foreach (var section in sections) + { + m_input.BaseStream.Position = section.IndexOffset; + var dir_names = m_input.ReadBytes (section.DirNamesSize); + if (section.DirNamesSize != dir_names.Length) + return null; + byte section_key = dir_names[section.DirNameLength-1]; + DecryptNames (dir_names, section_key); + + var dirs = new TcdDirEntry[section.DirCount]; + for (int i = 0; i < dirs.Length; ++i) + { + dirs[i].FileCount = m_input.ReadInt32(); + dirs[i].NamesOffset = m_input.ReadInt32(); + dirs[i].FirstIndex = m_input.ReadInt32(); + m_input.ReadInt32(); + } + var file_names = m_input.ReadBytes (section.FileNamesSize); + if (file_names.Length != section.FileNamesSize) + return null; + DecryptNames (file_names, section_key); + + var offsets = new uint[section.FileCount + 1]; + for (int i = 0; i < offsets.Length; ++i) + { + offsets[i] = m_input.ReadUInt32(); + } + + int dir_name_offset = 0; + foreach (var dir in dirs) + { + string dir_name = GetName (dir_names, section.DirNameLength, ref dir_name_offset); + int index = dir.FirstIndex; + int name_offset = dir.NamesOffset; + for (int i = 0; i < dir.FileCount; ++i) + { + string name = GetName (file_names, section.FileNameLength, ref name_offset); + name = Path.Combine (dir_name, name); + name = Path.ChangeExtension (name, section.Extension); + var entry = FormatCatalog.Instance.Create (name); + entry.Offset = offsets[index]; + entry.Size = offsets[index+1] - offsets[index]; + entry.Index = index; + ++index; + list.Add (entry); + } + } + } + return list; + } + + IList ReadSections (int count) + { + var sections = new List (count); + uint current_offset = 8; + for (int i = 0; i < count; ++i) + { + m_input.BaseStream.Position = current_offset; + var section = ReadSection (i); + if (section != null) + sections.Add (section); + current_offset += 0x20; + } + return sections; + } + + void DecryptNames (byte[] buffer, byte key) + { + for (int i = 0; i < buffer.Length; ++i) + buffer[i] -= key; + } + + protected abstract TcdSection ReadSection (int number); + + protected abstract string GetName (byte[] names, int name_length, ref int offset); + + #region IDisposable Members + bool _disposed = false; + public void Dispose () + { + if (!_disposed) + { + m_input.Dispose(); + _disposed = true; + } + } + #endregion + } + + internal class TcdReaderV2 : TcdIndexReader + { + public TcdReaderV2 (ArcView file) : base (file, 4) + { + } + + protected override TcdSection ReadSection (int number) + { + uint data_size = Input.ReadUInt32(); + if (0 == data_size) + return null; + var section = new TcdSection { DataSize = data_size }; + section.Extension = Extensions[number]; + section.FileCount = Input.ReadInt32(); + section.DirCount = Input.ReadInt32(); + section.IndexOffset = Input.ReadUInt32(); + section.DirNameLength = Input.ReadInt32(); + section.FileNameLength = Input.ReadInt32(); + section.DirNamesSize = section.DirNameLength; + section.FileNamesSize = section.FileNameLength; + return section; + } + + protected override string GetName (byte[] names, int name_length, ref int offset) + { + int name_end = Array.IndexOf (names, 0, offset); + if (-1 == name_end) + name_end = names.Length; + name_length = name_end - offset; + string name = Encodings.cp932.GetString (names, offset, name_length); + offset += name_length + 1; + return name; + } + } + + internal class TcdReaderV3 : TcdIndexReader + { + public TcdReaderV3 (ArcView file) : base (file, 5) + { + } + + protected override TcdSection ReadSection (int number) + { + uint data_size = Input.ReadUInt32(); + uint index_offset = Input.ReadUInt32(); + if (0 == index_offset) + return null; + var section = new TcdSection { DataSize = data_size }; + section.Extension = Extensions[number]; + section.IndexOffset = index_offset; + section.DirCount = Input.ReadInt32(); + section.DirNameLength = Input.ReadInt32(); + section.FileCount = Input.ReadInt32(); + section.FileNameLength = Input.ReadInt32(); + section.DirNamesSize = section.DirNameLength * section.DirCount; + section.FileNamesSize = section.FileNameLength * section.FileCount; + return section; + } + + protected override string GetName (byte[] names, int name_length, ref int offset) + { + string name = Binary.GetCString (names, offset, name_length); + offset += name_length; + return name; + } + } } diff --git a/supported.html b/supported.html index 5c65fb4f..3767a560 100644 --- a/supported.html +++ b/supported.html @@ -276,6 +276,7 @@ Ouka Ryouran
Oyako Ninjutsu Kunoichi PonPon!!
RGH ~Koi to Hero to Gakuen to~
Riding Incubus
+Rui wa Tomo o Yobu
Seirei Tenshou
Se-kirara
Sharin no Kuni, Himawari no Shoujo
@@ -291,12 +292,14 @@ Zettai Karen! Ojou-sama
*.tlgTLG0.0
TLG5.0
TLG6.0No *.ypfYPFYesYU-RIS Eroge! ~H mo Game mo Kaihatsu Zanmai~
+Koi Mekuri Clover
Mamono Musume-tachi to no Rakuen ~Slime & Scylla~
Mashou no Nie 3 ~Hakudaku no Umi ni Shizumu Injoku no Reiki~
Ryuuyoku no Melodia -Diva with the blessed dragonol-
Sei Monmusu Festival!!
Shin Chikan Ou
Unionism Quartet
+Usotsuki Ouji to Nayameru Ohime-sama -Princess Syndrome-
*.ycgYCGNo *.isaISM ARCHIVEDNoISMGreen ~Akizora no Screen~ @@ -333,6 +336,7 @@ Oshaburi Announcer
Sakura Machizaka Stories vol.1
Sakura Machizaka Stories vol.2
Shoujo Senki Soul Eater
+Trouble Succubus
*.prsYBNo *.wayWADYNo @@ -415,6 +419,7 @@ Itsuka, Dokoka de ~Ano Ameoto no Kioku~2.36 or 2.37ShiinaRio v2.41
Mahou Shoujo no Taisetsu na Koto ShiinaRio v2.47
Maki Fes! ShiinaRio v2.50
+Mimi o Sumaseba ShiinaRio v2.47
Nagagutsu wo Haita Deco ShiinaRio v2.39
Najimi no Oba-chan ShiinaRio v2.47
Niizuma to Yuukaihan ShiinaRio v2.45
@@ -741,6 +746,7 @@ Gigai no Alruna
sys4ini.bin
sys3ini.bin
*.alfS4IC
S3ICNoEushully Kuutei Senki ~Tasogare ni Shizumu Kusabi~
+Mahou ga Sekai o Sukuimasu
Meishoku no Reiki
Soukai no Oujo-tachi
Soukai no Valkyria
@@ -756,14 +762,15 @@ Saiminjutsu Re
Wizard Links
*.medMDNo -*.tcdTCD3NoTopCat +*.tcdTCD2
TCD3NoTopCat Atori no Sora to Shinchuu no Tsuki
Favorite Sweet!
Nanapuri
Sabae no Ou Scenario II
Soushinjutsu 3
+Wakan Kazoku
-*.spdSPDCNo +*.spdSPDC
SPD8No *.datCHE00
MYK00NoCherry Soft Blood Royal
Heroine
@@ -1051,6 +1058,7 @@ Gakkou Yarashii Kaidan
Hanamaru! 2
Hime Kami 1/2
In'youchuu Goku ~Ryoujoku Jigoku Taimaroku~
+In'youchuu Kyou ~Ryoujoku Byoutou Taimaroku~
In'youchuu Rei ~Ryoujoku Shiro Taima Emaki~
In'youchuu Shoku ~Ryoushokutou Taimaroku~
Koitsuma Biyori ~Yukino-san wa Hitozuma Kanrinin~
@@ -1140,10 +1148,11 @@ Triangle Heart 1-2-3
Pretty Devil Paradise ~Millium Makai Dakkan Shirei~
S Sensei no Koto
-*.epkEPKNoTamaSoft +*.epkEPKNoTamaSoft Lost Child
-*.surESURNo +*.surESURNo +*.esdESDNo

1 Non-encrypted only