В большинстве случаев слово «очередь» не вызывает положительных эмоций, тем более в сочетании со словом «увеличить». Но если вы любите играть с миллионами единиц ресурсов к началу игры, чтобы на десятой минуте бросить в бой тысячи солдат, то стандартного заказа по пять боевых единиц единиц с помощью клавиши Shift вам будет мало. Вот если бы можно было заказывать по 20 или по 50 солдат, или ещё лучше – иметь несколько разных клавиш-модификаторов...
Вступление
После публикации предыдущей статьи и возникшего интереса со стороны сообщества LCN меня спросили, смогу ли я увеличить объём очереди заказа боевых единиц с пяти до 20 или до 50. «Почему бы и нет», подумал я, «да и вообще – если повезёт, то нужно будет только один байт с 0x05 на 0x14 заменить, и всё».
Если бы я знал тогда, чем это обернётся… Но я не знал, так что поехали!
С чего начнём?
Естественно, с поиска нужного участка машинного кода. Тут есть выбор: можно искать место, в котором обрабатываются нажатия кнопок мыши и попытаться отследить код до ветки, отвечающей за заказ боевых единиц. Или же можно обойти все места, в которых проверяется состояние клавиши Shift. Мне второй вариант показался более перспективным. Запускаем всеми любимую для таких дел программу и смотрим в список импортированных функций.
Хм, GetKeyState звучит многообещающе. Что там у нас по перекрёстным ссылкам? Сто восемнадцать вызовов? Многовато, нужно отсеять те вызовы, в которых не проверяется клавиша Shift. Функция GetKeyState принимает только один параметр, а именно код клавиши, который для Shift равняется 0x10. В моём dmcr.exe это соответствует такому куску машинного кода:
push 10h 6A 10
call GetKeyState FF 15 EC C1 5C 00
Поиск этой последовательности байт выдал 38 адресов, на которых вызывается GetKeyState(VK_SHIFT). Расставляем по точке останова на каждый из них, запускаем отладчик и снимаем лишние, пока не доберёмся до нужной процедуры. Если быть точным, то процедур две: одна для заказа боевых единиц и одна для отмены. Но так как они различаются только адресом вызываемой в них функции, то далее мы будем рассматривать их как одну процедуру.
Вот что нас там ждёт:
Ну конечно. Что делает компилятор, когда видит маленький цикл с небольшим, но постоянным количеством выполнений? Правильно, разворачивает его в повторяющуюся последовательность инструкций тела цикла. Надежда на однобайтовый патч непринуждённо помахала ручкой.
Патч первый или зацикливаемся на ассемблере
Для цикла нам потребуются счётчик, инкрементирование, сравнение и условный прыжок. Тело цикла оставим без изменений за исключением правки смещения вызова функции соответственно новому адресу инструкции call.
После некоторого
; Сохраняем регистр перед изменениями
push cx 66 51
; Обнуляем регистр
xor cx, cx 66 31 C9
; Тело цикла
; Сохраняем регистр-счётчик перед каждым исполнением цикла
push cx 66 51
; Отрезок кода, отвечающий за заказ боевой единицы
mov dx, word ptr [ebp+arg_0] 66 8B 55 F0
push edx 52
xor eax, eax 33 C0
mov al, byte_10FC290 A0 90 C2 0F 01
xor eax, 85h 35 85 00 00 00
push eax 50
call sub_4FD01E E8 .. .. .. ..
add esp, 8 83 C4 08
; Восстанавливаем, инкрементируем и сравниваем регистр-счётчик
pop cx 66 59
inc cx 66 41
cmp cx, 14h 66 83 F9 14
; Прыжок в начало цикла, если счётчик меньше 20
jl 7C DA
; Восстанавливаем регистр после окончания цикла
pop cx 66 59
Несмотря на то, что этот патч приводит к желаемому результату, он имеет один большой недостаток: не вмешиваясь в логику игры можно переопределить размер очереди производства, создаваемой с помощью клавиши-модификатора Shift, но не более того. Патчить очередь перед каждой игрой в зависимости от условий игры не очень привлекательная перспектива, поэтому в сообществе довольно быстро было озвучено желание иметь разные модификаторы очереди на клавишах Shift, Alt, и ~. Что ж, вызов принят!
Патч второй или «программа максимум»
Заменив последовательность из пяти повторяющихся блоков одним циклом, мы освободили приличное количество байт. Но как в образовавшееся пространство встроить проверку нескольких клавиш и регулировку цикла в зависимости от результата? Самое простое на мой взгляд решение это последовательные вызовы GetKeyState, чередующиеся с присвоениями регистру соответствующего значения, с которым будет сравниваться счётчик в цикле. Если вызов GetKeyState показывает, что клавиша не нажата, то инструкция присвоения перепрыгивается. Таким образом у нас вместо развилок в зависимости от состояния клавиш будет ряд последовательных проверок и присвоений, завершающийся одним циклом:
; В том случае, если ни одна клавиша не нажата, цикл выполнится один раз
mov ebx, 01h BB 01 00 00 00
; Проверка клавиши
push 10h 6A 10
call GetKeyState FF 15 EC C1 5C 00
movsx ecx, ax 0F BF C8
and ecx, 8000h 81 E1 00 80 00 00
test ecx, ecx 85 C9
; В случае отрицательного результата перепрыгиваем mov, оставляя предыдущее значение в регистре
jz 74 05
mov ebx, 05h BB 05 00 00 00
; Следующая клавиша
push 12h 6A 12
call GetKeyState FF 15 EC C1 5C 00
[...]
В этот раз я решил использовать регистр ebx для сохранения количества исполнений цикла и регистр esi как счётчик цикла. Для этого есть две причины. Следуя соглашению о вызове функций эти регистры являются «постоянными», т.е. если в теле функции в них вносятся изменения, то функция обязана сохранить их значения в стеке и восстановить их перед завершением. Это освобождает меня от необходимости самому выполнять push и pop перед каждым исполнением цикла. Вторая причина в том, что в отличии от регистра cx мне больше не требуется префикс 0x66, а это экономия одного байта на каждой операции с регистрами кроме mov.
В итоге мы имеем клавиши-модификаторы Shift, Alt, TAB, F1 и F2. От клавиши ~ пришлось отказаться, так как на разных раскладках ей соответствуют разные идентификаторы, например VK_OEM_3 и VK_OEM_5.
; Сохраняем регистр перед изменением
push ebx 53
; В том случае, если ни одна клавиша не нажата, цикл выполнится один раз
mov ebx, 01h BB 01 00 00 00
; Shift: 5 боевых единиц
push 10h 6A 10
call GetKeyState FF 15 EC C1 5C 00
movsx ecx, ax 0F BF C8
and ecx, 8000h 81 E1 00 80 00 00
test ecx, ecx 85 C9
jz 74 05
mov ebx, 05h BB 05 00 00 00
; Alt: 20 боевых единиц
push 12h 6A 12
call GetKeyState FF 15 EC C1 5C 00
movsx ecx, ax 0F BF C8
and ecx, 8000h 81 E1 00 80 00 00
test ecx, ecx 85 C9
jz 74 05
mov ebx, 14h BB 14 00 00 00
; TAB: 50 боевых единиц
push 09h 6A 09
call GetKeyState FF 15 EC C1 5C 00
movsx ecx, ax 0F BF C8
and ecx, 8000h 81 E1 00 80 00 00
test ecx, ecx 85 C9
jz 74 05
mov ebx, 32h BB 32 00 00 00
; F1: 15 боевых единиц
push 70h 6A 70
call GetKeyState FF 15 EC C1 5C 00
movsx ecx, ax 0F BF C8
and ecx, 8000h 81 E1 00 80 00 00
test ecx, ecx 85 C9
jz 74 05
mov ebx, 0Fh BB 0F 00 00 00
; F2: 36 боевых единиц
push 71h 6A 71
call GetKeyState FF 15 EC C1 5C 00
movsx ecx, ax 0F BF C8
and ecx, 8000h 81 E1 00 80 00 00
test ecx, ecx 85 C9
jz 74 05
mov ebx, 24h BB 24 00 00 00
; Сохраняем и обнуляем регистр-счётчик цикла
push esi 56
xor esi, esi 31 F6
; Тело цикла
mov dx, word ptr [ebp+arg_0] 66 8B 55 F0
push edx 52
xor eax, eax 33 C0
mov al, byte_10FC290 A0 90 C2 0F 01
xor eax, 85h 35 85 00 00 00
push eax 50
call sub_4FD01E E8 .. .. .. ..
add esp, 8 83 C4 08
; Инкрементируем, сравниваем, прыгаем в начало цикла
inc esi 46
cmp esi, ebx 39 DE
jl 7C E1
; Восстанавливаем регистры
pop esi 5E
pop ebx 5B
Послесловие
На этом месте можно сказать, что задача выполнена и идти умывать руки. Или же можно написать миниатюрный патчер, позволяющий игрокам самим устанавливать размер очереди для каждой из клавиш-модификаторов… Но об этом в следующей статье.
Ссылки
Комментарии (24)
VBKesha
10.03.2016 20:14+5В казаков не играю а вот реверс-инжиниринг тема интересная. Спасибо за статью.
semenyakinVS
10.03.2016 23:13+2А разве в "Казаках" не было возможности зациклить производство на бесконечность? Или эта фича уже в "Завоевании Америки" появилась?
Ereb
11.03.2016 00:23+3Была, но в некоторых случаях она не так удобна, как кажется. Например, цена наёмных драгунов в дипломатическом центре растёт с каждым драгуном, которого вы уже наняли. Т.е. намного дешевле сразу заказать 500 единиц, чем ставить на бесконечность. Или если вы хотите не отвлекаясь заказать подряд ровно по взводу пикинёров и мушкетёров в одной казарме или чередовать производство каждые 50 единиц.
semenyakinVS
11.03.2016 00:31Хм, ну тоже правда. Тогда было бы ещё круче, кстати, если бы отряды сами формировались. В духе Supreme Commander.
dougrinch
11.03.2016 14:32Мне нравится как в planetary annihilation: titans (не знаю как в первой части, но скорее всего также) сделано. Там можно задать очередь вида: 2 танка одного типа, 1 второго, 3 третьего, повторить.
MacIn
11.03.2016 00:09+7Вторая причина в том, что в отличии от регистра cx мне больше не требуется префикс 0x66, а это экономия одного байта на каждой операции с регистрами кроме mov.
Собственно, а назачем было в 32битном режиме изначально использовать cx вместо ecx?
я и решил использовать вместо loop обычную комбинацию из inc, cmp и jl, в качестве регистра-счётчика я всё равно оставил cx
Зачем inc и cmp, если можно dec и jnz?
То же самое с ebx и esi: можно было использовать один регистр.Ereb
11.03.2016 00:37+3Да, без ошибок не обошлось. По поводу регистра cx: к нему я пришёл, когда искал информацию про циклы в асм. В конце концов я от него отказался, как и от использования loop. Ну а на счёт dec и jnz вы, конечно, правы. Как-то даже стыдно, что сам не додумался. С другой стороны, как я уже писал, опыта с ассемблером до реверса «Казаков» у меня не было от слова совсем. Так что спасибо за совет, в следующий раз учту)
grechnik
11.03.2016 00:44+2В 32-битном режиме loop контролируется регистром ecx, не cx.
Ereb
11.03.2016 00:54+1Точно, спасибо. Видимо, информация на том сайте относилась к архитектуре 8086.
MacIn
11.03.2016 01:05+1Ereb
11.03.2016 01:31+1Да, там тоже смотрел… Недостаточно тщательно. Зато сейчас вижу: том 2А, страница 3-496.
То есть, я мог бы сделать и так?mov ecx, 01h B9 01 00 00 00 [...] : Цикл push ecx 51 [...] pop ecx 59 loop E2 ..
MacIn
11.03.2016 16:57+1mov ecx, 24h
не 01h.
Да. Или так, чтобы не возиться со стеком, раз уж у нас stdcall:
mov esi, 24h m1: цикл dec esi jnz m1
Ereb
11.03.2016 19:00не 01h
В самом начале присваивается 0x01 на случай «обычного» заказа одной единицы, затем в сависимости от результатов GetKeyState другие значения (в примере выше 0x05, 0x14, 0x32, 0x0F или 0x24).
раз уж у нас stdcall
Честно говоря, достоверно мне это известно не было. Как можно проверить, что exe'шник придерживается calling convention и что регистр esi можно безопасно использовать?
И в случае stdcall я ведь всё равно обязан положить esi на стек, прежде чем использовать его в патче, а затем восстановить, не так ли?MacIn
11.03.2016 19:42+1Честно говоря, достоверно мне это известно не было. Как можно проверить, что exe'шник придерживается calling convention и что регистр esi можно безопасно использовать?
Никак — посмотреть какие регистры сохраняет/использует фнукция, как она работает со стеком (c call/sdtcall/pascal). Если честно, я посмотрел по диагонали, но опирался на вашу врезку с cx/ecx/loop и это:
В этот раз я решил использовать регистр ebx для сохранения количества исполнений цикла и регистр esi как счётчик цикла. Для этого есть две причины. Следуя соглашению о вызове функций эти регистры являются «постоянными», т.е. если в теле функции в них вносятся изменения, то функция обязана сохранить их значения в стеке и восстановить их перед завершением.
И в случае stdcall я ведь всё равно обязан положить esi на стек, прежде чем использовать его в патче, а затем восстановить, не так ли?
Да, я имел в виду сохранение/восстановление в цикле.
cyber-security
11.03.2016 08:40+3Добро дело делаете. Вы большой молодец.!
Вы дизассемблировали логику игры, я снимаю шляпу… Огромный труд.
Спасибо !
mydoom
11.03.2016 10:07А у вас нет в планах изменить потребность войск в жилье? На мой взгляд, это самый раздражающий момент в Казаках — необходимость полкарты домиками застраивать.
VRV
11.03.2016 10:22+4Домики — стратегическая часть игры, вы должны удерживать территорию, чтобы было место куда их воткнуть и должны потом ее защищать, чтобы их не захватили и не уничтожили. Плюсом это сильно меняет логику игры, тогда как патчи на очередь и на скорость игры ее никак не затрагивают, а только помогают улучшить юзабилити.
mydoom
11.03.2016 10:44+1Так я и не говорю о том, чтобы совсем от них отказаться. Речь о том, что когда с миллионами ресурсов играешь, домиков нужно строить сотни (что само по себе угнетающе монотонно), и в какой-то момент их стоимость взлетает до запредельных значений, и по-настоящему большую армию создать нет возможности. В конце концов, это Казаки, а не Сим Сити. Так что поправочный коэффициент на стоимость и/или вместимость домиков, мне кажется, был бы весьма уместен.
VRV
11.03.2016 12:53+2Вы не правы, я как многократный победитель турниров на миллионах говорю что там все сбалансировано, есть нации где хотелось бы получше — алжир, но она тем и примечательна. Во всех остальных же Городские центры и Казармы 18 века дают достаточно места для построения огромных армий.
mydoom
11.03.2016 14:27А сколько "Городских центров" вы строили на миллионах? Я несколько лет не играл, но, насколько я помню, уже пятый получается баснословно дорогим, равно как и в случае с "Казармами" XVIII в. (всегда строил по четыре штуки того и другого). Может, конечно, я чего-то недопонял в игровой механике, но после этого мне приходилось приниматься за массированную застройку домами.
VRV
11.03.2016 21:21+1если смотреть самую популярную номинацию DEF 10 пт, то стандартная застройка — 6 гц, 4 казармы 17 века, 8 конюшен, 6 казарм 18 века, 6 церквей, остального по 1му.
Можно экономить ресурсы и строить 5 гц и 5 церквей, всех больше места дает казарма 18 века. Чтобы построить данное кол-во зданий надо правильно использовать рыночный механизм в игре, ну или просто знать обмен)
TomashUA
11.03.2016 12:47+1Строил максимум один дом (наверное для открытия других возможностей) или не строил вообще, уже не помню. В основном создавал несколько городских центров, компактнее чем дома строить + сразу рабочая сила из нескольких источников генерировалась. При этом увеличивалось максимальное количество населения, которое можно создать.
mydoom
11.03.2016 14:29Естественно, что "Городских центров" нужно строить больше одного. Я говорю о ситуации, когда построить следующий уже нет возможности.
Ereb
Патчер, о котором говорится в конце статьи, доступен по ссылке:
https://yadi.sk/d/z6rTlJfRpvRn8