
Много ли вы вспомните российских игр? Качественных? Запоминающихся? Да, такие были. Если вам больше 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 и сделать финальный рендер на память:

Эпилог
Наша краткая экскурсия по файлам Проклятых Земель подошла к концу. Мы рассмотрели устройство большей части форматов, а особо заинтересованные наверняка уже воспользовались схемами и написали свой собственный конвертер или просмотровщик.
Я надеюсь, что эта статья пригодится фанатам игры или привлечёт новых людей в коммьюнити. Теперь же шансы сделать что-то новое хоть немного, но возросли — кто-нибудь захочет написать редактор карт, это привлечёт ещё больше заинтересованных людей. Эх, мечты-мечты...
На этом я прощаюсь — до встреч на просторах Кании!
UPD (23.01.2019):
Так как политика Хабра изменилась в лучшую сторону, я могу оставить ссылку на репозиторий с кодом и обновлённой версией описаний форматов: github.
Так же хотелось бы поблагодарить всех неравнодушных читателей, которые указали мне на несколько неточностей в описании форматов (например, у карт "гребёнка" полигонов была не в ту сторону).
Общая информация об игре
- http://ru.nival.com/games/history/zemli — официальная страница игры
- https://ru.wikipedia.org/wiki/Проклятые_земли — статья на Википедии
- https://allods.gipat.ru/index.php?p=ei — обобщённая информация от "Вселенной Аллодов"
- https://evil-islands.github.io — сборник ссылок, файлов и полезной информации
Инструменты с открытыми исходниками
- http://gipatgroup.org/utilities — EiEdit (.res, .*db), MobSurgeon (.mob)
- http://svn.gipat.org/trac/GGWiki — EiEdit (.res, .*db), MobSurgeon (.mob), информация о форматах .mp, .sec, .*db
- https://github.com/demothorg/eifixer — исправление некоторых проблем игры
- https://github.com/konstvest/ei_figer — аддон для Blender для редактирования .lnk, .fig, .bon, .anm
- https://github.com/demothorg/ei-tools — библиотека для работы с несколькими форматами (.mob, .lnk, .mpr, .res) + набор инструментов
- https://github.com/konstvest/ei_maper — редактирование карты (.mpr, .mp, .sec, .mob)
- https://github.com/chemmalion/EIDBEditor — редактирование баз данных игры (.*db)
- https://gitlab.com/ykurganov/open-evil-islands — проект по разработке открытого альтернативного движка игры, поддержка чтения большей части форматов
Коммьюнити
- https://vk.com/evil.islands — группа ВКонтакте
- http://allods.gipat.ru — "Вселенная Аллодов"
- http://gipat.ru — форум "Город Джунов"
- http://gipatgroup.org/forum — форум GipatGroup (на данный момент — не функционирует)
- http://honestgroup.net/forum — форум HonestGroup
