Среда, 01.05.2024, 12:47
Personal dimanche13 site
Главная | Регистрация | Вход Приветствую Вас Гость | RSS
Категории каталога
Мои статьи [22]
BlitzMax [17]
раздел содержит статьи относящиеся к языку программирования BlitzMax
SDL [9]
раздел содержит статьи на тему SDL
Code::Blocks [3]
в этом разделе рассказывается как прикрутить движок к интегрированной среде разработки(IDE) Code::Blocks
Форма входа
Поиск
Друзья сайта
Главная » Статьи » Мои статьи

Формат pud-файла
О формате 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

Категория: Мои статьи | Добавил: dimanche (05.01.2009)
Просмотров: 2817 | Комментарии: 2 | Рейтинг: 0.0/0 |
Всего комментариев: 2
2 BrandonCex  
0
stuffy head remedies <a href=""> https://forums.dieviete.lv/profils/127605/forum/ </a> best earache remedy

1 SBJoker  
0
Отличная статья!

Имя *:
Email *:
Код *:
Copyright MyCorp © 2024