[Из песочницы] «Камень я не дам» или как устроены ресурсы игры «Проклятые Земли»
Много ли вы вспомните российских игр? Качественных? Запоминающихся? Да, такие были. Если вам больше 35 или вы фанат российского игропрома, то с «Проклятыми Землями» вы наверняка знакомы.
История начиналась весьма прозаично: лето, жара. Делать особо нечего, а при ленивом просмотре содержимого жёсткого диска ноутбука взгляд зацепился за папку со знакомой иконкой-дракончиком, лежащую без дела уже пару лет.
Какому фанату игры не будет интересно узнать, что же там внутри?
Информация об игре
Проклятые Земли — или, как они назывались за пределами СНГ, Evil Islands: Curse of the Lost Soul, stealth-RPG игра, вышедшая в 2000 году. Разработкой игры занималась студия Nival Interactive, на тот момент уже зарекомендовавшая себя серией игр Аллоды (Rage of Mages за рубежом). Работали в ней, в основном, выпускники МГУ — им было вполне по силам реализовать одну из первых игр с полностью трёхмерным миром.
В 2010 году права на название перешли Mail.Ru (информация), однако игра продаётся в магазине GOG всё ещё от лица Nival.
Относительно недавно игре исполнилось 18 лет — днём рождения считается 26 октября, дата выхода в СНГ. Несмотря на возраст, официальный мастер-сервер ещё в строю: периодически кто-то решает поползать по лесам Гипата да стукнуть десяток-другой скелетов с отрядом товарищей.
Коротко о статье
Изначально, моей целью было лишь написать односторонний конвертер «для себя» на Python 3, причём с использованием исключительно стандартных библиотек. Однако в процессе плавно началось написание документации по форматам, попытки как-то стандартизировать вывод. Для части форматов была описана структура с помощью Kaitai Struct. В результате всё вылилось в написание данной статьи и wiki по форматам.
Сразу отмечу: большей частью, файлы игры уже исследованы, к ним были написаны фанатские редакторы. Однако информация крайне разрознена, а более-менее целостного описания форматов в открытом доступе нет, как и адекватного набора для создания модификаций.
… и о том, как её читать
Для всех форматов приведены схемы (.ksy файлы), которые можно в два клика сконвертировать в код на нескольких самых популярных языках.
К сожалению, уже на последних этапах написания этой статьи, я обнаружил, что многоуважаемый Хабр не умеет в подсветку YAML (и JSON), а все схемы использует именно его. Это не должно стать большой проблемой, но если читать схему неудобно, могу посоветовать скопировать в сторонний редактор, например, NPP.
Игра представляет собой портативное приложение, содержащее движок с библиотеками, лаунчер и, собственно, упакованные ресурсы.
Это интересно: настройки игры практически целиком хранятся в реестре. Баг камеры в GOG версии связан с тем, что установщик не прописывает корректные значения по-умолчанию.
При первом взгляде на содержимое папки game, мы сразу заметим пару новых расширений файлов: ASI и REG.
Первый — динамическая библиотека, которую рассматривать мы не будем (этим занимаются специалисты по реверс-инжинирингу), а вот второе — первый собственный формат файлов игры.
REG
Файлы этого типа — бинарная сериализация общеизвестных текстовых INI файлов.
Содержимое разбивается на секции, хранящие ключи и их значения. REG файл сохраняет эту иерархию, однако ускоряет чтение и разбор данных — в 2000 году это, видимо, было критично.
В общем виде, можно описать структуру данной диаграммой:
meta:
id: reg
title: Evil Islands, REG file (packed INI)
application: Evil Islands
file-extension: reg
license: MIT
endian: le
doc: Packed INI file
seq:
- id: magic
contents: [0xFB, 0x3E, 0xAB, 0x45]
doc: Magic bytes
- id: sections_count
type: u2
doc: Number of sections
- id: sections_offsets
type: section_offset
doc: Sections offset table
repeat: expr
repeat-expr: sections_count
types:
section_offset:
doc: Section position in file
seq:
- id: order
type: s2
doc: Section order number
- id: offset
type: u4
doc: Global offset of section in file
instances:
section:
pos: offset
type: section
types:
section:
doc: Section representation
seq:
- id: keys_count
type: u2
doc: Number of keys in section
- id: name_len
type: u2
doc: Section name lenght
- id: name
type: str
encoding: cp1251
size: name_len
doc: Section name
- id: keys
type: key
doc: Section's keys
repeat: expr
repeat-expr: keys_count
types:
key:
doc: Named key
seq:
- id: order
type: s2
doc: Key order in section
- id: offset
type: u4
doc: Key offset in section
instances:
key_record:
pos: _parent._parent.offset + offset
type: key_data
key_data:
seq:
- id: packed_type
type: u1
doc: Key value info
- id: name_len
type: u2
doc: Key name lenght
- id: name
type: str
encoding: cp1251
size: name_len
doc: Key name
- id: value
type: value
doc: Key value
instances:
is_array:
value: packed_type > 127
doc: Is this key contain array
value_type:
value: packed_type & 0x7F
doc: Key value type
types:
value:
doc: Key value
seq:
- id: array_size
type: u2
if: _parent.is_array
doc: Value array size
- id: data
type:
switch-on: _parent.value_type
cases:
0: s4
1: f4
2: string
repeat: expr
repeat-expr: '_parent.is_array ? array_size : 1'
doc: Key value data
string:
doc: Sized string
seq:
- id: len
type: u2
doc: String lenght
- id: value
type: str
encoding: cp1251
size: len
doc: String
Это интересно: в 2002 году Nival поделился некоторыми инструментами с коммьюнити игры (снапшот сайта) — одним из них был сериализатор INI в REG. Как можно догадаться, почти сразу появился и десериализатор, пусть и не официальный.
Со стартовой папкой разобрались, перейдём к подкаталогам.
Первым взгляд падает на папку Cameras, содержащую CAM файлы.
CAM
Очень простой формат — просто упаковки положений камер во времени. Камера описывается позицией и вращением. Два остальных поля — предположительно, время и шаг в последовательности перемещений.
meta:
id: cam
title: Evil Islands, CAM file (cameras)
application: Evil Islands
file-extension: cam
license: MIT
endian: le
doc: Camera representation
seq:
- id: cams
type: camera
repeat: eos
types:
vec3:
doc: 3d vector
seq:
- id: x
type: f4
doc: x axis
- id: y
type: f4
doc: y axis
- id: z
type: f4
doc: z axis
quat:
doc: quaternion
seq:
- id: w
type: f4
doc: w component
- id: x
type: f4
doc: x component
- id: y
type: f4
doc: y component
- id: z
type: f4
doc: z component
camera:
doc: Camera parameters
seq:
- id: unkn0
type: u4
doc: unknown
- id: unkn1
type: u4
doc: unknown
- id: position
type: vec3
doc: camera's position
- id: rotation
type: quat
doc: camera's rotation
В соседней папке — Res, хранятся (неожиданно!) RES файлы, являющиеся архивами.
RES
Этот формат иногда прячется под другими расширениями, но оригинальное всё же именно RES.
Структура данных весьма типична для архива с произвольным доступом к файлам: есть таблицы для хранения информации о файлах внутри, таблица имён, само содержимое файлов.
Структура каталогов содержится прямо в именах.
Стоит отметить два крайне интересных факта:
- Архив оптимизирован под загрузку информации о файлах в связный список с закрытым хэшированием.
- Можно хранить содержимое файла один раз, но ссылаться на него под разными именами. Насколько мне известно, этот факт использовался в фанатском репаке, где за счёт этого был сильно уменьшен размер игры. В оригинальном дистрибутиве оптимизация архивов не использовалась.
meta:
id: res
title: Evil Islands, RES file (resources archive)
application: Evil Islands
file-extension: res
license: MIT
endian: le
doc: Resources archive
seq:
- id: magic
contents: [0x3C, 0xE2, 0x9C, 0x01]
doc: Magic bytes
- id: files_count
type: u4
doc: Number of files in archive
- id: filetable_offset
type: u4
doc: Filetable offset
- id: nametable_size
type: u4
doc: Size of filenames
instances:
nametable_offset:
value: filetable_offset + 22 * files_count
doc: Offset of filenames table
filetable:
pos: filetable_offset
type: file_record
repeat: expr
repeat-expr: files_count
doc: Files metadata table
types:
file_record:
doc: File metadata
seq:
- id: next_index
type: s4
doc: Next file index
- id: file_size
type: u4
doc: Size of file in bytes
- id: file_offset
type: u4
doc: File data offset
- id: last_change
type: u4
doc: Unix timestamp of last change time
- id: name_len
type: u2
doc: Lenght of filename
- id: name_offset
type: u4
doc: Filename offset in name array
instances:
name:
io: _root._io
pos: name_offset + _parent.nametable_offset
type: str
encoding: cp1251
size: name_len
doc: File name
data:
io: _root._io
pos: file_offset
size: file_size
doc: Content of file
Это интересно: в русской версии игры, архив Speech.res содержит два подкаталога s и t с полностью идентичным содержанием, из-за чего размер архива в два раза больше — именно поэтому игра не помещается на один CD.
Теперь можно распаковать все архивы (могут быть вложенными):
- RES — просто архив,
- MPR — ландшафт игровых уровней,
- MQ — информация о заданиях мультиплеера,
- ANM — набор анимаций,
- MOD — 3d модель,
- BON — расположение костей модели.
Если файлы внутри архива не имеют расширения, будем ставить расширение родителя — касается BON и ANM архивов.
Также можно разбить все полученные файлы на четыре группы:
- Текстуры;
- Базы данных;
- Модели;
- Файлы уровня.
Начнём с простого — с текстур.
MMP
Собственно, текстура. Имеет небольшой заголовок, указывающий на параметры изображения, число MIP уровней и использованное сжатие. После заголовка располагаются MIP уровни изображения по убыванию размера.
meta:
id: mmp
title: Evil Islands, MMP file (texture)
application: Evil Islands
file-extension: mmp
license: MIT
endian: le
doc: MIP-mapping texture
seq:
- id: magic
contents: [0x4D, 0x4D, 0x50, 0x00]
doc: Magic bytes
- id: width
type: u4
doc: Texture width
- id: height
type: u4
doc: Texture height
- id: mip_levels_count
type: u4
doc: Number of MIP-mapping stored levels
- id: fourcc
type: u4
enum: pixel_formats
doc: FourCC label of pixel format
- id: bits_per_pixel
type: u4
doc: Number of bits per pixel
- id: alpha_format
type: channel_format
doc: Description of alpha bits
- id: red_format
type: channel_format
doc: Description of red bits
- id: green_format
type: channel_format
doc: Description of green bits
- id: blue_format
type: channel_format
doc: Description of blue bits
- id: unused
size: 4
doc: Empty space
- id: base_texture
type:
switch-on: fourcc
cases:
'pixel_formats::argb4': block_custom
'pixel_formats::dxt1': block_dxt1
'pixel_formats::dxt3': block_dxt3
'pixel_formats::pnt3': block_pnt3
'pixel_formats::r5g6b5': block_custom
'pixel_formats::a1r5g5b5': block_custom
'pixel_formats::argb8': block_custom
_: block_custom
types:
block_pnt3:
seq:
- id: raw
size: _root.bits_per_pixel
block_dxt1:
seq:
- id: raw
size: _root.width * _root.height >> 1
block_dxt3:
seq:
- id: raw
size: _root.width * _root.height
block_custom:
seq:
- id: lines
type: line_custom
repeat: expr
repeat-expr: _root.height
types:
line_custom:
seq:
- id: pixels
type: pixel_custom
repeat: expr
repeat-expr: _root.width
types:
pixel_custom:
seq:
- id: raw
type:
switch-on: _root.bits_per_pixel
cases:
8: u1
16: u2
32: u4
instances:
alpha:
value: '_root.alpha_format.count == 0 ? 255 : 255 * ((raw & _root.alpha_format.mask) >> _root.alpha_format.shift) / (_root.alpha_format.mask >> _root.alpha_format.shift)'
red:
value: '255 * ((raw & _root.red_format.mask) >> _root.red_format.shift) / (_root.red_format.mask >> _root.red_format.shift)'
green:
value: '255 * ((raw & _root.green_format.mask) >> _root.green_format.shift) / (_root.green_format.mask >> _root.green_format.shift)'
blue:
value: '255 * ((raw & _root.blue_format.mask) >> _root.blue_format.shift) / (_root.blue_format.mask >> _root.blue_format.shift)'
channel_format:
doc: Description of bits for color channel
seq:
- id: mask
type: u4
doc: Binary mask for channel bits
- id: shift
type: u4
doc: Binary shift for channel bits
- id: count
type: u4
doc: Count of channel bits
enums:
pixel_formats:
0x00004444: argb4
0x31545844: dxt1
0x33545844: dxt3
0x33544E50: pnt3
0x00005650: r5g6b5
0x00005551: a1r5g5b5
0x00008888: argb8
Возможные форматы упаковки пикселей:
fourcc | Описание |
---|---|
44 44 00 00 | ARGB4 |
44 58 54 31 | DXT1 |
44 58 54 33 | DXT3 |
50 4E 54 33 | PNT3 — RLE сжатый ARGB8 |
50 56 00 00 | R5G5B5 |
51 55 00 00 | A1R5G5B5 |
88 88 00 00 | ARGB8 |
Если формат изображения PNT3, то структура пикселей после распаковки — ARGB8; bits_per_pixel
— размер сжатого изображения в байтах.
Распаковка PNT3
n = 0
destination = b""
while src < size:
v = int.from_bytes(source[src:src + 4], byteorder='little')
src += 4
if v > 1000000 or v == 0:
n += 1
else:
destination += source[src - (1 + n) * 4:src - 4]
destination += b"\x00" * v
n = 0
Это интересно: часть текстур отражена по вертикали (или некоторые не отражены?).
А ещё игра весьма ревностно относится к прозрачности — если изображение с альфа каналом, цвет прозрачных пикселов должен быть точно чёрным. Или белым — тут как повезёт.
Простые форматы закончились, переходим к более жёстким — в своё время, ряды модмейкеров яростно хранили свои собственные инструменты редактирования следующих форматов, и не зря. Я вас предупредил.
Базы данных (*DB и иже с ними)
Этот формат крайне неудобно описывать — по существу, это сериализованное дерево нод (или таблиц записей). Файл состоит из нескольких таблиц с заданными типами полей. Общая структура: таблицы вложены в общую «корневую» ноду, записи — ноды внутри таблицы.
В каждой ноде задаётся её тип и размер:
unsigned char type_index;
unsigned char raw_size; // не используется вне этого блока
unsigned length; // не читается из файла
read(raw_size);
if (raw_size & 1)
{
length = raw_size >> 1;
for (int i = 0; i < 3; i++)
length <<= 8;
read(raw_size);
length += raw_size;
}
else
length = raw_size >> 1;
Тип поля таблицы берётся по индексу из форматной строки для таблицы, по полученному значению определяется реальный тип.
обозначение | описание |
---|---|
S | string |
I | 4b int |
U | 4b unsigned |
F | 4b float |
X | bits byte |
f | float array |
i | int array |
B | bool |
b | bool array |
H | unknown hex bytes |
T | time |
0 | not stated |
1 | 0FII |
2 | SUFF |
3 | FFFF |
4 | 0SISS |
5 | 0SISS00000U |
Предметы (.idb)
таблица | структура |
---|---|
Материалы | SSSIFFFIFIFfIX |
Оружие | SSISIIIFFFFIFIXB00000IHFFFfHHFF |
Броня | SSISIIIFFFFIFIXB00000ffBiHH |
Быстрые предметы | SSISIIIFFFFIFIXB00000IIFFSbH |
Квестовые предметы | SSISIIIFFFFIFIXB00000Is |
Продаваемые предметы | SSISIIIFFFFIFIXB00000IHI |
Переключатели (.ldb)
таблица | структура |
---|---|
Прототип переключателя | SfIFTSSS |
Умения и навыки (.pdb)
таблица | структура |
---|---|
Умения | SSI0000000s |
Навыки | SSI0000000SSIIIFFFIIIIBI |
Следы (prints.db)
таблица | структура |
---|---|
Следы крови | 0S11 |
Следы пламени | 0S110000001 |
Следы ног | 0S11 |
Заклинания (.sdb)
таблица | структура |
---|---|
Прототипы | SSSFIFIFFFFIIIIUSSIIbIXFFFFF |
Модификаторы | SSFIFFISX |
Шаблоны | 0SssSX |
Шаблоны для брони | 0SssSX |
Шаблоны для оружия | 0SssSX |
Существа (.udb)
таблица | структура |
---|---|
Повреждаемые части | SffUU |
Расы | SUFFUUFfFUUf222222000000000000SssFSsfUUfUUIUSBFUUUU |
Прототипы монстров | SSIUIFFFSFFFFFFFFFUFFFFFFff33sfssSFFFFFUFUSF |
NPC | SUFFFFbbssssFUB |
Выкрики (acks.db)
таблица | структура |
---|---|
Ответы | 0S0000000044444444444444444444445444444444444 |
Крики | 0S0000000044444 |
Прочее | 0S0000000044 |
Задания (.qdb)
таблица | структура |
---|---|
Задания | SFIISIIs |
Брифинги | SFFsSsssssI |
Это интересно: 16.01.2002 Nival выложил исходные базы для мультиплеера в csv формате, а также утилиту-конвертер в игровой формат (снапшот сайта). Естественно, обратный конвертер не замедлил появиться. Также есть минимум два документа с описанием полей и их типов от модмейкеров, но читать их весьма тяжело.
ADB
База данных анимации для конкретного типа юнитов. В отличии от упомянутых выше *DB, достаточно «человечна» — это одноуровневая таблица со статичными размерами полей.
meta:
id: adb
title: Evil Islands, ADB file (animations database)
application: Evil Islands
file-extension: adb
license: MIT
endian: le
doc: Animations database
seq:
- id: magic
contents: [0x41, 0x44, 0x42, 0x00]
doc: Magic bytes
- id: animations_count
type: u4
doc: Number of animations in base
- id: unit_name
type: str
encoding: cp1251
size: 24
doc: Name of unit
- id: min_height
type: f4
doc: Minimal height of unit
- id: mid_height
type: f4
doc: Middle height of unit
- id: max_height
type: f4
doc: Maximal height of unit
- id: animations
type: animation
doc: Array of animations
repeat: expr
repeat-expr: animations_count
types:
animation:
doc: Animation's parameters
seq:
- id: name
type: str
encoding: cp1251
size: 16
doc: Animation's name
- id: number
type: u4
doc: Index in animations array
- id: additionals
type: additional
doc: Packed structure with animation parameters
- id: action_probability
type: u4
doc: Percents of action probability
- id: animation_length
type: u4
doc: Lenght of animation in game ticks
- id: movement_speed
type: f4
doc: Movement speed
- id: start_show_hide1
type: u4
- id: start_show_hide2
type: u4
- id: start_step_sound1
type: u4
- id: start_step_sound2
type: u4
- id: start_step_sound3
type: u4
- id: start_step_sound4
type: u4
- id: start_hit_frame
type: u4
- id: start_special_sound
type: u4
- id: spec_sound_id1
type: u4
- id: spec_sound_id2
type: u4
- id: spec_sound_id3
type: u4
- id: spec_sound_id4
type: u4
types:
additional:
seq:
- id: packed
type: u8
instances:
weapons:
value: 'packed & 127'
allowed_states:
value: '(packed >> 15) & 7'
action_type:
value: '(packed >> 18) & 15'
action_modifyer:
value: '(packed >> 22) & 255'
animation_stage:
value: '(packed >> 30) & 3'
action_forms:
value: '(packed >> 36) & 63'
Это интересно: для нескольких юнитов используется частично урезанный формат базы, практически не исследованный.
Разобравшись с базами данных, объявляем рекламную паузу. Но рекламировать ничего не будем — не наш метод. Лучше обозначим то, что пригодится дальше — как именуются файлы существ.
Формат имён моделей
Имя собирается из групп по два символа — сокращений логического «уровня».
Например, персонаж-женщина будет unhufe
— Unit > Human > Female
, а initwesp
— Inventory > Item > Weapon > Spear
, то есть, копьё в инвентаре (не спине, и то хорошо).
un: # unit
an: # animal
wi: # wild
ti # tiger
ba # bat
bo # boar
hy # hyen
de # deer
gi # rat
ra # rat
cr # crawler
wo # wolf
ho: # home
co # cow
pi # pig
do # dog
ho # horse
ha # hare
or: # orc
fe # female
ma # male
mo: # monster
co # column (menu)
un # unicorn
cu # Curse
be # beholder
tr # troll
el # elemental
su # succub (harpie)
ba # banshee
dr # driad
sh # shadow
li # lizard
sk # skeleton
sp # spider
go # golem, goblin
ri # Rick
og # ogre
zo # zombie
bi # Rik's dragon
cy # cyclope
dg # dragon
wi # willwisp
mi # octopus
to # toad
hu: # human
fe # female
ma # male
in: # inventory
it: # item
qu # quest
qi # interactive
ar: # armor
pl # plate
gl # gloves
lg # leggins
bt # boots
sh # shirt
hl # helm
pt # pants
li: # loot
mt # material
tr # trade
we: # weapon
hm # hammer
dg # dagger
sp # spear
cb # crossbow
sw # sword
ax # axe
bw # bow
gm # game menu
fa: # faces
un: # unit
an: # animal
wi: # wild
ti: # tiger
face # face
ba: # bat
face # face
bo: # boar
face # face
de: # deer
face # face
ra: # rat
face # face
cr: # crawler
face # face
wo: # wolf
face # face
ho: # home
co: # cow
face # face
pi: # pig
face # face
do: # dog
face # face
ho: # horse
face # face
ha: # hare
face # face
hu: # human
fe: # female
fa #
me #
th #
ma: # male
fa #
me #
th #
mo: # monster
to: # toad
face # face
tr: # troll
face # face
or: # orc
face # face
sp: # spider
face # face
li: # lizard
face # face
na: # nature
fl: # flora
bu # bush
te # termitary
tr # tree
li # waterplant
wa # waterfall
sk # sky
st # stone
ef: # effects
cu #
ar #
co # components
st: # static
si # switch
bu: # building
to # tower
ho # house
tr # trap
br # bridge
ga # gate
we # well (waterhole)
wa: # wall
me # medium
li # light
to # torch
st # static
Это интересно: по данной классификации, грибы — деревья, големы с гоблинами — братья, а Тка-Рик — монстр. Также тут можно заметить «рабочие» имена монстров, подозрительно похожие на таковые из D&D — beholder (злобоглаз), succub (гарпия), ogre (людоед), driad (лесовики).
Морально отдохнув, окунёмся с головой в модели. Они представлены несколькими форматами, которые компонуются между собой.
LNK
Логически — основа модели. Описывает иерархию частей модели, в терминах современного 3d моделирования — иерархию костей.
meta:
id: lnk
title: Evil Islands, LNK file (bones hierarchy)
application: Evil Islands
file-extension: lnk
license: MIT
endian: le
doc: Bones hierarchy
seq:
- id: bones_count
type: u4
doc: Number of bones
- id: bones_array
type: bone
repeat: expr
repeat-expr: bones_count
doc: Array of bones
types:
bone:
doc: Bone node
seq:
- id: bone_name_len
type: u4
doc: Length of bone's name
- id: bone_name
type: str
encoding: cp1251
size: bone_name_len
doc: Bone's name
- id: parent_name_len
type: u4
doc: Length of bone's parent name
- id: parent_name
type: str
encoding: cp1251
size: parent_name_len
doc: Bone's parent name
Имя родителя основной кости — пустая строка (длины 0).
Кости есть, однако недостаточно назвать их и сложить кучкой — нужно собрать их в скелет.
BON
Ранее упоминавшийся, этот формат (если он не архив) задаёт положение частей (костей) модели относительно части-родителя. Хранится лишь смещение, без вращения — одно из отличий от современных форматов.
meta:
id: bon
title: Evil Islands, BON file (bone position)
application: Evil Islands
file-extension: bon
license: MIT
endian: le
doc: Bone position
seq:
- id: position
type: vec3
doc: Bone translation
repeat: eos
types:
vec3:
doc: 3d vector
seq:
- id: x
type: f4
doc: x axis
- id: y
type: f4
doc: y axis
- id: z
type: f4
doc: z axis
Как можно заметить, чисел здесь слишком много для одного смещения — дело в том, что здесь мы впервые наткнулись на одну из ключевых фишек движка игры — трилинейную интерполяцию моделей.
Как это работает: у модели есть три параметра интерполяции — условно, сила, ловкость, рост. Также есть 8 крайних состояний модели. Используя параметры, можем получить итоговую модель трилинейной интерполяцией.
def trilinear(val, coefs=[0, 0, 0]):
# Linear interpolation by str
t1 = val[0] + (val[1] - val[0]) * coefs[1]
t2 = val[2] + (val[3] - val[2]) * coefs[1]
# Bilinear interpolation by dex
v1 = t1 + (t2 - t1) * coefs[0]
# Linear interpolation by str
t1 = val[4] + (val[5] - val[4]) * coefs[1]
t2 = val[6] + (val[7] - val[6]) * coefs[1]
# Bilinear interpolation by dex
v2 = t1 + (t2 - t1) * coefs[0]
# Trilinear interpolation by height
return v1 + (v2 - v1) * coefs[2]
Это интересно: трилинейная интерполяция модели используется для анимации некоторых объектов, например, открытия каменной двери и сундуков.
Теперь самое время посмотреть на сами части модели.
FIG
Пожалуй, этот формат понять слёту невозможно. В сети можно найти его описание и плагин для блендера, но даже с ними осознание приходит не сразу. Взгляните:
meta:
id: fig
title: Evil Islands, FIG file (figure)
application: Evil Islands
file-extension: fig
license: MIT
endian: le
doc: 3d mesh
seq:
- id: magic
contents: [0x46, 0x49, 0x47, 0x38]
doc: Magic bytes
- id: vertex_count
type: u4
doc: Number of vertices blocks
- id: normal_count
type: u4
doc: Number of normals blocks
- id: texcoord_count
type: u4
doc: Number of UV pairs
- id: index_count
type: u4
doc: Number of indeces
- id: vertex_components_count
type: u4
doc: Number of vertex components
- id: morph_components_count
type: u4
doc: Number of morphing components
- id: unknown
contents: [0, 0, 0, 0]
doc: Unknown (aligment)
- id: group
type: u4
doc: Render group
- id: texture_index
type: u4
doc: Texture offset
- id: center
type: vec3
doc: Center of mesh
repeat: expr
repeat-expr: 8
- id: aabb_min
type: vec3
doc: AABB point of mesh
repeat: expr
repeat-expr: 8
- id: aabb_max
type: vec3
doc: AABB point of mesh
repeat: expr
repeat-expr: 8
- id: radius
type: f4
doc: Radius of boundings
repeat: expr
repeat-expr: 8
- id: vertex_array
type: vertex_block
doc: Blocks of raw vertex data
repeat: expr
repeat-expr: 8
- id: normal_array
type: vec4x4
doc: Packed normal data
repeat: expr
repeat-expr: normal_count
- id: texcoord_array
type: vec2
doc: Texture coordinates data
repeat: expr
repeat-expr: texcoord_count
- id: index_array
type: u2
doc: Triangles indeces
repeat: expr
repeat-expr: index_count
- id: vertex_components_array
type: vertex_component
doc: Vertex components array
repeat: expr
repeat-expr: vertex_components_count
- id: morph_components_array
type: morph_component
doc: Morphing components array
repeat: expr
repeat-expr: morph_components_count
types:
morph_component:
doc: Morphing components indeces
seq:
- id: morph_index
type: u2
doc: Index of morphing data
- id: vertex_index
type: u2
doc: Index of vertex
vertex_component:
doc: Vertex components indeces
seq:
- id: position_index
type: u2
doc: Index of position data
- id: normal_index
type: u2
doc: Index of normal data
- id: texture_index
type: u2
doc: Index of texcoord data
vec2:
doc: 2d vector
seq:
- id: u
type: f4
doc: u axis
- id: v
type: f4
doc: v axis
vec3:
doc: 3d vector
seq:
- id: x
type: f4
doc: x axis
- id: y
type: f4
doc: y axis
- id: z
type: f4
doc: z axis
vec3x4:
doc: 3d vector with 4 values per axis
seq:
- id: x
type: f4
doc: x axis
repeat: expr
repeat-expr: 4
- id: y
type: f4
doc: y axis
repeat: expr
repeat-expr: 4
- id: z
type: f4
doc: z axis
repeat: expr
repeat-expr: 4
vertex_block:
doc: Vertex raw block
seq:
- id: block
type: vec3x4
doc: Vertex data
repeat: expr
repeat-expr: _root.vertex_count
vec4x4:
doc: 4d vector with 4 values per axis
seq:
- id: x
type: f4
doc: x axis
repeat: expr
repeat-expr: 4
- id: y
type: f4
doc: y axis
repeat: expr
repeat-expr: 4
- id: z
type: f4
doc: z axis
repeat: expr
repeat-expr: 4
- id: w
type: f4
doc: w axis
repeat: expr
repeat-expr: 4
В чём сложность? Так ведь данные нормалей и вершин хранятся в блоках по 4, а вершины ещё и скомпонованы в 8 блоков для интерполяции.
Это интересно: предположительно, такая группировка сделана для ускорения обработки с помощью SSE инструкций, появившихся в процессорах Intel с 1999.
Что ж, модель мы прочли и составили, однако чего-то не хватает. Точно — анимации!
ANM
Анимация хранится в виде ключевых состояний покомпонентно. Интересен тот факт, что реализована поддержка не только скелетной анимации, но и повершинного морфинга.
meta:
id: anm
title: Evil Islands, ANM file (bone animation)
application: Evil Islands
file-extension: anm
license: MIT
endian: le
doc: Bone animation
seq:
- id: rotation_frames_count
type: u4
doc: Number of rotation frames
- id: rotation_frames
type: quat
repeat: expr
repeat-expr: rotation_frames_count
doc: Bone rotations
- id: translation_frames_count
type: u4
doc: Number of translation frames
- id: translation_frames
type: vec3
repeat: expr
repeat-expr: translation_frames_count
doc: Bone translation
- id: morphing_frames_count
type: u4
doc: Number of morphing frames
- id: morphing_vertex_count
type: u4
doc: Number of vertices with morphing
- id: morphing_frames
type: morphing_frame
repeat: expr
repeat-expr: morphing_frames_count
doc: Array of morphing frames
types:
vec3:
doc: 3d vector
seq:
- id: x
type: f4
doc: x axis
- id: y
type: f4
doc: y axis
- id: z
type: f4
doc: z axis
quat:
doc: quaternion
seq:
- id: w
type: f4
doc: w component
- id: x
type: f4
doc: x component
- id: y
type: f4
doc: y component
- id: z
type: f4
doc: z component
morphing_frame:
doc: Array of verteces morphing
seq:
- id: vertex_shift
type: vec3
repeat: expr
repeat-expr: _parent.morphing_vertex_count
doc: Morphing shift per vertex
Всё — теперь у нас есть полноценная модель, можно полюбоваться на свежеотрендеренного ящера-отшельника:
Узнать, что нужно Ящеру
Разговор с ящером в его жилище
Ящер-Отшельник: Ты пришел, человек. Это хорошо.
Зак: Это все, что ты хотел мне сказать?
Ящер-Отшельник: Ты опять торопишься. Я помню твои вопросы и буду на них отвечать. Я пришел к людям в железе, чтобы заключить сделку. Но я увидел, как они поступили с тобой. Они не держат слова, я перестал им верить. Ты сдержал слово. Сделка будет предложена тебе.
Ящер-Отшельник: Люди любят золото. Ящерам золото неинтересно. Ты выполнишь мое задание, и я дам тебе золото, которое есть у меня. Этого золота много.
Зак (задумчиво и без особой заинтересованности): Хм… Золото… Оно, конечно, не помешает…
Зак: Было бы лучше, если бы ты помог мне узнать, где живет старый маг, которого я так долго ищу. Ведь ящеры — древний народ, и вы можете это знать!
Ящер-Отшельник: Ты прав. Ящеры — древний народ. Я могу собрать все, что нам известно про старика. Ты согласен выполнить мое задание?
Зак: О чем разговор! Считай, что все уже сделано.
Ящер-Отшельник (серьезно): Уже сделано? Ты хочешь меня обмануть?
Зак: Вообще-то я хотел пошутить, а то ты уж больно серьезен.
Ящер-Отшельник: Понимаю. Это шутка. Наверное, я тоже смогу пошутить. Потом. А сейчас мне надо, чтобы ты вернул воду в Канал. Воду украли у нас орки.
Ящер-Отшельник: Иди на юг вдоль воды. Увидишь плотину и Канал. Плотину надо поднять. Рычагом. Я его дам. Канал нужно завалить. Камнем. Камень я не дам. Он уже лежит на краю Канала. Вверх по течению от плотины. Камень тяжелый. Когда орки копали, они его поднимали долго. Если ты его толкнешь, обратно он будет падать быстро.
Ящер-Отшельник: После этого возвращайся. Я расскажу тебе все, что узнаю про старого Мага.
Зак: По рукам! Но, кстати, если ты добавишь к рассказу немножко монет, я вовсе не обижусь.
Ящер-Отшельник: За монетами отправляйся к моим сородичам, которые живут на отмелях дальше, на юге. Пройди на самый дальний песчаный остров, третий по счету. Сокровища будут твоими!
Ящер-Отшельник (сам себе): Странно. Этот человек любит юмор. Я пошутил. Человек не засмеялся. Очень странно.
Теперь — самое интересное: как хранится карта.
MP
Это — заголовочный файл карты. По несчастливому стечению обстоятельств, расширение совпадает с таковым у файлов сохранения мультиплеера, которые мы рассматривать не будем.
Сначала нужно дать общую характеристику ландшафту:
- число «чанков» — кусков карты 32×32 метра;
- максимальную высоту (так как высота вершин хранится в целочисленной шкале);
- число тайловых атласов.
Дополнительно идёт описание материалов карты, а также анимированных тайлов — например, воды или лавы.
meta:
id: mp
title: Evil Islands, MP file (map header)
application: Evil Islands
file-extension: mp
license: MIT
endian: le
doc: Map header
seq:
- id: magic
contents: [0x72, 0xF6, 0x4A, 0xCE]
doc: Magic bytes
- id: max_altitude
type: f4
doc: Maximal height of terrain
- id: x_chunks_count
type: u4
doc: Number of sectors by x
- id: y_chunks_count
type: u4
doc: Number of sectors by y
- id: textures_count
type: u4
doc: Number of texture files
- id: texture_size
type: u4
doc: Size of texture in pixels by side
- id: tiles_count
type: u4
doc: Number of tiles
- id: tile_size
type: u4
doc: Size of tile in pixels by side
- id: materials_count
type: u2
doc: Number of materials
- id: animated_tiles_count
type: u4
doc: Number of animated tiles
- id: materials
type: material
doc: Map materials
repeat: expr
repeat-expr: materials_count
- id: id_array
type: u4
doc: Tile type
repeat: expr
repeat-expr: tiles_count
enum: tile_type
- id: animated_tiles
type: animated_tile
doc: Animated tiles
repeat: expr
repeat-expr: animated_tiles_count
types:
material:
doc: Material parameters
seq:
- id: type
type: u4
doc: Material type by
enum: terrain_type
- id: color
type: rgba
doc: RGBA diffuse color
- id: self_illumination
type: f4
doc: Self illumination
- id: wave_multiplier
type: f4
doc: Wave speed multiplier
- id: warp_speed
type: f4
doc: Warp speed multiplier
- id: unknown
size: 12
types:
rgba:
doc: RGBA color
seq:
- id: r
type: f4
doc: Red channel
- id: g
type: f4
doc: Green channel
- id: b
type: f4
doc: Blue channel
- id: a
type: f4
doc: Alpha channel
enums:
terrain_type:
0: base
1: water_notexture
2: grass
3: water
animated_tile:
doc: Animated tile parameters
seq:
- id: start_index
type: u2
doc: First tile of animation
- id: length
type: u2
doc: Animation frames count
enums:
tile_type:
0: grass
1: ground
2: stone
3: sand
4: rock
5: field
6: water
7: road
8: empty
9: snow
10: ice
11: drygrass
12: snowballs
13: lava
14: swamp
15: highrock
terrain type | Тип |
---|---|
0 | Базовый ландшафт |
1 | Вода без текстуры |
2 | Текстурированная трава |
3 | Текстурированная вода |
material type | Тип |
---|---|
0 | grass |
1 | ground |
2 | stone |
3 | sand |
4 | rock |
5 | field |
6 | water |
7 | road |
8 | (empty) |
9 | snow |
10 | ice |
11 | drygrass |
12 | snowballs |
13 | lava |
14 | swamp |
15 | highrock |
Тип материала должен влиять на проходимость, судя по информации в файле Res/aiinfo.res/tileDesc.reg
.
Это интересно: во всех общедоступных описаниях формата, допущена ошибка — поля земли и воды перепутаны по типам.
И опять же: можно спутать эти файлы с сохранениями мультиплеера.
Теперь мы готовы обработать сами части карты. За дело!
SEC
Файл хранит единичный сектор карты — кусок 32×32 метра. Положение на карте хранится в имени файла, которое имеет вид ZonenameXXXYYY
.
meta:
id: sec
title: Evil Islands, SEC file (map sector)
application: Evil Islands
file-extension: sec
license: MIT
endian: le
doc: Map sector
seq:
- id: magic
contents: [0x74, 0xF7, 0x4B, 0xCF]
doc: Magic bytes
- id: liquids
type: u1
doc: Liquids layer indicator
- id: vertexes
type: vertex
doc: Vertex array 33x33
repeat: expr
repeat-expr: 1089
- id: liquid_vertexes
type: vertex
doc: Vertex array 33x33
if: liquids != 0
repeat: expr
repeat-expr: 'liquids != 0 ? 1089 : 0'
- id: tiles
type: tile
doc: Tile array 16x16
repeat: expr
repeat-expr: 256
- id: liquid_tiles
type: tile
doc: Tile array 16x16
if: liquids != 0
repeat: expr
repeat-expr: 'liquids != 0 ? 256 : 0'
- id: liquid_material
type: u2
doc: Index of material
if: liquids != 0
repeat: expr
repeat-expr: 'liquids != 0 ? 256 : 0'
types:
vertex:
doc: Vertex data
seq:
- id: x_shift
type: s1
doc: Shift by x axis
- id: y_shift
type: s1
doc: Shift by y axis
- id: altitude
type: u2
doc: Height (z position)
- id: packed_normal
type: normal
doc: Packed normal
normal:
doc: Normal (3d vector)
seq:
- id: packed
type: u4
doc: Normal packed in 4b
instances:
x:
doc: Unpacked x component
value: packed >> 11 & 0x7FF
y:
doc: Unpacked y component
value: packed & 0x7FF
z:
doc: Unpacked z component
value: packed >> 22
tile:
doc: Tile parameters
seq:
- id: packed
type: u2
doc: Tile information packed in 2b
instances:
index:
doc: Tile index in texture
value: packed & 63
texture:
doc: Texture index
value: packed >> 6 & 255
rotation:
doc: Tile rotation (*90 degrees)
value: packed >> 14 & 3
Тут разработчики размахнулись на славу — практически все данные хранятся в запакованном виде.
Распаковка нормали
10 бит на ось z, по 11 на x и y
unsigned packed_normal;
float x = ((float)((packed_normal >> 11) & 0x7FF) - 1000.0f) / 1000.0f;
float y = ((float)(packed_normal & 0x7FF) - 1000.0f) / 1000.0f;
float z = (float)(packed_normal >> 22) / 1000.0f;
Информация о текстуре
6 бит на индекс в атласе, 8 на номер текстуры, 2 на вращение
unsigned short texture;
unsigned char tile_index = f & 63;
unsigned char texture_index = (f >> 6) & 255;
unsigned char rotation = (f >> 14) & 3;
Получение ландшафта
Вершины идут по 33 элемента в 33 строки, то есть, образуя 32×32 клетки. Длина клетки по стороне — 1 условная единица.
Позиция вершины:
x = индекс по x + x_offset / 254
y = индекс по y + y_offset / 254
z = altitude / 65535 * max_altitude (из .mp файла)
Вершины объединяются в полигоны «гребёнкой», при этом четыре вершины образуют два полигона:
0 1 2
*-*-*
|/|/| ~
33 *-*-*
|/|/| ~
66 *-*-*
~ ~ ~
Текстура накладывается на сразу че