Skip to content

Instantly share code, notes, and snippets.

@CrazyHackGUT
Last active January 7, 2019 20:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save CrazyHackGUT/eaa11ff6cb7f8160f6245b3236aed277 to your computer and use it in GitHub Desktop.
Save CrazyHackGUT/eaa11ff6cb7f8160f6245b3236aed277 to your computer and use it in GitHub Desktop.
Creation Kit Mod :: File Structure
using System;
namespace CKM
{
/// <summary>
/// Всевозможные флаги на файле плагина.
/// Скопировано отсюда: https://github.com/KerberX/Kortex-Mod-Manager/blob/master/Kortex/Source/GameData/PluginManager/IBethesdaPluginReader.h
/// </summary>
enum BethesdaHeaderFlags : UInt32
{
None = 0,
/// <summary>
/// Флаг мастер-файла.
/// </summary>
Master = (1 << 0),
/// <summary>
/// У плагина имеется внешняя локализация (папка data/strings)
/// </summary>
Localized = (1 << 7),
/// <summary>
/// ???
/// </summary>
Light = (1 << 9),
/// <summary>
/// ???
/// </summary>
Ignored = (1 << 12),
}
}
namespace CKM
{
public class Constants
{
/// <summary>
/// Магический заголовок .esp/.esm файлов.
/// Если проще - TES4.
/// </summary>
public const uint PluginHeader = 0x34534554;
/// <summary>
/// Магический заголовок .bsa файлов.
/// Если проще - BSA.
/// </summary>
public const uint ArchiveHeader = 0x00415342;
}
}
using System;
using System.IO;
namespace CKM
{
/// <summary>
/// Единая входная точка для распаковки CKM-файлов.
/// </summary>
public class Unpacker
{
/// <summary>
/// Выполняет распаковку CKM-файла в указанную папку.
/// </summary>
/// <param name="pathFile">Путь к файлу, который надо распаковать.</param>
/// <param name="targetDirectory">Путь к папке, в которую надо распаковать мод.</param>
/// <param name="modName">Кодовое название мода (будет использовано в формировании названий файлов).</param>
public static void Unpack(string pathFile, string targetDirectory, string modName)
{
using (var fileStream = File.Open(pathFile, FileMode.Open, FileAccess.Read))
{
using (var binaryReader = new BinaryReader(fileStream))
{
Unpack(binaryReader, targetDirectory, modName);
}
}
}
/// <summary>
/// Выполняет распаковку CKM-структуры из стрима в указанную папку.
/// </summary>
/// <param name="reader">Стрим для чтения структуры. Обратите внимание, что никакой валидации входного стрима не происходит из-за особенностей структуры файла.</param>
/// <param name="targetDirectory">Путь к папке, в которую надо распаковать мод.</param>
/// <param name="modName">Кодовое название мода (будет использовано в формировании названий файлов).</param>
public static void Unpack(BinaryReader reader, string targetDirectory, string modName)
{
/**
* В Bethesda не стали особо париться со структурой файла CKM.
*
* 1. В файле всегда гарантированно первым идёт байт с размером BSA-архива.
* 2. После размера идёт сам архив.
* 3. После архива до конца потока - ESP-файл.
*
* Вроде бы, Creation Kit разрешает публикацию модов без BSA-архива, потому надо
* предусмотреть возможность, что архива может и не быть. Тогда первым байтом будет
* 0?..
* Надо будет проверить, как-нибудь.
*/
// 1. Подготовим базовый путь для BSA и ESP.
var basePath = $"{targetDirectory}/{modName}";
// 2. Вычитываем размер BSA-архива.
var bsaSize = ReadBSASize(reader);
// 3. Вычитываем и записываем сам BSA-архив, если он есть.
if (bsaSize > 0)
{
UnpackBSA(reader, $"{basePath}.bsa", bsaSize);
}
// Странная херня. После BSA (даже если его нет
// (таки нашёл CKM без BSA)), гарантированно
// идёт четыре нулевых байта. Выравнивают что-то?..
// Их надо пропустить.
reader.BaseStream.Seek(4, SeekOrigin.Current);
// 4. Вычитываем и записываем сам ESP-файл.
UnpackESP(reader, $"{basePath}.es");
}
/// <summary>
/// Производит копирование BSA из текущего потока.
/// </summary>
/// <param name="reader">Поток для чтения.</param>
/// <param name="path">Имя файла, куда сохранить.</param>
/// <param name="size">Кол-во байт с BSA.</param>
private static void UnpackBSA(BinaryReader reader, string path, uint size)
{
// Сначала проверим хедер. Он должен быть BSA.
if (reader.ReadUInt32() != Constants.ArchiveHeader)
{
throw new IOException("Invalid Bethesda Archive File header!");
}
// Теперь запишем архив в файл.
// Поскольку часть данных (только хедер) мы уже считали, сами их и запишем.
using (var fileStream = File.Open(path, FileMode.Create, FileAccess.Write))
{
using (var writer = new BinaryWriter(fileStream))
{
writer.Write(Constants.ArchiveHeader);
CopyBinaryToStream(reader, writer, size - 4);
}
}
}
/// <summary>
/// Производит копирование ESP/ESM из текущего потока.
/// </summary>
/// <param name="reader">Поток для чтения.</param>
/// <param name="path">Путь, куда должен быть записан файл. Требуется .es на конце.</param>
private static void UnpackESP(BinaryReader reader, string path)
{
// Сначала проверим хедер. Он должен быть TES4.
if (reader.ReadUInt32() != Constants.PluginHeader)
{
throw new IOException("Invalid Bethesda Plugin File header!");
}
// После хедера идёт, вроде бы, версия.
var version = reader.ReadUInt32();
// Считываем флаги и смотрим на метку мастера.
var flags = (BethesdaHeaderFlags)reader.ReadUInt32();
// Теперь запишем плагин в файл.
// Поскольку часть данных (хедер, версия, флаги) мы уже считали, сами их и запишем.
var fileExtension = ((flags & BethesdaHeaderFlags.Master) == BethesdaHeaderFlags.Master) ? 'm' : 'p';
using (var fileStream = File.Open(path + fileExtension, FileMode.Create, FileAccess.Write))
{
using (var writer = new BinaryWriter(fileStream))
{
writer.Write(Constants.PluginHeader);
writer.Write(version);
writer.Write((UInt32)flags);
CopyBinaryToStream(reader, writer);
}
}
}
/// <summary>
/// Производит чтение размера BSA-архива.
/// </summary>
/// <param name="reader">Указатель на поток чтения.</param>
/// <returns>Размер BSA-архива.</returns>
protected static uint ReadBSASize(BinaryReader reader)
{
// Чисто теоретически, это гарантированно неподписанное число.
// Т.е. только положительное. Но это не факт.
return reader.ReadUInt32();
}
/// <summary>
/// Производит копирование данных из входного потока на диск.
/// </summary>
/// <param name="reader">Поток чтения.</param>
/// <param name="filePath">Путь к файлу.</param>
/// <param name="size">Размер.</param>
protected static void CopyBinaryToFile(BinaryReader reader, string filePath, uint size = 0)
{
using (var fileStream = File.Open(filePath, FileMode.Create, FileAccess.Write))
{
using (var writer = new BinaryWriter(fileStream))
{
CopyBinaryToStream(reader, writer, size);
}
}
}
/// <summary>
/// Производит копирование данных из входного потока в другой поток.
/// </summary>
/// <param name="reader">Поток чтения.</param>
/// <param name="writer">Поток записи.</param>
/// <param name="size">Размер копируемых данных.</param>
protected static void CopyBinaryToStream(BinaryReader reader, BinaryWriter writer, uint size = 0)
{
if (size != 0)
{
while (size > 0)
{
// Мы будем копировать по 8192 байт за раз.
// Если оставшийся размер - меньше, то будем вычитывать меньше.
var copySize = size > 8192 ? 8192 : size;
var data = reader.ReadBytes(Convert.ToInt32(copySize));
writer.Write(data);
size -= Convert.ToUInt32(data.Length);
}
return;
}
CopyBinaryToStream(reader, writer, Convert.ToUInt32(reader.BaseStream.Length - reader.BaseStream.Position));
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment