As promised, here's the first in a series of follow-up posts on my initial 'introduction to Source hacking' post. This time we'll be adding the crucial missing feature from our skin changer: the ability to set custom knives.

Getting started

We'll be starting from where we left off last time. Since the code from this post has already been pushed to the csgo-skinchanger-linux repository we're going to be working from an older commit.

Making a knife changer is surprisingly straightforward. Essentially, we just overwrite the item definition index of the default team knife with the custom knife of choice. Finally, we have to update the weapon model in our hands and then we're done.

First up, picking a knife to use. Personally, I'll be using my signature M9 Bayonet | Crimson Web on the Terrorists side and my trusty Karambit | Tiger Tooth on the Counter-Terrorists side. You can pick whichever you like but your mileage will vary with some of the newer knives such as the Butterfly Knife, Flip Knife and Shadow Daggers which have many unique animations that don't play nice with our changes. There's a workaround for local servers but that's a topic for another post.

Item definition indexes

Let's add two new cases to our switch statement. In them, we'll override the item definition index value to that of the replacement knife. If you're curious where these indexes came from, you can either take a look at the TF2B item listings or the raw items_game.txt file in the csgo/scripts/items folder.

/* Karambit | Tiger Tooth */
case WEAPON_KNIFE:
  *weapon->GetEntityQuality() = 3;
  *weapon->GetItemDefinitionIndex() = WEAPON_KNIFE_KARAMBIT;
  *weapon->GetFallbackPaintKit() = 409; break;

/* M9 Bayonet | Crimson Web */
case WEAPON_KNIFE_T:
  *weapon->GetEntityQuality() = 3;
  *weapon->GetItemDefinitionIndex() = WEAPON_KNIFE_M9_BAYONET;
  *weapon->GetFallbackPaintKit() = 12; break;

Setting the entity quality is optional but you do want the red glow on the weapon switcher icon, don't you? Anyway, don't compile just yet, we still need to fix up our weapon view model.

Why would you ever do this. If you think this looks good then maybe you're already finished.

View model replacements

There are a lot of entities in the Source Engine. Everything from physics props, weapons, buy zones, bomb sites, team spawnpoints and even your hands are all different entities with their own unique classes and variables. Just start up a local server and run report_entities in Console for a full list.

] report_entities 
Class: ai_network (1)
Class: cs_gamerules (1)
Class: cs_player_manager (1)
Class: cs_team_manager (4)
Class: env_cascade_light (1)
Class: func_buyzone (5)
Class: info_player_counterterrorist (32)
Class: info_player_terrorist (30)
Class: info_target (1)
Class: player (1)
Class: predicted_viewmodel (1)
Class: scene_manager (1)
Class: soundent (1)
Class: vote_controller (1)
Class: weapon_hkp2000 (1)
Class: weapon_knife (1)
Class: weaponworldmodel (2)
Class: worldspawn (1)
Total 86 entities (0 empty, 24 edicts)

See the predicted_viewmodel entity? That's the model of our hands and weapon we see when playing or spectating someone else. To keep things simple, make sure you're in a server by yourself. Remove any bots from the game if needed with the bot_kick command. Next, enable cheats with sv_cheats 1 and finally, run cl_find_ent predicted_viewmodel in Console.

] cl_find_ent predicted_viewmodel 
Searching for client entities with classname containing substring: 'predicted_viewmodel'
   'predicted_viewmodel' (entindex 81)  
Found 1 matches.

Now we know our view model entity is positioned at the 81st position in the entity list. There's a pretty cool command included in the Source Engine which can show you a load of information about an entity. Try it out by running cl_pdump followed by the index of your predicted_viewmodel entity in Console.

I love Valve so much for stuff like this. cl_pdump 81

There's a lot to look at here but I'll draw your attention to the m_nModelIndex and m_hWeapon network variables. You'll notice as you switch between your guns and knife that these two will change. Let's take a quick look at some code from the Source Engine and see what they're doing to cause this change.

void CBaseViewModel::SetWeaponModel(const char* modelname, CBaseCombatWeapon* weapon) {
  m_hWeapon = weapon;
  SetModel(modelname);
}

At the time of writing, I'm using this virtual function in the Windows version of my skin changer. Seems good enough, we can just find this function and call it. This function looks simple though, maybe we should look a little further.

void CBaseEntity::SetModel(const char* modelname) {
  UTIL_SetModel(this, modelname);
}
void UTIL_SetModel(CBaseEntity* entity, const char* modelname) {
  int i = modelinfo->GetModelIndex(modelname);
  
  entity->SetModelIndex(i);
  entity->SetModelName(AllocPooledString(modelname));
}
void CBaseEntity::SetModelIndex(int index) {
  m_nModelIndex = index;
  DispatchUpdateTransmitState();
}

May as well forget about calling the function, looks like writing the new model index over m_nModelIndex might be enough. However, we can't do that without our view model entity. We could iterate the entity list and compare m_hOwner against our local player but surely there's a better way. Perhaps our player entity can shed some light on this dilemma?

] cl_find_ent CSPlayer
Searching for client entities with classname containing substring: 'CSPlayer'
   '10C_CSPlayer' (entindex 1)  
Found 1 matches.
] cl_pdump 1
Good text formatting. cl_pdump 1

Oh nice, it was that easy. We'll just get the entity from the m_hViewModel handle. cl_pdump saves the day again. Looks like it's time to get these offsets. I know, this part sucks. I promise that the next post will be on dynamic NetVar scanning and then we'll never have to do this again.

.text:00000000007CF5FF                 lea     rsi, aM_nmodelindex ; "m_nModelIndex"
.text:00000000007CF606                 mov     edx, 28Ch
.text:0000000000C8104D                 mov     edx, 3AD4h
.text:0000000000C81052                 lea     rsi, aM_hviewmodel0 ; "m_hViewModel[0]"
.text:00000000007E54F3                 mov     edx, 3060h
.text:00000000007E54F8                 lea     rsi, aM_hweapon ; "m_hWeapon"
#define m_nModelIndex 0x28C
#define m_hViewModel 0x3AD4
#define m_hWeapon 0x3060
int* C_BaseEntity::GetModelIndex() {
  return reinterpret_cast<int*>(uintptr_t(this) + m_nModelIndex);
}

int C_BasePlayer::GetViewModel() {
  return *reinterpret_cast<int*>(uintptr_t(this) + m_hViewModel);
}

int C_BaseViewModel::GetWeapon() {
  return *reinterpret_cast<int*>(uintptr_t(this) + m_hWeapon);
}

Right. We're almost ready, there's still one more thing to do. In case you didn't notice, we have no idea what to set the model index to yet. Luckily, there is such a function to convert model filenames into indexes in the IVModelInfoClient interface!

$ strings ./bin/linux64/engine_client.so | grep "VModelInfoClient"
VModelInfoClient004
#define VMODELINFO_CLIENT_INTERFACE_VERSION "VModelInfoClient004"

class IVModelInfoClient {
  public:
    int GetModelIndex(const char* filename) {
      return GetVirtualFunction<int(*)(void*, const char*)>(this, 3)(this, filename);
    }
};
modelinfo = GetInterface<IVModelInfoClient>("./bin/linux64/engine_client.so", VMODELINFO_CLIENT_INTERFACE_VERSION);

Okay, this is actually the last thing left. We still don't know the filenames of the override knife models. There are a few ways to find this but it's probably easiest to search for occurences of model_player in the items_game.txt file. Alternatively, you could take a look in the models/weapons folder in pak01_dir.vpk but that's a bit more complicated.

$ grep "model_player" ./csgo/scripts/items/items_game.txt
"model_player"    "models/weapons/v_knife_default_ct.mdl"
"model_player"    "models/weapons/v_knife_default_t.mdl"
"model_player"    "models/weapons/v_knife_bayonet.mdl"
"model_player"    "models/weapons/v_knife_flip.mdl"
"model_player"    "models/weapons/v_knife_gut.mdl"
"model_player"    "models/weapons/v_knife_karam.mdl"
"model_player"    "models/weapons/v_knife_m9_bay.mdl"
"model_player"    "models/weapons/v_knife_tactical.mdl"
"model_player"    "models/weapons/v_knife_falchion_advanced.mdl"
"model_player"    "models/weapons/v_knife_survival_bowie.mdl"
"model_player"    "models/weapons/v_knife_butterfly.mdl"
"model_player"    "models/weapons/v_knife_push.mdl"

I've cut out all the other weapons from the above output but if you really wanted to replace your knife with a gun.. well, nobody is stopping you. Anyway, we really do have everything now! Let's go back to our FrameStageNotify hook.

Implementation

Start off by getting the view model entity. Place this after all the loop through m_hMyWeapons.

int viewmodel_entindex = localplayer->GetViewModel() & 0xFFF;
C_BaseViewModel* viewmodel = reinterpret_cast<C_BaseViewModel*>(entitylist->GetClientEntity(viewmodel_entindex));

if (!viewmodel) {
  break;
}

Next, we'll get the weapon entity from the view model.

int weapon_entindex = viewmodel->GetWeapon() & 0xFFF;
C_BaseCombatWeapon* active_weapon = reinterpret_cast<C_BaseCombatWeapon*>(entitylist->GetClientEntity(weapon_entindex));

if (!active_weapon) {
  break;
}

Now we'll check if this is a custom knife. If it matches the item definition index we set earlier then we'll overwrite the m_nModelIndex variable. I initially wanted to store the replacement model indexes in a static variable, but the indexes actually change between maps. There are still better ways of doing this, but this'll do the job for now.

switch (*active_weapon->GetItemDefinitionIndex()) {
  case WEAPON_KNIFE_KARAMBIT:
    *active_weapon->GetModelIndex() = modelinfo->GetModelIndex("models/weapons/v_knife_karam.mdl");
    *viewmodel->GetModelIndex() = modelinfo->GetModelIndex("models/weapons/v_knife_karam.mdl"); break;
  case WEAPON_KNIFE_M9_BAYONET:
    *active_weapon->GetModelIndex() = modelinfo->GetModelIndex("models/weapons/v_knife_m9_bay.mdl");
    *viewmodel->GetModelIndex() = modelinfo->GetModelIndex("models/weapons/v_knife_m9_bay.mdl"); break;
}

All done! That's probably the shortest implementation section yet. There are a few different ways to achieve the same effect but this one is the shortest I know of. Compile, load and enjoy.

Nostalgic map. Just like before, you can get creative with the paint kits.

Post note: After the 29 Nov 2016 update you must also set the model index on the active weapon. Failing to do this will leave you unable to apply paint kits to the replacement knife. An example implementation of this fix can be viewed here.

Last updated Thursday, 24 February 2022 at 10:58 AM.