using Syroot.BinaryData; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using Toolbox.Library; using Toolbox.Library.IO; namespace FirstPlugin { class NARC : IArchiveFile, IFileFormat { public FileType FileType { get; set; } = FileType.Archive; public bool CanSave { get; set; } = true; public string[] Description { get; set; } = new string[] { "Nitro Archive (NARC)" }; public string[] Extension { get; set; } = new string[] { "*.narc" }; public string FileName { get; set; } public string FilePath { get; set; } public IFileInfo IFileInfo { get; set; } public Type[] Types => new Type[0]; public bool CanAddFiles { get; set; } = true; public bool CanRenameFiles { get; set; } = true; public bool CanReplaceFiles { get; set; } = true; public bool CanDeleteFiles { get; set; } = true; public IEnumerable Files => files; // private private Header header; private byte[] bfntUnk = new byte[] { 0x00, 0x00, 0x01, 0x00 }; private List files = new List(); public bool AddFile(ArchiveFileInfo archiveFileInfo) { files.Add(new FileEntry(this) { FileData = archiveFileInfo.FileData, FileName = archiveFileInfo.FileName, }); return true; } public void ClearFiles() => files.Clear(); public bool DeleteFile(ArchiveFileInfo archiveFileInfo) { return files.Remove((FileEntry)archiveFileInfo); } public bool Identify(Stream stream) { using(FileReader reader = new FileReader(stream, true)) return reader.CheckSignature(4, "NARC"); } public void Load(Stream stream) { using(FileReader reader = new FileReader(stream)) { //DirectoryEntry currentDirEntry = rootDir; string currentDir = string.Empty; header = new Header(reader); reader.ReadSignature(4, "BTAF"); // The positions where the sections' reading was left last time. long bfatIndex = reader.Position + 8; long bfntIndex = reader.Position + reader.ReadUInt32(); // From the BFAT length. long fimgIndex; uint fileCount = reader.ReadUInt32(); uint currentFileOffset; uint currentFileEnd; reader.Position = bfntIndex; fimgIndex = reader.Position + reader.ReadUInt32() + 4; // Sets FIMG section begining. bfntUnk = reader.ReadBytes(reader.ReadInt32() - 4); for(int i = 0; i < fileCount; i++) { byte nameLength = reader.ReadByte(); if(nameLength == 0x00) { // End of the "folder". List slashIndices = new List(); for(int j = 0; j < currentDir.Length; j++) if(currentDir[j] == '/') slashIndices.Add(j); int lastSlash = slashIndices.Last(); slashIndices.Remove(lastSlash); int slashBeforeLast = slashIndices.Count > 0 ? slashIndices.Last() : 0; currentDir = currentDir.Remove(slashBeforeLast + 1); if(currentDir.Length == 1) currentDir = string.Empty; i--; continue; } if(nameLength >= 0x80) { // If it is a "folder". // Disable file modifications: CanSave = false; CanAddFiles = false; CanRenameFiles = false; CanReplaceFiles = false; CanDeleteFiles = false; string dirName = reader.ReadString(nameLength & 0x7F); currentDir += dirName + "/"; reader.Position += 2; i--; continue; } // Read BFAT section: using(reader.TemporarySeek()) { reader.Position = bfatIndex; currentFileOffset = reader.ReadUInt32(); currentFileEnd = reader.ReadUInt32(); bfatIndex = reader.Position; } string name = reader.ReadString(nameLength); using(reader.TemporarySeek()) { reader.Position = fimgIndex + currentFileOffset; files.Add(new FileEntry(this) { FileName = currentDir + name, BlockData = reader.ReadBytes((int) (currentFileEnd - currentFileOffset)) }); } } } } public class FileEntry : ArchiveFileInfo { public byte[] BlockData; public override Dictionary ExtensionImageKeyLookup { get { return new Dictionary() { {".cbfmd", "bfres" }, {".cbfa", "bfres" }, {".cbfsa", "bfres" }, {".cbntx", "bntx" }, }; } } public NARC ArchiveFile; public FileEntry(NARC narc) { ArchiveFile = narc; } public override byte[] FileData { get { return DecompressBlock(); } set { BlockData = value; } } private bool IsTexturesLoaded = false; public override IFileFormat OpenFile() { var FileFormat = base.OpenFile(); bool IsModel = FileFormat is BFRES; if (IsModel && !IsTexturesLoaded) { IsTexturesLoaded = true; foreach (var file in ArchiveFile.Files) { if (Utils.GetExtension(file.FileName) == ".ctex") { file.FileFormat = file.OpenFile(); } } } return base.OpenFile(); } private byte[] DecompressBlock() { byte[] data = BlockData; var reader = new FileReader(data); reader.ByteOrder = Syroot.BinaryData.ByteOrder.BigEndian; byte compType = reader.ReadByte(); if (compType == 0x50) { reader.Seek(4, System.IO.SeekOrigin.Begin); uint decompSize = reader.ReadUInt32(); uint compSize = (uint)reader.BaseStream.Length - 8; var comp = new STLibraryCompression.MTA_CUSTOM(); return comp.Decompress(data, decompSize); } else if (compType == 0x30 || compType == 0x20) { uint decompSize = reader.ReadUInt32(); uint compSize = (uint)reader.BaseStream.Length - 16; reader.SeekBegin(16); ushort signature = reader.ReadUInt16(); bool IsGZIP = signature == 0x1F8B; bool IsZLIB = signature == 0x789C || signature == 0x78DA; byte[] filedata = reader.getSection(16, (int)compSize); reader.Close(); reader.Dispose(); if (IsGZIP) data = STLibraryCompression.GZIP.Decompress(filedata); else data = STLibraryCompression.ZLIB.Decompress(filedata, false); } return data; } } public void Unload() { } internal class Header { public ushort ByteOrder; public ushort Version; public uint FileSize; public ushort HeaderSize; public ushort DataBlocks; public Header(FileReader reader) { reader.ByteOrder = Syroot.BinaryData.ByteOrder.LittleEndian; reader.ReadSignature(4, "NARC"); ByteOrder = reader.ReadUInt16(); reader.CheckByteOrderMark(ByteOrder); Version = reader.ReadUInt16(); FileSize = reader.ReadUInt32(); HeaderSize = reader.ReadUInt16(); DataBlocks = reader.ReadUInt16(); } } // Mostly everything from this point was imported from: // https://github.com/Jenrikku/NARCSharp public void Save(Stream stream) { using(BinaryDataWriter writer = new BinaryDataWriter(stream)) { #region Header writer.Write("NARC", BinaryStringFormat.NoPrefixOrTermination, Encoding.ASCII); // Magic. writer.ByteOrder = (ByteOrder) header.ByteOrder; writer.Write((ushort) 0xFFFE); // ByteOrder. writer.Write(header.Version); // Version. long headerLengthPorsition = writer.Position; writer.Position += 4; // Skips length writing. writer.Write(header.HeaderSize); // Header length. writer.Write(header.DataBlocks); // Section count. #endregion #region BTAF preparation long btafPosition = WriteSectionHeader("BTAF"); // Header. writer.Write((uint) files.Count); // File count. for(uint i = 0; i < files.Count; i++) // Reads unset bytes per file. (Reserved space for later) writer.Position += 8; WriteSectionLength(btafPosition); #endregion #region BTNF long btnfPosition = WriteSectionHeader("BTNF"); // Header. writer.Write(bfntUnk.Length + 4); writer.Write(bfntUnk); foreach(ArchiveFileInfo file in files) writer.Write(file.FileName, BinaryStringFormat.ByteLengthPrefix); writer.Write((byte) 0x00); writer.Align(128); WriteSectionLength(btnfPosition); #endregion #region GMIF long gmifPosition = WriteSectionHeader("GMIF"); // Header. long btafCurrentPosition = btafPosition + 12; // First offset-size position. (BTAF) foreach(ArchiveFileInfo file in files) { WriteBTAFEntry(); // BTAF offset writer.Write(file.FileData); WriteBTAFEntry(); // BTAF size. writer.Align(128); } WriteSectionLength(gmifPosition); #endregion writer.Position = headerLengthPorsition; writer.Write((uint) writer.BaseStream.Length); // Total file length. long WriteSectionHeader(string magic) { long startPosition = writer.Position; writer.Write(magic, BinaryStringFormat.NoPrefixOrTermination, Encoding.ASCII); // Magic. writer.Position += 4; // Skips length position. return startPosition; } void WriteSectionLength(long startPosition) { using(writer.TemporarySeek()) { long finalLength = (uint) writer.Position; writer.Position = startPosition + 4; writer.Write((uint) (finalLength - startPosition)); } } void WriteBTAFEntry() { uint value = (uint) (writer.Position - (gmifPosition + 8)); using(writer.TemporarySeek()) { writer.Position = btafCurrentPosition; writer.Write(value); } btafCurrentPosition += 4; } } } } }