Много ли вы вспомните российских игр? Качественных? Запоминающихся? Да, такие были. Если вам больше 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 *-*-*
~ ~ ~
Текстура накладывается на сразу четыре таких клетки, то есть, 16х16 тайлов. Длина тайла — 2 условные единицы. Тайл может быть повёрнут на угол, кратный 90 градусам.
Сектор может содержать информацию о жидкостях на уровне. В таком случае, помимо вершин и текстурной информации, в конце файла указывается ID материала воды, являющийся индексом в таблице материалов из MP файла.
Это интересно: как и для MP, в описаниях формата допущена ошибка, но здесь она уже гораздо более весомая: указание ID материала считали указанием видимости тайла, из-за чего меш строился бы некорректно.
Также ID разбивает жидкости уровня на несколько групп — подъём воды после применения рычага как раз использует это.
Отлично — теперь у нас есть готовый ландшафт:
Осталось совсем чуть-чуть — добавить на него объекты, а заодно и рассмотреть последний в данной статье формат.
MOB
Если вам понравился (или наоборот) формат баз данных, то здесь используется несколько иной, но чисто теоретически схожий принцип: сериализованное дерево записей. Причём отличие от формата баз огромно и в хорошую сторону — нет чёткой структуры "по шаблону", зато есть правило составления полей.
Поле может быть либо записью и содержать другие поля, либо быть ключом (хранить значение в конкретном формате).
Очень краткое представление:
typedef structure
{
unsigned type_id;
unsigned size;
byte data[size - 8];
} node;
Описание структуры (опять же, неполное)
meta:
id: mob
title: Evil Islands, MOB file (map entities)
application: Evil Islands
file-extension: mob
license: MIT
endian: le
doc: Map entities tree
seq:
- id: root_node
type: node
doc: Root node
types:
node:
doc: Entity node
seq:
- id: type_id
type: u4
doc: Node children type ID
- id: size
type: u4
doc: Node full size
- id: data
type: node_data
size: size - 8
doc: Node stored data
node_data:
doc: Node data
seq:
- id: value
type:
switch-on: _parent.type_id
cases:
0xA000: node
0x00001E00: node
0x00001E01: node
0x00001E02: node
0x00001E03: node
0x00001E0B: node
0x00001E0E: node
0x0000A000: node
0x0000AA01: node
0x0000ABD0: node
0x0000B000: node
0x0000B001: node
0x0000CC01: node
0x0000DD01: node
0x0000E000: node
0x0000E001: node
0x0000F000: node
0x0000FF00: node
0x0000FF01: node
0x0000FF02: node
0xBBAB0000: node
0xBBAC0000: node
0xBBBB0000: node
0xBBBC0000: node
0xBBBD0000: node
0xBBBE0000: node
0xBBBF0000: node
0xDDDDDDD1: node
_: u1
doc: Node elements
repeat: eos
тип данных | размер (обычно) | описание |
---|---|---|
AiGraph | граф проходимости | |
AreaArray | ||
Byte | 1 | 1б беззнаковое целое |
Diplomacy | 4096 | 32x32 матрица из 2б целых |
Dword | 4 | 4б беззнаковое целое |
Float | 4 | 4б вещественное |
LeverStats | 12 | параметры рычага |
Null | 0 | пустая нода |
Plot | 12 | 3 floats (vec3) |
Plot2DArray | ||
Quaternion | 16 | 4 floats (vec4) |
Record | >8 | контейнер нод |
Rectangle | ||
String | строка | |
StringArray | >4 | массив строк |
StringEncrypted | >4 | зашифрованный скрипт уровня |
UnitStats | 180 | параметры существа |
Unknown |
type_id | Тип данных | Имя поля |
---|---|---|
0x00000000 | Record | ROOT |
0x00001E00 | Record | VSS_SECTION |
0x00001E01 | Record | VSS_TRIGER |
0x00001E02 | Record | VSS_CHECK |
0x00001E03 | Record | VSS_PATH |
0x00001E04 | Dword | VSS_ID |
0x00001E05 | Rectangle | VSS_RECT |
0x00001E06 | Dword | VSS_SRC_ID |
0x00001E07 | Dword | VSS_DST_ID |
0x00001E08 | String | VSS_TITLE |
0x00001E09 | String | VSS_COMMANDS |
0x00001E0A | Byte | VSS_ISSTART |
0x00001E0B | Record | VSS_LINK |
0x00001E0C | String | VSS_GROUP |
0x00001E0D | Byte | VSS_IS_USE_GROUP |
0x00001E0E | Record | VSS_VARIABLE |
0x00001E0F | StringArray | VSS_BS_CHECK |
0x00001E10 | StringArray | VSS_BS_COMMANDS |
0x00001E11 | String | VSS_CUSTOM_SRIPT |
0x0000A000 | Record | OBJECTDBFILE |
0x0000AA00 | Null | LIGHT_SECTION |
0x0000AA01 | Record | LIGHT |
0x0000AA02 | Float | LIGHT_RANGE |
0x0000AA03 | String | LIGHT_NAME |
0x0000AA04 | Plot | LIGHT_POSITION |
0x0000AA05 | Dword | LIGHT_ID |
0x0000AA06 | Byte | LIGHT_SHADOW |
0x0000AA07 | Plot | LIGHT_COLOR |
0x0000AA08 | String | LIGHT_COMMENTS |
0x0000ABD0 | Record | WORLD_SET |
0x0000ABD1 | Plot | WS_WIND_DIR |
0x0000ABD2 | Float | WS_WIND_STR |
0x0000ABD3 | Float | WS_TIME |
0x0000ABD4 | Float | WS_AMBIENT |
0x0000ABD5 | Float | WS_SUN_LIGHT |
0x0000B000 | Record | OBJECTSECTION |
0x0000B001 | Record | OBJECT |
0x0000B002 | Dword | NID |
0x0000B003 | Dword | OBJTYPE |
0x0000B004 | String | OBJNAME |
0x0000B005 | Null | OBJINDEX |
0x0000B006 | String | OBJTEMPLATE |
0x0000B007 | String | OBJPRIMTXTR |
0x0000B008 | String | OBJSECTXTR |
0x0000B009 | Plot | OBJPOSITION |
0x0000B00A | Quaternion | OBJROTATION |
0x0000B00B | Null | OBJTEXTURE |
0x0000B00C | Plot | OBJCOMPLECTION |
0x0000B00D | StringArray | OBJBODYPARTS |
0x0000B00E | String | PARENTTEMPLATE |
0x0000B00F | String | OBJCOMMENTS |
0x0000B010 | Null | OBJ_DEF_LOGIC |
0x0000B011 | Byte | OBJ_PLAYER |
0x0000B012 | Dword | OBJ_PARENT_ID |
0x0000B013 | Byte | OBJ_USE_IN_SCRIPT |
0x0000B014 | Byte | OBJ_IS_SHADOW |
0x0000B015 | Null | OBJ_R |
0x0000B016 | String | OBJ_QUEST_INFO |
0x0000C000 | Null | SC_OBJECTDBFILE |
0x0000CC00 | Null | SOUND_SECTION |
0x0000CC01 | Record | SOUND |
0x0000CC02 | Dword | SOUND_ID |
0x0000CC03 | Plot | SOUND_POSITION |
0x0000CC04 | Dword | SOUND_RANGE |
0x0000CC05 | String | SOUND_NAME |
0x0000CC06 | Dword | SOUND_MIN |
0x0000CC07 | Dword | SOUND_MAX |
0x0000CC08 | String | SOUND_COMMENTS |
0x0000CC09 | Null | SOUND_VOLUME |
0x0000CC0A | StringArray | SOUND_RESNAME |
0x0000CC0B | Dword | SOUND_RANGE2 |
0x0000CC0D | Byte | SOUND_AMBIENT |
0x0000CC0E | Byte | SOUND_IS_MUSIC |
0x0000D000 | Null | PR_OBJECTDBFILE |
0x0000DD00 | Null | PARTICL_SECTION |
0x0000DD01 | Record | PARTICL |
0x0000DD02 | Dword | PARTICL_ID |
0x0000DD03 | Plot | PARTICL_POSITION |
0x0000DD04 | String | PARTICL_COMMENTS |
0x0000DD05 | String | PARTICL_NAME |
0x0000DD06 | Dword | PARTICL_TYPE |
0x0000DD07 | Float | PARTICL_SCALE |
0x0000E000 | Record | DIRICTORY |
0x0000E001 | Record | FOLDER |
0x0000E002 | String | DIR_NAME |
0x0000E003 | Dword | DIR_NINST |
0x0000E004 | Dword | DIR_PARENT_FOLDER |
0x0000E005 | Byte | DIR_TYPE |
0x0000F000 | Record | DIRICTORY_ELEMENTS |
0x0000FF00 | Record | SEC_RANGE |
0x0000FF01 | Record | MAIN_RANGE |
0x0000FF02 | Record | RANGE |
0x0000FF05 | Dword | MIN_ID |
0x0000FF06 | Dword | MAX_ID |
0x31415926 | AiGraph | AIGRAPH |
0xACCEECCA | String | SS_TEXT_OLD |
0xACCEECCB | StringEncrypted | SS_TEXT |
0xBBAB0000 | Record | MAGIC_TRAP |
0xBBAB0001 | Dword | MT_DIPLOMACY |
0xBBAB0002 | String | MT_SPELL |
0xBBAB0003 | AreaArray | MT_AREAS |
0xBBAB0004 | Plot2DArray | MT_TARGETS |
0xBBAB0005 | Dword | MT_CAST_INTERVAL |
0xBBAC0000 | Record | LEVER |
0xBBAC0001 | Null | LEVER_SCIENCE_STATS |
0xBBAC0002 | Byte | LEVER_CUR_STATE |
0xBBAC0003 | Byte | LEVER_TOTAL_STATE |
0xBBAC0004 | Byte | LEVER_IS_CYCLED |
0xBBAC0005 | Byte | LEVER_CAST_ONCE |
0xBBAC0006 | LeverStats | LEVER_SCIENCE_STATS_NEW |
0xBBAC0007 | Byte | LEVER_IS_DOOR |
0xBBAC0008 | Byte | LEVER_RECALC_GRAPH |
0xBBBB0000 | Record | UNIT |
0xBBBB0001 | Null | UNIT_R |
0xBBBB0002 | String | UNIT_PROTOTYPE |
0xBBBB0003 | Null | UNIT_ITEMS |
0xBBBB0004 | UnitStats | UNIT_STATS |
0xBBBB0005 | StringArray | UNIT_QUEST_ITEMS |
0xBBBB0006 | StringArray | UNIT_QUICK_ITEMS |
0xBBBB0007 | StringArray | UNIT_SPELLS |
0xBBBB0008 | StringArray | UNIT_WEAPONS |
0xBBBB0009 | StringArray | UNIT_ARMORS |
0xBBBB000A | Byte | UNIT_NEED_IMPORT |
0xBBBC0000 | Record | UNIT_LOGIC |
0xBBBC0001 | Null | UNIT_LOGIC_AGRESSIV |
0xBBBC0002 | Byte | UNIT_LOGIC_CYCLIC |
0xBBBC0003 | Dword | UNIT_LOGIC_MODEL |
0xBBBC0004 | Float | UNIT_LOGIC_GUARD_R |
0xBBBC0005 | Plot | UNIT_LOGIC_GUARD_PT |
0xBBBC0006 | Byte | UNIT_LOGIC_NALARM |
0xBBBC0007 | Byte | UNIT_LOGIC_USE |
0xBBBC0008 | Null | UNIT_LOGIC_REVENGE |
0xBBBC0009 | Null | UNIT_LOGIC_FEAR |
0xBBBC000A | Float | UNIT_LOGIC_WAIT |
0xBBBC000B | Byte | UNIT_LOGIC_ALARM_CONDITION |
0xBBBC000C | Float | UNIT_LOGIC_HELP |
0xBBBC000D | Byte | UNIT_LOGIC_ALWAYS_ACTIVE |
0xBBBC000E | Byte | UNIT_LOGIC_AGRESSION_MODE |
0xBBBD0000 | Record | GUARD_PT |
0xBBBD0001 | Plot | GUARD_PT_POSITION |
0xBBBD0002 | Null | GUARD_PT_ACTION |
0xBBBE0000 | Record | ACTION_PT |
0xBBBE0001 | Plot | ACTION_PT_LOOK_PT |
0xBBBE0002 | Dword | ACTION_PT_WAIT_SEG |
0xBBBE0003 | Dword | ACTION_PT_TURN_SPEED |
0xBBBE0004 | Byte | ACTION_PT_FLAGS |
0xBBBF0000 | Record | TORCH |
0xBBBF0001 | Float | TORCH_STRENGHT |
0xBBBF0002 | Plot | TORCH_PTLINK |
0xBBBF0003 | String | TORCH_SOUND |
0xDDDDDDD1 | Record | DIPLOMATION |
0xDDDDDDD2 | Diplomacy | DIPLOMATION_FOF |
0xDDDDDDD3 | StringArray | DIPLOMATION_PL_NAMES |
0xFFFFFFFF | Unknown | UNKNOWN |
Этот файл содержит всю информацию об уровне — дипломатию, расположение и параметры юнитов, информацию для легендарного редактора Nival, а самое интересное — скрипт уровня, причём в зашифрованном виде (ключ лежит рядом, не беспокойтесь).
unsigned key;
for (size_t i = 0; i < size; i++)
{
key += (((((key * 13) << 4) + key) << 8) - key) * 4 + 2531011;
data[i] ^= key >> 16;
}
Это интересно: этот формат очень важен для модмейкеров, однако отреверсить файлы и расшифровать скрипт (а потом зашифровать обратно) было очень непростой задачей. В давние времена, одна из команд написала свою утилиту, в которой шифрование производилось одним постоянным ключом, с целью выявления факта использования конкурирующей командой.
Тот самый легендарный редактор уровней (взято с форумов, точная дата неизвестна, однако на скриншоте — Windows 98):
Это интересно: скриншот редактора несколько раз появлялся на форумах, где его страстно желали заполучить. Естественно, что его так никому и не дали (кроме как, возможно, разработчикам "Проклятые Земли: Затерянные в Астрале", но информации у меня нет).
Вот теперь, получив всю необходимую нам информацию, мы наконец-то можем сконвертировать всё в более-менее известный формат файлов, например, Collada и сделать финальный рендер на память:
Эпилог
Наша краткая экскурсия по файлам Проклятых Земель подошла к концу. Мы рассмотрели устройство большей части форматов, а особо заинтересованные наверняка уже воспользовались схемами и написали свой собственный конвертер или просмотровщик.
Я надеюсь, что эта статья пригодится фанатам игры или привлечёт новых людей в коммьюнити. Теперь же шансы сделать что-то новое хоть немного, но возросли — кто-нибудь захочет написать редактор карт, это привлечёт ещё больше заинтересованных людей. Эх, мечты-мечты...
На этом я прощаюсь — до встреч на просторах Кании!
Автор: Aspadm