• Уважаемые гости и новички, приветствуем Вас на нашем форуме
    Здесь вы можете найти ответы практически на все свои вопросы о серии игр «Готика» (в том числе различных модах на нее), «Ведьмак», «Ризен», «Древние свитки», «Эра дракона» и о многих других играх. Можете также узнать свежие новости о разработке новых проектов, восхититься творчеством наших форумчан, либо самим показать, что вы умеете. Ну и наконец, можете обсудить общие увлечения или просто весело пообщаться с посетителями «Таверны».

    Чтобы получить возможность писать на форуме, оставьте сообщение в этой теме.
    Удачи!

Инструкция C++17 Plugin Template

Slavemaster


Модостроитель
Регистрация
10 Июн 2019
Сообщения
1.081
Благодарности
1.901
Баллы
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
    1616656899937.png
    4. Установить активной G2A Debug конфигурацию
    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. Если всё равно - не добавляйте
    1616658016051.png
    9. Напишите следующий текст в новом файле:
    C++:
    namespace NAMESPACE
    {
        Sub helloWorld(ZSUB(GameEvent::Execute), []()
            {
                Message::Info(A"Hello from " + ZENDEF("G1", "G1A", "G2", "G2A") + "!");
            });
    }
    10. Скомпилируйте проект и нажмите Ctrl+F5: zPluginName.patch (он просто загружает вашу dll) и zPluginName.dll будут созданы в выходной директории и Gothic2.exe будет запущен. Во время загрузки плагина должно появиться сообщение:
    1616658583643.png
    Описание утилит
    Код:
    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'
            });
    }
    2. GameEvent::NoEvent деактивирует объект. Используется макросом ZSUB чтобы выполнялся код предназначенный для правильного движка (ZSUB - почти то же самое, что и ZTEST, только используется исключительно для GameEvent). Нужно понимать, что, например, код представленный выше при мультиплатформенной компиляции порождает 4 объекта setX:
    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;
    Соответственно, три из них должны быть неактивны. Для этого и нужны макросы ZSUB и ZTEST (макросы IvkEnabled и ZENFOR также выполняют подобную функцию).

    Также можно привязать 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;
        }
    }
    Хук будет подключен при загрузке опции (если её не было в Gothic.ini или её значение было не нулевым).
    Если опция изменится на 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
    Для разблокировки кода как массива байт фиксированной длины используйте Unlocked<std::array<byte, количество_байт>> (понадобится #include <array> в самом начале файла). Фрагмент кода для отключения стрейфа вокруг фокуса:
    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();
                });
        }
    }
    Используется очевидным образом: Options::HyperKey->GetPressed() или Options::NoKey->GetToggled().
    Класс 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 Started
    Description:
    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
    1616656899937.png
    4. Set
    G2A Debug Solution Configuration
    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
    1616658016051.png
    9. Set the following file content:
    C++:
    namespace NAMESPACE
    {
        Sub helloWorld(ZSUB(GameEvent::Execute), []()
            {
                Message::Info(A"Hello from " + ZENDEF("G1", "G1A", "G2", "G2A") + "!");
            });
    }
    10. Build the project and press Ctrl+F5: zPluginName.patch (it just loads your dll) and zPluginName.dll will be created in the output directory and Gothic2.exe will be executed. Message Box should appear during the plugin loading:
    1616658583643.png
    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'
            });
    }
    2. GameEvent::NoEvent deactivates object. Used by ZSUB in order to execute code for the right engine (ZSUB is very similat to ZTEST, but is used only with GameEvent). You need to understand that, for example, the code above during multiplatform compiling creates 4 objects setX:
    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;
    So, three of them must be inactive. For such purposes macroses ZSUB и ZTEST are created (macroses IvkEnabled and ZENFOR serve the similar function).

    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;
        }
    }
    The hook becomes active when the option is loaded (if it was not exist in Gothic.ini or had non-zero value).
    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
    For unlocking memory as byte array with fixed size use Unlocked<std::array<byte, size>> (you need #include <array> in the very begining of the file). Code fragment that disables strafe around focus:
    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;)
    };
    Usage example:
    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);
    }
    Macroses are aware about the following types:
    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();
                });
        }
    }
    Usage: Options::HyperKey->GetPressed() or Options::NoKey->GetToggled().
    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);
    }
 
Последнее редактирование:

MEG@VOLT

★★★★★★★★★
ТехАдмин
Регистрация
24 Мар 2006
Сообщения
9.860
Благодарности
6.740
Баллы
1.625

MEG@VOLT

★★★★★★★★★
ТехАдмин
Регистрация
24 Мар 2006
Сообщения
9.860
Благодарности
6.740
Баллы
1.625
Вот честно. В лом вообще это читать!
 

MW 7


Модостроитель
Регистрация
26 Мар 2004
Сообщения
2.001
Благодарности
971
Баллы
295
что то пытаюсь установить , а не получается :)
1698776663109.png
 

Slavemaster


Модостроитель
Регистрация
10 Июн 2019
Сообщения
1.081
Благодарности
1.901
Баллы
320
MW 7, не знаю, я не могу там больше хозяйничать. Попробуй оффлайн установщик.
 

Вложения

  • Cpp17_ProjectTemplate.exe.zip
    10,5 MB · Просмотры: 13

MW 7


Модостроитель
Регистрация
26 Мар 2004
Сообщения
2.001
Благодарности
971
Баллы
295
Slavemaster, подскажи пожалуйста а могу ли я поменять название пространства на другое?
C++:
namespace NAMESPACE
{
 

Slavemaster


Модостроитель
Регистрация
10 Июн 2019
Сообщения
1.081
Благодарности
1.901
Баллы
320
Сверху Снизу