- 20.06.2022
- 23 845
- 218
- 36
- Награды
- 10
- Пол
- Муж.
Репутация:
- Автор темы
- Администратор
- Модератор
- Команда форума
- #1
Итак, наша задача состоит в том, чтобы поменять всем деревьям модель. Для начала нужно найти список всех деревьев, и это не настолько очевидно. В 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/
Иногда также необходимо выставить масштаб, чтобы видимый размер модельки соответствовал реальному(иначе известные всем пеньки будут микроскопическими). Для этого ищем метод CGameSceneNode::SetLocalScale через удобный конвар
и видим, казалось бы, монструозную функцию:
Код:
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:
Прекрасно. Но это только начало! Ведь ваш $упер-$офт должен также уметь вернуть оригинальные деревья прямо в игре(и не пугайтесь из-за того, что современные p2c этого не могут, это не наша забота). Итак, приступим.
Для начала нужно сохранить оригинальную модель дерева. Гетнуть это не составляет труда, заходим в ReClass и внимательно наблюдаем
оффсет m_hModel у CModelState. Это CStrongHandle, имя модели у которого находится в двух местах. Также можете напрямую взять по нетвару m_ModelName, вариаций масса! Оформляем нашему C_BaseEntity геттер:
C++:
const char* GetModelName() {
return *GetGameSceneNode()->Member<NormalClass*>(0x200)->Member<const char**>(8);
}
Сохраняем в структуру на выбор, делаем функцию восстановления через тот же SetModel, нажимаем в своём списке деревьев Default и... куда они пропали? Деревья резко стали невидимыми, но не теряем надежду. Заходим в VRF и смотрим на любую дефолтную модельку дерева(я взял пальму с одного из ландшафтов):
Тоже ничего. Заметили новый пункт "Mesh Groups" в меню слева? Нажмите на пустые чекбоксы и увидите дерево. Меш-группы это как бы "submodels", то есть части одной модельки. Когда вы съедаете дерево, оно не исчезает, оно становится пеньком. Делать отдельную модельку на ствол и пень было бы крайне затратно, поэтому Valve сделали механизм проще — переключаешь меш-группу, и вот у дерева исчез ствол. Теперь изучим интересующие нас пункты:
- GetMeshGroupMask — нетвары CSkeletonInstance::m_modelState и CModelState::m_MeshGroupMask(первый хранится прямо в объекте, не поинтером)
- SetMeshGroupMask — может показаться, что без сигнатуры не обойтись, но у CModelState есть интересный коллбек "skeletonMeshGroupMaskChanged", следовательно анализируем его параметры в IDA (осторожно, improper итерация!)
Сохраняем в новую структуру, делаем рестор на основе флага(как и 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 на другую модель
Фулл код .
Последние темы в этом разделе:
- База знаний по Dota 2 Tools
- Самые популярные герои на рейтинге 8000+ в патче 7.36c
- Гайд по подбору аккаунта Dota 2 - Как избежать смурф бана в Dota 2
- Как играть на рекрутах на новых аккаунтах в Dota 2
- Оптимизируем DOTA 2 для слабых ПК
- Замена курьеров в чите Hake.me
- Делаем свой AHK скрипт для авто принятия игры Dota 2
- Как собрать vpk файлы самому
- Пак звуков для Dota 2 [Soundpad][Оск. Рамзеса Алохи и Санекинга][Папич]
- Самые необычные настройки в Dota 2 у про-игроков