Подписывайтесь на наш Telegram и не пропускайте важные новости! Перейти

Гайд CSS — Фикс "ERROR" моделей на кастомных серверах

Sloppy
Начинающий
Начинающий
Статус
Оффлайн
Регистрация
13 Фев 2026
Сообщения
706
Реакции
18
Бесят гигантские красные надписи "ERROR" вместо моделей на серверах с горой кастомного мусора?

Знакомая ситуация для любого, кто заходит на Zombie Mod или паблики с кривыми скинами без желания выкачивать гигабайты текстур. Вместо того чтобы ждать загрузку или ломать глаза об огромные надписи-ошибки, можно заставить движок подставлять стандартные модели на лету.

Техническая часть
Суть метода заключается в подмене индекса модели (m_nModelIndex) прямо перед выполнением PostDataUpdate. Для этого перехватываем FrameStageNotify на стадии 2 (FRAME_NET_UPDATE_POSTDATAUPDATE_START). В этот момент RecvProxies уже получили от сервера кастомный индекс, а мы его принудительно перезаписываем на дефолтный для этого класса сущности.

Почему это эффективно:
  1. Минимальный оверхед — PostDataUpdate видит изменение, сам дергает SetModelIndex и настраивает model_t, кости и анимации.
  2. Стабильность — если в следующих кадрах индекс не меняется, движок просто скипает лишние операции.
  3. Чистый визуал — никакие красные надписи больше не закрывают обзор.

Код:
Expand Collapse Copy
// 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
Если какая-то модель все равно проскочила, можно добавить фильтр в хук отрисовки. Это гарантированно уберет остатки "ошибок" с экрана:

Код:
Expand Collapse Copy
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?
 
Последнее редактирование модератором:
Назад
Сверху Снизу