О формате pud официально pud - файл именуется как Warcraft II Scenario File На самом деле так и есть ведь в нем много различных секций со всей важной информацией касающейся уровня игры. pud-файл может быть создан "родным" редактором карт, либо посторонним, я видел парочку таких. А это значит что содержимое Pud файлов давно стало известно общественности, не буду говорить кто вскрыл этот формат, потому как называние одного может обидеть другого и так далее. Думаю это было не очень трудной задачей. При достаточном терпении и логе сохранений из редактора карты, можно было понять о местонахождении различных значений в этом файле. Итак приступим:
Файл состоит из секций, каждая секция начинается одинаково 4 байта - название секции "header" 4 байта - длина секции в байтах "length" (без заголовка и длины секции) n - Байт - данные этой секции, размером length
Хочу заметить, что от файла к файлу количество секций может не совпадать по той причине, что некоторые из них опциональные, то есть не обязательные. Итак, зная выше озвученную информацию, мы уже можем составить достаточно простую программку для чтения pud-файла и вывода названия и размера всех содержащихся в нем секций.
'Сначала опишем структуру типовой секции: Type _Sector
Field header:String ' название секции Field length:Int ' размер Field data_ptr:Byte Ptr ' указатель на данные этой секции Global list:TList = New TList ' список всех секций ' при создании новой секции, сразу заносить ее в список всех секций Method New() list.addlast( Self ) End Method ' функция возвращает секцию по ее названию Function find:_Sector( name:String ) For Local o:_Sector = EachIn _Sector.List If name = o.header Then Return o Next End Function End Type
' теперь можем открыть нужный файл Local stream:TStream = OpenFile( "X.pud" )
' если ошибка открытия, то вываливаемся в панике :) If( Not stream) Then Print("file not found"); End
' читаем до конца файла While( Not stream.Eof() )
' выделяем 4 байта Local bptr:Byte Ptr = MemAlloc( 4 ) ' читаем из потока 4 байта stream.readbytes( bptr, 4) ' формируем из них строку, названия сектора Local header:String = String.Frombytes( bptr,4 ) ' читаем размер текущего сектора Local length:Int = stream.ReadInt() ' теперь создадим сектор и заполним его полученной информацией Local o:_Sector = New _Sector o.header = header o.length = length ' перепрыгнем к следующему сектору stream.seek( stream.pos() + length )
Wend
' все прочитано, поток надо закрыть stream.close()
' теперь пробежимся по всем прочитанным секциям и выведем их данные For Local o:_Sector = EachIn _Sector.List Print("signature : " + o.header) Print("length : " + o.length) Next
' конец End
вот что у меня получилось:
signature : TYPE length : 16 signature : VER length : 2 signature : DESC length : 32 signature : OWNR length : 16 signature : ERA length : 2 signature : DIM length : 4 signature : UDTA length : 5696 signature : UGRD length : 782 signature : SIDE length : 16 signature : SGLD length : 32 signature : SLBR length : 32 signature : SOIL length : 32 signature : AIPL length : 16 signature : MTXM length : 8192 signature : SQM length : 8192 signature : OILM length : 4096 signature : REGM length : 8192 signature : UNIT length : 96 signature : SIGN length : 4
У вас может получиться нечто другое, в зависимости от того какой файл вы прочитали. Согласитесь это было довольно просто. В одном файле двух секций с одинаковыми именами нет и быть не может, поэтому название секции однозначно идентифицирует данные содержащиеся в ней. Теперь неплохо было бы узнать что содержится в этих секциях.
'TYPE' - это идентификатор Pud-файла. Если первая секция имеет имя не TYPE, то можно смело вываливаться из программы и говорить, что это не файл сценария для Вар2. 'VER ' - идентификатор версии pud- файла 'DESC' - описание 'OWNR' - идентифицирует игроков 8-слотов компьютер/человек/нейтральный компьютер ... 'ERA ' - тип определяющий территорию или другими словами тайлсет (набор тайлов) для карты 'ERAX' - опционально, тоже определяет территорию, что и предыдущая секция 'DIM ' - размеры карты, как известно карты в Вар2 квадратные и имеют ряд стандартных размеров, 32х32, 64х64, 96х96 и 128х128. Говорят, что движок Вара может работать и не только с квадратными картами, но я не проверял. 'UDTA' - информация о юнитах 'ALOW' - информация об ограничениях, допускается ли апгрейдиться или использовать изучение и т.д. 'UGRD' - информация об апгрейдах, стоимость, время, материалы 'SIDE' - идентифицирует рассовую принадлежность каждого игрока орк/человек/нейтрал 'SGLD' - количество золота при старте 'SLBR' - количество леса при старте 'SOIL' - количество нефти при старте 'AIPL' - АИ для каждого игрока 'MTXM' - секция содержит, тайлы карты 'SQM ' - карта проходимости(коллизий) 'OILM' - карта нахождения нефтяных залежей 'REGM' - карта территорий 'UNIT' - информация о юнитах
Более подробная информация содержится в http://cade.datamax.bg/war2x/pudspec.html, а так же может быть без труда найдена в интернете.
Теперь задачка посложнее, отобразим карту на экране.
Для того чтобы отобразить любую карту, нам необходимо прочитать как минимум 3 секции, это тип территории, чтобы загрузить нужный тайлсет('ERA '), размеры карты('DIM ') и последовательность тайлов на карте('MTXM'), то есть массив тайлов этой карты.
Небольшое отступление. Близзарды славятся тем, что пакуют все ресурсы для игры в архивы, а не оставляют их на всеобщий доступ. И это в общем-то понятно, их игры всегда имели красивые и красочные картинки, анимации и соотвствующего качества звуками. Все это хранится в файле mainDat.war - этот архив можно открыть программой wardraft. А открыть его нам нужно для того чтобы выдрать тайлсеты. Всего их 4 штуки forest, winter, wateland, а так же swamp, последний используется в Warcraft II: Batlle Net Edition (BTE). В обычном Варике и редакторе к нему можно использовать только первые три тайлсета. Я уже выдрал тайлсеты, они лежат в папке tilesets, там же находятся и нестандартные тайлсеты сделанные энтузиастами. WarDraft помимо того, что сохраняет тайлсет в БМП-файле еще делает txt-файл, в котором дана расшифровка соответствия тайлов в тайлсете, к тайлам указанным в секции MTXM.
Ок. Теперь мы знаем достачно чтобы отобразить нашу карту на экране. Примерный алгоритм таков: узнаем тип территории карты (ERA), загружаем нужный тайлсет, узнаем размеры карты (DIM ), создаем массив таких же размеров и читаем в него секцию (MTXM). А затем выводим нужные тайлы на экран.
Перед кодом программы хочу сделать парочку замечаний:
1) в названии секции 4 символа. Даже, когда название содержит 3 буквы SQM - не стоит забывать, что последний символ это пробел или 0x20 в 16-ричном виде. 2) когда мы имеем некий массив памяти и указатель на него, то мы можем использовать указатель как массив этой памяти, а так же брать из него данные любого типа, естественно преобразовав указатель к нужному нам типу. 3) преобразование тайла карты в нужный тайл из тайлсета осуществляется через ini-парсер, после чего, так же создается массив нормальносоответствующих значений и помещается в файл "map.txt". Так как карты квадратны, то по размеру "map.txt", достаточно легко выяснить ее размеры. 4) можно попробовать менять тайлсеты к загруженным картам, но не все они могут быть применимы с любым типом карт. Вот таблица соответствий: forest -> jungle winter -> glacier, volcano wasteland -> desert swamp -> wetland, hell, kjungle
А теперь код программы:
SuperStrict
приинклудим парсер ини-файлов Include "_IniParser.bmx"
какую карту читаем? Global map_name:String = "x_swamp.pud"
размер тайла во все стороны один - 32 пикселя Global TILESIZE:Int = 32
здесь все уже известно Type _Sector
Field header:String Field length:Int Field data_ptr:Byte Ptr Global list:TList = New TList Method New() list.addlast( Self ) End Method Function find:_Sector( name:String ) For Local o:_Sector = EachIn _Sector.List If( name = o.header ) Then Return o Next End Function End Type
откроем файл Local stream:TStream = OpenFile( map_name )
эй а где файл, ара? If( Not stream) Then Print "file not found"; End
до конца потока While( Not stream.Eof() )
Local bptr:Byte Ptr = MemAlloc( 4 ) stream.readbytes( bptr, 4) Local header:String = String.Frombytes( bptr,4 ) Local length:Int = stream.ReadInt() Local o:_Sector = New _Sector o.header = header o.length = length
выделим память o.data_ptr = MemAlloc( o.length ) прочитаем массив байтов в выделенную память stream.readbytes( o.data_ptr, length )
Wend
закроем stream.close()
покажем For Local o:_Sector = EachIn _Sector.List Print( "signature : " + o.header ) Print( "length : " + o.length ) Next
посмотрим каковы размеры карты Local o:_Sector = _Sector.find( "DIM " ) If( Not o ) Print( "ooops. ~qDIM~q section not found."); End
смотрим какие размеры, хочу заметить, что достаточно смотреть один размер из-за квадратности Print "~nmap dimensions : " Local x_size:Short = Short Ptr(o.data_ptr)[0] Local y_size:Short = Short Ptr(o.data_ptr)[1]
Print "x : " + x_size Print "y : " + y_size
смотрим какая территория у этой карты o:_Sector = _Sector.find( "ERA " ) If( Not o ) Print( "ooops. ~qERA~q section not found."); End
Local ter_type:String Local ok:Int = True
определяем какой тайлсет грузить Select( Short Ptr(o.data_ptr)[0] ) Case $00 ter_type = "forest" Case $01 ter_type = "winter" Case $02 ter_type = "wasteland" Case $03 ter_type = "swamp" Default ter_type = "unknown type, but may be forest."; ok = False End Select Print "~nterrain type : " + ter_type + " --> " + Short Ptr(o.data_ptr)[0] If( Not ok ) Then Print("change terrain type and try again."); End
грузим необходимый тайлсет и карту соответствия тайлов Local ini_file:String = "tilesets/" + ter_type + ".txt" Local tileset_file:String = "tilesets/" + ter_type + ".bmp" Local num_tiles:Int = Int( ini_tiles.get("Layout/Total") ) Local tileset_image:Timage = LoadAnimImage( tileset_file, TILESIZE, TILESIZE, 0, num_tiles )
If( Not tileset_image) Then Print( "Oops." + tileset_file + " did not find." ); End
теперь переходим непосредственно к массивам тайлов o:_Sector = _Sector.find( "MTXM" ) If( Not o ) Print( "ooops. ~qMTXM~q section not found."); End
выделим место под карту Local map:Short[ , ] = New Short[ x_size , y_size ] Local outstream:TStream = WriteFile( "map.txt" ) If( Not outstream) Then Print("I cannot start new stream for write."); End
Global ini_tiles:_IniParser = _IniParser.Open( ini_file ) If( Not ini_tiles) Then Print( ini_file + " file not found."); End
перекодируем тайлы и запихнем их в массив For Local y:Int = 0 Until y_size For Local x:Int = 0 Until x_size
map[ x , y ] = get_tile( Short Ptr( o.data_ptr )[ y * x_size + x ] ) outstream.WriteShort( map[ x , y ] ) Next Next
закроем выходной поток outstream.close()
'----------------------------- main loop ----------------------------- Const ScreenWidth:Float = 800, ScreenHeight:Float = 600, FullScreen:Int = 0 Graphics(ScreenWidth, ScreenHeight, FullScreen)
SetBlend( ALPHABLEND )
Local mtx:Int, mty:Int Local pos_x:Int, pos_y:Int Local speed:Float = 3.0
знаю, что надо немного оптимизировать рендер, но это не основная наша задача
Repeat mtx = MouseX() mty = MouseY() If ( KeyDown(Key_Up) ) pos_y :- speed If ( KeyDown(Key_Down) ) pos_y :+ speed If ( KeyDown(Key_Left) ) pos_x :- speed If ( KeyDown(Key_Right) ) pos_x :+ speed Cls SetOrigin( -pos_x, -pos_y ) For Local y:Int = 0 Until y_size For Local x:Int = 0 Until x_size DrawImage( tileset_image, x * TILESIZE, y * TILESIZE, map[ x , y ]
) Next Next SetOrigin( 0 , 0 ) DrawText("mx : " + mtx, 0, 60) DrawText("my : " + mty, 0, 80) Flip Until( KeyDown(KEY_ESCAPE) Or AppTerminate() ) End
функция перекодировки тайла из карты в нужный нам Function get_tile:Short( value:Short ) Local x:Int, y:Int, res:Short
Local str:String = ini_tiles.get("mapping/$" + show_hex( value , 2 ) ) Local str2:String = ini_tiles.get("Megatiles/" + str ) Local tiles_in_row:Int = Int( ini_tiles.get("Layout/XTiles") )
str = str2[1 .. str2.length - 1] Local coord:String[ ] = str.split(",") x = Int( coord[0] ) / TILESIZE y = Int( coord[1] ) / TILESIZE res = y * tiles_in_row + x ' Print "res : " + res
Return res End Function
просто ф-ция возвращает число в hex-исполнении с нужным количеством байтов Function show_hex:String( str_int:Int, bytes:Int = 4) Local hex_str:String = Hex( str_int ) Local end_of_string:Int = hex_str.length Local from:Int = end_of_string - bytes * 2 Return hex_str[ from .. ] End Function
Вот в общем-то и все. Тут наверное уже заключительное слово, но как было сказано, Близзард отличная команда давно зарекомендовавшая себя как хит-мейкер на рынке комп. игр и то как они делают игры и все их составляющие нельзя обойти вниманием. Данный формат является достаточно гибким, обладает расширяемостью, а так же легок в понимании и редактировании. И в связи с этим не стоит пренебрегать опытом других людей, и смотря на этот формат можно многое намотать на ус, что создание секций в файле намного облегчает использование этого файла, ведь можно изменить всего пару секций не трогая основной формат, можно добавлять и удалять секции и т.д. и т.п. Думаю для начинающих разработчиков комп. игр это будет небольшим и бесплатным уроком от титанов индустрии.
:)
http://dimanche.ucoz.ru/war2.zip
|