Добро пожаловать на сайт - wlux.net!
FAQ по форуму
1. Все сообщения до группы местный проходят модерацию от 1 минуты до 24 часа
2. Сообщения учитываються в следующих разделах: Читать
3.Что-бы скачать вложение нужно 2 сообщения.
4.Личные переписки работают только с Администрацией форума
5. Запрещено: Просить скрытый текст , спам, реклама, скам, ддос, кардинг и другая чернуха, нарушать любые законы РФ/СНГ = бан аккаунта
6. Внимание! Мы не удаляем аккаунты с форума! Будьте внимательны ДО регистрации! Как удалить аккаунт на форуме?!
5.Не понимаю, как и что тут работает у вас?!Как создавать темы, писать сообщения, как получать реакции. Почему не засчитывает сообщения. Все ответы здесь
Репутация:
C_DOTA_MapTree : C_DOTA_BinaryObject
bool m_bInitialized 0x808;
// found by Morphling
setMdl = Address(
tree->GetVFunc(7).ptr // tree = C_DOTA_MapTree
).Offset(0x1c0).GetAbsoluteAddress(1);
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);
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;
}
const char* GetModelName() {
return *GetGameSceneNode()->Member<NormalClass*>(0x200)->Member<const char**>(8);
}
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;
}
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"
};
Репутация:
Отличный рабочий ченджер спасибо большоеПосмотреть вложение 9950
Итак, наша задача состоит в том, чтобы поменять всем деревьям модель. Для начала нужно найти список всех деревьев, и это не настолько очевидно. В EntitySystem такого нет, а поинтеры на 0x110 и далее не являются стабильным решением. Поглядим на класс дерева в Схеме:
Код:C_DOTA_MapTree : C_DOTA_BinaryObject bool m_bInitialized 0x808;
Ага, BinaryObject. В доте есть статическая геймсистема CDOTA_BinaryObjectSystem, в которой, ожидаемо, на 0x18 лежит CUtlVector<C_DOTA_MapTree*>. Этот список работает таким же образом, как и entity list, поэтому там будут и nullptrы. Отлично, деревья наши.
Теперь нужно изменить им модель и масштаб. Для первого нам нужен SetModel. Находим по хрефам,
Посмотреть вложение 9951
но не спешим брать по сигнатуре, можно и без неё:
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 через удобный конвар
Посмотреть вложение 9952
и видим, казалось бы, монструозную функцию:
Код: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:
Посмотреть вложение 9953
Прекрасно. Но это только начало! Ведь ваш $упер-$офт должен также уметь вернуть оригинальные деревья прямо в игре(и не пугайтесь из-за того, что современные p2c этого не могут, это не наша забота). Итак, приступим.
Для начала нужно сохранить оригинальную модель дерева. Гетнуть это не составляет труда, заходим в ReClass и внимательно наблюдаем
Посмотреть вложение 9954
оффсет m_hModel у CModelState. Это CStrongHandle, имя модели у которого находится в двух местах. Также можете напрямую взять по нетвару m_ModelName, вариаций масса! Оформляем нашему C_BaseEntity геттер:
C++:const char* GetModelName() { return *GetGameSceneNode()->Member<NormalClass*>(0x200)->Member<const char**>(8); }
Сохраняем в структуру на выбор, делаем функцию восстановления через тот же SetModel, нажимаем в своём списке деревьев Default и... куда они пропали? Деревья резко стали невидимыми, но не теряем надежду. Заходим в VRF и смотрим на любую дефолтную модельку дерева(я взял пальму с одного из ландшафтов):
Посмотреть вложение 9955
Тоже ничего. Заметили новый пункт "Mesh Groups" в меню слева? Нажмите на пустые чекбоксы и увидите дерево. Меш-группы это как бы "submodels", то есть части одной модельки. Когда вы съедаете дерево, оно не исчезает, оно становится пеньком. Делать отдельную модельку на ствол и пень было бы крайне затратно, поэтому Valve сделали механизм проще — переключаешь меш-группу, и вот у дерева исчез ствол. Теперь изучим интересующие нас пункты:
Меш-группы ставятся uint64_t-"масками".
- 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 на другую модель
Фулл код .
Репутация:
огромное спасибо за гайд, наконец то с кайфом поиграюПосмотреть вложение 9950
Итак, наша задача состоит в том, чтобы поменять всем деревьям модель. Для начала нужно найти список всех деревьев, и это не настолько очевидно. В EntitySystem такого нет, а поинтеры на 0x110 и далее не являются стабильным решением. Поглядим на класс дерева в Схеме:
Код:C_DOTA_MapTree : C_DOTA_BinaryObject bool m_bInitialized 0x808;
Ага, BinaryObject. В доте есть статическая геймсистема CDOTA_BinaryObjectSystem, в которой, ожидаемо, на 0x18 лежит CUtlVector<C_DOTA_MapTree*>. Этот список работает таким же образом, как и entity list, поэтому там будут и nullptrы. Отлично, деревья наши.
Теперь нужно изменить им модель и масштаб. Для первого нам нужен SetModel. Находим по хрефам,
Посмотреть вложение 9951
но не спешим брать по сигнатуре, можно и без неё:
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 через удобный конвар
Посмотреть вложение 9952
и видим, казалось бы, монструозную функцию:
Код: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:
Посмотреть вложение 9953
Прекрасно. Но это только начало! Ведь ваш $упер-$офт должен также уметь вернуть оригинальные деревья прямо в игре(и не пугайтесь из-за того, что современные p2c этого не могут, это не наша забота). Итак, приступим.
Для начала нужно сохранить оригинальную модель дерева. Гетнуть это не составляет труда, заходим в ReClass и внимательно наблюдаем
Посмотреть вложение 9954
оффсет m_hModel у CModelState. Это CStrongHandle, имя модели у которого находится в двух местах. Также можете напрямую взять по нетвару m_ModelName, вариаций масса! Оформляем нашему C_BaseEntity геттер:
C++:const char* GetModelName() { return *GetGameSceneNode()->Member<NormalClass*>(0x200)->Member<const char**>(8); }
Сохраняем в структуру на выбор, делаем функцию восстановления через тот же SetModel, нажимаем в своём списке деревьев Default и... куда они пропали? Деревья резко стали невидимыми, но не теряем надежду. Заходим в VRF и смотрим на любую дефолтную модельку дерева(я взял пальму с одного из ландшафтов):
Посмотреть вложение 9955
Тоже ничего. Заметили новый пункт "Mesh Groups" в меню слева? Нажмите на пустые чекбоксы и увидите дерево. Меш-группы это как бы "submodels", то есть части одной модельки. Когда вы съедаете дерево, оно не исчезает, оно становится пеньком. Делать отдельную модельку на ствол и пень было бы крайне затратно, поэтому Valve сделали механизм проще — переключаешь меш-группу, и вот у дерева исчез ствол. Теперь изучим интересующие нас пункты:
Меш-группы ставятся uint64_t-"масками".
- 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 на другую модель
Фулл код .
We use cookies and similar technologies for the following purposes:
Do you accept cookies and these technologies?
We use cookies and similar technologies for the following purposes:
Do you accept cookies and these technologies?