Многие играли в эту замечательную игру. Интересный сюжет, хорошая музыка, неплохой геймплей. Только есть пара моментов, которые мне не нравятся. Бег персонажей очень ограниченный, буквально несколько секунд, а запас сил восстанавливается долго. Система начисления опыта не стимулирует брать напарников, потому что опыт распределяется на всех поровну, и лучше бегать одному, чтобы забирать весь опыт себе. Возьмем отладчик и попробуем это исправить.
Нам понадобятся 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
Можно предположить, что это полученный и потраченный опыт, а текущий рассчитывается как их разность.
Уберем этот брейкпойнт и поставим новый на [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
Также деление на число участников есть выше:
.text:00591324 fdiv [ebp+var_18]
Но я не нашел, когда выполняется этот код. Там трогать не будем.
Поставим брейкпойнт на 00591521 и поучаствуем в битве еще раз.
fdivr делит аргумент на st(0) и результат записывает в st(0): st(0) = arg / st(0). В st(0) находится значение [ebp+var_18], в котором находится 2 — число персонажей. В [ebp+arg_4] находится 4.0 — опыт за противника. При выполнении миссии начисление тоже происходит здесь.
Теперь через 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
Теперь персонажи и бегают нормально, и запас сил не бесконечный, на заклинания расходуется как обычно, механика игры сохранена. А напарники получают свой независимый опыт, который можно тратить на их развитие.
Поехали!
Автор: Михаил