Gratt

Модостроитель
- Регистрация
- 14 Ноя 2014
- Сообщения
- 3.457
- Благодарности
- 4.819
- Баллы
- 625
Данное практическое руководство предназначено для введения читателя в основы Gothic API, Union API, а также в базовые принципы дизассемблирования.
В качестве результата будет создан простой, но легко расширяемый плагин Free Aim. Для упрощения, материал рассчитан под платформу Gothic II: NoTR. Полная версия с поддержкой Gothic 1 будет на Git.
Ютуб
Неютуб
Git
В рамках данного материала рассматриваются:
В результате прохождения руководства будут получены следующие практические навыки:
1. Создание проекта
2. Создание прицела
В качестве результата будет создан простой, но легко расширяемый плагин Free Aim. Для упрощения, материал рассчитан под платформу Gothic II: NoTR. Полная версия с поддержкой Gothic 1 будет на Git.
Ютуб
Неютуб
Git
В рамках данного материала рассматриваются:
- создание прицела с использованием внутриигровых объектов и стандартных API-вызовов Union;
- изменение поведения камеры в режиме дальнего боя и магии средствами обычных хуков;
- реализация свободного вращения камеры в режиме дальнего боя и магии с использованием частичных хуков;
- изменение условий взятия объектов в фокус с использованием обычных и частичных хуков;
- реализация простой баллистики стрел с использованием штатных возможностей движка с использованием частичных хуков;
- реализация направленного каста заклинаний с использованием штатных возможностей движка с использованием частичных хуков.
В результате прохождения руководства будут получены следующие практические навыки:
- Gothic API — базовое понимание структуры и принципов работы движка;
- Union API — работа с системами перехвата: стандартные API-вызовы, хуки и частичные хуки;
- Дизассемблирование — вводный обзор машинных инструкций и их практического применения.
1. Создание проекта
Вначале следует произвести набор подготовительных мероприятий.
В первую очередь у вас должна быть работоспособная игра с установленным на нее Union.
Далее нужно подготовить проект. Процесс его создания описан тут: Создание плагина с использованием шаблона · Wiki · Union Framework / Union API · GitLab
Поэтому эти шаги предлагается сделать самостоятельно.
Как только проект будет подготовлен, соберите его. В /out будет лежать dll, рекомендуется создать символическую ссылку этой библиотеки в Gothic/System/Autorun, чтобы игра загружала библиотеку прямо из каталога вашего проекта.
В первую очередь у вас должна быть работоспособная игра с установленным на нее Union.
Далее нужно подготовить проект. Процесс его создания описан тут: Создание плагина с использованием шаблона · Wiki · Union Framework / Union API · GitLab
Поэтому эти шаги предлагается сделать самостоятельно.
Как только проект будет подготовлен, соберите его. В /out будет лежать dll, рекомендуется создать символическую ссылку этой библиотеки в Gothic/System/Autorun, чтобы игра загружала библиотеку прямо из каталога вашего проекта.
2. Создание прицела
Подготовка
Идея
Для реализации прицела на экране будем использовать классы и объекты внутриигрового интерфейса.
Ключевым элементом здесь является класс zCView — универсальный интерфейсный объект, предназначенный для вывода текста и изображений на экран. Он может выступать как самостоятельный элемент, как вьюпорт, а также как базовый класс для большинства компонентов игрового UI. Размеры и положение zCView рассчитываются в виртуальных координатах в диапазоне от 0 до 8192, независимо от фактического разрешения экрана.
Отдельного внимания заслуживает глобальный объект screen — основной экземпляр zCView, автоматически подогнанный под размеры экрана. Он используется в качестве контейнера для размещения других элементов интерфейса. Его важная особенность заключается в том, что весь текст, выведенный через методы Print, автоматически очищается на следующем кадре.
Ключевым элементом здесь является класс zCView — универсальный интерфейсный объект, предназначенный для вывода текста и изображений на экран. Он может выступать как самостоятельный элемент, как вьюпорт, а также как базовый класс для большинства компонентов игрового UI. Размеры и положение zCView рассчитываются в виртуальных координатах в диапазоне от 0 до 8192, независимо от фактического разрешения экрана.
Отдельного внимания заслуживает глобальный объект screen — основной экземпляр zCView, автоматически подогнанный под размеры экрана. Он используется в качестве контейнера для размещения других элементов интерфейса. Его важная особенность заключается в том, что весь текст, выведенный через методы Print, автоматически очищается на следующем кадре.
Подключение событий Union
На начальном этапе нам понадобятся стандартные API-вызовы Union. Это набор функций, привязанных к ключевым игровым событиям. В шаблоне проекта они объявлены в файле Plugin.hpp.
В рамках данного раздела будут использоваться следующие события:
Для их активации необходимо раскомментировать соответствующие хуки Hook_oCGame_Init и Partial_zCWorld_Render, представленные в шаблоне проекта.
В рамках данного раздела будут использоваться следующие события:
- Game_Init — вызывается при входе в игру, когда основные подсистемы движка уже инициализированы;
- Game_Loop — покадровый цикл, выполняющийся во время нахождения игрока в игровом мире.
- LoadEnd - вызывается после завершения любой загрузки мира (новая игра, загрузка сохранения, смена уровня).
Для их активации необходимо раскомментировать соответствующие хуки Hook_oCGame_Init и Partial_zCWorld_Render, представленные в шаблоне проекта.
C++:
void __fastcall oCGame_Init(oCGame* self, void* vtable);
auto Hook_oCGame_Init = Union::CreateHook(reinterpret_cast<void*>(zSwitch(0x00636F50, 0x0065D480, 0x006646D0, 0x006C1060)), &oCGame_Init, Union::HookType::Hook_Detours);
void __fastcall oCGame_Init(oCGame* self, void* vtable)
{
Hook_oCGame_Init(self, vtable);
Game_Init();
}
void __fastcall oCGame_MainWorld_Render(Union::Registers& reg);
auto Partial_zCWorld_Render = Union::CreatePartialHook(reinterpret_cast<void*>(zSwitch(0x0063DC76, 0x0066498B, 0x0066BA76, 0x006C87EB)), &oCGame_MainWorld_Render);
void __fastcall oCGame_MainWorld_Render(Union::Registers& reg)
{
Game_Loop();
}
void __fastcall oCGame_LoadGame(oCGame* self, void* vtable, int slot, const zSTRING& levelPath);
auto Hook_oCGame_LoadGame = Union::CreateHook(reinterpret_cast<void*>(zSwitch(0x0063C070, 0x00662B20, 0x00669970, 0x006C65A0)), &oCGame_LoadGame, Union::HookType::Hook_Detours);
void __fastcall oCGame_LoadGame(oCGame* self, void* vtable, int slot, const zSTRING& levelPath)
{
Game_LoadBegin_NewGame();
Hook_oCGame_LoadGame(self, vtable, slot, levelPath);
Game_LoadEnd_NewGame();
}
void __fastcall oCGame_LoadSaveGame(oCGame* self, void* vtable, int slot, zBOOL loadGlobals);
auto Hook_oCGame_LoadSaveGame = Union::CreateHook(reinterpret_cast<void*>(zSwitch(0x0063C2A0, 0x00662D60, 0x00669BA0, 0x006C67D0)), &oCGame_LoadSaveGame, Union::HookType::Hook_Detours);
void __fastcall oCGame_LoadSaveGame(oCGame* self, void* vtable, int slot, zBOOL loadGlobals)
{
Game_LoadBegin_SaveGame();
Hook_oCGame_LoadSaveGame(self, vtable, slot, loadGlobals);
Game_LoadEnd_SaveGame();
}
void __fastcall oCGame_ChangeLevel(oCGame* self, void* vtable, const zSTRING& levelpath, const zSTRING& startpoint);
auto Hook_Game_Load_ChangeLevel = Union::CreateHook(reinterpret_cast<void*>(zSwitch(0x0063CD60, 0x00663950, 0x0066A660, 0x006C7290)), &oCGame_ChangeLevel, Union::HookType::Hook_Detours);
void __fastcall oCGame_ChangeLevel(oCGame* self, void* vtable, const zSTRING& levelpath, const zSTRING& startpoint)
{
Game_LoadBegin_ChangeLevel();
Hook_Game_Load_ChangeLevel(self, vtable, levelpath, startpoint);
Game_LoadEnd_ChangeLevel();
}
void __fastcall oCGame_TriggerChangeLevel(oCGame* self, void* vtable, const zSTRING& levelpath, const zSTRING& startpoint);
auto Hook_oCGame_TriggerChangeLevel = Union::CreateHook(reinterpret_cast<void*>(zSwitch(0x0063D480, 0x00664100, 0x0066AD80, 0x006C7AF0)), &oCGame_TriggerChangeLevel, Union::HookType::Hook_Detours);
void __fastcall oCGame_TriggerChangeLevel(oCGame* self, void* vtable, const zSTRING& levelpath, const zSTRING& startpoint)
{
Game_LoadBegin_TriggerChangeLevel();
Hook_oCGame_TriggerChangeLevel(self, vtable, levelpath, startpoint);
Game_LoadEnd_TriggerChangeLevel();
}
Структура файлов
Для вынесения логики работы с прицелом рекомендуется создать отдельный файл Aim.hpp. Разместите его в том же каталоге, где находится Plugin.hpp.
После этого подключите новый файл в Sources.hpp, расположив #include "Aim.hpp" выше, чем подключение Plugin.hpp. Это важно для корректной инициализации используемых типов и объявлений.
После этого подключите новый файл в Sources.hpp, расположив #include "Aim.hpp" выше, чем подключение Plugin.hpp. Это важно для корректной инициализации используемых типов и объявлений.
C++:
#include "Aim.hpp"
#include "Plugin.hpp"
Пространство имён
Внутри файла Aim.hpp определим пространство имён GOTHIC_NAMESPACE, в котором будет размещена логика, связанная с прицелом. Это необходимо делать в каждом новом созданном файле.
C++:
namespace GOTHIC_NAMESPACE
{
// TODO
}
Идея
Реализуем художественный эффект, в котором положение прицела будет зависеть напрямую от положения персонажа в мире.
1. Сперва придумаем точку, которая одновременно будет красиво расположена на экране, сможет четко передать движение персонажа, достаточно удобна для прицеливания.
2. Прицел все еще должен выполнять функцию наведения, а значит необходимо сопоставить точку на экране с местом, куда персонаж должен выстрелить.
3. Прицел будет непосредственно управлять наведением. Поскольку планируется добавление баллистики, и персонаж должен стрелять навесом, добавим параметр
elevation, для рассчета конечной точки выстрела с учетом навеса.
1. Сперва придумаем точку, которая одновременно будет красиво расположена на экране, сможет четко передать движение персонажа, достаточно удобна для прицеливания.
2. Прицел все еще должен выполнять функцию наведения, а значит необходимо сопоставить точку на экране с местом, куда персонаж должен выстрелить.
3. Прицел будет непосредственно управлять наведением. Поскольку планируется добавление баллистики, и персонаж должен стрелять навесом, добавим параметр
elevation, для рассчета конечной точки выстрела с учетом навеса.
Реализация
К этому моменту у нас уже есть две ключевые точки в мире, которые необходимо вычислять:
Взаимодействие с прицелом
- положение прицела в мировых координатах;
- точка, в которую персонаж будет производить выстрел.
Вспомогательные объекты и константы
В файле Aim.hpp определим следующие объекты и параметры:
C++:
zCVob* helper_aim_vob_hit; // Объект-цель, в которую персонаж будет производить выстрел
zVEC3 helper_aim_vob_marker; // Точка в мире, в которой визуально располагается прицел
zCView* helper_aim_view; // Изображение прицела
constexpr float aim_shift_at_length = 500.0f; // Смещение прицела вперёд от персонажа
constexpr float aim_shift_up_length = 100.0f; // Смещение прицела вверх
constexpr float aim_shift_right_length = 60.0f; // Смещение прицела вправо
constexpr float far_hit_point_distance = 50000.0f; // Дистанция размещения точки стрельбы
Использование zCVob для точки попадания вместо zVEC3 как у маркера прицела сделано намеренно. В дальнейшем, при перехвате функций наведения, это упростит интеграцию: движку привычнее работать с объектом, в который нужно целиться, чем с абстрактной координатой.zCVob — базовый 3D-объект движка. Любые объекты мира и персонажи являются zCVob или его производными. Объекту можно задать положение, вращение, масштаб и визуальную модель.
zVEC3 — трёхмерный вектор или точка в пространстве (float[3]).
Вычисление положения прицела и цели
Порядок вычислений следующий.
Положение прицела:
Положение прицела:
- получаем векторы направлений камеры;
- получаем положение персонажа в мире;
- прибавляем к положению персонажа смещения вдоль направлений камеры.
- получаем положение камеры;
- вычисляем вектор от камеры к прицелу — направление выстрела;
- продолжаем этот вектор на фиксированную дистанцию вперёд;
- добавляем вертикальную поправку (elevation), зависящую от дистанции.
Пример реализации
C++:
void SetHelperVobPosition(float elevation)
{
auto world = ogame->GetGameWorld(); // Игровой мир: геометрия, объекты, триггеры
auto camera_vob = ogame->GetCameraVob(); // Объект камеры
auto player_position = player->GetPositionWorld();
auto at_vector = camera_vob->GetAtVectorWorld(); // Направление вперёд
auto up_vector = camera_vob->GetUpVectorWorld(); // Направление вверх
auto right_vector = camera_vob->GetRightVectorWorld(); // Направление вправо
// Вычисляем позицию маркера прицела относительно персонажа
helper_aim_vob_marker = player_position
+ at_vector * aim_shift_at_length
+ up_vector * aim_shift_up_length
+ right_vector * aim_shift_right_length;
// Вычисляем точку, в которую будет произведён выстрел
auto camera_position = camera_vob->GetPositionWorld();
auto hit_vector = (helper_aim_vob_marker - camera_position).Normalize();
auto hit_position = camera_position
+ hit_vector * far_hit_point_distance
+ up_vector * (far_hit_point_distance * elevation);
helper_aim_vob_hit->SetPositionWorld(hit_position);
}
Отображение прицела на экране
Для отображения прицела на экране необходимо добавить его в screen, так же как и любой другой элемент интерфейса.
Общий порядок действий следующий:
Общий порядок действий следующий:
- задать актуальную текстуру прицела;
- добавить объект прицела в screen;
- спроецировать положение маркера прицела из мировых координат в экранные;
- обновить размер и позицию элемента с учётом текущего разрешения.
C++:
void ShowHelperView(const char* tex_name)
{
// Задаём актуальную текстуру прицела
helper_aim_view->InsertBack(tex_name);
// Перевставляем объект на экран, чтобы
// удерживать его поверх других элементов интерфейса
screen->RemoveItem(helper_aim_view);
screen->InsertItem(helper_aim_view);
// Активируем текущую камеру и переводим позицию
// маркера прицела в её локальную систему координат
zCCamera::activeCam->Activate();
zVEC3 local_target_position =
zCCamera::activeCam->camMatrix * helper_aim_vob_marker;
// Если точка находится перед камерой
if (local_target_position[VZ] > 0)
{
// Проецируем мировые координаты в экранные (пиксели)
int x, y;
zCCamera::activeCam->Project(&local_target_position, x, y);
// Переводим пиксели в виртуальные координаты интерфейса
int position_x = screen->anx(x);
int position_y = screen->any(y);
// Преобразуем размер прицела из пикселей
// в виртуальные координаты интерфейса
constexpr int aim_size = 64;
int size_x = screen->anx(aim_size);
int size_y = screen->any(aim_size);
helper_aim_view->SetSize(size_x, size_y);
helper_aim_view->SetPos(
position_x - size_x / 2,
position_y - size_y / 2
);
}
}
Скрытие прицела
Удаление прицела с экрана выполняется значительно проще, чем его отображение. Достаточно исключить соответствующий zCView из screen.
После удаления из screen элемент интерфейса перестаёт участвовать в отрисовке и не требует дополнительной очистки. Такой подход удобно использовать при выходе из режима дальнего боя, отключении магии или временной деактивации прицела.[/code]
C++:
void HideHelperView()
{
screen->RemoveItem(helper_aim_view);
}
После удаления из screen элемент интерфейса перестаёт участвовать в отрисовке и не требует дополнительной очистки. Такой подход удобно использовать при выходе из режима дальнего боя, отключении магии или временной деактивации прицела.[/code]
Взаимодействие с прицелом
Вернёмся к тому, зачем вообще нужны события Game_Init, Game_Loop и LoadEnd и какую роль каждое из них играет в механике прицела.
Здесь важно сразу зафиксировать ключевую идею:
прицел — это не единый объект, а связка UI + мировых данных, у которых разный жизненный цикл. Именно поэтому одного события нам недостаточно.
Это делает его идеальным местом для создания UI-элементов, не зависящих от мира.
В нашем случае здесь создаётся zCView, в котором будет отрисовываться изображение прицела:
Почему не позже:
При смене уровня или загрузке сохранения предыдущий мир полностью уничтожается, а вместе с ним — все zCVob.
Поэтому такие объекты необходимо:
Таким образом:
Именно здесь определяется:
Здесь стоит обратить внимание на несколько моментов:
Здесь важно сразу зафиксировать ключевую идею:
прицел — это не единый объект, а связка UI + мировых данных, у которых разный жизненный цикл. Именно поэтому одного события нам недостаточно.
Game_Init — инициализация UI
Game_Init вызывается один раз за запуск игры, когда базовые подсистемы движка уже готовы, но игровой мир ещё может не существовать.Это делает его идеальным местом для создания UI-элементов, не зависящих от мира.
В нашем случае здесь создаётся zCView, в котором будет отрисовываться изображение прицела:
C++:
void Game_Init()
{
// После инициализации движка создаём объект интерфейса,
// который будет использоваться для отображения прицела.
helper_aim_view = new zCView();
}
Почему не позже:
- zCView не привязан к миру;
- он живёт столько же, сколько и игровая сессия;
- пересоздавать его при каждой загрузке уровня нет смысла.
LoadEnd — инициализация объектов, зависящих от мира
В отличие от UI, вспомогательная цель (vob) напрямую зависит от текущего мира.При смене уровня или загрузке сохранения предыдущий мир полностью уничтожается, а вместе с ним — все zCVob.
Поэтому такие объекты необходимо:
- создавать после каждой загрузки мира;
- не сохранять их в сейвы;
- корректно отдавать управление их жизненным циклом движку.
C++:
void LoadEnd()
{
// При каждой загрузке мира создаём новый вспомогательный vob — цель
helper_aim_vob_hit = new zCVob();
ogame->GetGameWorld()->AddVob(helper_aim_vob_hit);
// Запрещаем сохранение объекта в сейвы
helper_aim_vob_hit->dontWriteIntoArchive = TRUE;
// Передаём управление временем жизни движку
helper_aim_vob_hit->Release();
}
Таким образом:
- при каждой загрузке мира мы гарантированно работаем с валидным объектом;
- не получаем висячих указателей;
- не рискуем утечками памяти.
Game_Loop — логика отображения и поведения прицела
Game_Loop — это сердце всей механики. Он вызывается каждый кадр, пока игрок находится в игровом мире.Именно здесь определяется:
- нужно ли вообще показывать прицел;
- какой тип прицела использовать;
- как должна вести себя цель в мире.
C++:
void Game_Loop()
{
switch (player->GetWeaponMode()) {
case NPC_WEAPON_BOW:
case NPC_WEAPON_CBOW:
// Для луков поднимаем цель выше относительно дистанции,
// чтобы компенсировать баллистику стрелы
SetHelperVobPosition(0.1f);
ShowHelperView("CROSS.TGA");
break;
case NPC_WEAPON_MAG:
// Для магии вертикальная поправка не требуется
SetHelperVobPosition(0.0f);
ShowHelperView("CIRCLE.TGA");
break;
default:
// Во всех остальных режимах прицел не нужен
HideHelperView();
}
}
Здесь стоит обратить внимание на несколько моментов:
- прицел отображается только в дальних режимах боя;
- тип текстуры напрямую привязан к типу оружия;
- вертикальная поправка (elevation) применяется осознанно — для луков она компенсирует раннее падение стрелы под действием физики;
- при выходе из режима дальнего боя прицел просто удаляется из screen.
Глоссарий к разделу
В данном разделе мы затронули следующие объекты и классы:
Используется для:
Используется для:
Используется для:
Используется для:
Используется для:
Используется для:
Используется для:
Используется как:
Классы
zCView
Базовый класс интерфейса.Используется для:
- отображения текста и изображений;
- построения UI-элементов;
- размещения элементов внутри других zCView.
- работает в виртуальных координатах 0..8192;
- может быть добавлен или удалён из screen;
- преобразование из пикселей в виртуальные координаты осуществляется функциями anx/any
- преобразование из виртуальных координат в пиксели осуществляется функциями nax/nay
zCVob
Базовый класс всех объектов игрового мира.Используется для:
- представления персонажей, предметов, эффектов;
- взаимодействия с физикой и трассировкой.
- живёт внутри zCWorld;
- автоматически удаляется при выгрузке мира;
- может быть исключён из сохранений (dontWriteIntoArchive).
zVEC3
Трёхмерный вектор или точка в пространстве.Используется для:
- хранения координат;
- задания направлений;
- математических операций в 3D-пространстве.
zCCamera
Класс активной камеры.Используется для:
- получения матрицы камеры;
- проекции мировых координат в экранные.
- Activate()
- Project(...)
oCGame
Центральный класс управления игровым процессом.Используется для:
- доступа к текущему миру;
- получения общей информации об игровой сессии.
- GetGameWorld()
zCWorld
Класс, представляющий игровой мир.Используется для:
- хранения всех zCVob;
- добавления и удаления объектов;
- управления жизненным циклом мира;
- хранит геометрию мира.
oCNpc
Класс персонажа (в том числе игрока).Используется для:
- определения текущего режима боя;
- получения состояния персонажа;
- доступа к оружию и поведению.
- GetWeaponMode()
Стандартные глобальные объекты
screen
Глобальный экземпляр zCView, представляющий весь экран.Используется как:
- корневой контейнер UI;
- точка добавления и удаления интерфейсных элементов.
- автоматически масштабируется под текущее разрешение;
- очищает временный текст каждый кадр.
ogame
Глобальный указатель на текущий oCGame.player
Глобальный указатель на игрока (oCNpc).zCCamera::activeCam
Статический указатель на активную камеру.3. Изменение положения камеры
Чтобы персонаж не «занимал весь экран» при дальнем бою или магии, удобнее сместить камеру за правое плечо. Сделать это можно двумя способами:
Для вычисления позиции камеры используется класс zCMovementTracker. Он обрабатывает:
Чтобы сместить камеру за правое плечо, достаточно изменить position. Например, на 60 см вправо:
constexpr float camera_right_shift_length = 60.0f;
В дереве проекта найдите каталог userapi. Создайте в нем файл в формате имя_класса.inl:
userapi/zCMovementTracker.inl
И добавьте туда новую сигнатуру:
void UpdatePlayerPos_Hooked(const zVEC3&);
Для нас достаточно оставить ядру выбор типа по умолчанию.
Создаём хук с помощью Union::CreateHook:
- Изменить игровые скрипты — но это сильно снижает совместимость с модами.
- Перехватить поведение камеры и задать смещение программно — именно этот способ мы будем использовать.
Подготовка
Создаём файл Camera.hpp, добавляем в него пространство имён GOTHIC_NAMESPACE и включаем его в Sources.hpp выше, чем Plugin.hpp.Для вычисления позиции камеры используется класс zCMovementTracker. Он обрабатывает:
- перемещение и вращение камеры;
- разрешение коллизий;
- отслеживание позиции игрока через метод:
Чтобы сместить камеру за правое плечо, достаточно изменить position. Например, на 60 см вправо:
constexpr float camera_right_shift_length = 60.0f;
Перехват метода
В Gothic API есть каталог UserAPI, где можно добавлять новые методы классов, не меняя исходные классы.В дереве проекта найдите каталог userapi. Создайте в нем файл в формате имя_класса.inl:
userapi/zCMovementTracker.inl
И добавьте туда новую сигнатуру:
void UpdatePlayerPos_Hooked(const zVEC3&);
Создание хука
Union поддерживает два типа перехвата функций:- call patch — переписывает все вызовы функции на наш метод;
- detours — переписывает только пролог функции.
Для нас достаточно оставить ядру выбор типа по умолчанию.
Создаём хук с помощью Union::CreateHook:
C++:
auto hook_CMovementTracker_UpdatePlayerPos = Union::CreateHook(
SIGNATURE_OF(&zCMovementTracker::UpdatePlayerPos),
&zCMovementTracker::UpdatePlayerPos_Hooked
);
- from — адрес оригинальной функции (SIGNATURE_OF позволяет ядру найти его автоматически).
- to — наш новый метод.
- Тип хука оставляем по умолчанию.
Реализация метода с учётом смещения
Ключевой момент: вызов оригинальной функции через указатель хука, разыменованный на текущем объекте:
C++:
void zCMovementTracker::UpdatePlayerPos_Hooked(const zVEC3& source_position) {
auto weapon_mode = player->GetWeaponMode();
auto dest_position = source_position;
if (weapon_mode >= NPC_WEAPON_BOW && weapon_mode <= NPC_WEAPON_MAG)
{
// target — это объект, за которым следует камера, обычно player
auto shift_vector = target->GetRightVectorWorld() * camera_right_shift_length;
dest_position += shift_vector;
}
// Вызов оригинальной функции с пересчитанной позицией
(this->*hook_CMovementTracker_UpdatePlayerPos)(dest_position);
}
4. Свободное вращение камеры в режимах дальнего боя и магии
В Gothic камера по умолчанию фиксируется на персонаже во время использования дальнего оружия или магии, и игрок не может её свободно вращать. Весь основной функционал для луков и магии реализован в методах:
Хук создаётся через Union::CreatePartialHook(from, to), где второй аргумент — функция с сигнатурой:
void __fastcall proc(Union::Registers& reg)
Далее мы можем модифицировать регистры и выполнять произвольные действия до того, как выполнение вернётся в оригинальную функцию.
BowMode (Bow.hpp)
Воспользуемся дизассемблером (в моем случае это IDA) и перейдем к методу BowMode. Первое, на что я обращаю внимание, это где происходит проверка на тип управления.
s_bUseOldControls - это переменная, которая отвечает на вопрос "использовать старый тип управления?". Видно, как по адресу 00695F0E значение переменной помещается в регистр eax, а по адресу 00695F1D происходит проверка его значения, что изменяет состояние флага ZF. Далее этот флаг использует только инструкция jz по адресу 00695F2B. То есть весь этот сегмент можно укоротить до интересующего нас отрезка:
Если используется старое управление, то выполняется инструкция, следующая за jz. Если новое - то происходит переход на 696391 - пока запомним это.
Реализуем хук, который будет вставлен вместо инструкции jz, то есть по адресу 00695F2B. Важно отметить, что вставляемый хук не должен пересекать собой действительную метку перехода. Но поскольку `jz loc_696391` имеет длину 6 байтов, а нам надо всего 5, то хук будет безопасен.
Хочу обратить внимание на то, что внутри хука переопределен eip - это значит, что инструкция по этому адресу будет выполнена следующей, после выхода из хука. Если бы этой строчки не было, по умолчанию был бы вызван `jz loc_696391` (все затертые хуком инструкции сохраняются и вызываются после).
Если посмотреть по дизассемблеру, 0x00695F31 - это начало блока старой схемы управления. То есть данный код автоматически отключает проверку и использует только ту, которую мы предпочитаем.
Теперь необходимо разрешить вращение камерой - это делается через метод PC_Turnings(TRUE) класса oCAIHuman. Но сперва необходимо получить текущий humanai (this).
Обычно this находится в регистре ecx, но это действительно в прологе функции. Опять вернемся в disassembler и посмотрим в каких сценариях используется ecx.
Мы видим, что по адресу 00695F22 значение ecx копируется в esi (mov esi, ecx). А строчкой ниже ecx изменяет свое значение на this->npc (mov ecx, [esi+12Ch]). До адреса перехвата (00695F31) более ничего не менялось, а значит наш this лежит в esi.
Разыменуем и разрешим вращение.
Аналогичную операцию проделываем с MagicMode (Magic.hpp).
Оставляем точки интереса:
Схема идентичная. Без лишних рассуждений - ecx помещается в esi, сравнение управления через eax, переход по адресу 00473032 - его мы и хукаем. Возврат через eip указываем на 0x00473038.
- oCAIHuman::BowMode
- oCAIHuman::MagicMode
Подготовка файлов
Создаём два новых файла проекта:- Bow.hpp
- Magic.hpp
Теория частичных хуков
Частичные (точечные) хуки позволяют вставить свой C++ код прямо в середину существующей функции. Это удобно, когда нужно изменить лишь отдельный кусок логики, не затрагивая остальную часть.Хук создаётся через Union::CreatePartialHook(from, to), где второй аргумент — функция с сигнатурой:
void __fastcall proc(Union::Registers& reg)
Далее мы можем модифицировать регистры и выполнять произвольные действия до того, как выполнение вернётся в оригинальную функцию.
Разрешение свободного вращения и выбор схемы управления
Наша задача — выполнить два действия одновременно:- Разрешить вращение камерой в боевых режимах.
- Переключить игру на старый тип управления (опционально).
BowMode (Bow.hpp)
Воспользуемся дизассемблером (в моем случае это IDA) и перейдем к методу BowMode. Первое, на что я обращаю внимание, это где происходит проверка на тип управления.
Код:
.text:00695F00 mov eax, large fs:0
.text:00695F06 push 0FFFFFFFFh
.text:00695F08 push offset ?BowMode@oCAIHuman@@IAEHH@Z_SEH
.text:00695F0D push eax
.text:00695F0E mov eax, ?s_bUseOldControls@oCGame@@0HA ; int oCGame::s_bUseOldControls
.text:00695F13 mov large fs:0, esp
.text:00695F1A sub esp, 28h
.text:00695F1D test eax, eax
.text:00695F1F push ebx
.text:00695F20 push ebp
.text:00695F21 push esi
.text:00695F22 mov esi, ecx
.text:00695F24 mov ecx, [esi+12Ch] ; this
.text:00695F2A push edi
.text:00695F2B jz loc_696391
s_bUseOldControls - это переменная, которая отвечает на вопрос "использовать старый тип управления?". Видно, как по адресу 00695F0E значение переменной помещается в регистр eax, а по адресу 00695F1D происходит проверка его значения, что изменяет состояние флага ZF. Далее этот флаг использует только инструкция jz по адресу 00695F2B. То есть весь этот сегмент можно укоротить до интересующего нас отрезка:
Код:
.text:00695F0E mov eax, ?s_bUseOldControls@oCGame@@0HA ; int
.text:00695F1D test eax, eax
.text:00695F2B jz loc_696391
Если используется старое управление, то выполняется инструкция, следующая за jz. Если новое - то происходит переход на 696391 - пока запомним это.
Реализуем хук, который будет вставлен вместо инструкции jz, то есть по адресу 00695F2B. Важно отметить, что вставляемый хук не должен пересекать собой действительную метку перехода. Но поскольку `jz loc_696391` имеет длину 6 байтов, а нам надо всего 5, то хук будет безопасен.
C++:
static auto hook_BowModeStartup = Union::CreatePartialHook(reinterpret_cast<void*>(0x00695F2B), [](Union::Registers& reg) {
// TODO
reg.eip = 0x00695F31;
});
Хочу обратить внимание на то, что внутри хука переопределен eip - это значит, что инструкция по этому адресу будет выполнена следующей, после выхода из хука. Если бы этой строчки не было, по умолчанию был бы вызван `jz loc_696391` (все затертые хуком инструкции сохраняются и вызываются после).
Если посмотреть по дизассемблеру, 0x00695F31 - это начало блока старой схемы управления. То есть данный код автоматически отключает проверку и использует только ту, которую мы предпочитаем.
Теперь необходимо разрешить вращение камерой - это делается через метод PC_Turnings(TRUE) класса oCAIHuman. Но сперва необходимо получить текущий humanai (this).
Обычно this находится в регистре ecx, но это действительно в прологе функции. Опять вернемся в disassembler и посмотрим в каких сценариях используется ecx.
Мы видим, что по адресу 00695F22 значение ecx копируется в esi (mov esi, ecx). А строчкой ниже ecx изменяет свое значение на this->npc (mov ecx, [esi+12Ch]). До адреса перехвата (00695F31) более ничего не менялось, а значит наш this лежит в esi.
Разыменуем и разрешим вращение.
C++:
static auto hook_BowModeStartup = Union::CreatePartialHook(reinterpret_cast<void*>(0x00695F2B), [](Union::Registers& reg) {
auto humanai = reinterpret_cast<oCAIHuman*>(reg.esi);
humanai->PC_Turnings(1);
reg.eip = 0x00695F31;
});
Аналогичную операцию проделываем с MagicMode (Magic.hpp).
Код:
.text:00472FD0 push 0FFFFFFFFh
.text:00472FD2 push offset ?MagicMode@oCAIHuman@@IAEHXZ_SEH
.text:00472FD7 mov eax, large fs:0
.text:00472FDD push eax
.text:00472FDE mov large fs:0, esp
.text:00472FE5 push ecx
.text:00472FE6 push ebx
.text:00472FE7 push ebp
.text:00472FE8 push esi
.text:00472FE9 mov esi, ecx
.text:00472FEB mov ecx, [esi+12Ch] ; this
.text:00472FF1 push edi
.text:00472FF2 xor ebp, ebp
.text:00472FF4 call ?GetSpellBook@oCNpc@@QAEPAVoCMag_Book@@XZ ; oCNpc::GetSpellBook(void)
.text:00472FF9 fld flt_99B3D8
.text:00472FFF fcomp ds:__real@00000000
.text:00473005 mov ebx, eax
.text:00473007 fnstsw ax
.text:00473009 test ah, 44h
.text:0047300C jp short loc_473023
.text:0047300E pop edi
.text:0047300F pop esi
.text:00473010 pop ebp
.text:00473011 xor eax, eax
.text:00473013 pop ebx
.text:00473014 mov ecx, [esp+10h+var_C]
.text:00473018 mov large fs:0, ecx
.text:0047301F add esp, 10h
.text:00473022 retn
.text:00473023 ; ---------------------------------------------------------------------------
.text:00473023
.text:00473023 loc_473023: ; CODE XREF: oCAIHuman::MagicMode(void)+3C↑j
.text:00473023 mov eax, ?s_bUseOldControls@oCGame@@0HA ; int oCGame::s_bUseOldControls
.text:00473028 test eax, eax
.text:0047302A mov ecx, [esi+12Ch]
.text:00473030 push 1
.text:00473032 jz loc_473206
Оставляем точки интереса:
Код:
.text:00472FE9 mov esi, ecx
.text:00472FEB mov ecx, [esi+12Ch] ; this
.text:00473023 mov eax, ?s_bUseOldControls@oCGame@@0HA ; int oCGame::s_bUseOldControls
.text:00473028 test eax, eax
.text:00473032 jz loc_473206
Схема идентичная. Без лишних рассуждений - ecx помещается в esi, сравнение управления через eax, переход по адресу 00473032 - его мы и хукаем. Возврат через eip указываем на 0x00473038.
C++:
auto hook_oCAIHuman_MagicMode_ = Union::CreatePartialHook(reinterpret_cast<void*>(0x00473032), [](Union::Registers& reg) {
auto humanai = reinterpret_cast<oCAIHuman*>(reg.esi);
humanai->PC_Turnings(1);
reg.eip = 0x00473038;
});
5. изменение условий взятия объектов в фокус
В движке объект, взятый вокус, отображает имя персонажа или триггера. Также эти объекты служат целью, в которую полетит снаряд или заклинание.
На данном этапе существуют две проблемы:
1. При зажатой ПКМ мы не можем менять фокус, вращая камерой. Примечательно, что NPC в игре умеют реагировать, если прицелиться в них из лука.
2. Персонаж в фокусе не используется как цель - для этого у нас есть отдельная мишень.
Такми образом перед нами встает задача - разделить фокус на Подсвечиваемый объект и Цель для выстрела.
Подсветка объекта при зажатой ЛКМ
Сперва решим проблему блокироки смены объекта в фокусе при зажатой ЛКМ.
За взятие в фокус объектов отвечают две основные функции:
В IDA необходимо проделать следующие действия:
Видим, что блокировка происходит в 0069B959-0069B967
Рассмотрим первое условие.
В нем проверяется валидность любого VOB в фокусе.
Отрывок кода, в котором полученный объект в фокусе записывается в var_4 (результат выполнения функции обычно лежит в eax)
Рассмотрим второе условие.
Это аналогичная проверка наличия NPC в фокусе
Тоже самое, только вместо переменной результат кладется в регистр ebx.
Судя по адресам переходов, блокирующими являются именно условие 2 и 3 (последнее можно его не рассматривать).
Поэтому добавим между условияи 1 и 2 дополнительное - на проверку боевого режима главного персонажа.
Создаем частичный хук по адресу 0x0069B963 (Camera.hpp).
Ближайшие инструкции говорят нам о том, что this находится в регистре esi.
Приводим reg.esi к oCAIHuman. Из него получаем npc (хотя можно и просто через player).
Если режим боя дальний, то переходим к блоку, который вызывает пересчет объекта в фокусе.
Мишень
Для того, чтобы произвести выстрел именно в мишень, а не в объект в фокусе, при этом не ломая механики, связанные с AI персонажей, можно подсунуть движку другую цель, но только на время выстрела. Схема следующая:
- Перед выстрелом запоминаем текущий объект в фокусе
- Подставляем в фокус мишень
- Производим выстрел
- Возвращаем исходный фокусный объект
Нам нужно получить доступ ДО и ПОСЛЕ функций oCAIHuman::BowMode и oCAIHuman::MagicMode.
Создадим файл oCAIHuman.inl и добавим в него два метода:
int BowMode_Hooked( int );
int MagicMode_Hooked();
Bow.hpp
Magic.hpp
На данном этапе существуют две проблемы:
1. При зажатой ПКМ мы не можем менять фокус, вращая камерой. Примечательно, что NPC в игре умеют реагировать, если прицелиться в них из лука.
2. Персонаж в фокусе не используется как цель - для этого у нас есть отдельная мишень.
Такми образом перед нами встает задача - разделить фокус на Подсвечиваемый объект и Цель для выстрела.
Подсветка объекта при зажатой ЛКМ
Сперва решим проблему блокироки смены объекта в фокусе при зажатой ЛКМ.
За взятие в фокус объектов отвечают две основные функции:
- oCAIHuman::CheckFocusVob
- oCNpc::CollectFocusVob (вызывается из предыдущей)
В IDA необходимо проделать следующие действия:
- Выбрать отладчик (например, Windgb)
- Установить два брейкпоинта: один в начале метода CheckFocusVob, второй - в конце
- Добавить условие брейкпоинтам - срабатывание только при зажатой ПКМ (чтобы была возможность подготовить интересующее состояние главного персонажа) - dword(0x8D1670) != 0 - это статическая переменная, в которой хранится состояние ПКМ в движке. Ее возвращает виртуальный метод zCInput_Win32::GetMouseButtonPressedRight.
- При остановке на брейкпоинте выбрать Debugger -> Tracing -> Instruction tracing
- Нажать Continue
- При попадании на второй брейкпоинт будут выделены инструкции, которые реально выполнялись
- Левый - при нормальном состоянии главного персонажа
- Правый - при зажатой ЛКМ и при наличии объекта в фокусе
Видим, что блокировка происходит в 0069B959-0069B967
Рассмотрим первое условие.
В нем проверяется валидность любого VOB в фокусе.
Код:
.text:0069B959 mov eax, [esp+1Ch+var_4]
.text:0069B95D test eax, eax
.text:0069B95F jz short loc_69B969
Код:
.text:0069B8B4 call ?GetFocusVob@oCNpc@@QAEPAVzCVob@@XZ ; oCNpc::GetFocusVob(void)
.text:0069B8BF mov [esp+1Ch+var_4], eax
Рассмотрим второе условие.
Это аналогичная проверка наличия NPC в фокусе
Код:
.text:0069B961 test ebx, ebx
.text:0069B963 jz short loc_69B9A5
Код:
.text:0069B8C3 call ?GetFocusNpc@oCNpc@@QAEPAV1@XZ ; oCNpc::GetFocusNpc(void)
.text:0069B8C8 mov ebx, eax
Судя по адресам переходов, блокирующими являются именно условие 2 и 3 (последнее можно его не рассматривать).
Поэтому добавим между условияи 1 и 2 дополнительное - на проверку боевого режима главного персонажа.
Создаем частичный хук по адресу 0x0069B963 (Camera.hpp).
C++:
static auto rangedFocusCollecting = Union::CreatePartialHook(reinterpret_cast<void*>(0x0069B963), [](Union::Registers& reg) {
auto humanai = reinterpret_cast<oCAIHuman*>(reg.esi);
auto npc = humanai->npc;
auto weapon_mode = npc->GetWeaponMode();
// Если активирован режим дальнего боя, то пропускаем
// проверки наличия фокус-воба и разрешаем обновлять его
if (weapon_mode >= NPC_WEAPON_BOW && weapon_mode <= NPC_WEAPON_MAG)
reg.eip = 0x0069B969;
});
Ближайшие инструкции говорят нам о том, что this находится в регистре esi.
Приводим reg.esi к oCAIHuman. Из него получаем npc (хотя можно и просто через player).
Если режим боя дальний, то переходим к блоку, который вызывает пересчет объекта в фокусе.
Мишень
Для того, чтобы произвести выстрел именно в мишень, а не в объект в фокусе, при этом не ломая механики, связанные с AI персонажей, можно подсунуть движку другую цель, но только на время выстрела. Схема следующая:
- Перед выстрелом запоминаем текущий объект в фокусе
- Подставляем в фокус мишень
- Производим выстрел
- Возвращаем исходный фокусный объект
Нам нужно получить доступ ДО и ПОСЛЕ функций oCAIHuman::BowMode и oCAIHuman::MagicMode.
Создадим файл oCAIHuman.inl и добавим в него два метода:
int BowMode_Hooked( int );
int MagicMode_Hooked();
Bow.hpp
C++:
static auto hook_oCAIHuman_BowMode = Union::CreateHook(SIGNATURE_OF(&oCAIHuman::BowMode), &oCAIHuman::BowMode_Hooked);
int oCAIHuman::BowMode_Hooked(int pressed)
{
// Сохраняем старые фокусные объекты
auto saved_enemy = npc->enemy;
auto saved_focus_vob = npc->GetFocusVob();
// Устанавливаем мишень, в которую будет производиться выстрел
npc->SetEnemy(0);
npc->SetFocusVob(helper_aim_vob_hit);
// Отрабатываем один такт режима дальнего боя
auto result = (this->*hook_oCAIHuman_BowMode)(pressed);
// Восстанавливаем фокусные объекты
npc->SetEnemy(saved_enemy);
npc->SetFocusVob(saved_focus_vob);
return result;
}
Magic.hpp
C++:
static auto hook_oCAIHuman_MagicMode = Union::CreateHook(SIGNATURE_OF(&oCAIHuman::MagicMode), &oCAIHuman::MagicMode_Hooked);
int oCAIHuman::MagicMode_Hooked()
{
// Сохраняем старые фокусные объекты
auto saved_enemy = npc->enemy;
auto saved_focus_vob = npc->GetFocusVob();
// Устанавливаем мишень, в которую будет производиться выстрел
npc->SetEnemy(0);
npc->SetFocusVob(helper_aim_vob_hit);
// Отрабатываем один такт режима дальнего боя
auto result = (this->*hook_oCAIHuman_MagicMode)();
// Восстанавливаем фокусные объекты
npc->SetEnemy(saved_enemy);
npc->SetFocusVob(saved_focus_vob);
return result;
}
6. реализация простой баллистики стрел с использованием штатных возможностей движка
Если вы замечали, то при столкноверии с поверхностью стрела отлетает от нее и падает. В момент удара у стрелы просто активируется физика. То есть фактически физику можно включить еще на этапе ее полета, добавив тем самым параболическую траекторию полета.
Все параметры стрелы задаются в методе oCAIArrow::SetupAIVob.
За поведение стрелы отвечает объект rigidBody. Поэтому нас интересует только конец функции, где он используется.
В следующем участке кода задается направление и скорость полета стрелы. Эти параметры нам необходимо поменять.
Создаем хук по адресу 006A1413
Комментарии:
Все параметры стрелы задаются в методе oCAIArrow::SetupAIVob.
За поведение стрелы отвечает объект rigidBody. Поэтому нас интересует только конец функции, где он используется.
В следующем участке кода задается направление и скорость полета стрелы. Эти параметры нам необходимо поменять.
Код:
.text:006A140C call ?GetRigidBody@zCVob@@QAEPAVzCRigidBody@@XZ ; zCVob::GetRigidBody(void)
.text:006A1411 mov ecx, eax ; this
.text:006A1413 call ?SetVelocity@zCRigidBody@@QAEXABVzVEC3@@@Z ; zCRigidBody::SetVelocity(zVEC3 const &)
Создаем хук по адресу 006A1413
C++:
static auto hook_StartupAIVob = Union::CreatePartialHook(reinterpret_cast<void*>(0x006A1413), [](Union::Registers& reg) {
// Забираем npc-стрельца из второго аргумента,
// устанавливаем запрет на исполнение кода не-игроку
auto npc_shooter = *reinterpret_cast<oCNpc**>(reg.esp + 0xF4);
if (npc_shooter != player)
return;
// Извлекаем вектор со стека и ускоряем стрелу
auto& vector = **reinterpret_cast<zVEC3**>(reg.esp);
vector.Normalize();
vector *= 4000.0f;
// Воруем физичное тело из регистра ecx перед вызовом
// установки вектора скорости и включаем ему физику падения
auto rigid_body = reinterpret_cast<zCRigidBody*>(reg.ecx);
rigid_body->gravityOn = 1;
});
- обязательно делаем проверку на то, что целевой NPC - это игрок.
- стандартный velocity вектор = 2000 см/сек, это мало. Меняем на 4000
- активируем физику стрелы прямо на старте
7. реализация направленного каста заклинаний с использованием штатных возможностей движка
В движке есть несколько типов поведения заклинаний. Нас интересуют только заклинания, летящие из точки А в точку Б.
Определить это можно через поле oCSpell::targetCollectAlgo. Если значение равно TARGET_COLLECT_FOCUS_FALLBACK_NONE, то это заклинание нам подходит.
Инициализация эффекта заклинания происходит в методе oCSpell::Setup. Однако в нем есть блокирующая проверка - мишенью может быть только NPC.
На изображении esi - это наша мишень. Блок 00484A25-00484A3C - это zDYNAMIC_CAST, который на основе виртуальной таблицы (edx) сравнивает классы друг с другом. Если мишень - не NPC, то она обнуляется (xor esi, esi). В случае заклинаний типа TARGET_COLLECT_FOCUS_FALLBACK_NONE эта проверка не нужна.
Создаем частичный хук по адресу 0x00484A25
Текущее заклинание находится в регистре ebp. Проверяем тип targetCollectAlgo. Если это наш тип заклинания, то проверяем, есть ли заклинатель и мишень.
Если все ОК, заранее инициализируем эффект, для дальнейшего обхода IsValidTarget.
С помощью изменения eip обходим проверку мишени на принадлежность типу NPC.
Определить это можно через поле oCSpell::targetCollectAlgo. Если значение равно TARGET_COLLECT_FOCUS_FALLBACK_NONE, то это заклинание нам подходит.
Инициализация эффекта заклинания происходит в методе oCSpell::Setup. Однако в нем есть блокирующая проверка - мишенью может быть только NPC.
На изображении esi - это наша мишень. Блок 00484A25-00484A3C - это zDYNAMIC_CAST, который на основе виртуальной таблицы (edx) сравнивает классы друг с другом. Если мишень - не NPC, то она обнуляется (xor esi, esi). В случае заклинаний типа TARGET_COLLECT_FOCUS_FALLBACK_NONE эта проверка не нужна.
Создаем частичный хук по адресу 0x00484A25
C++:
auto hook_oCSpell_Setup = Union::CreatePartialHook(reinterpret_cast<void*>(0x00484A25), [](Union::Registers& reg) {
auto spell = reinterpret_cast<oCSpell*>(reg.ebp);
if (spell->targetCollectAlgo == TARGET_COLLECT_FOCUS_FALLBACK_NONE)
{
auto caster = *reinterpret_cast<zCVob**>(reg.esp+0x80 + 4);
auto target = *reinterpret_cast<zCVob**>(reg.esp+0x80 + 8);
if (caster && target)
{
// Инициализируем заранее, чтобы далее по функции обойди IsValidTarget
spell->effect->Init(caster, target, nullptr);
reg.eip = 0x00484A40;
}
}
});
Если все ОК, заранее инициализируем эффект, для дальнейшего обхода IsValidTarget.
С помощью изменения eip обходим проверку мишени на принадлежность типу NPC.
8. дополнительно
Можно заметить, что в Gothic 2 если направлять камеру вертикально вверх или вниз, то по ощущениям углы наклона персонажа сильно больше ожидаемых.
Это можно исправить внутри oCAIHuman::BowMode.
За определение углов отвечает oCNpcFocus::IsInAngle(vob_type, ele, azi), вызов которого находится по адресу 006961E5.
Перед тем, как будет вызвана данная функция, отредактируем передаваемые параметры так, чтобы угол наклона тела получился раза в 2 меньше.
vob_type играет небольшую роль в том, как сильно тело будет наклоняться. Экспериментально могу сказать, что самый стабильный вариант - zTVobType::zVOB_TYPE_NSC. При таком значении тело при сильно задранной камере не сбрасывает значение наклона.
Также в MagicMode нужно убрать прилипание к цели, потому что камера может зафиксироваться и на нашей мишени. Хотя концепция такова, что вращение у нас полностью разблокировано. В Gothic 2 за это отвечает метод oCNpc::TurnToEnemy(). Найдем вызов этого метода в oCAIHuman::MagicMode и пропустим.
Magic.hpp
Это можно исправить внутри oCAIHuman::BowMode.
За определение углов отвечает oCNpcFocus::IsInAngle(vob_type, ele, azi), вызов которого находится по адресу 006961E5.
Перед тем, как будет вызвана данная функция, отредактируем передаваемые параметры так, чтобы угол наклона тела получился раза в 2 меньше.
C++:
auto hook_BowMode_GetAngles = Union::CreatePartialHook(reinterpret_cast<void*>(0x006961E5), [](Union::Registers& reg) {
// Аргументы, которые движок уже положил на стек (передал в функцию)
auto& azi = *reinterpret_cast<float*>(reg.esp + 8); // x
auto& ele = *reinterpret_cast<float*>(reg.esp + 4); // y
auto& vob_type = *reinterpret_cast<int*>(reg.esp + 0); // type
// Реальное хранимое значение вертикального угла. Получаемое значение
// ненормально большое, поэтому смело делим его вдвое.
auto& elevation = *reinterpret_cast<float*>(reg.esp + 0x28);
elevation *= 0.5;
ele = elevation;
// Самый стабильный вариант, не сбрасывает
// анимацию при больших значениях
vob_type = zTVobType::zVOB_TYPE_NSC;
});
vob_type играет небольшую роль в том, как сильно тело будет наклоняться. Экспериментально могу сказать, что самый стабильный вариант - zTVobType::zVOB_TYPE_NSC. При таком значении тело при сильно задранной камере не сбрасывает значение наклона.
Также в MagicMode нужно убрать прилипание к цели, потому что камера может зафиксироваться и на нашей мишени. Хотя концепция такова, что вращение у нас полностью разблокировано. В Gothic 2 за это отвечает метод oCNpc::TurnToEnemy(). Найдем вызов этого метода в oCAIHuman::MagicMode и пропустим.
Magic.hpp
C++:
auto hook_oCAIHuman_MagicMode_DontTurnToEnemy =
Union::CreatePartialHook(reinterpret_cast<void*>(0x00473282),
[](Union::Registers& reg)
{
reg.eip = 0x0047328D;
});
Вложения
Последнее редактирование:

