Slavemaster
Модостроитель
- Регистрация
- 10 Июн 2019
- Сообщения
- 1.086
- Благодарности
- 1.904
- Баллы
- 320
Русский English
Общее описание и начало использованияОписание:
Шаблон плагина Union для Visual Studio 2019 использующий тулсет vc141_xp (последний поддерживаемый Windows XP, может быть установлен с помощью инсталлятора Visual Studio) и стандарт C++17.
Также включены некоторые утилиты.
Использование скомпилированного плагина подразумевает установленный пакет Visual C++ 2015 Redistributable (x86).
Начало использования:
1. Установить шаблон плагина из менеджера ресурсов
2. Перезапустить Visual Studio
3. Создать новый проект (в новом решении или в уже существующем) используя шаблон C++17 Union Plugin
5. Открыть свойства проекта и установить корректную выходную директорию
По умолчанию проект использует следующие пути:
D:\Gothic\g1\system\
D:\Gothic\g1a\system\
D:\Gothic\g2\system\
D:\Gothic\g2a\system\
Вы можете или установить игру в соответствующие папки или поменять пути в настройках проекта
6. Также можно открыть файл Workspace\Assembly\Assembly.cpp (считайте его единственным компилируемым cpp файлом, код, который вы пишете, будет текстуально включён в этот файл) для того, чтобы Visual Studio смогла правильно подсветить весь код и сгенерировать кэш подсказок
7. Кликните правой кнопкой по фильтру Entry, выберете Add New Item и используйте C++17 Union Header file (.h)
8. Введите имя нового файлв и, если вам не всё равно на файловую структуру, добавьте \Workspace\Entry\ в поле Location. Если всё равно - не добавляйте
C++:namespace NAMESPACE { Sub helloWorld(ZSUB(GameEvent::Execute), []() { Message::Info(A"Hello from " + ZENDEF("G1", "G1A", "G2", "G2A") + "!"); }); }
Описание утилитКод:ZENDEF(g1, g1a, g2, g2a) ZENDEF2(g1, g2) // выбирают значение в соответствии с логическим движком (текущее значение ENGINE) // не заворачивают результат в скобки: ZENDEF2(1+3,1+3)*ZENDEF2(1+3,1+3) == 1+3*1+3 ZTEST(exp) // возвращает значение выражения exp, если логический движок совпадает с физическим (версия игры, в которую загружен плагин) // иначе возвращает значение по умолчанию того же типа, что и само выражение (0 - для чисел, "" - для строк и т.д.) ZENFOR(g1, g1a, g2, g2a) // если логический движок совпадает с физическим, то выбирает значение из списка // иначе - значение по умолчанию того же типа LOG(exp) // выводит выражение и его значение в консоль // LOG(AHEX32(player)); -> "AHEX32(player): 0x12345678" LOGS(exp) // выводит выражение и его значение на экран // следует вызывать из игрового цикла // Пример: // int y = 3000; // положение первой строки // LOGS(AHEX32(player)); // LOGS(player->refCtr * 2); ASSERT(exp) // Отображает информативное сообщение, если (exp) != true COA(a, b, c, d) // от одного до шести аргументов // возвращает значение a->b->c->d, если ((a) && (a->b) && (a->b->c)) == true // иначе возвращает значение по умолчанию // zSTRING enemyOfMyFocus = COA(player, GetFocusNpc(), enemy, objectName); // если, например, player->GetFocusNpc() == nullptr, то вернётся пустая строка
Класс ActiveValue<T> хранит в себе значение типа T и позволяет отслеживать его изменение
C++:namespace NAMESPACE { ActiveValue<string> message; // начальное значение - пустая строка // сразу подписываемся на изменение переменной message Sub listenMessage(ZSUB(GameEvent::Execute), []() { message.onChange += []() { Message::Info(A"Переменная message была изменена: " + *message); }; }); // меняем значение переменной Sub setMessage(ZSUB(GameEvent::Execute), []() { message = "Hello!"; // отобразится окно с сообщением message = "Hello!"; // ничего не произойдёт message = "Hi!"; // окно с сообщением отобразится снова }); }
Экземпляр класса ActiveOption<T> обеспечивает связь плагина с отдельным значением из Gothic.ini.
Аналогично классу ActiveValue<T> имеет событие onChange, которое выстреливает не только при изменении опции, но и при её первой загрузке.
Создавать экземпляры этого класса следует с помощью макроса ZOPTION в файле Workspace/Assembly/Options.h
C++:// Workspace/Assembly/Options.h namespace NAMESPACE { namespace Options { // определяем опцию в Gothic.ini // [ZPLUGINNAME] // SomeFloatValue = 777.6 ZOPTION(SomeFloatValue, 777.6f); // Пока что значение опции равно 0.0f и его нельзя менять // Значение 777.6f или любое другое из Gothic.ini будет установлено позже // (по событию DefineExternals, в котором свяжем все опции плагина с опциями движка) } namespace Options { Sub load(ZSUB(GameEvent::DefineExternals), []() { // вызываем после парсинга Gothic.ini движком // DefineExternals - подходящее место ActiveOptionBase::LoadAll(); }); } }
Класс Sub<T> предназначен для работы с основными событиями игры, которые перечислены в типе GameEvents.
Помимо стандартных событий, есть 2 специальных:
1. GameEvent::Execute - если передать это значение переменной класса Sub<T>, то соответствующий код выполнится единожды при конструировании переменной. Таким образом, если переменная класса Sub<T> является глобальной, то код выполнится при загрузке плагина:
C++:namespace NAMESPACE { int x = 5; Sub/*<void>*/ setX(ZSUB(GameEvent::Execute), []() { x = 99; }); int y = x; Sub showY(ZSUB(GameEvent::Execute), []() { Message::Info(A"y = " + y); // выведет 'y = 99' }); }
C++:Sub<void> Gothic_I_Classic::setX; Sub<void> Gothic_I_Addon::setX; Sub<void> Gothic_II_Classic::setX; Sub<void> Gothic_II_Addon::setX;
Также можно привязать Sub к ActiveOption<T> или ActiveValue<T>. Тогда код будет выполняться только в том случае, если связанный объект преобразуется в true:
C++:namespace NAMESPACE { ActiveValue<bool> debug = false; // Sub<ActiveValue<bool>> Sub printDebugInfo(ZSUB(GameEvent::Loop), debug, []() { int y = 2000; LOGS(AHEX32(player->focus_vob)); // будет выводится на экран, если static_cast<bool>(debug) == true }); // Sub<void> Sub setDebug(ZSUB(GameEvent::Loop), []() { if (zinput->GetMouseButtonPressedRight()) debug = true; // включаем отладку else debug = false; // выключаем отладку }); }
Класс Hook<Func, Option> несёт те же функции, что и CInvoke / ModulePatchCallInvoker, плюс, имеет возможность привязки к ActiveValue<T> и ActiveOption<T>.
C++:// Workspace/Assembly/Options.h namespace NAMESPACE { namespace Options { // [ZPLUGINNAME] // LogDamage = 1 ZOPTION(LogDamage, true); } namespace Options { Sub load(ZSUB(GameEvent::DefineExternals), []() { ActiveOptionBase::LoadAll(); }); } } // Workspace/Entry/LogDamage.h namespace NAMESPACE { void __fastcall Hook_oCNpc_OnDamage_Hit(oCNpc*, void*, oCNpc::oSDamageDescriptor&); Hook<void(__thiscall*)(oCNpc*, oCNpc::oSDamageDescriptor&), ActiveOption<bool>> Ivk_oCNpc_OnDamage_Hit(ZENFOR(0x00731410, 0x007700A0, 0x0077D390, 0x00666610), &Hook_oCNpc_OnDamage_Hit, HookMode::Patch, Options::LogDamage); void __fastcall Hook_oCNpc_OnDamage_Hit(oCNpc* _this, void* vtable, oCNpc::oSDamageDescriptor& a0) { int oldHitpoints = _this->attribute[NPC_ATR_HITPOINTS]; Ivk_oCNpc_OnDamage_Hit(_this, a0); int damage = oldHitpoints - _this->attribute[NPC_ATR_HITPOINTS]; if (damage != 0) cmd << _this->name[0] << " получил " << damage << " единиц урона" << endl; } }
Если опция изменится на 0 движком (с помощью меню или из другого плагина), то хук будет автоматически отцеплен.
Другой способ привязать хук к этой опции:
C++:// oCNpc.inl // Supported with union (c) 2020 Union team // User API for oCNpc // Add your methods here void OnDamage_Hit_Union(oCNpc::oSDamageDescriptor&); // Workspace/Entry/LogDamage.h namespace NAMESPACE { FASTHOOK_PATCH_OPT(oCNpc, OnDamage_Hit, Options::LogDamage); void oCNpc::OnDamage_Hit_Union(oCNpc::oSDamageDescriptor& a0) { int oldHitpoints = attribute[NPC_ATR_HITPOINTS]; THISCALL(Hook_oCNpc_OnDamage_Hit)(a0); int damage = oldHitpoints - attribute[NPC_ATR_HITPOINTS]; if (damage != 0) cmd << name[0] << " получил " << damage << " единиц урона" << endl; } }
Класс VarScope<T> запоминает значение типа T и восстанавливает его в своём деструкторе. Обычно используется в сочетании с функцией AssignTemp:
C++:namespace NAMESPACE { void __fastcall Hook_oCGame_UpdatePlayerStatus(oCGame*, void*); Hook<void(__thiscall*)(oCGame*)> Ivk_oCGame_UpdatePlayerStatus(ZENFOR(0x00638F90, 0x0065F4E0, 0x00666640, 0x006C3140), &Hook_oCGame_UpdatePlayerStatus, HookMode::Patch); void __fastcall Hook_oCGame_UpdatePlayerStatus(oCGame* _this, void* vtable) { // получаем предмет в фокусе игрока oCItem* focusItem = COA(player, focus_vob, CastTo<oCItem>()); // создаём неактивный объект VarScope<zSTRING> scope; // если в фокусе квестовый предмет, то временно меняем его фокусную надпись (до конца функции) if (focusItem && focusItem->HasFlag(ITM_FLAG_MI)) scope = AssignTemp(focusItem->name, Z"Неизвестный квестовый предмет"); return Ivk_oCGame_UpdatePlayerStatus(_this); // если мы изменили имя предмета, то оно будет восстановлено здесь } }
Класс Unlocked<T> предназначен для типобезопасного разблокирования кода движка. Приведу пример отключения поворота ГГ к противнику при отскоках:
C++:// Workspace/Assembly/JumpBackNoTurn.h // код имеет смысл только для Готики 2 #if ENGINE >= Engine_G2 namespace NAMESPACE { // новое определение для oCNpc::TurnToEnemy, которое не поворачивает ГГ к противнику при отскоках void __fastcall oCNpc_TurnToEnemy_Hook(oCNpc* _this, void* vtable) { // если функция не вызвана для игрока, то просто вызываем оригинальную функцию if (_this != player) return _this->TurnToEnemy(); oCAniCtrl_Human* aniCtrl = _this->GetAnictrl(); // проверка на всякий случай if (!aniCtrl) return _this->TurnToEnemy(); // получаем текущую анимацию игрока zCModelAniActive* hitAni = COA(_this, GetModel(), GetActiveAni(aniCtrl->hitAniID)); zCModelAni* protoAni = COA(hitAni, protoAni); // если анимация не проигрывается или не является отскоком назад, то вызываем оригинальную функцию if (!protoAni || !protoAni->aniName.HasWord("JUMPB")) return _this->TurnToEnemy(); // иначе не делаем поворот к врагу } // подцепляемся к опции из Gothic.ini Sub listenJumpBackNoTurn(ZSUB(GameEvent::Execute), []() { Options::JumpBackNoTurn.onChange += []() { // этот код выполнится при загрузке или изменении булевой опции JumpBackNoTurn // в эту переменную запомним оригинальный код движка static std::optional<int> originalValue{}; // если опция отключена с самого начала игры, не трогаем движок вообще if (!Options::JumpBackNoTurn && !originalValue.has_value()) return; // адрес внутри oCAIHuman::DoAI, по которому записан относительный адрес // вызываемой функции oCNpc::TurnToEnemy int address = ZENDEF(0x00000000, 0x00000000, 0x0063FA6A, 0x0069C23A); // разблокируем 4 байта как значение int по этому адресу Unlocked<int> relativeAddress = address; // если мы еще не запомнили оригинальный код (патчим в первый раз) // то запоминаем его if (!originalValue.has_value()) originalValue = relativeAddress; // если опция активна, то подменяем вызов oCNpc::TurnToEnemy на вызов нашей функции if (Options::JumpBackNoTurn) // относительный адрес отсчитывается от конца инструкции // поэтому от адреса нашей функции отнимаем адрес конца инструкции relativeAddress = reinterpret_cast<int>(&oCNpc_TurnToEnemy_Hook) - (address + 4); // иначе восстанавливаем оригинальный код движка else relativeAddress = originalValue.value(); }; }); } #endif
C++:Unlocked<std::array<byte, 5>> left = ZENDEF(0x00614AC3, 0x006378D6, 0x0063E4E7, 0x0069AD37); Unlocked<std::array<byte, 5>> right = ZENDEF(0x00614B4F, 0x00637967, 0x0063E557, 0x0069ADA7); left = { 0x31, 0xC0, 0x90, 0x90, 0x90 }; // xor eax, eax right = { 0x31, 0xC0, 0x90, 0x90, 0x90 };
Класс Symbol является обёрткой над классом zCPar_Symbol. Он хранит указатели на zCPar_Symbol, на zСParser (которому принадлежит этот символ), а также индекс символа. Основное назначение класса - определять логический тип символа:
C++:enum class Type { Unknown, // символ не найден ClassVar, // поле класса (например, C_NPC.NAME) Class, // класс (например, class C_SomeClass {};) ExternalVar, // не используется ExternalFunc, // внешняя функция (например, Npc_HasItems) Instance, // инстанция с кодом (например, instance SomeNpc(C_NPC){};) Prototype, // прототип (например, prototype SomeProtoNpc(C_NPC){};) Func, // функция с кодом (например, func void SomeFunc() {};) VarInstance, // экземпляр класса (например, var C_NPC npc;) VarString, // переменная или константа строкового типа (например, сonst string names[2] = { "Aba", "caba"};) VarFloat, // переменная или константа вещественного типа (например, const float x = 5.0;) VarInt // переменная или константа типа func (например, const func f = B_AssessMagic;) };
C++:namespace NAMESPACE { void ShowBurningTorchSymbol() { Symbol symbol{ parser, "ItLsTorchBurning" }; ASSERT(symbol.GetType() == Symbol::Type::Instance); LOG(symbol.GetIndex()); LOG(symbol.GetSymbol()->name); int scriptCodeAddress = symbol.GetValue<int>(0); LOG(scriptCodeAddress); } Sub showTorch(ZSUB(GameEvent::Init), &ShowBurningTorchSymbol); // Возможный выхлоп: // symbol.GetIndex(): 7430 // symbol.GetSymbol()->name: ITLSTORCHBURNING // scriptCodeAddress : 208559 }
Для создания внешних функции существуют несколько вспомогательных макросов. Рассмотрим их на примере: определим внешнюю функцию, которая удаляет из инвентаря NPC все предметы определённой категории и возвращает количество удалённых предметов.
C++:namespace NAMESPACE { // func int Npc_ClearCategory(var C_NPC npc, var int category, var int ignoreEquipped); int __cdecl Npc_ClearCategory() { // сохраняем состояние текущего парсера: // - текущую инстанцию (используется в коде инстансов) // - переменные HERO, SELF, OTHER, VICTIM, ITEM (если мы в основном парсере) ParserScope scope{ zCParser::GetParser() }; oCNpc* npc; int category; bool ignoreEquipped; // достаём аргументы из парсера ZARGS(npc, category, ignoreEquipped); // если NPC не валидный или он в боевом режиме, то // отображаем окно с ошибкой ASSERT(npc); ASSERT(npc->GetWeaponMode() == NPC_WEAPON_NONE); std::vector<oCItem*> removeList; int removeAmount = 0; // пробегаемся по предметам инвентаря for (oCItem* item : npc->inventory2) { // если категория предмета не соответствует, то оставляем предмет нетронутым if (item->mainflag != category) continue; // если включено игнорирование экипированных предметов // и предмет экипирован, то не трогаем этот предмет if (ignoreEquipped && item->HasFlag(ITM_FLAG_ACTIVE)) continue; // добавляем предмет в список удаляемых removeList += item; removeAmount += item->amount; }; // удаляем каждый предмет из списка for (oCItem* item : removeList) { oCItem* removed = npc->RemoveFromInv(item, item->amount); // удалённый из инвентаря предмет логически добавляется в мир // удаляем его и оттуда ogame->GetGameWorld()->RemoveVob(removed); } // возвращаем результат - суммарное количество удалённых предметов ZRETURN(removeAmount); return false; } // регистрируем функцию в основном парсере // ZEXTERNAL(Имя_Функции, тип_возвращаемого_значения, типы_аргументов...) ZEXTERNAL(Npc_ClearCategory, int, oCNpc*, int, bool); }
1. zCPar_Symbol* и Symbol - интерпретируется как передача инстанции через символ
2. Все указатели, кроме zCPar_Symbol* - интерпретируется как передача инстанции через передачу адреса
3. int и bool - интерпретируется как передача целого значения
4. float - интерпретируется как передача вещественного значения
5. string, zSTRING - интерпретируется как передача строки
6. unsigned - интерпретируется как передача функции
Если вам нужно вызвать функции парсера из плагина, используйте функцию CallParser. Вы не должны вызывать с её помощью внешние функции (но можете), поэтому следующий код лишь для демонстрации:
C++:void CallSomeFunc() { // вызов по имени string result = CallParser<string>(parser, "ConcatStrings", Z"Got", "hic"); // можно передавать сырые строки в качестве аргументов ASSERT(result == "Gothic"); // вызов по индексу int amount = CallParser<int>(parser, parser->GetIndex("Npc_HasItems"), player, parser->GetIndex("ItLsTorch")); }
Класс KeyCombo используется для работы с набором сочетаний физических клавиш. Может использовать с ZOPTION. Привязка хуков к такой опции также может осуществляться (хук будет активным, если хотя бы одна клавиша задана).
C++:namespace NAMESPACE { namespace Options { namespace Helpers { KeyCombo CreateComplexCombo() { std::vector<std::vector<int>> combos; combos += { KEY_LSHIFT, KEY_RSHIFT }; combos += { KEY_LCONTROL, MOUSE_BUTTONRIGHT }; combos += { KEY_RCONTROL, MOUSE_BUTTONLEFT }; return combos; } } // [ZPLUGINNAME] // SimpleKey=KEY_U ZOPTION(SimpleKey, KeyCombo({ KEY_U })); // [ZPLUGINNAME] // HyperKey=KEY_LSHIFT + KEY_RSHIFT, KEY_LCONTROL + MOUSE_BUTTONRIGHT, KEY_RCONTROL + MOUSE_BUTTONLEFT ZOPTION(HyperKey, Helpers::CreateComplexCombo()); // [ZPLUGINNAME] // NoKey=# ZOPTION(NoKey, KeyCombo{}); } namespace Options { Sub load(ZSUB(GameEvent::DefineExternals), []() { ActiveOptionBase::LoadAll(); }); } }
Класс SaveData является базовым классом для объектов, которые вы хотите сохранять/загружать. Для примера реализуем хеш-таблицу строк, доступ к которой может осуществляться из скриптов:
C++:namespace NAMESPACE { // открыто наследуем свой класс от класса SaveData class HashTable : public SaveData { private: // определяем свои данные: таблицу строк std::unordered_map<string, string> table; public: #pragma region Обязательные методы HashTable(const string& name) : SaveData{ name } // вызываем конструктор базового класса { } // очистка данных // перед загрузкой вызывается автоматически virtual void Clear() override { // опустошаем хеш-таблицу table.clear(); } // вызывается при сохранении virtual void Archive(zCArchiver& arc) override { // пишем количество строк, чтобы знать сколько их считывать при загрузке arc.WriteInt("Size", table.size()); // пишем все пары строк (Key, Value) for (const auto& pair : table) { arc.WriteString("Key", pair.first); arc.WriteString("Value", pair.second); } } // вызывается при загрузке (если есть файл name.SAV) // при автоматическом вызове гарантируется, что объект очищен (вызван Clear()) virtual void Unarchive(zCArchiver& arc) override { // считываем данные аналогично тому, как их записывали int size = arc.ReadInt("Size"); for (int i = 0; i < size; i++) { string key = arc.ReadString("Key"); string value = arc.ReadString("Value"); table[key] = value; } } #pragma endregion // обеспечиваем доступ к хеш-таблице string& operator[](const string& key) { return table[key]; } }; // поскольку у нас планируется только одна хеш-таблица, то // можно сделать более удобный доступ к ней // иначе вместо `GetHashTable()` придётся писать `SaveData::Get<HashTable>("HashTable")` HashTable& GetHashTable() { // создаём и запоминаем объект при первом вызове // файл с данными будет называться "HASHTABLE.SAV" static HashTable& hashTable = SaveData::Get<HashTable>("HashTable"); return hashTable; } Sub saveData(ZSUB(GameEvent::SaveBegin), []() { // нет смысла сохранять/загружать данные при переходе между мирами - // пусть остаются в памяти if (!SaveLoadGameInfo.changeLevel) // указываем событие, по которому сохраняемся, чтобы правильно определился слот // например, в событии SaveEnd записывать в папку current уже поздно - нужно использовать savegameXX GetHashTable().Save(GameEvent::SaveBegin); }); Sub loadData(ZSUB(GameEvent::LoadEnd), []() { if (!SaveLoadGameInfo.changeLevel) GetHashTable().Load(GameEvent::LoadEnd); }); // определяем внешние функции для доступа к хеш-таблице из скриптов: // func string Hlp_GetString(var string key); // func void Hlp_SetString(var string key, var string value); int __cdecl Hlp_GetString() { string key; ZARGS(key); ZRETURN(GetHashTable()[key]); return false; } int __cdecl Hlp_SetString() { string key, value; ZARGS(key, value); GetHashTable()[key] = value; return false; } ZEXTERNAL(Hlp_GetString, string, string); ZEXTERNAL(Hlp_SetString, void, string, string); }
General description and Getting StartedDescription:
Visual Studio 2019 Union Plugin Template targeting vc141_xp toolset (the last supported by Windows XP, should be installed via Visual Studio installer) and C++17 Language Standard.
Some utility libraries are also included.
The players need Visual C++ 2015 Redistributable package to be installed.
Getting Started:
1. Install Project Template from Resource Manager
2. (Re)open Visual Studio
3. Create New Project (you can create new Solution or add the new project to existing) using template C++17 Union Plugin
5. Open project properties and set correct output directory for G2A Debug configuration
By default the project targets the following paths:
D:\Gothic\g1\system\
D:\Gothic\g1a\system\
D:\Gothic\g2\system\
D:\Gothic\g2a\system\
You may either change it in project properties or just create Gothic installations in concordant folder(s)
6. You may open file Workspace\Assembly\Assembly.cpp (treat it as single .cpp file being compiled, every code you write just textually included here) in order IntelliSense is able to higlight all the code
7. Right click on Entry filter and choose Add New Item and C++17 Union Header file (.h)
8. Type the name of the new file and if you care about file structure consistency add \Workspace\Entry\ to the Location. If you dont care - dont care
C++:namespace NAMESPACE { Sub helloWorld(ZSUB(GameEvent::Execute), []() { Message::Info(A"Hello from " + ZENDEF("G1", "G1A", "G2", "G2A") + "!"); }); }
Utilities descriptionКод:ZENDEF(g1, g1a, g2, g2a) ZENDEF2(g1, g2) // choose the value according to logical engine (current value of ENGINE macros) // the result isn't embraced by parentheses: ZENDEF2(1+3,1+3)*ZENDEF2(1+3,1+3) == 1+3*1+3 ZTEST(exp) // returns exp, if logical engine matches physical engine (version of Gothic which loaded the plugin) // else returns default value of same type (0 for numbers, "" for strings and etc.) ZENFOR(g1, g1a, g2, g2a) // returns the value from list if logical and physical engines match each other // else returns default value of same type LOG(exp) // prints expression and its value to console // LOG(AHEX32(player)); -> "AHEX32(player): 0x12345678" LOGS(exp) // prints expression and its value to screen // should be called from game loop // Example: // int y = 3000; // first expression vertical position // LOGS(AHEX32(player)); // LOGS(player->refCtr * 2); ASSERT(exp) // Shows the informative message box if (exp) != true COA(a, b, c, d) // from 1 to 6 arguments supported // returns value a->b->c->d, if ((a) && (a->b) && (a->b->c)) == true // else returns the default value of same type as (a->b->c->d) // zSTRING enemyOfMyFocus = COA(player, GetFocusNpc(), enemy, objectName); // if, for example, player->GetFocusNpc() == nullptr, empty zSTRING will be returned
An instance of class ActiveValue<T> stores a value of type T and allows you to monitor its changing
C++:namespace NAMESPACE { ActiveValue<string> message; // initial value is empty string // subscribe on message changing Sub listenMessage(ZSUB(GameEvent::Execute), []() { message.onChange += []() { Message::Info(A"The message has been changed: " + *message); }; }); // sets the message value Sub setMessage(ZSUB(GameEvent::Execute), []() { message = "Hello!"; // message box will be shown message = "Hello!"; // no effect message = "Hi!"; // message box will be shown again }); }
An instance of class ActiveOption<T> provides binding between plugin and a single option from Gothic.ini.
Like ActiveValue<T> has event onChange, which fires when the option changed or when the option loaded.
You should create instances of the class in file Workspace/Assembly/Options.h by ZOPTION macros
C++:// Workspace/Assembly/Options.h namespace NAMESPACE { namespace Options { // define option in Gothic.ini // [ZPLUGINNAME] // SomeFloatValue = 777.6 ZOPTION(SomeFloatValue, 777.6f); // Here the value is still 0.0f // The value 777.6f or any other from Gothic.ini will be set later // (just after the event DefineExternals) } namespace Options { Sub load(ZSUB(GameEvent::DefineExternals), []() { // load all plugin options ActiveOptionBase::LoadAll(); }); } }
Class Sub<T> is designed for working with standard game events, which are enumerated in type GameEvents.
Besides standard events, there are 2 special:
1. GameEvent::Execute - if you pass this value to Sub<T>, then the related code is executed once, when the variable is created. Thus, if an instance of class Sub<T> is global, then the code is executed while plugin is loading:
C++:namespace NAMESPACE { int x = 5; Sub/*<void>*/ setX(ZSUB(GameEvent::Execute), []() { x = 99; }); int y = x; Sub showY(ZSUB(GameEvent::Execute), []() { Message::Info(A"y = " + y); // shows 'y = 99' }); }
C++:Sub<void> Gothic_I_Classic::setX; Sub<void> Gothic_I_Addon::setX; Sub<void> Gothic_II_Classic::setX; Sub<void> Gothic_II_Addon::setX;
You can bind Sub to ActiveOption<T> or to ActiveValue<T>. In this case the related code could be executed only if binded variable is convertible to true:
C++:namespace NAMESPACE { ActiveValue<bool> debug = false; // Sub<ActiveValue<bool>> Sub printDebugInfo(ZSUB(GameEvent::Loop), debug, []() { int y = 2000; LOGS(AHEX32(player->focus_vob)); // showed while static_cast<bool>(debug) == true }); // Sub<void> Sub setDebug(ZSUB(GameEvent::Loop), []() { if (zinput->GetMouseButtonPressedRight()) debug = true; // enable debugging else debug = false; // disable debugging }); }
Class Hook<Func, Option> provides the same functionality as CInvoke / ModulePatchCallInvoker. Additionaly, it could be binded to ActiveValue<T> and ActiveOption<T>.
C++:// Workspace/Assembly/Options.h namespace NAMESPACE { namespace Options { // [ZPLUGINNAME] // LogDamage = 1 ZOPTION(LogDamage, true); } namespace Options { Sub load(ZSUB(GameEvent::DefineExternals), []() { ActiveOptionBase::LoadAll(); }); } } // Workspace/Entry/LogDamage.h namespace NAMESPACE { void __fastcall Hook_oCNpc_OnDamage_Hit(oCNpc*, void*, oCNpc::oSDamageDescriptor&); Hook<void(__thiscall*)(oCNpc*, oCNpc::oSDamageDescriptor&), ActiveOption<bool>> Ivk_oCNpc_OnDamage_Hit(ZENFOR(0x00731410, 0x007700A0, 0x0077D390, 0x00666610), &Hook_oCNpc_OnDamage_Hit, HookMode::Patch, Options::LogDamage); void __fastcall Hook_oCNpc_OnDamage_Hit(oCNpc* _this, void* vtable, oCNpc::oSDamageDescriptor& a0) { int oldHitpoints = _this->attribute[NPC_ATR_HITPOINTS]; Ivk_oCNpc_OnDamage_Hit(_this, a0); int damage = oldHitpoints - _this->attribute[NPC_ATR_HITPOINTS]; if (damage != 0) cmd << _this->name[0] << " was damaged for " << damage << " points" << endl; } }
If the option is set to zero by the engine (using menu or another plugin), then the hook is deactivated.
Another way to bind the hook to the option:
C++:// oCNpc.inl // Supported with union (c) 2020 Union team // User API for oCNpc // Add your methods here void OnDamage_Hit_Union(oCNpc::oSDamageDescriptor&); // Workspace/Entry/LogDamage.h namespace NAMESPACE { FASTHOOK_PATCH_OPT(oCNpc, OnDamage_Hit, Options::LogDamage); void oCNpc::OnDamage_Hit_Union(oCNpc::oSDamageDescriptor& a0) { int oldHitpoints = attribute[NPC_ATR_HITPOINTS]; THISCALL(Hook_oCNpc_OnDamage_Hit)(a0); int damage = oldHitpoints - attribute[NPC_ATR_HITPOINTS]; if (damage != 0) cmd << name[0] << << " was damaged for " << damage << " points" << endl; } }
An instance of class VarScope<T> stores a value of type T and restores it in the destructor. Usually created via AssignTemp call:
C++:namespace NAMESPACE { void __fastcall Hook_oCGame_UpdatePlayerStatus(oCGame*, void*); Hook<void(__thiscall*)(oCGame*)> Ivk_oCGame_UpdatePlayerStatus(ZENFOR(0x00638F90, 0x0065F4E0, 0x00666640, 0x006C3140), &Hook_oCGame_UpdatePlayerStatus, HookMode::Patch); void __fastcall Hook_oCGame_UpdatePlayerStatus(oCGame* _this, void* vtable) { // get player's focus item oCItem* focusItem = COA(player, focus_vob, CastTo<oCItem>()); // create inactive object VarScope<zSTRING> scope; // if focus item is quest item, then temporary change its focus name if (focusItem && focusItem->HasFlag(ITM_FLAG_MI)) scope = AssignTemp(focusItem->name, Z"Unknown quest item"); return Ivk_oCGame_UpdatePlayerStatus(_this); //if we changed the name of the item, its original value is restored here } }
Class Unlocked<T> is designed for typesafe engine code patching. Example of activating/deactivating player turning to enemy during jumps back:
C++:// Workspace/Assembly/JumpBackNoTurn.h // meaningful for G2 & G2A only #if ENGINE >= Engine_G2 namespace NAMESPACE { // new definition of oCNpc::TurnToEnemy, which doesn't turn player to enemy if he is in jumback state void __fastcall oCNpc_TurnToEnemy_Hook(oCNpc* _this, void* vtable) { // do additional logic for player only if (_this != player) return _this->TurnToEnemy(); oCAniCtrl_Human* aniCtrl = _this->GetAnictrl(); // just safety check if (!aniCtrl) return _this->TurnToEnemy(); // get current active animation zCModelAniActive* hitAni = COA(_this, GetModel(), GetActiveAni(aniCtrl->hitAniID)); zCModelAni* protoAni = COA(hitAni, protoAni); // if there is no active animation or it is not jumpback then call original function if (!protoAni || !protoAni->aniName.HasWord("JUMPB")) return _this->TurnToEnemy(); // else do nothing (no turn to enemy) } // listen Gothic.ini option changing Sub listenJumpBackNoTurn(ZSUB(GameEvent::Execute), []() { Options::JumpBackNoTurn.onChange += []() { // executed when option JumpBackNoTurn is loaded or changed // here we will store original engine code static std::optional<int> originalValue{}; // if option disabled from the begining then dont touch the engine if (!Options::JumpBackNoTurn && !originalValue.has_value()) return; // address inside oCAIHuman::DoAI, here relative addres of oCNpc::TurnToEnemy is written int address = ZENDEF(0x00000000, 0x00000000, 0x0063FA6A, 0x0069C23A); // unlock 4 bytes as int value Unlocked<int> relativeAddress = address; // if original code isn't stored (first time patching), then store it if (!originalValue.has_value()) originalValue = relativeAddress; // if option is active change call of oCNpc::TurnToEnemy to our hook function if (Options::JumpBackNoTurn) // relative adress is computed against the end of the instruction // so, substract it from address of our function relativeAddress = reinterpret_cast<int>(&oCNpc_TurnToEnemy_Hook) - (address + 4); // else restore the original code else relativeAddress = originalValue.value(); }; }); } #endif
C++:Unlocked<std::array<byte, 5>> left = ZENDEF(0x00614AC3, 0x006378D6, 0x0063E4E7, 0x0069AD37); Unlocked<std::array<byte, 5>> right = ZENDEF(0x00614B4F, 0x00637967, 0x0063E557, 0x0069ADA7); left = { 0x31, 0xC0, 0x90, 0x90, 0x90 }; // xor eax, eax right = { 0x31, 0xC0, 0x90, 0x90, 0x90 };
Class Symbol is wrapper for zCPar_Symbol. It stores pointers to zCPar_Symbol, to zСParser (in which zCPar_Symbol is stored), and symbol index. The main purpose of the class is symbol's logical type determinition:
C++:enum class Type { Unknown, // not found ClassVar, // class member (ex., C_NPC.NAME) Class, // class (ex., class C_SomeClass {};) ExternalVar, // not used ExternalFunc, // external function (ex., Npc_HasItems) Instance, // instance with code (ex., instance SomeNpc(C_NPC){};) Prototype, // prototype (ex., prototype SomeProtoNpc(C_NPC){};) Func, // function with code (ex., func void SomeFunc() {};) VarInstance, // class instance (ex., var C_NPC npc;) VarString, // string variable or constant (ex., сonst string names[2] = { "Aba", "caba"};) VarFloat, // float variable or constant (ex., const float x = 5.0;) VarInt // variable or constant of type func (ex., var func f;) };
C++:namespace NAMESPACE { void ShowBurningTorchSymbol() { Symbol symbol{ parser, "ItLsTorchBurning" }; ASSERT(symbol.GetType() == Symbol::Type::Instance); LOG(symbol.GetIndex()); LOG(symbol.GetSymbol()->name); int scriptCodeAddress = symbol.GetValue<int>(0); LOG(scriptCodeAddress); } Sub showTorch(ZSUB(GameEvent::Init), &ShowBurningTorchSymbol); // Possible output: // symbol.GetIndex(): 7430 // symbol.GetSymbol()->name: ITLSTORCHBURNING // scriptCodeAddress : 208559 }
For external function definition several utility macroses are used. Lets define external function, which removes from inventory all items of particular category and returns total amount of deleted items:
C++:namespace NAMESPACE { // func int Npc_ClearCategory(var C_NPC npc, var int category, var int ignoreEquipped); int __cdecl Npc_ClearCategory() { // save current parser state: // - current instance (used inside script instance code) // - variables HERO, SELF, OTHER, VICTIM, ITEM (if we are in the main parser) ParserScope scope{ zCParser::GetParser() }; oCNpc* npc; int category; bool ignoreEquipped; // pop the arguments ZARGS(npc, category, ignoreEquipped); // if NPC is invalid or is in fight mode show message box with error ASSERT(npc); ASSERT(npc->GetWeaponMode() == NPC_WEAPON_NONE); std::vector<oCItem*> removeList; int removeAmount = 0; // traverse the inventory for (oCItem* item : npc->inventory2) { // if category doesn't match then skip the item if (item->mainflag != category) continue; // if equiped item ignoring enabled and item is equiped // then skip the item if (ignoreEquipped && item->HasFlag(ITM_FLAG_ACTIVE)) continue; // else add item to the remove list removeList += item; removeAmount += item->amount; }; // remove items for (oCItem* item : removeList) { oCItem* removed = npc->RemoveFromInv(item, item->amount); // removed from inventory item is logically added to the world // so, remove it from there ogame->GetGameWorld()->RemoveVob(removed); } // return the removed amount ZRETURN(removeAmount); return false; // ParserScope restores the state of parser here } // register the function in the main parser // ZEXTERNAL(function_name, return_type, argument_types...) ZEXTERNAL(Npc_ClearCategory, int, oCNpc*, int, bool); }
1. zCPar_Symbol* and Symbol - pass instance via symbol
2. all pointers besides zCPar_Symbol* - pass instance via address
3. int and bool - pass integer
4. float - pass real value
5. string, zSTRING - pass string value
6. unsigned - pass function
In order to call some parser function from the plugin use CallParser function. You shouldn't use it for external functions calling, so, the following code is for demonstration purposes:
C++:void CallSomeFunc() { // call by name string result = CallParser<string>(parser, "ConcatStrings", Z"Got", "hic"); // you are allowed to pass raw strings here ASSERT(result == "Gothic"); // call by function index int amount = CallParser<int>(parser, parser->GetIndex("Npc_HasItems"), player, parser->GetIndex("ItLsTorch")); }
An instance of class KeyCombo represents a set of physical key combinations. Can be used with ZOPTION. Hook binding to a such option is also possible (hook is active while KeyCombo is not empty).
C++:namespace NAMESPACE { namespace Options { namespace Helpers { KeyCombo CreateComplexCombo() { std::vector<std::vector<int>> combos; combos += { KEY_LSHIFT, KEY_RSHIFT }; combos += { KEY_LCONTROL, MOUSE_BUTTONRIGHT }; combos += { KEY_RCONTROL, MOUSE_BUTTONLEFT }; return combos; } } // [ZPLUGINNAME] // SimpleKey=KEY_U ZOPTION(SimpleKey, KeyCombo({ KEY_U })); // [ZPLUGINNAME] // HyperKey=KEY_LSHIFT + KEY_RSHIFT, KEY_LCONTROL + MOUSE_BUTTONRIGHT, KEY_RCONTROL + MOUSE_BUTTONLEFT ZOPTION(HyperKey, Helpers::CreateComplexCombo()); // [ZPLUGINNAME] // NoKey=# ZOPTION(NoKey, KeyCombo{}); } namespace Options { Sub load(ZSUB(GameEvent::DefineExternals), []() { ActiveOptionBase::LoadAll(); }); } }
Objects you want to be persisted in savegame should be inherited from class SaveData. As example we implement a string hash-table that could be accessed from the scripts:
C++:namespace NAMESPACE { // public inheritance from SaveData class HashTable : public SaveData { private: // data to persist: string hash-table std::unordered_map<string, string> table; public: #pragma region Required methods HashTable(const string& name) : SaveData{ name } // just call base class constructor { } // clearing the data // automatically invoked before loading virtual void Clear() override { // clear the hash-table table.clear(); } // invoked from SaveData::Save virtual void Archive(zCArchiver& arc) override { // write number of entries arc.WriteInt("Size", table.size()); // write each entry (Key, Value) for (const auto& pair : table) { arc.WriteString("Key", pair.first); arc.WriteString("Value", pair.second); } } // invoked from SaveData::Load (if file name.SAV exists) after method Clear virtual void Unarchive(zCArchiver& arc) override { // read the data in same format we wrote it int size = arc.ReadInt("Size"); for (int i = 0; i < size; i++) { string key = arc.ReadString("Key"); string value = arc.ReadString("Value"); table[key] = value; } } #pragma endregion // provide access to hash-table string& operator[](const string& key) { return table[key]; } }; // convinience method HashTable& GetHashTable() { // create and store the object on first use // file with stored data will be named "HASHTABLE.SAV" static HashTable& hashTable = SaveData::Get<HashTable>("HashTable"); return hashTable; } Sub saveData(ZSUB(GameEvent::SaveBegin), []() { // no need to save/load data on world changing: just leave it in RAM if (!SaveLoadGameInfo.changeLevel) GetHashTable().Save(GameEvent::SaveBegin); }); Sub loadData(ZSUB(GameEvent::LoadEnd), []() { if (!SaveLoadGameInfo.changeLevel) GetHashTable().Load(GameEvent::LoadEnd); }); // define external functions for accessing the hash-table from the scripts // func string Hlp_GetString(var string key); // func void Hlp_SetString(var string key, var string value); int __cdecl Hlp_GetString() { string key; ZARGS(key); ZRETURN(GetHashTable()[key]); return false; } int __cdecl Hlp_SetString() { string key, value; ZARGS(key, value); GetHashTable()[key] = value; return false; } ZEXTERNAL(Hlp_GetString, string, string); ZEXTERNAL(Hlp_SetString, void, string, string); }
Последнее редактирование: