«Heroes of Might and Magic IV»: баг с таверной или классика патчинга +67


AliExpress RU&CIS

Эта короткая история описывает одну из работ, проведенную в рамках проекта «Equilibris» — неофициального мода для игры «Heroes of Might and Magic IV». С точки зрения как реверс-инжиниринга, так и патчинга она не представляет особого интереса — несколько забавным оказался только лишь финал.

image

Как известно, в данной серии игр в каждой таверне игрок может нанимать лишь одного нового Героя в неделю. Однако…

Описание бага: Если во внешней таверне не было найма, то, начиная с 8-го дня, можно купить двух героев в течение двух дней.

Для работы используется дизассемблированный файл heroes4.exe из последнего официального аддона «Winds of War». Процедура работы таверны найдена командой ранее и расположена по адресу 4705E0. Из всего алгоритма ее работы меня интересует место, в котором определяется, можно ли в данный момент нанять в таверне Героя, либо необходимо ожидание. В игре это проявляется выводом соответствующего сообщения:


С программной точки зрения это новое окно, которое в игре создается с помощью функции NewWindowCreate (720C80) (распознанным в дизассемблере функциям даны собственные имена). В процедуре таверны несколько вызовов этой функции, и первым претендентом является вызов по адресу 470823. С помощью отладчика убеждаюсь, что, действительно, этот вызов создает искомое диалоговое окно. Код, управляющий этим вызовом NewWindowCreate, находится выше — по адресу 470645:

00470638                 call    HeroesPricesInTavern_Lost
0047063D                 mov   al, [ebp+48h]  // 0 – если таверна работает; 1 – если героя нанять нельзя (ждешь 7 дней).
00470640                 add     esp, 8
00470643                 test    al, al
00470645                jz      loc_470866 // Если таверна работает, пропустить вывод сообщения по адресу 470823

Покупаю в таверне Героя, затем устанавливаю «бряк» на запись на ячейку, адресуемую [ebp+48h], после чего жду 7 игровых дней. Когда таверна «освобождается», отладчик всплывает по адресу 470DFF. Давайте посмотрим окружающий код:

00470DF0 TavernCountDays proc near               
00470DF0                 mov     dl, [ecx+48h] // ECX+48h – флаг работы таверны:
DL=0 – если таверна работает;
DL=1 – если Героя нанять нельзя (ждешь 7 дней)
00470DF3                 xor     eax, eax 
00470DF5                 cmp     dl, al
00470DF7                 jz      short loc_470E06
00470DF9                 cmp     dword ptr [ecx+4Ch], 7 // В [ECX+4Ch] - число дней с момента найма последнего героя в таверне. Если меньше 7 – выходим. 
00470DFD                 jl      short loc_470E06
00470DFF                mov     [ecx+48h], al  // Таверна работает (AL=0)
00470E02                 mov     [ecx+4Ch], eax  // Обнулить число дней
00470E05                 retn
00470E06
00470E06 loc_470E06:                             
00470E06                                         
00470E06                 inc     dword ptr [ecx+4Ch] // Увеличить число дней с момента найма последнего Героя в таверне
00470E09                 retn
00470E09 TavernCountDays endp

Эта небольшая процедура служит для проверки числа дней, в которые таверна закрыта для найма. Замечу, что она вызывается для каждой таверны на карте в каждый игровой день. Что же порождает баг? Программа зачем-то продолжает вести подсчет числа дней, в течении которых в таверне не было найма Героя и по истечении недели, в которую таверна была закрыта (см. счетчик по адресу 470E06). В результате получаем следующую картину. Пусть первый найм Героя происходит только на восьмой игровой день. На входе в процедуру значение флага доступности таверны по адресу [ecx+48h] будет равно «1» (таверна закрыта), а значение счетчика дней по адресу [ecx+4Ch] будет равно «8». Однако при этом, после сравнения по адресу 470DF9, управление получит код по адресу 470DFF, вновь открывающий таверну для найма! При этом счетчик дней сбросится, и после найма второго Героя алгоритм уже отработает, как задумывали авторы. Но через две игровых недели весь цикл повторится.

Самый простой способ пофиксить баг – отказаться от подобного подсчета дней. Пусть счетчик работает только тогда, когда таверна закрыта (что логичнее), а в остальное время зададим его равным нулю. Это достигается очень просто — изменением перехода по адресу 00470DF7 в конец функции:

00470DF5                 cmp     dl, al
00470DF7                 jz      short loc_470E09

Теперь остается лишь пропатчить имеющийся код. Для этого смотрим исходный


и измененный


варианты.

Как видно, необходимого результата можно достичь, заменив 0D на 10 по адресу 470DF8. Классика жанра: пропатчить баг, заменив всего лишь один байт!




Комментарии (24):

  1. GreedyIvan
    /#22334544

    А почему это баг, а не фича?

    • SinsI
      /#22335280

      Может, дизайнеры и хотели не «ждите неделю», а «приходите на следующей неделе?»
      Если обычные здания на карте пополняются каждую неделю, то почему таверна должна работать по-другому?

      • DoNotPanic
        /#22346218

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

  2. avdx
    /#22334646 / +1

    Приведенный код — хороший пример того, как не надо писать. Зачем заводить две сущности — флаг доступности таверны и число дней с момента покупки героя, когда достаточно одной — числа дней? Из-за этого код и получается запутанным и в нем легко ошибиться. С одной сущностью (числом дней) логика была бы гораздо проще и ошибиться в ней было бы гораздо сложнее.
    Но к самому патчу это, конечно, не имеет отношения. Понятное дело, что приходится работать с тем, что есть :)

    • mSnus
      /#22335014

      Видимо, доступность таверны определяется не только числом дней, а ещё и какими-то событиями сценария

      • avdx
        /#22335090

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

        • mSnus
          /#22335254

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

    • Roma_letchik
      /#22337470

      Можно купить героя на 7 день 1 недели и затем купить ещё одного на 1й день 2 недели. Нужен только "флаг куплен на этой неделе?"

      • avdx
        /#22337506

        Судя по коду (оригинальному и исправленному) нового героя можно купить только через 7 дней после покупки предыдущего. Флаг здесь означает — прошло или нет 7 дней с покупки предыдущего героя, при этом параллельно ведется количество прошедших дней. От номера недели, на которой был куплен герой, ничего не зависит и в коде номер недели никак не фигурирует.

        • Roma_letchik
          /#22337970

          Ваша правда, увидел на картинке с всплывающим окошком

  3. stanislavskijvlad
    /#22335034

    Расскажите о багах homm 3
    Разгон на быстрых существах, например )

    • bfDeveloper
      /#22335182

      А почему это баг? Скорость героя зависит от скорости существ, всё логично. Да, очки ходов не пересчитываются при передаче войск, но это как раз фича. В игре не задуманы очки передвижения существ, поэтому всё логично. А вот шляпа капитана и перенадевание сапог это баг, Redwhait до сих пор носит звание адмирала youtu.be/4fBaacaNeMU

    • ftdgoodluck
      /#22335698

      это точно не баг, это скорее фича (возможно незапланированная), которая существенно расширила стратегическую часть игры

  4. zaq1xsw2cde3vfr4
    /#22335222

    Не прошло и 20 лет с момента выхода 4х героев…

  5. sanyaa
    /#22335502

    «Как известно, в данной серии игр в каждой таверне игрок может нанимать лишь одного нового Героя в неделю.» — ну в третьих это точно не так.

  6. Sirion
    /#22336532

    Никак не связано с самой статьёй, но неудержимо вспомнилось, прошу прощения за оффтоп.

    Лет 10 назад я пытался пропатчить широко известную в узких кругах игру «Severance: Blade of Darkness». Я очень хотел увеличить количество оружия, которое способен носить с собой персонаж игрока. Потому что в процессе прохождения попадалось много разного интересного оружия, но в итоге приходилось брать с собой лишь несколько наиболее эффективных экземпляров. Сейчас даже и не вспомню, сколько. Кажется, четыре.

    Опыт в правке чужого бинарного кода у меня был нулевой. Самое близкое — один семестр ассемблера 8086 с достаточно простыми лабами. Однако пытливый ум не знает преград; по крайней мере, так мне казалось. Поставил Иду, пошёл искать. Убил несколько дней, пытаясь понять, где задаётся количества оружия. Так и не понял. Расстроился, забил.

    До сих пор мне этот эпизод не даёт покоя, периодически всплывая в памяти. Но не настолько, чтобы снова брать в руки дизассемблер. Да и игра не уверен, что пойдёт на десятке)

    • TimsTims
      /#22336956

      Думаю, ассемблером это бы не полечилось просто поменяв байты. Оружие имеет целую кучу свойств, и на него наверняка завязано куча триггеров, которые могут работать только с первыми двумя слотами. Другими словами, вам пришлось бы менять кучу логики в навес.

      • Sirion
        /#22337612

        Зависит от того, как написан код. Я представлял себе, что в нём есть константа КОЛИЧЕСТВО_ОРУЖИЯ, инициализируется массив объектов оружия с такой длиной, и так далее, и тому подобное. И вся работа с оружием ведётся с учётом константы КОЛИЧЕСТВО_ОРУЖИЯ. Так бы я писал этот код, и я не вижу причин, по которым в этом случае мы не смогли бы пропатчить игру заменой одного байта.

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

        • TimsTims
          /#22338890

          Игра вышла в 2001 году. Тогда ещё разрабы очень экономили количество памяти, и всячески её оптимизировали, не использовали безразмерных массивов, когда от геймдизайнеров было понятно, что оружий будет всего 2 (хотя предполагаю, что к концу разработки, геймдевы захотели увеличить это число, но было уже слишком поздно). Не забывайте, что их ещё как минимум закреплять надо на персонажах (показывать положение итд), а значит тесная интеграция с графикой, а не просто «вытащить из рюкзака топор №1». Всё это чудо делать с резиновым массивом было бы как минимум больно.

    • Xobotun
      /#22336980

      Раз тут есть ветка прохладных историй, немного продолжу.


      Во многих старых играх бывает дисбаланс, вызванный то ли кривостью рук игрока, то ли тем, что его стиль игры отличается от того, который планировали разработчики и проверяли тестировщики. В любом случае, игра перестаёт приносить удовольствие, поскольку становится слишком сложна. А на уровне сложности ниже – слишком проста. Хочется иметь какую-то крутилку, чтобы уменьшить наносимый урон на 25%, скажем.


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


      Временами хочется заново научиться вмешиваться в ассемблерный код через Cheat Engine, динамически искать указатели и писать подпрограммы на lua – но вспоминаешь, как больно это бывает временами, и продолжаешь заниматься чем-то другим.

    • thealfest
      /#22337788 / +1

      В Blade Of Darkness игровая логика написана на питоне, поэтому дизассемблер не нужен.

      • Sirion
        /#22337828

        Кажется, 10 лет назад я был дебил.

        • Xobotun
          /#22350328

          Я десять лет назад тоже о программировании знал только bind i 'impulse 101' или что-то в этом духе. :D

  7. DrGluck07
    /#22338452

    В древние времена у меня была русская версия Silent Hill 2 и не было интернетов. Зато был баг с кодом от сейфа. Он неправильно отображался в этой версии, уже не помню подробностей.
    Интернета у меня, повторюсь, не было, зато был SoftIce и очень большое желание пройти игру. За пару дней разобрался где это место и сделал трейнер, который на лету патчит отображение кода. Сейчас просто скачал бы рабочую версию русификатора.