c928879082
* Fixed multiple issues with NARC library - Support for NARC files with folders (read-only) - Fixed alignment of files. - Optimised loading. * Quick fixes * Cleanup
353 lines
12 KiB
C#
353 lines
12 KiB
C#
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<ArchiveFileInfo> Files => files;
|
|
|
|
// private
|
|
private Header header;
|
|
private byte[] bfntUnk = new byte[] { 0x00, 0x00, 0x01, 0x00 };
|
|
|
|
private List<FileEntry> files = new List<FileEntry>();
|
|
|
|
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<int> slashIndices = new List<int>();
|
|
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<string, string> ExtensionImageKeyLookup
|
|
{
|
|
get
|
|
{
|
|
return new Dictionary<string, string>()
|
|
{
|
|
{".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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|