За время, прошедшее с Нового Года, WERD достаточна оформилась и теперь у меня есть о ней чёткое представление. Написал что-то вроде краткой статьи о ней — вдруг кто-то захочет сделать нечто подобное, только на высокоуровневом языке.
Аббревиатура WERD изначально является палиндромом имени Drew, так как автор платформы страдает ДБГМ и СПГС.
Основу WERD составляет MoP.exe и собственно Werd.dll, написанная на flat assembler-е (fasm) – свободно распространяемом компиляторе языка ассемблера, чьим автором является Tomasz Grysztar.
Упор WERD сделан на скорость и компактность с сохранением достаточной читаемости исходного кода.
Сутью замены ERM является всего один хук, открывающий WERD доступ ко всем ERM-триггерам. По получении номера вызываемого триггера происходит быстрое сканирование (repne scasd) в таблице номеров триггеров. Если найденный триггер соответствует вызванному – вызывается функция, чей адрес расположен в другой таблице (WERD-триггеры) на том же смещении относительно базового адреса блока данных. Таким образом, каждому ERM-триггеру соответствует всего одна процедура.
Исходя из сказанного выше, нетрудно догадаться, что можно дополнительно регулировать скорость вызова отдельных триггерных процедур, просто поставив их в начало таблицы. В первую очередь это – ведение мышью, передвижение героя, клики и прочие очень часто вызываемые триггеры. Впрочем, даже при списке в тысячу триггеров этот процесс не может заметно замедлиться.
Благодаря нужному макросу, две таблицы в исходниках представляют собой одну, что исключает возможность запутаться, какой триггер какой процедуре соответствует.
Удаление или закомментирование триггера в таблице автоматически исключает его из списка компиляции, так как одной из особенностей fasm-а является то, что он не компилирует «мёртвый код» — процедуры, которые нигде не вызываются и даже не присутствуют в виде бинарных данных.
В исходниках каждый триггер начинается в файле \WERD\Begin\Название_триггера.inc объявлением процедуры и заканчивается в файле \WERD\End\Название_триггера.inc её завершением. В первом файле триггер получает данные и кладёт их в свои локальные переменные. В последнем файле могут находиться некоторые специфические данные триггера (особые функции, свитчи и пр.), но чаще всего там просто завершение процедуры.
Так как хук, реализующий WERD-триггеры, сохраняет в стеке регистры процессора, то эти регистры нет нужды пересохранять при вызове триггерной процедуры. WERD-триггер по сути – автономное образование, влияющее на последующий за ним код только через вложенные в него вызовы функций или через глобальные переменные.
Каждому триггеру соответствует файл с листингом include-ов – файлов с кодом, который присоединяется к триггеру в процессе компиляции. Листинг начинается с файла из папки Begin и заканчивается файлом из папки End. Файлы с триггерной «начинкой» находятся в папках, структурированных по игровым элементам.
Файлы с архитектурой триггеров также собраны в список include-ов в файле Werd.inc, определяющем порядок их положения в коде.
Естественным дополнением к этой системе являются описания игровых структур, константы, переменные, строки и различного рода макросы.
Главной достопримечательностью WERD является её нарастающая коллекция переходников к игровым функциям. Если специфические функции WERD расположены в самой библиотеке, то практически весь доступ к игровым процедурам осуществляется через короткую конструкцию push %Адрес функции%; ret. Остальная работа по оптимизации уже проделана в MoP.exe, как то:
1. Неудобные вог-процедуры с си-соглашением получают stdcall-ные переходники к ним.
2. Функции с постоянными параметрами (вроде ecx = [постоянный адрес]) обрезаются в переходнике по этим параметрам.
3. Частая последовательность вызова одних и тех же процедур для такого же постоянного действия (вроде инициализации DL-диалога) оборачивается в одну функцию с минимальным количеством параметров.
4. Короткие, но важные функции (вроде получения адреса структуры какого-либо игрового объекта или некой математической операции над массивами данных), тоже прописаны в exe.
Описанный подход неоднозначен. С одной стороны, именно эта особенность, главным образом, делает WERD несовместимой с другим exe, а с другой – обуславливает свободу в пределах данного мода. Сам список переходников обёрнут макросом в таблицу, которая служит и самим кодом, и наглядной документацией к нему (параметры вызова подробно комментируются). Это делает Werd.dll чрезвычайно компактной и нетребовательной. Кроме того: таблица переходников может быть экспортирована целиком в другую длл, если таковая появится в моде, и займёт в ней ничтожное пространство.
WERD поддерживает все необходимые соглашения вызова процедур – stdcall, ccall, thiscall, fastcall и стандартный вызов API-процедур – invoke.
Большинство констант, функций, структур, переменных и даже самих source-файлов в WERD имеет русские названия. Причём эти названия обычно очень длинные и подробные. Ассемблер вообще к этому приучает…
WERD – очень разветвлённая система. На настоящий момент её исходники состоят из 55 папок с более тремястами файлов при весе самой готовой библиотеки всего чуть более 20 Кб.
За месяц, прошедший с выхода версии мода Seek & Destroy, система WERD поглотила более 1000 строк ERM-кода в различных участках скрипта, при этом едва увеличив свой объём вдвое.
Пример кода, реализующего то, что изображено на скрине выше. Блоки, расположенные в разных файлах, отделены коммент-символами.
; Правый клик на карте приключений
include 'WERD\Begin\Правый клик на карте приключений.inc'
.if signed Активный_герой > -1
include 'WERD\Переименование героя и города\Клик в окне статуса - переименование героя.inc'
include 'WERD\XL-портреты\Выбор в окне статуса.ASM'
include 'WERD\Показ бонусов Морали и Удачи героя при кликах в окне статуса\Показ бонусов Морали и Удачи героя при кликах в окне статуса - правый клик.ASM'
.endif
.if signed Активный_город > -1
include 'WERD\Переименование героя и города\Клик в окне статуса - переименование города.inc'
include 'WERD\Покупка существ в городе\Подсказка к кнопке Купить Всех в окне статуса.ASM'
.endif
.if [Тип_объекта_в_кликнутой_клетке] = 20 | [Тип_объекта_в_кликнутой_клетке] = 17
include 'WERD\Улучшенные подсказки по внешним жилищам\Улучшенные подсказки по внешним жилищам.ASM'
include 'WERD\Улучшение поселения\Улучшение поселения.ASM'
.endif
include 'WERD\Сброс артефактов на карту\Подсказка к Мешку по ПКМ.ASM'
include 'WERD\Показ количества Дуалита\Показ количества Дуалита.ASM'
include 'WERD\Объект Древо Жизни\Подсказка по ПКМ.ASM'
include 'WERD\Магистрат\Подсказка по ПКМ.ASM'
include 'WERD\RB\Подсказка по ПКМ.ASM'
include 'WERD\Подсказки к артефактам на карте приключений\Подсказка по ПКМ.ASM'
include 'WERD\End\Правый клик на карте приключений.inc'
; ¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤
.if Область_клика = Окно_статуса_по_городу_!_Иконка_Форта & Подтип_клика = Нажата_правая_кнопка_мыши
stdcall Подсказка_к_кнопке_Купить_Всех, [Структура_активного_города]
ret
.endif
; ¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤
proc Подсказка_к_кнопке_Купить_Всех uses ebx esi edi, TownStr
locals
Ресурсы_игрока dd 7 dup (?)
Тип_существ dd ?
Количество_существ dd ?
Номер_города dd ?
endl
Отмена_стандартной_реакции_клика
mov eax, Структура_текущего_игрока
stdcall Копирование_памяти, addr eax + Структура_игрока.Дерево, addr Ресурсы_игрока, 4*7
mov ebx, [TownStr]
movsx eax, [ebx + Структура_города.Номер_города]
mov [Номер_города], eax
rv esi, stdcall, Инициализация_игрового_диалога, Диалог_подсказки_к_кнопке_Купить_Всех, _Шаблон_диалога_подсказки_к_кнопке_Купить_Всех
stdcall Настройка_элемента_игрового_диалога, esi, 100, 13, Текущий_игрок
ccall Преобразование_текста_с_включёнными_подстроками, Буфер_для_текста, стр 452 MoPSpec, [ebx + Структура_города.Название]
stdcall Настройка_элемента_игрового_диалога, esi, 101, 3, Буфер_для_текста
; Настройка верхних слотов
mov edi, 6
.Цикл_слотов:
fastcall Проверка_доступности_существа_в_городе, [Номер_города], edi
.if word [ERM_Flag_2]
stdcall Настройка_элемента_игрового_диалога, esi, addr edi + 201, 9, mwcrport_def
lea eax, [edi * 2 + ebx + Структура_города.Количество_улучшенных_существ_0]
mov ecx, [Номер_города]
movsx edx, [ebx + Структура_города.Тип]
Умножить edx, 56
Умножить ecx, 504
add ecx, edx
lea edx, [edi * 4 + ecx + Структура_типов_обитателей_для_каждого_города + 28]
.if ~byte [ERM_Flag_3]
sub eax, 14
sub edx, 28
.endif
movsx eax, word [eax]
mov [Количество_существ], eax
m2m dword [edx], [Тип_существ]
stdcall Конвертирование_числа_в_текст, eax, Буфер_для_текста
stdcall Настройка_элемента_игрового_диалога, esi, addr edi + 31, 3, Буфер_для_текста
mov edx, [Тип_существ]
add edx, 2
stdcall Настройка_элемента_игрового_диалога, esi, addr edi + 201, 4, edx
; Настройка нижних слотов
stdcall Вычисление_количества_нанимаемых_монстров, [Тип_существ], [Количество_существ], addr Ресурсы_игрока
.if eax
stdcall Конвертирование_числа_в_текст, eax, Буфер_для_текста
stdcall Настройка_элемента_игрового_диалога, esi, addr edi + 131, 3, Буфер_для_текста
stdcall Настройка_элемента_игрового_диалога, esi, addr edi + 301, 9, mwcrport_def
mov edx, [Тип_существ]
add edx, 2
stdcall Настройка_элемента_игрового_диалога, esi, addr edi + 301, 4, edx
.endif
.endif
dec edi
jge .Цикл_слотов
; Настройка Портала Вызова (верхний слот)
mov eax, Размер_структуры_городских_реликвий
imul eax, [Номер_города]
Проверка_бита [ebx + Структура_города.Биты_строений1], Бит_Портал_Вызова
.if CARRY? & [ebx + Структура_города.Тип] = Темница | [eax + Адрес_структуры_городских_реликвий + Структура_городских_реликвий.Тёмный_Круг_Баа_за_Дерис]
stdcall Настройка_элемента_игрового_диалога, esi, 208, 9, mwcrport_def
mov eax, [ebx + Структура_города.Тип_существа_в_Портале_Вызова]
mov [Тип_существ], eax
add eax, 2
stdcall Настройка_элемента_игрового_диалога, esi, 208, 4, eax
movsx eax, [ebx + Структура_города.Количество_существ_в_Портале_Вызова]
mov [Количество_существ], eax
stdcall Конвертирование_числа_в_текст, eax, Буфер_для_текста
stdcall Настройка_элемента_игрового_диалога, esi, 38, 3, Буфер_для_текста
; Настройка Портала Вызова (нижний слот)
stdcall Вычисление_количества_нанимаемых_монстров, [Тип_существ], [Количество_существ], addr Ресурсы_игрока
.if eax
stdcall Конвертирование_числа_в_текст, eax, Буфер_для_текста
stdcall Настройка_элемента_игрового_диалога, esi, 138, 3, Буфер_для_текста
stdcall Настройка_элемента_игрового_диалога, esi, 308, 9, mwcrport_def
mov edx, [Тип_существ]
add edx, 2
stdcall Настройка_элемента_игрового_диалога, esi, 308, 4, edx
.endif
.endif
mov edi, 6; счётчик ресурсов
.Цикл_ресурсов:
stdcall Конвертирование_числа_в_текст, [edi * 4 + Ресурсы_игрока], Буфер_для_текста
stdcall Настройка_элемента_игрового_диалога, esi, addr edi + 63, 3, Буфер_для_текста
dec edi
jge .Цикл_ресурсов
thiscall Показ_игрового_диалога_Pop_Up, Диалог_подсказки_к_кнопке_Купить_Всех
ret
endp
proc Вычисление_количества_нанимаемых_монстров uses ebx edi, тип, количество, адрес_строки_вычитания_ресурсов
xor edi, edi; счётчик существ, которые могут быть наняты
mov ebx, [тип]
Умножить ebx, Размер_структуры_монстра
lea ebx, [ebx + Адрес_структуры_монстров + Структура_монстра.Цена_в_дереве]
.while signed edi < [количество]
stdcall Сравнение_значений_двух_массивов_двойных_слов, ebx, [адрес_строки_вычитания_ресурсов], 7
test eax, eax
je .недостаточно_ресурсов
inc edi
fastcall Вычитание_массива_из_массива, [адрес_строки_вычитания_ресурсов], ebx, 2, 7
.endw
.недостаточно_ресурсов:
mov eax, edi
ret
endp
Думаю, первый вопрос будет: «На каком скрине?».
На этом: