Как уже было сказано ранее, в статье о формате pud-файла, Близзарды не горят желанием светить всю медиа-составляющую своих игр. А потому они тщательно ее скрывают. Нетрудно догадаться что один из самых больших файлов игры, как раз-таки содержит то, что от нас так поспешно хотели скрыть. Файл Maindat.war в папке Data из игры Вар2, наталкивает на мысль о том, что в нем находятся нужные нам вкусности, не только своим названием, но и своим размером. WAR - Warcraft ARchive. Сразу хочу оговориться, что я не считаю себя супер-хакером или кем-то в этом духе, но открыв файл в 16-ричном редакторе, сразу бросается в глаза стройность рядов 00 в определенных местах, а как говорят бывалые вскрыватели ресурсов игр, это ни что иное как FAT (или таблица расположения файлов - по нашему). Первые 4 байта как правило у Близзардов, да и не только у них - это заголовок. Дальше 2 байта(слово) с количеством файлов(секций или секторов) в этом файле, дальше 2 байта чепухи, и наконец начинаются смещения к началу секторов, друг за другом, количеством прочитанным ранее. До этого я докумекал сам, пока не нашел формат этого файла всрытый уже кем-то другим. Скачал с сайта wotsit.org - там много информации о файлах по их расширениям. Вот то, что было указано там: WAR File Format
00:0000-00:0003 L Id caption number $19000000 00:0004-00:0005 W Number of entries ( max enrty number +1 ) 00:0006-00:0007 W Archive Id ( used for entry determination) 1000=Maindat.war 2000=Snddat.war 3000=Rezdat.war 4000=Strdat.war 5000=Sfxdat.sud 6000=Muddat.cud 00:0008-XX:XXXX L Entry offsets in in file $FFFFFFFF=unavailable (used in demo version) XX:XXXX RXX Entries
PseudoEntry=everything shorter than 4 bytes (counted by substracting offsets)
UsefulEntry
Index : Feature :
00 W Lower Entry Length word =entry datasize-4 02 B Higher Entry Length word 03 B LZSS Compression flag 00=uncompressed 20=compressed
LZSS Compression scheme :
4096 Bytes buffering system
00 B Flags bits=bytes in next 8 records Bit set = any duplicity , use the same byte in buffer on current position record = byte Bit clear = duplicity on position in lower byte and lower nibble offset of length in higher byte higher nibble Информации скажу я вам немного, зато все по делу. Как видим мы были недалеко от истины в наших рассуждениях. Первые 4 байта и правда заголовок, потом количество секций и идентификатор архива, разный для разных файлов. Да, как видите архивы Варкрафта могут храниться в фалах не только с расширением war, но и sud, cud и наверняка других. Вслед за идентефикатором файла идут смещения (размер 4 байта каждое смещение) относительно начала файла до начала секции. Стоит остановиться. Почему я называю части этого архива секциями, а не файлами? Просто в моем понимании файл - это часть данных имеющих название, ведь у вас на диске каждый файл имеет название, а у секций названий нет, только порядковые номера. Так вот, а вначале каждой секции, если перейти на ее начало, есть тоже маленький заголовок в 4 байта: 1- байт это флаг обозначающий, заархивирована ли секция ( в этом случае байт = 0х20 ), либо не заархивирова( в этом случае этот байт = 0х00 ) и остальные 3 байта - это размер распакованной секции, если она конечно заархивирована. Отлично. Теперь мы знаем что: 1) файл имеет заголовок (0х00000019 для вар2 и 0х00000018 для первого вар-а) 2) кол-во секций в файле 3) идентификатор архива (никакой смысловой нагрузки не несет) 4) смещения от начала файла до секций, ровно стока штук скока в пункте 2 5) сами секции Хорошо, уже можем написать немного кода: ' это тип для сектора, который описывает файл в архиве Type _Sector
Field start_address:Int ' начальный адрес файла Field length:Int ' размер в байтах Field num:Int ' порядковый номер Field comp_flag:Byte ' флаг компрессии Field uncomp_length:Int ' длина разархивированного участка Field data_ptr:Byte Ptr ' указатель на массив данных Global list:TList = New TList ' список всех секторов Method New() list.addlast( Self ) End Method End Type
' ================================= Global file_name:String= "Maindat.war"
Local stream:TStream = OpenFile( file_name ) If( Not stream) Then Print "in file not found"; End
Local out_stream:TStream = WriteFile( file_name + ".debug.txt" ) If( Not out_stream) Then Print "out file not found"; End
Local header:Int = stream.ReadInt() Local count:Int = stream.ReadShort() Local unknown:Short = stream.ReadShort()
out_stream.WriteLine("---------------START------------------") out_stream.WriteLine("header is : " + show_hex( header ) ) out_stream.WriteLine("section count : " + show_hex( count , 2 ) + " dec: " + count ) out_stream.WriteLine("unknown : " + show_hex( unknown , 2 ) )
Global last_pos:Int
' пройдемся по всем файлам For Local i:Int = 0 Until count ' прочитаем начальный адрес Local start_address:Int = stream.ReadInt() ' запомним позицию last_pos = stream.pos() ' прочитаем адрес следующего файла Local end_address:Int = stream.ReadInt() ' длина текущего файла Local length:Int
' перейдем на начало текущего файла stream.seek( start_address ) ' если текущий - это последний, то он кончается там же, где кончается и файл If( i = (count - 1) ) Then end_address = stream.size() ' рассчитаем длинну файла length = end_address - start_address ' создадим новый сектор и заполним его данными Local o:_Sector = New _Sector o.start_address = start_address o.length = Max( 1 , length - 4 ) ' ( еще 4 байта служебной информации вначале ) o.num = i ' если длина файла больше 4 байт If( length > 4 )
' читаем еще 4 байта Local temp:Int = stream.ReadInt()
' из них 3 байта - это размер файла после разархивирования o.uncomp_length = temp & $00FFFFFF ' и 1 байт - показывает архивный ли файл или нет o.comp_flag = temp Shr 24 End If
' читаем файл в буффер o.data_ptr = MemAlloc( o.length ) stream.readbytes( o.data_ptr, o.length ) ' сохраним информацию о заполненой секции в файл out_stream.WriteLine("--------------------------------------") out_stream.WriteLine("num: " + i ) out_stream.WriteLine("start_adddress : " + show_hex( start_address) ) out_stream.WriteLine("end_adddress : " + show_hex( end_address) ) out_stream.WriteLine("length : " + o.length ) out_stream.WriteLine("comp_flag : " + show_hex( o.comp_flag , 1 ) ) out_stream.WriteLine("uncomp_length : " + show_hex( o.uncomp_length ) + " dec : " + o.uncomp_length )
' вернемся обратно к запомненной позиции stream.seek( last_pos ) Next
' выведем для сведения скока все-таки получилось файлов Print "~ncount : " + _Sector.List.count()
' закроем поток stream.close() out_stream.WriteLine("----------------END-------------------") out_stream.close()
end Я уже использовал тип _Sector для примера с pud-файлом, и мне кажется что это наиболее удачное решение для организации одной секции такого вида информации. Для текущих целей я добавил еще парочку полей с данными необходимых нам для работы с war-архивами. Я сделал достаточно коментариев чтобы понять что творится в каждой строке программы. Можно было бы уже на этом этапе извлекать секции в отдельные файлы, но разве это имеет смысл, когда выяснилось что только 5 или 6 из них не заархивированны, а остальные содержат архивы. В небезызвестном документе, который я уже приводил как пример лаконичности и четкости рамок мысли, есть даже подсказка что это LZSS-архивация и даже, то как ее расшифровать. Честно скажу ниасилил, да и не пытался особо, я люблю когда описание эээ немного подлиннее и поконкретнее, поэтому я решил использовать для разархивирования наработки уже сделанные другими... Поиски мои привели меня даже на сайт Apple где лежала функция обрадовавшая меня своим названием (decompress_lzss) и ничего что на С, этот язык мне тоже знаком, тем более что я давно хотел проверить как подключать С-функции к БМаксу. Но как я не пытался, эта функция работать как надо не хотела. Она выдавала все что угодно, только не правильный результат. Тогда, слави Гугл-единый, откопал код такого же извиняюсь археолога Варкрафта как и я, в опен сорс. Оказывается не я один копаюсь в дерьме мамонтов (я думаю никто не посмеет оспорить, что это мамонты ). :) Так вот из кода была выдрана, самым наглым образом, функция разархивации, подрейхтована напильником и подключена к БМаксу. Надо бы по честному связаться с ним и сказать спасибо, правда в том месте стоит коммент // FIXME: If the decompression is too slow, optimise this loop :-> думаю это означает, что он и сам не очень доволен кодом, а значит не будет кричать о копирайтах и другой чуши. :) Так вот функция была внедрена и показала себя отлично. Все секции были извлечены. Да, но остался вопрос, что каждая секция означает, какую информацию она в себе несет? Эти вопросы не так сложны как кажутся, но и тут есть подвох. Мы привыкли (в виндовз) что расширение файла означает его принадлежность к какому-либо формату. А у секций, нет расширений, да что говорить у них и имен-то нет. (Эко их Близзарды уделали). Но не беда, открыв файлы 16-ричным редактором легко заметить по первым байтам, к каким категориям принадлежат эти файлы. Так например файл начинающийся символами FORM - означает звуковой файл XMI. RIFF - звуковой файл WAV , SMK2 - анимационный ролик и т.д. По размеру секции тоже можно кое-что догадаться, например размер 768- байт сразу наталкивает о мысли, что в нем хранится палитра. Так как игра старая, то она использовала только 256 цветов, которые брала из палитры. А палитра заполнялсь значениями RGB, три байта на 1 цвет палитры (3 * 256 = 768). В связи с этим рисунки имеют структуру простого массива, где в каждой ячейке лежит байт указывающий на цвет (индекс в таблице палитры), другими словами RAW-рисунок. Так, например, размер 64004 наталкивает на мысль, что это RAW-рисунок во весь экран режим VGA( 320x200 ) 320 х 200 = 64000 и еще 4 байта это наверняка его размеры в первых 4 байтах. Так и есть первые 4 байта 4001С800 - что значит 0х0140 = это 320 в десятичной и С8 - это 200. Обычно секции рисунков и палитр находятся рядом. Этим же образом смотрятся и другие файлы. С файлами анимации и стадий постройки сданий, несколько сложнее, там тоже свой формат. Но вас это пугать не должно, он тоже описан :) Спасибо за внимание, в аттаче исходник и файл распаковщик в ехе. http://dimanche.ucoz.ru/war_extractor.zip
|