- Статус
- Оффлайн
- Регистрация
- 13 Фев 2026
- Сообщения
- 706
- Реакции
- 18
Бесят гигантские красные надписи "ERROR" вместо моделей на серверах с горой кастомного мусора?
Знакомая ситуация для любого, кто заходит на Zombie Mod или паблики с кривыми скинами без желания выкачивать гигабайты текстур. Вместо того чтобы ждать загрузку или ломать глаза об огромные надписи-ошибки, можно заставить движок подставлять стандартные модели на лету.
Техническая часть
Суть метода заключается в подмене индекса модели (m_nModelIndex) прямо перед выполнением PostDataUpdate. Для этого перехватываем FrameStageNotify на стадии 2 (FRAME_NET_UPDATE_POSTDATAUPDATE_START). В этот момент RecvProxies уже получили от сервера кастомный индекс, а мы его принудительно перезаписываем на дефолтный для этого класса сущности.
Почему это эффективно:
Дополнительная страховка в DrawModelExecute
Если какая-то модель все равно проскочила, можно добавить фильтр в хук отрисовки. Это гарантированно уберет остатки "ошибок" с экрана:
Метод работает безотказно годами, отлично подходит как база для простых external/internal проектов, чтобы не засорять клиент лишними загрузками.
Интересно, кто-нибудь пробовал аналогичным образом подменять звук шагов или другие ресурсы на серверах с жесткой проверкой файлов через Source Engine?
Знакомая ситуация для любого, кто заходит на Zombie Mod или паблики с кривыми скинами без желания выкачивать гигабайты текстур. Вместо того чтобы ждать загрузку или ломать глаза об огромные надписи-ошибки, можно заставить движок подставлять стандартные модели на лету.
Техническая часть
Суть метода заключается в подмене индекса модели (m_nModelIndex) прямо перед выполнением PostDataUpdate. Для этого перехватываем FrameStageNotify на стадии 2 (FRAME_NET_UPDATE_POSTDATAUPDATE_START). В этот момент RecvProxies уже получили от сервера кастомный индекс, а мы его принудительно перезаписываем на дефолтный для этого класса сущности.
Почему это эффективно:
- Минимальный оверхед — PostDataUpdate видит изменение, сам дергает SetModelIndex и настраивает model_t, кости и анимации.
- Стабильность — если в следующих кадрах индекс не меняется, движок просто скипает лишние операции.
- Чистый визуал — никакие красные надписи больше не закрывают обзор.
Код:
// FrameStageNotify, stage 2
if (stage == 2) {
int local_idx = engine->GetLocalPlayer();
int highest = entitylist->GetHighestEntityIndex();
for (int e = 1; e <= highest; e++) {
if (e == local_idx) continue;
IClientEntity* ent = entitylist->GetClientEntity(e);
if (!ent) continue;
C_BaseEntity* entity = ent->GetBaseEntity();
if (!entity || entity->IsDormant()) continue;
CBaseHandle owner = entity->GetOwnerEntity();
if (owner.IsValid() && owner.GetEntryIndex() == local_idx) continue;
ClientClass* cc = entity->GetClientClass();
if (!cc) continue;
const char* default_path = nullptr;
switch (cc->m_ClassID) {
case ClassId_CCSPlayer:
default_path = (entity->GetTeamNumber() == TEAM_CT)
? "models/player/ct_urban.mdl"
: "models/player/t_phoenix.mdl";
break;
case ClassId_CAK47: default_path = "models/weapons/w_rif_ak47.mdl"; break;
case ClassId_CWeaponM4A1: default_path = "models/weapons/w_rif_m4a1.mdl"; break;
case ClassId_CWeaponAug: default_path = "models/weapons/w_rif_aug.mdl"; break;
case ClassId_CWeaponSG552: default_path = "models/weapons/w_rif_sg552.mdl"; break;
case ClassId_CWeaponGalil: default_path = "models/weapons/w_rif_galil.mdl"; break;
case ClassId_CWeaponFamas: default_path = "models/weapons/w_rif_famas.mdl"; break;
case ClassId_CWeaponAWP: default_path = "models/weapons/w_snip_awp.mdl"; break;
case ClassId_CWeaponScout: default_path = "models/weapons/w_snip_scout.mdl"; break;
case ClassId_CWeaponSG550: default_path = "models/weapons/w_snip_sg550.mdl"; break;
case ClassId_CWeaponG3SG1: default_path = "models/weapons/w_snip_g3sg1.mdl"; break;
case ClassId_CWeaponMP5Navy: default_path = "models/weapons/w_smg_mp5.mdl"; break;
case ClassId_CWeaponMAC10: default_path = "models/weapons/w_smg_mac10.mdl"; break;
case ClassId_CWeaponTMP: default_path = "models/weapons/w_smg_tmp.mdl"; break;
case ClassId_CWeaponUMP45: default_path = "models/weapons/w_smg_ump45.mdl"; break;
case ClassId_CWeaponP90: default_path = "models/weapons/w_smg_p90.mdl"; break;
case ClassId_CWeaponGlock: default_path = "models/weapons/w_pist_glock18.mdl"; break;
case ClassId_CWeaponUSP: default_path = "models/weapons/w_pist_usp.mdl"; break;
case ClassId_CWeaponP228: default_path = "models/weapons/w_pist_p228.mdl"; break;
case ClassId_CDEagle: default_path = "models/weapons/w_pist_deagle.mdl"; break;
case ClassId_CWeaponFiveSeven: default_path = "models/weapons/w_pist_fiveseven.mdl"; break;
case ClassId_CWeaponElite: default_path = "models/weapons/w_pist_elite.mdl"; break;
case ClassId_CWeaponM3: default_path = "models/weapons/w_shot_m3super90.mdl"; break;
case ClassId_CWeaponXM1014: default_path = "models/weapons/w_shot_xm1014.mdl"; break;
case ClassId_CWeaponM249: default_path = "models/weapons/w_mach_m249para.mdl"; break;
case ClassId_CKnife: default_path = "models/weapons/w_knife_t.mdl"; break;
case ClassId_CC4: default_path = "models/weapons/w_c4.mdl"; break;
case ClassId_CHEGrenade: default_path = "models/weapons/w_eq_fraggrenade.mdl"; break;
case ClassId_CFlashbang: default_path = "models/weapons/w_eq_flashbang.mdl"; break;
case ClassId_CSmokeGrenade: default_path = "models/weapons/w_eq_smokegrenade.mdl"; break;
case ClassId_CBaseCSGrenadeProjectile: default_path = "models/weapons/w_eq_fraggrenade.mdl"; break;
case ClassId_CHostage: default_path = "models/hostage/hostage.mdl"; break;
default: continue;
}
if (!default_path) continue;
int default_idx = modelinfo->GetModelIndex(default_path);
if (default_idx <= 0) continue;
short* model_index = reinterpret_cast<short*>((uintptr_t)entity + 0xCC);
if (*model_index == static_cast<short>(default_idx)) continue;
*model_index = static_cast<short>(default_idx);
}
}
// DrawModelExecute fallback: skip error models not covered above
void DrawModelExecute_hook(/* params */) {
if (info.pModel) {
studiohdr_t* hdr = modelinfo->GetStudiomodel(info.pModel);
if (hdr && strstr(hdr->name, "error"))
return;
}
original(/* params */);
}
Дополнительная страховка в DrawModelExecute
Если какая-то модель все равно проскочила, можно добавить фильтр в хук отрисовки. Это гарантированно уберет остатки "ошибок" с экрана:
Код:
void DrawModelExecute_hook(/* params */) {
if (info.pModel) {
studiohdr_t* hdr = modelinfo->GetStudiomodel(info.pModel);
if (hdr && strstr(hdr->name, "error"))
return;
}
original(/* params */);
}
Метод работает безотказно годами, отлично подходит как база для простых external/internal проектов, чтобы не засорять клиент лишними загрузками.
Интересно, кто-нибудь пробовал аналогичным образом подменять звук шагов или другие ресурсы на серверах с жесткой проверкой файлов через Source Engine?
Последнее редактирование модератором: