mirror of
https://github.com/sampletext32/ParkanPlayground.git
synced 2026-08-15 02:57:49 +04:00
fpopov thanks
This commit is contained in:
+28
-27
@@ -1,4 +1,4 @@
|
||||
namespace NResLib;
|
||||
namespace NResLib;
|
||||
|
||||
/// <summary>
|
||||
/// Архив NRes (файл NRes)
|
||||
@@ -8,40 +8,41 @@ public record NResArchive(NResArchiveHeader Header, List<ListMetadataItem> Files
|
||||
/// <summary>
|
||||
/// Заголовок файла
|
||||
/// </summary>
|
||||
/// <param name="NRes">[0..4] ASCII NRes</param>
|
||||
/// <param name="Version">[4..8] Версия кодировщика (должно быть всегда 0x100)</param>
|
||||
/// <param name="FileCount">[8..12] Количество файлов </param>
|
||||
/// <param name="TotalFileLengthBytes">[12..16] Длина всего архива</param>
|
||||
/// <param name="NRes">[0..4] ASCII NRes.</param>
|
||||
/// <param name="Version">[4..8] Версия кодировщика, обычно 0x100.</param>
|
||||
/// <param name="FileCount">[8..12] Количество записей каталога.</param>
|
||||
/// <param name="TotalFileLengthBytes">[12..16] Длина всего архива.</param>
|
||||
public record NResArchiveHeader(string NRes, int Version, int FileCount, int TotalFileLengthBytes);
|
||||
|
||||
/// <summary>
|
||||
/// В конце файла есть список метаданных,
|
||||
/// каждый элемент это 64 байта,
|
||||
/// найти начало можно как (Header.TotalFileLengthBytes - Header.FileCount * 64)
|
||||
/// 64-байтная запись каталога NRes.
|
||||
///
|
||||
/// Важный нюанс RE-имен:
|
||||
/// - ElementCount/Magic1/ElementSize оставлены как удобные имена для исследования, но на уровне
|
||||
/// общего NRes-контейнера это attr1/attr2/attr3. Их смысл зависит от TypeId ресурса.
|
||||
/// - FileName на диске занимает 36 байт: [20..56]. Старый разбор читал только [20..40].
|
||||
/// - Старые Magic3..Magic6 оказались не отдельными полями формата, а байтами хвоста name_raw.
|
||||
/// - SortIndex на диске это перестановка для бинарного поиска по имени, а не порядковый
|
||||
/// индекс записи каталога. Порядковый индекс хранится отдельно в DirectoryIndex.
|
||||
/// </summary>
|
||||
/// <param name="FileType">[0..4] ASCII описание типа файла, например TEXM или MAT0</param>
|
||||
/// <param name="ElementCount">[4..8] Количество элементов в файле (если файл составной, например .trf) или версия, например у материалов или флаги</param>
|
||||
/// <param name="Magic1">[8..12] Неизвестное число</param>
|
||||
/// <param name="FileLength">[12..16] Длина файла в байтах</param>
|
||||
/// <param name="ElementSize">[16..20] Размер элемента в файле (если файл составной, например .trf) </param>
|
||||
/// <param name="FileName">[20..40] ASCII имя файла</param>
|
||||
/// <param name="Magic3">[40..44] Неизвестное число</param>
|
||||
/// <param name="Magic4">[44..48] Неизвестное число</param>
|
||||
/// <param name="Magic5">[48..52] Неизвестное число</param>
|
||||
/// <param name="Magic6">[52..56] Неизвестное число</param>
|
||||
/// <param name="OffsetInFile">[56..60] Смещение подфайла от начала NRes (именно самого NRes) в байтах</param>
|
||||
/// <param name="Index">[60..64] Индекс в файле (от 0, не больше чем кол-во файлов)</param>
|
||||
public record ListMetadataItem(
|
||||
/// <param name="TypeId">[0x00..0x04] Resource type id.</param>
|
||||
/// <param name="FileType">[0x00..0x04] Resource type id как ASCII или hex bytes для RE.</param>
|
||||
/// <param name="ElementCount">[0x04..0x08] ElementCount. смысл зависит от TypeId.</param>
|
||||
/// <param name="Magic1">[0x08..0x0C] смысл зависит от TypeId.</param>
|
||||
/// <param name="FileLength">[0x0C..0x10] Длина payload в байтах.</param>
|
||||
/// <param name="ElementSize">[0x10..0x14] ElementSize. смысл зависит от TypeId.</param>
|
||||
/// <param name="FileName">[0x14..0x38] C-string из 36-байтного name_raw.</param>
|
||||
/// <param name="OffsetInFile">[0x38..0x3C] Смещение payload от начала архива.</param>
|
||||
/// <param name="SortIndex">[0x3C..0x40] Индекс перестановки для бинарного поиска по имени.</param>
|
||||
/// <param name="DirectoryIndex">Индекс записи в порядке каталога; не хранится отдельным полем NRes.</param>
|
||||
public sealed record class ListMetadataItem(
|
||||
uint TypeId,
|
||||
string FileType,
|
||||
uint ElementCount,
|
||||
int Magic1,
|
||||
int FileLength,
|
||||
int ElementSize,
|
||||
string FileName,
|
||||
int Magic3,
|
||||
int Magic4,
|
||||
int Magic5,
|
||||
int Magic6,
|
||||
int OffsetInFile,
|
||||
int Index
|
||||
);
|
||||
int SortIndex,
|
||||
int DirectoryIndex);
|
||||
|
||||
@@ -30,9 +30,9 @@ public class NResExporter
|
||||
extension = ".bin";
|
||||
}
|
||||
|
||||
var targetFilePath = Path.Combine(targetDirectoryPath, $"{archiveFile.Index}_{archiveFile.FileType}_{fileName}{extension}");
|
||||
var targetFilePath = Path.Combine(targetDirectoryPath, $"{archiveFile.DirectoryIndex}_{archiveFile.FileType}_{fileName}{extension}");
|
||||
|
||||
File.WriteAllBytes(targetFilePath, buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+71
-33
@@ -35,6 +35,16 @@ public static class NResParser
|
||||
TotalFileLengthBytes: BinaryPrimitives.ReadInt32LittleEndian(buffer[12..16])
|
||||
);
|
||||
|
||||
if (header.Version != 0x100)
|
||||
{
|
||||
return new NResParseResult(null, $"Неожиданная версия NRes: 0x{header.Version:X}");
|
||||
}
|
||||
|
||||
if (header.FileCount < 0)
|
||||
{
|
||||
return new NResParseResult(null, $"Некорректное количество записей NRes: {header.FileCount}");
|
||||
}
|
||||
|
||||
if (header.TotalFileLengthBytes != nResFs.Length)
|
||||
{
|
||||
return new NResParseResult(
|
||||
@@ -45,7 +55,14 @@ public static class NResParser
|
||||
);
|
||||
}
|
||||
|
||||
nResFs.Seek(-header.FileCount * 64, SeekOrigin.End);
|
||||
var directorySize = header.FileCount * 64L;
|
||||
var directoryOffset = header.TotalFileLengthBytes - directorySize;
|
||||
if (directoryOffset < 16 || directoryOffset + directorySize != header.TotalFileLengthBytes)
|
||||
{
|
||||
return new NResParseResult(null, "Некорректное расположение каталога NRes");
|
||||
}
|
||||
|
||||
nResFs.Seek(directoryOffset, SeekOrigin.Begin);
|
||||
|
||||
var elements = new List<ListMetadataItem>(header.FileCount);
|
||||
|
||||
@@ -53,41 +70,32 @@ public static class NResParser
|
||||
for (int i = 0; i < header.FileCount; i++)
|
||||
{
|
||||
nResFs.ReadExactly(metaDataBuffer);
|
||||
var type = "";
|
||||
var typeId = BinaryPrimitives.ReadUInt32LittleEndian(metaDataBuffer[0..4]);
|
||||
var type = FormatType(typeId, metaDataBuffer[0..4]);
|
||||
var attr1 = BinaryPrimitives.ReadUInt32LittleEndian(metaDataBuffer[4..8]);
|
||||
var attr2 = BinaryPrimitives.ReadUInt32LittleEndian(metaDataBuffer[8..12]);
|
||||
var fileLength = BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[12..16]);
|
||||
var attr3 = BinaryPrimitives.ReadUInt32LittleEndian(metaDataBuffer[16..20]);
|
||||
var name = ReadNResName(metaDataBuffer[20..56]);
|
||||
var offset = BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[56..60]);
|
||||
var sortIndex = BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[60..64]);
|
||||
|
||||
for (int j = 0; j < 4; j++)
|
||||
if (offset < 16 || fileLength < 0 || (long)offset + fileLength > directoryOffset)
|
||||
{
|
||||
if (!char.IsLetterOrDigit((char)metaDataBuffer[j]))
|
||||
{
|
||||
type += metaDataBuffer[j]
|
||||
.ToString("X2") + " ";
|
||||
}
|
||||
else
|
||||
{
|
||||
type += (char)metaDataBuffer[j];
|
||||
}
|
||||
return new NResParseResult(null, $"Запись '{name}' выходит за границы data region");
|
||||
}
|
||||
|
||||
var type2 = BinaryPrimitives.ReadUInt32LittleEndian(metaDataBuffer.Slice(4));
|
||||
|
||||
type = type.Trim();
|
||||
|
||||
elements.Add(
|
||||
new ListMetadataItem(
|
||||
FileType: type,
|
||||
ElementCount: type2,
|
||||
Magic1: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[8..12]),
|
||||
FileLength: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[12..16]),
|
||||
ElementSize: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[16..20]),
|
||||
FileName: Encoding.ASCII.GetString(metaDataBuffer[20..40]).TrimEnd('\0'),
|
||||
Magic3: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[40..44]),
|
||||
Magic4: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[44..48]),
|
||||
Magic5: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[48..52]),
|
||||
Magic6: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[52..56]),
|
||||
OffsetInFile: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[56..60]),
|
||||
Index: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[60..64])
|
||||
)
|
||||
);
|
||||
elements.Add(new ListMetadataItem(
|
||||
typeId,
|
||||
type,
|
||||
attr1,
|
||||
unchecked((int)attr2),
|
||||
fileLength,
|
||||
unchecked((int)attr3),
|
||||
name,
|
||||
offset,
|
||||
sortIndex,
|
||||
i));
|
||||
|
||||
metaDataBuffer.Clear();
|
||||
}
|
||||
@@ -99,4 +107,34 @@ public static class NResParser
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatType(uint typeId, ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
bool formattable = true;
|
||||
foreach (var b in bytes)
|
||||
{
|
||||
if (!char.IsLetterOrDigit((char)b))
|
||||
{
|
||||
formattable = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!formattable)
|
||||
{
|
||||
return Encoding.ASCII.GetString(bytes);
|
||||
}
|
||||
|
||||
return string.Join(" ", bytes.ToArray().Select(x => x.ToString("X2")));
|
||||
}
|
||||
|
||||
private static string ReadNResName(ReadOnlySpan<byte> raw)
|
||||
{
|
||||
var nul = raw.IndexOf((byte)0);
|
||||
if (nul < 0)
|
||||
{
|
||||
return Encoding.ASCII.GetString(raw);
|
||||
}
|
||||
|
||||
return Encoding.ASCII.GetString(raw[..nul]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user