Русский
Русский
English
Статистика
Реклама

Modding

Перевод Хотите как в (средневековой) Европе? легализуем гей-браки в Crusader Kings III с помощью Ghidra

16.04.2021 10:20:09 | Автор: admin
image

Crusader Kings III отличная игра. Замечательна она не только своим официальным контентом, но и мощными инструментами моддинга. Ещё до её выпуска меня привлекли обещания разработчиков о расширении возможностей моддинга.

Хотя игра позволяет игроку реформировать средневековые культуры, привив им терпимость к однополым парам, в CK3 версии 1.3.1 пока нет возможности заключения однополых браков. Однако они должны быть приемлемы; ведь для этого и нужны моды!

Проблема моддинга


Довольно большой объём кода CK3 реализован на собственном скриптовом языке Paradox Interactive (он немного напоминает LISP без скобок). Также в игре есть удобные средства для создания, упаковки и распространения игроками собственных скриптов, например, через Мастерскую Steam.

Сделать так, чтобы два персонажа поженились (без каких-то сложных условностей), довольно просто. Достаточно такого скрипта:

do_ye_a_marriage = {    category = interaction_category_friendly    auto_accept = yes    is_shown = { always = yes }    on_auto_accept = {        scope:actor = {            marry = scope:recipient        }    }}

Он добавляет опцию в меню, открываемое при нажатии правой клавишей мыши на персонаже:


При её выборе герцогиня Тосканская без лишних вопросов выйдет за того персонажа, которым вы играете.


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

ничего не случится? Ну, по крайней мере, в игре. CK3 ведёт довольно подробные логи ошибок, которые в Windows обычно хранятся в папке C:\Users\<Username>\Documents\Paradox Interactive\Crusader Kings III\logs\error.log. В них мы увидим следующее:

[jomini_script_system.cpp:169]: Script system error!  Error: marry effect [ Svend Estrid of k_denmark (Internal ID: 15228 - Historical ID 101515) is not allowed to marry Erik Stenkiling of k_sweden (Internal ID: 17969 - Historical ID 100525) ]

К сожалению, оказалось, что логика женитьбы двух персонажей находится не в файлах скриптов, а в двоичном файле игры, а игровой движок в одностороннем порядке запрещает однополые браки. Из-за этого попытка поженить двух однополых персонажей Cruisader Kings 3 становится недопустимой операцией.

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

Моддеры игр, привыкшие самостоятельно браться за решение проблем с геймплеем, могут при наличии достаточного количества времени исправить что угодно. Вдохновившись недавним исправлением GTA Online, я решил потратить немного времени на поверхностный реверс-инжиниринг движка игры. Благодаря нему мне удалось создать неофициальный 390-байтный патч, обходящий встроенные запреты на однополые браки, а также реализующий другие эффекты, необходимые однополым партнёрам, чтобы брать приёмных детей. После публикации патча на официальных форумах последовала такая реакция:


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

Я мог бы попробовать найти какой-нибудь другой сайт, который бы согласился выложить патч, но у этого решения были свои недостатки. Созданный мной патч подходил только для текущей Steam-версии CK3 в моей ОС и с моей архитектурой процессора. Но я могу дать вам нечто более универсальное: знания!

Итак, давайте без лишних предисловий изучим пошаговую инструкцию, позволяющую любому, обладающему начальными техническими знаниями, выполнить реверс-инжиниринг игры наподобие Crusader Kings 3.

(Если у вас не так много технических знаний или вы просто нетерпеливый моддер вроде меня, то можете сразу переходить к разделу Быстрый патч ближе к концу статьи)


Подготавливаем инструменты


Ghidra это пакет инструментов для реверс-инжиниринга, созданный Агентством национальной безопасности правительства США, чтобы взламывать китайцев. В 2019 году он был выложен в open source и с тех пор любопытные кодеры могут с его помощью изучать всевозможные программы.

Не буду писать руководство по установке, оно есть на официальном веб-сайте. Запустив программу и прощёлкав туториал, вы увидите такое окно:


Для начала нужно будет выполнить формальности создания нового проекта. Эта операция выполняется через меню File -> New Project.... Укажите название проекта и место его хранения, после чего можно будет импортировать файл игры.

Для этого нужно будет найти, где находится ck3.exe. Мой расположен в версии Steam, установленной в D:\SteamLibrary\steamapps\common\Crusader Kings III\binaries, но вам, возможно, придётся поискать его самостоятельно.

Найдя файл ck3.exe, можно импортировать его через интерфейс File -> Import File.... Это будет выглядеть примерно так:


После нажатия на OK и загрузки двоичного файла вы увидите отчёт, в котором можно просто нажать OK, и новый файл в панели Active Project. Дважды щёлкните на этом файле, чтобы начать веселье.

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

При первом открытии двоичного файла вам предложат начать его анализ:


Ghidra может анализировать большой объём информации файла. Если вы пока не уверены, что ищете конкретно, то проще всего оставить стандартные настройки и нажать на Analyze. Для двоичного файла размера ck3.exe анализ может занять много времени. Для анализа своего файла я оставил программу работать на ночь. После завершения анализа можно нажать на кнопку сохранения в левом верхнем углу, чтобы анализ сохранился при повторном открытии файла.

Идём по следу


И здесь нам открывается целый мир. Иногда самое сложное в реверс-инжиниринге конкретной функции разобраться, с чего начинать. К счастью, у нас есть зацепка: то самое сообщение об ошибке, с которого всё началось. Часто используемые тексты обычно хранятся как последовательность в файле программы. Текст типа Svend Estrid of k_denmark, вероятно, генерируется динамически, однако текст is not allowed to marry, скорее всего, хранится в самом файле.

В панели меню выберите Search -> For Strings... и нажмите на Search, не меняя стандартных опций. Здесь вы сможете искать строку текста при помощи поля Filter. После инициализации программа будет (медленно) обыскивать файл игры в поисках строк, соответствующих фильтру.


Нам повезло! Дважды щёлкнув на эту строку, мы перейдём к строке текста в панели Listing (в другом окне; привыкайте к переключению окон).


Здесь отображается информация о строке текста и её расположении в памяти. Пока сама строка даёт нам не так много сведений. Нам больше интересен текст, использующий эту строку. Нажав правой клавишей мыши на выделенной строке, можно выбрать References -> Show References to Address, чтобы найти такие тексты.


Похоже, на строку ссылаются два участка памяти. Можно начать с первого. Дважды щёлкнем на первой строке, чтобы перейти к её местоположению. В основном окне местоположение при должно открыться в панелях Listing и Decompile (если у вас нет панели Decompile, то её можно открыть через меню Window -> Decompile).

Изучаем внутренности


На панель Decompile выводится много тарабарщины. Игры наподобие CK3 обычно пишутся на чём-то вроде C++, а затем разработчики обычно удаляют из файлов игры имена функций и переменных, чтобы уменьшить их размер. Чтобы компенсировать это, Ghidra даёт большинству функций и переменных произвольные имена.

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


Мы можем изучить вызываемую функцию, нажав на FUN_14034cb90 (спойлер: по большей части она просто выводит ошибку), но нам интереснее сделать так, чтобы она просто не работала. Поднявшись к началу блока if, в котором находится эта функция, мы видим следующее:


Итак, cVar2 является результатом функции FUN_140a1b9c0, и если этот результат равен нулю, срабатывает код обработки ошибок. Учитывая то, что ошибка связана с невозможностью женитьбы двух персонажей, вполне можно предположить, что lVar7 и lVar3 как раз и являются этими проверяемыми персонажами. Если мы дважды щёлкнем на FUN_140a1b9c0, то можем посмотреть, что у функции внутри.

И здесь нам очень поможет знание программирования на C. Мы ищем место, где сравниваются друг с другом входящие параметры и возвращается ноль. Ближе к началу функции я нашёл такой блок:


Он получает наши входящие данные (которые, как предполагается, представляют собой некие структуры персонажей) и сравнивает байт по смещению 0x124 в каждом из них. Если байты равны, то вся функция возвращает ноль. Похоже, что она может сравнивать пол персонажей, поскольку пол персонажа можно уместить в один байт (а от тех, кто играет с Cheat Engine я знаю, что это единственный бит, 0 мужчина, 1 женщина).

Начинается колдовство с битами


Лучше всего проверить нашу догадку о конструкции if, попробовав её обойти. При нажатии на if в панели Decompiler панель Listing перейдёт к соответствующей ассемблерной команде.


Это самый нижний уровень, на который мы можем опуститься при исследовании двоичного файла. JZ это сокращение от Jump if Zero. Команда берёт результат предыдущего сравнения (в данном случае это два пола) и переходит к новой позиции в памяти, если это сравнение приводит к нулю (что бывает, когда оба пола равны). Если мы проследуем по чёрной линии в левой части команд, то она приведёт нас к коду, который заставляет функцию возвращать ноль.

Один из способов удаления такой команды изменение места, в которое она осуществляет переход. Для этого нажмём правой кнопкой мыши на строке в панели Listing и выберем Patch Instruction.


Обратите внимание, что теперь место перехода выглядит очень похожим на место в памяти. Если мы хотим, чтобы наша команда JZ никогда не выполняла переход, то мы можем изменить её место перехода на адрес следующей строки, то есть 140a1ba00. В результате этого мы будем оказываться там вне зависимости от равенства значений полов персонажей.


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

Тестируем изменения


Все изменения мы пока выполняли только в проекте Ghidra. Если мы хотим протестировать изменения, нужно вернуть их в двоичный файл. Известно, что Ghidra имеет проблемы с экспортом файлов в используемом нами виде, поэтому придётся воспользоваться свободным скриптом, чтобы компенсировать это.

Официальные инструкции, написанные по ссылке, у меня не сработали, поэтому я поместил файл SavePatch.py в папку <папка установки Ghidra>\Ghidra\Features\Python\ghidra_scripts\. После этого нужно выделить перетаскиванием ассемблерной строки в панели Listing, чтобы вся она стала зелёной. А затем нажать на кнопку Script Manager рядом с верхней частью интерфейса:


Введите SavePatch в поле Filter, отобразится скачанный скрипт.


Дважды нажмите на найденную строку, после чего нужно будет указать путь к файлу ck3.exe, который мы изначально импортировали, и выбрать его (возможно, стоит сначала скопировать его как резервную копию). Если всё пройдёт правильно, то ваша игра на диске будет пропатчена модифицированной командой перехода.

Если мы снова запустим игру и попробуем применить к королю Швеции do_ye_a_marriage, то

всё равно ничего не получим. Но в логах ошибок появилось новое сообщение:

[pdx_assert.cpp:568]: Assertion failed: Can only marry characters of different gender.

Раунд второй


Похоже, у разработчиков Paradox есть какие-то дополнительные проверки для собственного удобства. Однако мы можем ещё раз воспользоваться той же методикой. Search -> For Strings..., фильтр по Can only marry characters of different gender, затем на найденной строке References -> Show References to Address.


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


Эта конструкция похожа на первую. Она сравнивает два параметра по смещению 0x124 и проверяет их равенство. После выбора if соответствующий ассемблерный код выглядит немного иначе:


Наша подсвеченная команда это JNZ, или команда Jump if Not Zero, а стрелка указывает на место после строки Can only marry. Поэтому нам нужно, чтобы эта команда всегда выполняла переход. Для этого снова нажмём правой клавишей мыши на выделенной строке и выберем Patch Instruction. Затем заменим JNZ на JMP, то есть на команду безусловного перехода.


Чтобы применить это изменение к игре, повторим тот же процесс: выделение, Script Manager, выбор SavePatch.py.

После этого можно запустить игру, и


наконец-то после всего пережитого добиться личной унии Дании и Швеции, о которой мы всегда мечтали.

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

Король и я, тоже король


Но что делают все эти изменения? В CK3 удалённые нами проверки дублируются в файлах скриптов, поэтому базовая игра не должна вообще видеть никаких изменений. Я уже довольно много играл с простыми модами, реализующими гей-браки, и пока самым заметным багом был такой:


Оказалось, что эффект marry в CK3 не имеет проверки на женитьбу на себе. Некоторые проверки, например set_father, явным образом препятствуют тому, чтобы персонаж становился сам себе отцом, но любой персонаж всегда имеет с самим собой один пол, поэтому запрет гей-браков служит и запретом на брак с собой. Вероятно, моддерам стоит ввести явную проверку этого в своих модах.


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

(Если это случайно прочтёт кто-то из разработчиков CK3, то я бы рекомендовал всё-таки добавить специальную проверку женитьбы на самом себе.)

Быстрый патч


Но что если вы не хотите начинать карьеру в сфере реверс-инжиниринга ПО? Ну, если у вас Windows 10 и Steam-версия CK3 1.3.1, то вы можете открыть файл ck3.exe в любом шестнадцатеричном редакторе и внести следующие изменения:


Результат будет таким же, только меньше мороки. Однако при создании модов учитывайте проблему женитьбы на себе (впрочем, если вы нарцисс, то не берите в голову).
Подробнее..

Weapon wheel в Doom 1993

29.06.2020 18:10:05 | Автор: admin
Приветствую.
Многие из нас с теплотой относятся к олдскульным видеоиграм, вышедшим на стыке веков. У них превосходная атмосфера, бешеная динамика и множество оригинальных решений, которые не устарели спустя десятилетия. Однако в наши дни видение интерфейса игр несколько изменилось на смену запутанным уровням пришли линейные коридоры, на смену аптечкам регенерация, а вместо длинного ряда клавиш 0-9 для выбора арсенала пришли сначала колесико мыши, а затем виртуальное колесо. Именно о нем сегодня и пойдет речь.
image


Историческая сводка
Раньше, во время появления жанра шутеров как таковых, вопрос об управлении мышкой не стоял для управления протагонистом использовалась только клавиатура. Причем единого формата управления тоже не было WASD стал стандартом чуть позднее. Более подробно о старых игровых раскладках клавиатуры можно почитать вот тут
Соответственно, в тех играх, где была реализована возможность выбора снаряжения (Doom, Wolfenstein, Quake etc) был реализован единственным интуитивным на тот момент способом с помощью цифровых клавиш на клавиатуре. И на многие годы этот способ был единственным.
Потом, в конце 90х годов, появилась возможность смены вооружения колесиком мышки. Однозначной информации на эту тему найти не удалось, однако в CS 1.6 такая возможность включалась через консоль. Впрочем, возможно такие прецеденты были и ранее в таком случае, просьба указать на это в комментариях или в ЛС. А вот в привычном в наше время виде Weapon Wheel вошло в использование лишь с Crysis'ом и его Suit menu, Хотя попытки сделать нечто похожее были начиная с HL2, в массы колесо пошло лишь в конце 00х годов, а сейчас является мейнстримом.

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

Постановка задач
Для того, что бы реализовать WW, нужно каким-либо образом перехватывать движения мышки, отслеживать ее перемещение, пока зажата клавиша селектора, и, по отпусканию, эмулировать нажатие на кнопку, соответствующую выбранному сектору.
Для этого мной был использован язык Java, в частности, перехват клавиш осуществляется за счет библиотеки jnativehook, а нажатие за счет awt.Robot. Обработка полученных хуков не представляет сложностей, поэтому производится вручную.

Реализация
Предварительно были разработаны классы, задающие пары координат, для определния вектора смещения.
В частности, класс Shift позволяет хранить двумерный вектор, а также определять его длину, а класс NormalisedShift, разработанный для хранения нормализованного вектора, помимо прочего, позволяет определить угол между перехваченным вектором и вектором (1,0)
Заголовок спойлера
class Shift{    int xShift;    int yShift;    public int getxShift() {        return xShift;    }    public int getyShift() {        return yShift;    }    public void setxShift(int xShift) {        this.xShift = xShift;    }    public void setyShift(int yShift) {        this.yShift = yShift;    }    double getLenght(){        return Math.sqrt(xShift*xShift+yShift*yShift);    }}class NormalisedShift{  double normalizedXShift;  double normalizedYShift;  double angle;  NormalisedShift (Shift shift){      if (shift.getLenght()>0)      {          normalizedXShift = -shift.getxShift()/shift.getLenght();        normalizedYShift = -shift.getyShift()/shift.getLenght();      }      else      {          normalizedXShift = 0;          normalizedYShift = 0;      }  }  void calcAngle(){      angle = Math.acos(normalizedXShift);  }  double getAngle(){      calcAngle();      return (normalizedYShift<0?angle*360/2/Math.PI:360-angle*360/2/Math.PI);    };};


Особого интереса они не представляют, и комментарий требуют только строки 73-74, нормализующие вектор. Помимо всего прочего, вектор переворачивается. у нег меняется система отсчета дело в том, что с точки зрения программного обеспечения и с точки зрения привычной математики вектора традиционно направляют по разному. Именно поэтому вектора класса Shift имеют начало координат слева сверху, а класса NormalizedShift слева снизу.

Для реализации работы программы был реализован класс Wheel, реализующий интерфейсы NativeMouseMotionListener и NativeKeyListener. Код под спойлером
Заголовок спойлера
public class Wheel  implements NativeMouseMotionListener, NativeKeyListener {    final int KEYCODE = 15;    Shift prev = new Shift();    Shift current = new Shift();    ButtomMatcher mathcer = new ButtomMatcher();    boolean wasPressed = false;    @Override    public void nativeMouseMoved(NativeMouseEvent nativeMouseEvent) {        current.setxShift(nativeMouseEvent.getX());        current.setyShift(nativeMouseEvent.getY());    }    @Override    public void nativeMouseDragged(NativeMouseEvent nativeMouseEvent) {    }    @Override    public void nativeKeyTyped(NativeKeyEvent nativeKeyEvent) {    }    @Override    public void nativeKeyPressed(NativeKeyEvent nativeKeyEvent) {        if (nativeKeyEvent.getKeyCode()==KEYCODE){            if (!wasPressed)            {                prev.setxShift(current.getxShift());                prev.setyShift(current.getyShift());            }            wasPressed = true;        }    }    @Override    public void nativeKeyReleased(NativeKeyEvent nativeKeyEvent) {        if (nativeKeyEvent.getKeyCode() == KEYCODE){            Shift shift = new Shift();            shift.setxShift(prev.getxShift() - current.getxShift());            shift.setyShift(prev.getyShift() - current.getyShift());            NormalisedShift normalisedShift = new NormalisedShift(shift);            mathcer.pressKey(mathcer.getCodeByAngle(normalisedShift.getAngle()));            wasPressed = false;        }    }


Разберемся, что тут происходит.
В переменной KEYCODE хранится код клавиши, служащей для вызова селектора. Обычно это TAB, но при необходимости, его можно изменить в коде или в идеале подтянуть из файла конфига.
prev хранит положение курсора мыши, которое было на момент вызова селектора. В сurrent поддерживается актуальное положение курсора в настоящий момент времени. Соответственно, при отпускании клавиши селектора происходит вычитание векторов и в переменную shift записывается смещение курсора за время удержания клавиши селектора.
Затем, в строке 140, вектор нормализуется, т.е. приводится к виду, когда его длина близка к единице. После чего, нормализованный вектор передается в матчер, который устанавливает соответствие между кодом клавиши, которую нужно нажать и углом проворота вектора. Из соображений читаемости, угол переводится в градусы, а так же ориентируется по полному единичному кругу (acos работает только с углами до 180 градусов).
В классе ButtonMatcher определяется соответствие между углом и выбранным кодом клавиши.
Заголовок спойлера
class ButtomMatcher{    Robot robot;    final int numberOfButtons = 6;    int buttonSection = 360/numberOfButtons;    int baseShift = 90-buttonSection/2;    ArrayList<Integer> codes = new ArrayList<>();    void matchButtons(){        for (int i =49; i<55; i++)            codes.add(i);    }    int getCodeByAngle(double angle){        angle= (angle+360-baseShift)%360;        int section = (int) angle/buttonSection;        System.out.println(codes.get(section));        return codes.get(section);    }    ButtomMatcher() {        matchButtons();        try        {            robot = new Robot();        }        catch (AWTException e) {            e.printStackTrace();        }    }    void pressKey(int keyPress)    {        robot.keyPress(keyPress);        robot.keyRelease(keyPress);    }}


Кроме того, переменная numberOfButtons определяет количество сектором и соответствующих им кнопок, baseShift задает угол поворота (В частности, обеспечивает симметрию относительно вертикальной оси и проворот колеса на 90 градусов так, что бы орудие ближнего боя было сверху), а массив codes хранит в себе коды клавиш на случай, если кнопки будут изменены, и коды не будут идти подряд. В более доработанной версии можно было бы подтягивать их из конфигурационного файла, но при стандартном расположении клавиш текущая версия вполне жизнеспособна.

Заключение
В рамках данной статьи была описана возможность кастомизации интерфейса классических шутеров для современных стандартов. Конечно, ни аптечек, ни линейности мы тут не добавляем для этого есть множество модов, но зачастую именно в подобных деталях и кроется дружелюбный и удобный интерфейс. Автор осознает, что, вероятно, описал не самый оптимальный способ достижения требуемого результата, а так же ждет в комментариях картинку с буханкой и троллейбусом, но тем не менее это был интересный опыт, который, возможно, сподвигнет какого-нибудь геймера открыть для себя удивительный мир Java.
Конструктивная критика приветствуется.
Исходники
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru