-
-
Save CrazyHackGUT/eaa11ff6cb7f8160f6245b3236aed277 to your computer and use it in GitHub Desktop.
Creation Kit Mod :: File Structure
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace CKM | |
{ | |
public class Constants | |
{ | |
/// <summary> | |
/// Магический заголовок .esp/.esm файлов. | |
/// Если проще - TES4. | |
/// </summary> | |
public const uint PluginHeader = 0x34534554; | |
/// <summary> | |
/// Магический заголовок .bsa файлов. | |
/// Если проще - BSA. | |
/// </summary> | |
public const uint ArchiveHeader = 0x00415342; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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