Гайд Меняем деревья как перчатки

Ревёрсер среднего звена
Пользователь
Статус
Оффлайн
Регистрация
24 Ноя 2022
Сообщения
303
Реакции[?]
107
Поинты[?]
56K
tree changer.png

Хотели супер-ультра-пропер tree changer? Нет? Да я знаю, что хотели

Итак, наша задача состоит в том, чтобы поменять всем деревьям модель. Для начала нужно найти список всех деревьев, и это не настолько очевидно. В EntitySystem такого нет, а поинтеры на 0x110 и далее не являются стабильным решением. Поглядим на класс дерева в Схеме:

Код:
C_DOTA_MapTree : C_DOTA_BinaryObject
    bool m_bInitialized 0x808;
Ага, BinaryObject. В доте есть статическая геймсистема CDOTA_BinaryObjectSystem, в которой, ожидаемо, на 0x18 лежит CUtlVector<C_DOTA_MapTree*>. Этот список работает таким же образом, как и entity list, поэтому там будут и nullptrы. Отлично, деревья наши.

Теперь нужно изменить им модель и масштаб. Для первого нам нужен SetModel.
Пожалуйста, авторизуйтесь для просмотра ссылки.
, но не спешим брать по сигнатуре, можно и без неё:

C++:
// found by Morphling
setMdl = Address(
    tree->GetVFunc(7).ptr // tree = C_DOTA_MapTree
).Offset(0x1c0).GetAbsoluteAddress(1);
Помимо this этот метод принимает путь к VPK-файлу модели(например "models/props_tree/ti7/ggbranch.vmdl"). Вызывать его необходимо в главном потоке доты, иначе будете ловить регулярные краши. Мой совет: вызывайте проверку на свой bool-флаг в чём-то типа RunFrame/FrameStageNotify, сам флаг сможете ставить где угодно. Сами модельки ищем в VPK-папке models/props_tree/

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

Код:
if ( a2 != *(float *)(this + 0xC4) )
  {
    isHierarchyTypeZero = *(_BYTE *)(this + 0xEC) == 0;
    v4 = 4;
    *(float *)(this + 0xC4) = a2;
    if ( isHierarchyTypeZero )
    {
       *куча mov'ов*
    }
    ownerIdentity = *(_QWORD *)(*(_QWORD *)(this + 0x30) + 16i64);
    if ( (*(_DWORD *)(ownerIdentity + 48) & 0x800) != 0 )
    {
      v11 = (*(_DWORD *)(ownerIdentity + 48) & 0x40) != 0;
    }
    else
    {
      v12 = 0x7FFF;
      if ( ownerIdentity )
      {
        v13 = *(_DWORD *)(ownerIdentity + 16);
        if ( v13 != -1 )
          v12 = v13 & 0x7FFF;
      }
      v11 = v12 < 0x4000;
    }
    if ( !v11 )
    {
      m_pOwner = *(_QWORD *)(this + 0x30);
      globals = (float *)g_pGlobalVars;
      if ( !*((_BYTE *)g_pGlobalVars + 61) && !*((_BYTE *)g_pGlobalVars + 60) )
      {
        v16 = (void (__fastcall *)(__int64))*((_QWORD *)g_pGlobalVars + 4);
        if ( v16 )
        {
          v16(1i64);
          m_pOwner = *(_QWORD *)(this + 48);
        }
      }
      sub_180235560(m_pOwner, globals[11]);
    }
    (*(void (__fastcall **)(__int64, _QWORD))(*(_QWORD *)this + 80i64))(this, v4);
Но не спешите брать её сигнатуру! На самом деле, единственное, что нам нужно — это первая и последняя строчки. Проанализировав функцию динамически, увидите, что первый и последний if у деревьев не выполняется. К тому же конструкция последнего является некой inline-функцией, сообщающей игре о проблемах с синхронизацией, такую же можете увидеть в GetGameTime. Вот и остаётся условие на изменённый нетвар CGameSceneNode::m_flScale и вызов его виртуальной функции. В итоге код будет примерно такой:

C++:
struct TreeModelInfo {
    const char* modelName;
    float scale;
};

// Вызываем где-то у себя
void Modules::M_TreeChanger::QueueModelUpdate(TreeModelInfo mdlInfo) {
    queuedModel = mdlInfo;
    needsUpdate = true; // тот самый bool-флаг
}

void Modules::M_TreeChanger::SetTreeModel(CBaseEntity* tree, const TreeModelInfo& mdl) {
    static Function setMdl = nullptr;

    if (!setMdl.ptr)
        setMdl = Address(tree->GetVFunc(7).ptr).Offset(0x1c0).GetAbsoluteAddress(1);

    setMdl(tree, mdl.modelName);
    if (tree->ModelScale() != mdl.scale) {
        tree->ModelScale() = mdl.scale;
        tree->Member<VClass*>(Netvars::C_BaseEntity::m_pGameSceneNode)->CallVFunc<10>(4);
    }
}

// Вызываем в RunFrame/FrameStageNotify
void Modules::M_TreeChanger::UpdateTreeModels() {
    if (!needsUpdate)
        return;

    if (needsUpdate) {
        auto trees = GameSystems::BinaryObjectSystem->GetTrees();
        for (auto tree : trees) {
            if (!tree)
                continue;
     
            SetTreeModel(tree, queuedModel);
        }
    }

    needsUpdate = false;
}
Поставим-ка models/props_tree/frostivus_tree.vmdl с масштабом 0.85:

Screenshot_1343.png

Прекрасно. Но это только начало! Ведь ваш $упер-$офт должен также уметь вернуть оригинальные деревья прямо в игре(и не пугайтесь из-за того, что современные p2c этого не могут, это не наша забота). Итак, приступим.
Для начала нужно сохранить оригинальную модель дерева. Гетнуть это не составляет труда,
Пожалуйста, авторизуйтесь для просмотра ссылки.
оффсет m_hModel у CModelState. Это CStrongHandle, имя модели у которого находится в двух местах. Также можете напрямую взять по нетвару m_ModelName, вариаций масса! Оформляем нашему C_BaseEntity геттер:

C++:
const char* GetModelName() {
    return *GetGameSceneNode()->Member<NormalClass*>(0x200)->Member<const char**>(8);
}
Сохраняем в структуру на выбор, делаем функцию восстановления через тот же SetModel, нажимаем в своём списке деревьев Default и... куда они пропали? Деревья резко стали невидимыми, но не теряем надежду. Заходим в VRF и смотрим на любую дефолтную модельку дерева(я взял пальму с одного из ландшафтов):

Screenshot_1345.png

Тоже ничего. Заметили новый пункт "Mesh Groups" в меню слева? Нажмите на пустые чекбоксы и увидите дерево. Меш-группы это как бы "submodels", то есть части одной модельки. Когда вы съедаете дерево, оно не исчезает, оно становится пеньком. Делать отдельную модельку на ствол и пень было бы крайне затратно, поэтому Valve сделали механизм проще — переключаешь меш-группу, и вот у дерева исчез ствол. Теперь изучим интересующие нас пункты:
  • GetMeshGroupMask — нетвары CSkeletonInstance::m_modelState и CModelState::m_MeshGroupMask(первый хранится прямо в объекте, не поинтером)
  • SetMeshGroupMask — может показаться, что без сигнатуры не обойтись, но у CModelState есть интересный коллбек "skeletonMeshGroupMaskChanged", следовательно анализируем его параметры в IDA
    Пожалуйста, авторизуйтесь для просмотра ссылки.
    (осторожно, improper итерация!)
Меш-группы ставятся uint64_t-"масками".

Сохраняем в новую структуру, делаем рестор на основе флага(как и Update), пихаем туда SetMeshGroupMask. Получаем следующее:

C++:
struct TreeModelInfo {
    const char* modelName;
    float scale;
};
struct SavedModelInfo : public TreeModelInfo{
    uint64_t meshGroupMask;
};

std::map<CBaseEntity*, SavedModelInfo> originalTrees;

TreeModelInfo queuedModel;

void QueueModelUpdate(TreeModelInfo mdlInfo) {
    queuedModel = mdlInfo;
    needsUpdate = true;
}
void QueueModelRestore() {
    needsRestore = true;
}

void Modules::M_TreeChanger::SetTreeModel(CBaseEntity* tree, const TreeModelInfo& mdl) {
    static Function setMdl = nullptr;

    if (!setMdl.ptr)
        setMdl = Address(tree->GetVFunc(7).ptr).Offset(0x1c0).GetAbsoluteAddress(1);

    setMdl(tree, mdl.modelName);
    if (tree->ModelScale() != mdl.scale) {
        tree->ModelScale() = mdl.scale;
        tree->Member<VClass*>(Netvars::C_BaseEntity::m_pGameSceneNode)->CallVFunc<10>(4);
    }
}

void Modules::M_TreeChanger::RestoreTreeModels() {
static void(*skeletonMeshGroupMaskChanged)(CBaseEntity::CModelState * mdl, CBaseEntity * owner, uint64_t * mask) = nullptr;

    if (!skeletonMeshGroupMaskChanged)
        for (const auto& data : Interfaces::NetworkMessages->GetNetvarCallbacks())
            if (IsValidReadPtr(data.m_szCallbackName) && std::string_view(data.m_szCallbackName) == "skeletonMeshGroupMaskChanged") {
                skeletonMeshGroupMaskChanged = (decltype(skeletonMeshGroupMaskChanged))data.m_CallbackFn;
                break;
            }

    auto trees = GameSystems::BinaryObjectSystem->GetTrees();

    for (auto& [tree, mdlInfo] : originalTrees) {
        if (!IsValidReadPtr(tree))
            continue;

        SetTreeModel(tree, mdlInfo);
        skeletonMeshGroupMaskChanged(tree->GetGameSceneNode()->GetModelState(), tree, &mdlInfo.meshGroupMask);
        tree->SetColor({ 255,255,255,255 });
    }

    originalTrees.clear();
}

void Modules::M_TreeChanger::UpdateTreeModels() {
    if (!needsUpdate && !needsRestore)
        return;

    if (needsUpdate) {
        auto trees = GameSystems::BinaryObjectSystem->GetTrees();
        bool shouldSaveOriginalTrees = originalTrees.empty();
        for (auto tree : trees) {
            if (!tree)
                continue;

            if (shouldSaveOriginalTrees)
                originalTrees[tree] = { tree->GetModelName(), tree->ModelScale(), tree->GetGameSceneNode()->GetModelState()->GetMeshGroupMask() };

            SetTreeModel(tree, queuedModel);
        }
    }
    else if (needsRestore)
        RestoreTreeModels();

    needsUpdate = false;
    needsRestore = false;
}
И вот теперь при вызове RestoreTreeModels(в мейн треде) вы увидите родные деревья. Поздравляю, Tree Changer готов!

BONUS:
Короткий, но ёмкий датасет моделей и скейлов от [ДАННЫЕ УДАЛЕНЫ]:
C++:
inline TreeModelList[] = {
{ "models/props_tree/newbloom_tree.vmdl", 1.0f },
{ "models/props_tree/mango_tree.vmdl", 1.0f },
{ "maps/journey_assets/props/trees/journey_armandpine/journey_armandpine_02_stump.vmdl", 4.5f },
{ "models/props_tree/frostivus_tree.vmdl", 0.85f },
{ "models/props_tree/ti7/ggbranch.vmdl", 1.0f },
{"models/props_structures/crystal003_refract.vmdl", 1},
{"models/props_structures/pumpkin001.vmdl", 1.08},
{"models/props_structures/pumpkin003.vmdl", 3},
{"models/props_diretide/pumpkin_head.vmdl", 3},
{"models/props_gameplay/pumpkin_bucket.vmdl", 1},

};

inline const char* TreeNameList[] = {
"Default",
"New Bloom",
"Mango",
"Stumps",
"Frostivus",
"GG Branch",
"Crystal",
"Pumpkins #1",
"Pumpkins #2",
"Pumpkins #3",
"Pumpkin Buckets"
};
models/props_tree/ti7/ggbranch.vmdl у вас будет бледно-белая, и это нормально. В зависимости от стадии седьмого инта она была разного цвета, я её помню жёлтой, изначально она была зелёная. Фиксится это выставлением m_clrRender на желаемый цвет и вызовом OnColorChanged, как это делают, например, при покраске иллюзий. Не забывайте возвращать его к { 255, 255, 255, 255 }, если меняетесь с GG branch на другую модель

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

Благодарности:
Morphling за базовый код и изначальную мотивацию
og за подсказки
[ДАННЫЕ УДАЛЕНЫ] за полуполезный говнокод на тему деревьев, рот твой ебал
 
Последнее редактирование:
Ревёрсер среднего звена
Пользователь
Статус
Оффлайн
Регистрация
24 Ноя 2022
Сообщения
303
Реакции[?]
107
Поинты[?]
56K
Пользователь
Статус
Оффлайн
Регистрация
8 Апр 2022
Сообщения
647
Реакции[?]
102
Поинты[?]
65K
Я отдельный класс не делал под деревья, вот и уточнил на всякий
ес чо вот так просто сделал и все

C++:
class C_DOTA_MapTree : public C_BaseModelEntity {
public:
    void set_model( const std::string_view& model_name ) {
        const auto C_DOTA_MapTree__Spawn = util::vmt( (std::uintptr_t)this, 7 );
        some_function C_BaseModelEntity__SetModel = util::get_absolute_address( C_DOTA_MapTree__Spawn + 0x1c0, 1, 5 );
        if ( !C_BaseModelEntity__SetModel.valid( ) ) return;

        C_BaseModelEntity__SetModel( this, model_name.data( ) );
    }
};
 
Участник
Статус
Оффлайн
Регистрация
23 Май 2019
Сообщения
770
Реакции[?]
329
Поинты[?]
61K
ес чо вот так просто сделал и все

C++:
class C_DOTA_MapTree : public C_BaseModelEntity {
public:
    void set_model( const std::string_view& model_name ) {
        const auto C_DOTA_MapTree__Spawn = util::vmt( (std::uintptr_t)this, 7 );
        some_function C_BaseModelEntity__SetModel = util::get_absolute_address( C_DOTA_MapTree__Spawn + 0x1c0, 1, 5 );
        if ( !C_BaseModelEntity__SetModel.valid( ) ) return;

        C_BaseModelEntity__SetModel( this, model_name.data( ) );
    }
};
не советую вот так делать
C++:
if ( !C_BaseModelEntity__SetModel.valid( ) ) return;
это же по факту критическая ошибка(габен чота обновил, надо менять оффсет или индекс. ну конкретно в данном случае вряд ли эта проверка провалится(насколько я понимаю это просто нуллптр чек, и даже если габен чето поменяет там вместо 0 будет хуета какаято рандомная), но я думаю у тебя не только там такой код, и не только у тебя), а ты ее просто игнорируешь - как минимум логни чтобы потом не искать причину почему не меняются модельки. я сам иногда люблю "поглащать"(иногрить) ошибки/исключения когда лень их нормально обрабатывать, ни к чему хорошему это еще не приводило.
 
Ревёрсер среднего звена
Пользователь
Статус
Оффлайн
Регистрация
24 Ноя 2022
Сообщения
303
Реакции[?]
107
Поинты[?]
56K
Обновил гайд в свете новейших открытий
 
Сверху Снизу