Журнал о программированнии на языках Blitz3D, BlitzPlus, BlitzMax

Формат pud-файла

Материал из Blitz Et Cetera

Перейти к: навигация, поиск

Официально pud - файл именуется как Warcraft II Scenario File.

На самом деле так и есть ведь в нем много различных секций со всей важной информацией касающейся уровня игры. Pud-файл может быть создан "родным" редактором карт, либо посторонним, я видел парочку таких. А это значит что содержимое Pud файлов давно стало известно общественности, не буду говорить кто вскрыл этот формат, потому как называние одного может обидеть другого и так далее. Думаю это было не очень трудной задачей. При достаточном терпении и логе сохранений из редактора карты, можно было понять о местонахождении различных значений в этом файле.

Итак приступим. Файл состоит из секций, каждая секция начинается одинаково:

  1. 4 байта - название секции "header"
  2. 4 байта - длина секции в байтах "length" (без заголовка и длины секции)
  3. 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, waterland, а так же 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

Вот в общем-то и все.

Тут наверное уже заключительное слово, но как было сказано, Близзард отличная команда, давно зарекомендовавшая себя как хит-мейкер на рынке комп. игр, и то, как они делают игры и все их составляющие, нельзя обойти вниманием. Данный формат является достаточно гибким, обладает расширяемостью, а так же легок в понимании и редактировании. И в связи с этим не стоит пренебрегать опытом других людей, и смотря на этот формат можно многое намотать на ус, что создание секций в файле намного облегчает использование этого файла, ведь можно изменить всего пару секций не трогая основной формат, можно добавлять и удалять секции и т.д. и т.п. Думаю для начинающих разработчиков комп. игр это будет небольшим и бесплатным уроком от титанов индустрии. :)


Автор: Dimanche13 (e-mail: dimanche13_sobaka_rambler.ru)

Другие

Друзья