Разбор карт Heroes of Might and Magic III: HotA и разработка парсера на Python

Введение

Мне стало любопытно, можно ли создать парсер карт HotA, способный «на лету» отвечать на игровые вопросы: «Где искать свиток «Городского портала»?», «В каком месте спрятан Чёрный шар?» или «Томится ли герой Джелу в тюрьме?».

Вместо поиска готовых спецификаций в сети я решил пойти сложным путем — разобраться во внутреннем устройстве формата самостоятельно. Будем считать, что интернета не существует, а в распоряжении есть только сами карты, hex-редактор и здоровое любопытство.

Эта статья — отчет о моих «низкоуровневых мучениях»: изучении байт-кода, сопоставлении файлов, поиске закономерностей и извлечении полезной информации. Если эта цифровая археология покажется вам утомительной, можете смело переходить к финалу — там лежит готовое решение, которое поможет вам находить нужные заклинания без лишних усилий.

Сразу оговорюсь: я не претендую на звание эксперта по реверс-инжинирингу, просто захотелось немного поиграть с шестнадцатеричным редактором, да и вручную «ковырять» карты в редакторе надоело.

Подготовка

Для работы я взял ImHex и HxD. Чтобы не запутаться в огромном массиве данных, я создал серию тестовых карт, меняя в каждой ровно один параметр: название, наличие подземелья или сложность. Если менять всё сразу, разобраться в «простыне» байтов будет невозможно, а при пошаговых изменениях файл сам начинает «рассказывать», где спрятаны нужные настройки.

Открыв первую карту через ImHex, я столкнулся с ожидаемым препятствием.

Карта в gzip. Надо всё распаковать.
Карта упакована в gzip. Нужно распаковать.

Карта упакована в gzip. Распаковав все файлы через WinRAR, я приступил к их сравнительному анализу.

Проверка на «шум»

Сначала я убедился, что редактор карт предсказуем: создал карту, сохранил её, а затем сделал копию под другим именем. Побайтное сравнение подтвердило, что файлы идентичны — лишнего «мусора», метаданных или случайных таймстампов программа не добавляет.

Анализ -> Сравнение данных -> Сравнить» title=»Анализ -> Сравнение данных -> Сравнить» sizes=»(max-width: 780px) 100vw, 50vw» srcset=»https://habrastorage.org/r/w780/getpro/habr/upload_files/ad8/61b/606/ad861b606fa8733d85326d904f96d7fa.png 780w,<br />
https://habrastorage.org/r/w1560/getpro/habr/upload_files/ad8/61b/606/ad861b606fa8733d85326d904f96d7fa.png 781w» loading=»lazy» decode=»async»><figcaption>Анализ через HxD</figcaption></figure>
<figure class=Файлы одинаковые
Данные полностью идентичны.

Отлично, фундамент чист.

Извлечение заголовка: название карты

Сравнивая карты с разными названиями (например, «A» и «ABC»), я заметил изменение в ячейке 0x2B. В первом случае там стояло 01, во втором — 03. Попробовав более длинное название, я увидел значение 10, что в шестнадцатеричной системе как раз соответствует десятичному 16 (количеству символов). Логика проста: этот байт хранит длину названия.

HxD64 нашёл отличие
Найдено различие в длине строки.

С русскими названиями всё также предсказуемо — используется стандартная кодировка Windows-1251.

Первые шаги в Python

Определив, что длина названия лежит по адресу 0x2B, а само имя начинается с 0x2F, я написал базовый скрипт для его извлечения.

import gzip

data = gzip.open("hota_lab_03_name_LONG.h3m", "rb").read()

# Читаем длину, используя little-endian
n = int.from_bytes(data[0x2B:0x2F], "little")
name = data[0x2F:0x2F + n]

print(name.decode("cp1251"))

Использование little-endian здесь критично: именно так младший байт числа записывается первым. Это универсальное правило для формата H3M.

Исследование объектов: свитки и заклинания

Самое интересное началось, когда я перешел к объектам. Положив на карту свиток с «Волшебной стрелой», а затем заменив его на «Городской портал», я вычислил байт, отвечающий за ID заклинания. Сравнивая другие заклинания, я подтвердил закономерность.

Magic Arrow     -> Town Portal     : 0F -> 09
Magic Arrow     -> Lightning Bolt  : 0F -> 11
Magic Arrow     -> Implosion       : 0F -> 12
...

Удивительно, но даже при смене типа объекта (скажем, со свитка на объект «Ученый») логика хранения данных сохраняется, хотя и усложняется наличием дополнительных флагов.

Итоговый парсер

Последовательно изучая каждый блок данных — координаты объекта, тип награды, наличие подземелья — я собрал библиотеку для анализа файлов.

Полный код доступен на GitHub: hota-map-parser.

Запуск предельно прост:

python .\analyze_h3m.py "my_map.h3m" spell "Town Portal"

Помимо прямого поиска, я интегрировал систему взаимодействия через ИИ-агентов (Codex). Теперь можно просто спросить у консоли: «Где на этой карте Джелу?», и агент сам просканирует файл, используя мой парсер.

Надеюсь, этот опыт поможет тем, кто тоже любит заглядывать «под капот» любимых игр. Enjoy!

 

Источник

Читайте также