Многие играли в эту замечательную игру. Интересный сюжет, хорошая музыка, неплохой геймплей. Только есть пара моментов, которые мне не нравятся. Бег персонажей очень ограниченный, буквально несколько секунд, а запас сил восстанавливается долго. Система начисления опыта не стимулирует брать напарников, потому что опыт распределяется на всех поровну, и лучше бегать одному, чтобы забирать весь опыт себе. Возьмем отладчик и попробуем это исправить.

Нам понадобятся ArtMoney, IDA, Hiew.

В действиях ничего сложного нет, здесь главное результат. Смещения приведены для версии 1.07.

Запускаем IDA, загружаем файл game.exe, ждем пока завершится анализ. Запускаем стартер, убираем настройку «Полноэкранный режим». Запускаем игру в отладчике, загружаем сохранение где уже можно взять напарника. Берем напарника, выходим на карту.




Бег


Смотрим, сколько запаса сил у персонажа. Здесь это 54. Запускаем ArtMoney. Ищем это значение. Тип «С точкой 4 байта». Пробегаем сколько-нибудь и отсеиваем новое значение. Повторяем сколько нужно, у меня сразу осталось одно значение.



Пробуем заморозить, если запас сил при беге восстанавливается, значит нашли правильно. Только надо снять потом. Включаем режим бега. Ставим игру на паузу (пробел).

Ставим на этот адрес брейкпойнт на запись в IDA. Перед этим обязательно надо сделать 'Pause process', иначе случаются вылеты. Переключаемся на игру, отправляем персонажа в какую-нибудь точку, снимаем с паузы.

Брейкпойнт срабатывает.

.text:00548315 loc_548315:
.text:00548315                 fld     dword ptr [edi+14h]
.text:00548318                 fld     dword ptr [edi+18h]
.text:0054831B                 fmul    ds:dbl_73F088
.text:00548321                 fsubp   st(1), st
.text:00548323                 fst     dword ptr [edi+14h]
.text:00548326 >               fcomp   ds:flt_73B858
.text:0054832C                 fnstsw  ax
.text:0054832E                 test    ah, 1
.text:00548331                 jz      short loc_548388

Запись происходит в инструкции fst dword ptr [edi+14h]. Можно поставить Operand type — Floating point на этот адрес и на соседний [edi+18h].



Судя по значениям, там хранится:

[edi+14h] — текущее значение
[edi+18h] — максимальное значение

Видно, что максимальный запас сил персонажа умножается на константу dbl_73F088, и результат вычитается из текущего значения. Поэтому все персонажи бегают одинаково.

.rdata:0073F088 dbl_73F088      dq 6.666666666666666e-3
; 6.666666666666666e-3 = 0.006666666666666666 = 1/150

То есть персонаж может пробежать примерно 150 «шагов», но это не совсем шаги, которые видно на анимации, потому что вычитание происходит гораздо чаще. Полный запас сил расходуется за 9-10 секунд, значит вычитание вызывается 15 — 16.66 раз в секунду.

Скорее всего это зеленые точки, обозначающие путь.




В байтах эта константа записывается так:

4E 1B E8 B4 81 4E 7B 3F

Открываем game.exe в Hiew и переходим на адрес ".73F088".



Других констант с таким значением нет, ссылка на нее есть только в рассмотренном коде. Можно ее поменять на любое значение, которое вам нужно. Я сделал себе в 3 раза меньше.
(1/150)/3 = 1/450 = 0.0022222222222222222

Для конвертации float/double в hex-представление можно воспользоваться онлайн-конвертером, например этим.

для справки
0.0066666666666666667 - 0x3F7B4E81B4E81B4F
0.006666666666666666  - 0x3F7B4E81B4E81B4E
0.0022222222222222222 - 0x3F623456789ABCDF
0.002222222222222222  - 0x3F623456789ABCDE


Получается красивое число 0x3F623456789ABCDF

DF BC 9A 78 56 34 62 3F

Заменяем, сохраняем, запускаем. Вот, так гораздо лучше.

Опыт


Тут немного сложнее. Он не хранится в явном виде. Нужно искать через связанные значения. Начинаем, впрочем, так же.

Смотрим, сколько опыта отображается у персонажа. Это можно сделать в режиме между картами. У меня это 116.




Ищем это значение. Здесь нужен тип «Целое 4 байта».

Это не исходная переменная, а вычисляемое значение, приведенное к int. Сам опыт хранится во float, но там другое значение, об этом ниже.

Теперь можно с кем-нибудь подраться. Выходить с карты чтобы посмотреть опыт нельзя, перезагружаться тоже, потому что память выделяется заново, и при повторном заходе на карту будут другие адреса. Надо прибавлять в уме. При этом надо учитывать округление. То есть, если за противника дается 5 очков опыта, и в команде 2 персонажа, то на экране будет отображаться опыт 2, но прибавлять надо 2.5 и брать целую часть.




После пары повторений у меня осталось 6 значений, которые изменяются синхронно.



Попробуем поставить брейкпойнты на запись на каждый адрес. Не забываем про «Pause process».

Подходит первый адрес. Остальные срабатывают по rep movsd.

.text:00522D00                 fld     dword ptr [ebx+700h]
.text:00522D06                 fadd    dword ptr [ebx+4]
.text:00522D09                 fsub    dword ptr [ebx+8]
.text:00522D0C                 fstp    [ebp+var_10]
.text:00522D0F                 fld     [ebp+var_10]
.text:00522D12                 fistp   [ebp+var_C]
.text:00522D15                 mov     edx, [ebp+var_C]
.text:00522D18                 mov     [edi+8], edx
.text:00522D1B >               mov     eax, [ebx+10h]
.text:00522D1E                 mov     [ebp+var_10], eax

Он срабатывает при беге или при бое любого из персонажей. Поэтому здесь лучше управлять только одним, а не группой, чтобы не путаться. В ebx находится адрес объекта персонажа. В [ebx+700h] находится 0.

Посмотрим значения.



387 — 271 = 116

Можно предположить, что это полученный и потраченный опыт, а текущий рассчитывается как их разность.

У меня игра падала несколько раз, поэтому я ставил брейкпойнт на код и загружал с сохранения. Поэтому тут 116, а не 120. Но это только подтверждает догадку.

Поставим новый брейкпойнт на [ebx+4].

Выбираем всю группу и нападаем на противника.




Брейкпойнт срабатывает до того, как это будет видно на экране.

.text:005239D7                 fld     ds:dbl_73E128
.text:005239DD                 fld     dword ptr [esi+20h]
.text:005239E0                 fsub    ds:flt_73E124
.text:005239E6                 call    __CIpow
.text:005239EB                 fmul    [ebp+arg_4]
.text:005239EE                 fadd    dword ptr [esi+700h]
.text:005239F4                 fcom    ds:flt_73B858
.text:005239FA                 fst     dword ptr [esi+700h]
.text:00523A00                 fnstsw  ax
.text:00523A02                 test    ah, 41h
.text:00523A05                 jnz     short loc_523A19
.text:00523A07                 fadd    dword ptr [esi+4]
.text:00523A0A                 mov     dword ptr [esi+700h], 0
.text:00523A14                 fstp    dword ptr [esi+4]
.text:00523A17 >               jmp     short loc_523A1B

В [esi+4] новое значение опыта. В [ebp+arg_4] число 2.0. За молодого кабана дается 4.0, значит деление находится до вызова функции.

Выходим из функции через Ctrl+F7. Это обертка, выходим еще раз.

Смотрим чуть выше, там находится такой код.

.text:00591521 loc_591521:
.text:00591521                 fild    [ebp+var_18]
.text:00591524                 xor     esi, esi
.text:00591526                 cmp     eax, edi
.text:00591528                 mov     [ebp+var_14], esi
.text:0059152B                 fdivr   [ebp+arg_4]
.text:0059152E                 fstp    [ebp+arg_4]
.text:00591531                 jle     short loc_5915A5
.text:00591533                 jmp     short loc_591537

Поставим брейкпойнт на 00591521 и поучаствуем в битве еще раз.

fdivr делит аргумент на st(0) и результат записывает в st(0): st(0) = arg / st(0). В st(0) находится значение [ebp+var_18], в котором находится 2 — число персонажей. В [ebp+arg_4] находится 4.0 — опыт за противника. При выполнении миссии начисление тоже происходит здесь.

Также деление на число участников есть выше:

.text:00591324                 fdiv    [ebp+var_18]

Но я не нашел, когда выполняется этот код. Там трогать не будем.

Теперь через Hiew можно убрать код для деления. Из-за особенностей fdivr заменяем на nop все 3 команды (9 байт).

; было
.00591521: DB45E8                         fild        d,[ebp][-018]
.00591524: 33F6                           xor         esi,esi
.00591526: 3BC7                           cmp         eax,edi
.00591528: 8975EC                         mov         [ebp][-014],esi
.0059152B: D87D0C                         fdivr       d,[ebp][00C]
.0059152E: D95D0C                         fstp        d,[ebp][00C]
.00591531: 7E72                           jle        .0005915A5
.00591533: EB02                           jmps       .000591537

; стало
.00591521: 909090                         nop
.00591524: 33F6                           xor         esi,esi
.00591526: 3BC7                           cmp         eax,edi
.00591528: 8975EC                         mov         [ebp][-014],esi
.0059152B: 909090                         nop
.0059152E: 909090                         nop
.00591531: 7E72                           jle        .0005915A5
.00591533: EB02                           jmps       .000591537

Теперь персонажи и бегают нормально, и запас сил не бесконечный, на заклинания расходуется как обычно, механика игры сохранена. А напарники получают свой независимый опыт, который можно тратить на их развитие.

Поехали!

Комментарии (14)


  1. q_styler
    12.08.2017 23:18
    +12

    Я как будто попал в свой 10й класс. Проклятые земли, артмани!


    1. tangro
      12.08.2017 23:53
      +4

      Да, знатный флешбек на полтора десятилетия… А ведь до этого были ещё и оригинальные «Аллоды» не испоганненые меилрушыщем.


  1. demoth
    13.08.2017 01:16
    +2

    Хорошая статья. Рад, что ещё есть те, кто помнят эту замечательную игру. :)


    Пара моментов:
    1) Патч 1.07 — это неофициальный фанатский патч, он изменяет только ресурсы игры, но не сам движок. Последний официальный патч — 1.06.
    2) Можно было бы обойтись без связки ArtMoney + IDA с помощью CheatEngine. Он умеет всё то, что и ArtMoney, но кроме этого ещё умеет отслеживать, какие инструкции обращаются к указанному адресу (на чтение и/или запись). Разумеется, для чего-то сложного IDA выкинуть уже не получится, но вот ArtMoney — запросто.


  1. Thero
    13.08.2017 03:10
    +1

    хорошая игра, ностальгический пост.


  1. Maximbl4
    13.08.2017 08:33
    -8

    В детстве, когда ещё не умел программировать уже ломал с помощью артмани проклятые земли) да и без всяких отладчиков…


    1. michael_vostrikov Автор
      13.08.2017 16:14
      +1

      Так многие ломали. А начисление опыта например без отладчика не поменять.


      Помню, хотел побегать в сетевой игре на сложных картах. Но там всякие зомби и тени, в одиночку не пройти, надо проходить командой и хорошо прокачанными. Набивал опыт на задании с циклопами, прокачивал ближний бой и кислотную магию, но все равно не хватало. Потом сообразил, как через Артмани взломать силу заклинания в конструкторе заклинаний. Сделал себе мощную молнию на 3 цели и прошел везде где еще не был)


  1. MorgenS
    13.08.2017 20:17
    +1

    Это все очень круто, но все же несколько ломает механику игры :) При равном распределении опыта между персом и напарниками вскоре возникнет дисбаланс.

    С разрешения автора утащу ссылочку для размещения на allods.gipat.ru :)


    1. michael_vostrikov Автор
      13.08.2017 20:47
      +1

      Играл на сложном, особого дисбаланса не заметил. Напарники же не переходят в другой мир, новых надо заново прокачивать. Разве что под конец чуть попроще, потому что в целом опыта много дается. А на Гипате очень даже к месту.


      Буду рад)


  1. xlenz
    13.08.2017 20:51
    +2

    Простите за то что не совсем по теме, но, скажите, есть ли более современный аналог этой игры? Прошёл её много раз, но хотелось бы другой сценарий и графу современную.


    1. michael_vostrikov Автор
      13.08.2017 23:03
      +1

      Не знаю, я за современными играми не слежу. Иногда читаю описания, но как-то ничего не цепляет. Из современных только в Ведьмака играл.


    1. LoadRunner
      14.08.2017 09:46

      аналог
      Это «action-rpg с активной паузой»? На ум приходят SW:KotOR, Jade Empire, Mass Effect (первый), Dragon Age. По сравнению с третьими Аллодами — это подходит под «более современный».


  1. tSmoker
    14.08.2017 17:03
    +2

    ...«Плотину надо поднять рычагом. Я его дам. Канал нужно завалить камнем. Камень я не дам»… что-то вспомнилось…