О бедном Arcanum замолвите слово
- Арканум
- Что?
- Арканум
- Арканум?
- Да, Арканум. Знаете, двухголовый брамин, Гилберт Бейтс...
...баги, битые диалоги, неработающие квесты, Тройка Геймс, мать его. Арканум!
Это первая из примерно трех обзорных статей по форматам этой замечательной игры, которые я планирую написать. Каждая статья будет посвящена одной общей теме для всех упомянутых в ней форматов.
Введение
Недавно промелькнула новость, что игровое издательство Sierra перешло под контроль Microsoft, а значит у нас с вами есть потенциальная возможность увидеть официальный ремейк культовой игры 2001 года. В тоже время, на мой взгляд, эта новость автоматически означает, что все попытки создания альтернативных ремейков, портов и ремастеров скорее всего будут пресечены новыми владельцами. А может и нет.. в любом случае это то самое время, когда надо заняться эдакой ретроспективой, вспомнить и обсудить, то какую работу проделали разработчики Troika Games, как хранили игровые данные.. и надо ли выгонять Вирджила из партии.
Несколько лет назад я набрел на известный в узких кругах форум мододелов и фанатов игры Arcanum: Of Steamworks and Magick Obscura, и был удивлен, что формат файлов игры был известен лишь частично, хотя игре к этому моменту было уже около 15 лет. По факту редактирование некоторых файлов было эдакой магией "открой файл hex-редактором, отступи N байт и поменяй значение, и если тебе повезет, то характеристика NPC изменится", что на мой взгляд крайне контрпродуктивно.
В действительности бинарный формат файлов был известен сразу двум-трем пользователям этого форума, а может быть и всего нашего интернета.. Первый, и наверно единственный, кто обладал полной информацией - это известный в узких кругах Crypton, моддер из восточной европы. Располагая полными данными он с разницей в несколько лет совершил две попытки создания альтернативного движка игры на QT и WebGL, периодически пропадая на месяцы и годы, да так что в последний раз сообщество было уверено, что он скорее мертв, чем жив.
В своей последней попытке создал аккаунт на Patreon, собрал средств на три банана и успокоился. Обещания расшарить исходники не сдержал и опять пропал из инфополя.
В этом наверно основная проблема современного сообщества моддеров. Тотальная огороженность и нежелание делиться информацией.. в надежде, что благодаря ней они смогут поиметь какой-то мелкий гешефт (нет). В результате ценность ее с каждым днем снижается из-за естественного уменьшения фан-базы ретроигр, а сообщество страдает прямо сейчас.
Забавы ради я решил заняться этой проблемой, и не могу сказать, что добился больших успехов, но кое-что у меня получилось.. с чем и собираюсь дебютировать на Хабре.
Типы файлов
В Arcanum, как и в любой другой игре, существует масса собственных бинарных форматов упаковки архивов, изображений, и файлов данных. И очевидно большая часть из них была ориентирована на уменьшение размера дистрибутива.
Хотя на мой взгляд, там все еще есть куда уменьшать, и при желании можно было не выпиливать львиную долю контента, который засветился в бета-верисии игры, и поместить на тоже количество компакт дисков. Но как мы знаем студия столкнулась с финансовыми трудностями, и нехваткой времени.. поэтому хорошо что игра вообще смогла увидеть свет.
Графические и сопутствующие файлы представлены двумя расширениями (форматами):
ART - собственно бинарный формат для тайлов изображений
FACWALK - формат обьединения отдельных тайлов для создания крупного декора, как на скриншоте выше
Другие интересные, но практически не связанные с графикой бинарные файлы с расширениями:
PROTO + MOB - файлы данных с описанием свойств любого объекта на карте
SEC - файлы фрагментов карты размером 64х64 тайла
Естественно, все это упаковано в архивы. Так же без внимания не останутся текстовые файлы диалогов, и скриптов. Но в рамках этой первой статьи я не буду перегружать вас информацией, и расскажу только о ART файлах.
*.ART
Формат этих файлов был уже известен к тому времени, как я присоединился к сообществу.. но не упомянуть о нем я не могу.
Представьте что вы находитесь в 1999 и только что "отпочковались" от Interplay, чтобы построить "свой лунапарк с блекджеком и магией". Современных методов упаковки изображений еще не придумали, а о шейдерах вы едва что-то слышали. Поэтому в Arcanum применяется в общем-то не новый в то время способ сжатия без потери качества основанный на использовании цветовых палитр и исключения повторяющихся пикселей. Все как в старых добрых консолях.
В отличии от того же Fallout, в Arcanum игровая сетка не гексагональная, а изометрическая. Тайловая сетка в столь похожих играх так же отличается. Легендарный Fallout использует триметрическую сетку, а Arcanum - классическую изометрию.
Собственно за совпадение сеток авторам пришлось заплатить контентом. Если для гексагональной сетки требуется всего шесть наборов тайлов движущихся обьектов для обозначения движения по шести же возможным направлениям, то в случае с изометрической сеткой - требуется все восемь. Конечно тайлы скорее всего были сгенерированы по примитивным трехмерным моделям, но это не спасало от 25% увеличения их количества.
Заголовок
От лирики перейдем к структуре файлов.
В начале каждого файла находится 36 байт заголовка, которые содержат базовые данные для верного декодирования остальной части файла.
02 00 00 00 // ART type : Static=0х01, Сritter=0x02, Font=0x04 и тд
0F 00 00 00 // FrameRate : Unknown
08 00 00 00 // Direction Count
// Количество "направлений" арта по изометрической сетке.
// Всегда равно восьми, и скорее всего является унифицированной
// информацией о сетке. Игнорируется нами так как не несет
// практической пользы. Даже Static с одним единственным фреймом
// имеют значение 0x08000000
f8 f9 12 00 // Блок из 16 байт с неопределенным содержимым
00 00 00 00 // Возможно это 4xRGBA, по числу политр,
00 00 00 00 // но цвет не коррелирует. Если палитра не используется,
00 00 00 00 // то замещено нулями для выравнивания
0E 00 00 00 // Action Frame : Стартовый фрейм для анимации или редактора
0F 00 00 00 // Frame Count: Количество фреймов для каждого
// "направления" движения
Как вы наверно поняли, каждый ART файл содержит в себе сразу несколько фреймов из которых игровой движок в дальнейшем собирал анимацию движения, стрельбы или смерти. Один файл может содержать только одну определенную анимацию помноженную на количество направлений движения.
Общее же количество фреймов внутри каждого ART файла легко вычислить по очевидной формуле [Frame Count] * [Direction Count].
Следом за количеством фреймов расположен интересный блок данных с опять же неопределенным содержимым. Это три блока по 32 байта.
Даже с подключенным отладчиком, определить назначение первых и последних 32 байт мне не удалось. Средние 32 байта это набор из восьми 32 битных чисел, каждое из которых является суммой длинн отдельных фреймов для каждого из направлений сетки. Это, а так же тот факт, что собственноручно созданный арт с нулями в этом месте без проблем подтягивается игрой, наводит на мысли, что остальные 64 байта - это исключительно служебная информация.
[StructLayout(LayoutKind.Sequential)]
public struct ArtHeader
{
[MarshalAs(UnmanagedType.U4)]
public ArtType Flags;
[MarshalAs(UnmanagedType.U4)]
public uint FrameRate;
[MarshalAs(UnmanagedType.U4)]
public uint DirectionCount;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public ArtColor[] BackColors;
[MarshalAs(UnmanagedType.U4)]
public uint ActionFrameNumber;
[MarshalAs(UnmanagedType.U4)]
public uint FrameCount;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
public uint[] Unk0;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
public uint[] Unk1;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
public uint[] Unk2;
};
Палитра цветов
Следом за заголовком идет от одного до четырех блоков цветовых палитр, количество которых зависит от количества ненулевых фонов в заголовке. Каждая отдельная палитра содержит 256 цветов по 4 байта каждый, при этом первый цвет в палитре - это цвет фона, он же импровизированный альфаканал потому что фон какбы прозрачен.
Как видите одна из особенностей (проблемой это сложно назвать) заключается в том, что если в палитру занесено 28 цветов, то память все равно выделяется для 256.. оставшееся незатребованным заполняется нулями. Частично это купируется тем, что ART файлы упаковываются в архивы, но байты оперативной памяти это совсем не экономит. По моему мнению в этом месте первым байтом должно было располагаться количество цветов в палитре, а дальше собственно сама палитра.
Другое неожиданное открытие состоит в том, что не все из цветов в палитре используются. Возможно следствие какого-то постпроцессинга после конвертации.
Блок информации о фреймах
Сразу после заголовка располагается участок в котором располагаются данные о размере каждого фрейма: размер в байтах, высота, ширина, отступы и смещения.
При кажущейся простоте тут тоже не обошлось без белых пятен. Например Delta_X, Delta_Y. О назначении этих полей до сих пор спорят на профильных форумах.
09 00 00 00 // Frame_Width
0D 00 00 00 // Frame_Heigth
38 00 00 00 // Frame_Size
// Размер фрейма в байтах, равен или меньше,
// чем [Frame Width] * [Frame Heigth] из-за пропуска
// повторяющихся пикселей
// Смещение от верхнего левого угла для экономии памяти
0D 00 00 00 // Offset_X
12 00 00 00 // Offset_Y
// Почти везде нули. Предположительно, может быть как "точкой крепления"
// к сетке, так и единицей смещения для анимации передвижения.
// Не проверено.
00 00 00 00 // Delta_X
00 00 00 00 // Delta_Y
Полезная пиксельная нагрузка
Сразу после информации о фреймах идет непосредственно массив пикселей. Каждый пиксель имеет размер один байт и содержит номер одного из 256 цветов в палитре.
Но все не так просто. Чтобы не хранить повторяющиеся пиксели в чистом виде, в том числе и фоновые пиксели, применяется небольшая хитрость. Пиксели уложены неравномерными фрагментами (или чанками, если вам так удобнее), а размер и тип чанка "зашифрован" в первом байте. Старший бит определяет тип фрагмента, а младшие 7 бит - количество циклов чтения или повторения.
long originFrameSize = frameInfo.Height * frameInfo.Width;
byte[] unpackDump = new byte[originFrameSize];
if (originFrameSize == frameInfo.Size)
{
ms.Read(unpackDump, 0, unpackDump.Length);
}
else
{
using MemoryStream msUnpack = new MemoryStream(unpackDump);
while (msUnpack.Position < originFrameSize)
{
int chunkInfo = ms.ReadByte();
int readOrRepeatCount = chunkInfo & 0x7F;
bool isRepeat = (chunkInfo & 0x80) == 0;
byte c = 0x00;
for(int i = 0; i < readOrRepeatCount; i++)
{
if (!isRepeat || i == 0)
{
c = (byte) ms.ReadByte();
}
msUnpack.WriteByte(c);
}
}
}
Сохранив полученный массив в любой из устраивающих вас форматов, вы получите набор из отдельных изображений.
Путем нехитрых манипуляций его можно превратить в атлас или GIF анимацию. Смотря какую задачу вам нужно выполнить.. только не забудьте применить смещения, без них анимации будут дерганными из-за неодинаковых размеров изображений.