static async Task Decompress(byte[] bts, string dir, NItem item) { if (item.IsCompressed == 0 || (item.Unk4 == item.Unk5 && item.OriginalLength == item.CompressedLength)) { //Console.WriteLine($"Unpack {item.Id:X8}"); var header = bts.Take(32).ToArray(); var fileName = InferExtension(header, item, out var category); Console.WriteLine($"Unpack: {fileName}"); var path = Path.Combine(dir, category); if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } using (var fs = File.Create(Path.Combine(path, fileName))) { await fs.WriteAsync(bts, 0, bts.Length); } } else { //Console.WriteLine($"Decompress: {item.Id:X8}"); using (var output = new MemoryStream((int)item.OriginalLength)) { try { using (var ms = new MemoryStream(bts, 2, bts.Length - 2)) using (var zs = new DeflateStream(ms, CompressionMode.Decompress, false)) { await zs.CopyToAsync(output); output.Position = 0; } } catch (InvalidDataException e) { Console.WriteLine($"Decompress {item.Id:X8}({item.Offset}) failed."); } var header = new byte[32]; await output.ReadAsync(header, 0, header.Length); var fileName = InferExtension(header, item, out var category); Console.WriteLine($"Decompress: {fileName}"); var path = Path.Combine(dir, category); if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } output.Position = 0; using (var fs = File.Create(Path.Combine(path, fileName))) { await output.CopyToAsync(fs); } } } }
static void Unpack(string npk, string dir) { if (!File.Exists(npk)) { Console.WriteLine("Can not find npk file."); return; } if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } var fs = File.OpenRead(npk); var br = new BinaryReader(fs); var len = fs.Length; fs.Seek(20, SeekOrigin.Begin); var mapOffset = br.ReadUInt32(); //begin: res bytes, end: map List <NItem> items = new List <NItem>(); fs.Seek(mapOffset, SeekOrigin.Begin); while (fs.Position + 4 * 7 <= len) { var item = new NItem { Id = br.ReadUInt32(), Offset = br.ReadUInt32(), CompressedLength = br.ReadUInt32(), OriginalLength = br.ReadUInt32(), Unk4 = br.ReadUInt32(), Unk5 = br.ReadUInt32(), IsCompressed = br.ReadUInt32(), }; //if (item.Id == 0x694FDF33 || item.Unk4 == 0x694FDF33 || item.Unk5 == 0x694FDF33) //{ // Console.WriteLine("maybe have name"); //} items.Add(item); } items.Sort((o1, o2) => (int)((long)o1.Offset - (long)o2.Offset)); List <Task> tasks = new List <Task>(items.Count); foreach (var it in items) { fs.Seek(it.Offset, SeekOrigin.Begin); var bts = br.ReadBytes((int)it.CompressedLength); tasks.Add(Decompress(bts, dir, it)); } Task.WaitAll(tasks.ToArray()); Console.WriteLine("Done."); br.Dispose(); }
static string InferExtension(byte[] header, NItem item, out string category) { var result = _sniffer.Match(header); if (result.Count == 0) { var headerStr = Encoding.ASCII.GetString(header); if (headerStr.StartsWith("KTX") || headerStr.StartsWith("«KTX")) { category = "texture"; return($"{item.Id:X8}.ktx"); } if (headerStr.StartsWith("PKM")) { category = "texture"; return($"{item.Id:X8}.pkm"); } if (headerStr.StartsWith("RGIS")) { category = "rgis"; return($"{item.Id:X8}.rgis"); } if (headerStr.StartsWith("<NeoX") || headerStr.StartsWith("<Neox")) { category = "NeoXml"; return($"{item.Id:X8}.NeoX.xml"); } if (headerStr.StartsWith("<FxGroup")) { category = "NeoXml"; return($"{item.Id:X8}.FxGroup.xml"); } if (headerStr.StartsWith("<SceneMusic")) { category = "NeoXml"; return($"{item.Id:X8}.SceneMusic.xml"); } if (headerStr.StartsWith("<MusicTriggers")) { category = "NeoXml"; return($"{item.Id:X8}.MusicTriggers.xml"); } if (headerStr.StartsWith("<cinematic")) { category = "NeoXml"; return($"{item.Id:X8}.cinematic.xml"); } if (headerStr.StartsWith("<EquipList")) { category = "NeoXml"; return($"{item.Id:X8}.EquipList.xml"); } if (headerStr.StartsWith("<SceneConfig")) { category = "NeoXml"; return($"{item.Id:X8}.SceneConfig.xml"); } if (headerStr.StartsWith("<SceneRoad")) { category = "NeoXml"; return($"{item.Id:X8}.SceneRoad.xml"); } if (headerStr.StartsWith("v ") || headerStr.StartsWith("vt ") || headerStr.StartsWith("f ") ) { category = "model"; return($"{item.Id:X8}.obj"); } if (headerStr.StartsWith("CocosStudio-UI")) { category = "ui"; return($"{item.Id:X8}.csb"); } if (headerStr.StartsWith("vec4") || headerStr.StartsWith("vec2") || headerStr.StartsWith("tex2D") || headerStr.StartsWith("tex3D") || headerStr.StartsWith("float") || headerStr.StartsWith("define") || headerStr.StartsWith("incloud") || headerStr.StartsWith("#if") || headerStr.StartsWith("#define") || headerStr.StartsWith("int ") || headerStr.StartsWith("precision ") ) { category = "shader"; return($"{item.Id:X8}.glsl"); } if (headerStr.StartsWith("{")) { category = "json"; return($"{item.Id:X8}.json"); } if (header.Length >= 32 && header[0] != 0 && header.Skip(16).Take(16).All(b => b == 0)) { category = "unknown"; return($"{item.Id:X8}.unkAnim"); } category = ""; return(item.Id.ToString("X8")); } var ext = result[0]; switch (ext) { case "jpg": case "jpeg": case "png": case "gif": case "ico": case "bmp": case "psd": category = "image"; break; case "xml": category = "xml"; break; case "ktx": category = "texture"; break; case "unkModel": category = "unknown"; break; default: category = ""; break; } return($"{item.Id:X8}.{ext}"); }