Adding Custom Weapon Script Loading

From Valve Developer Community
Jump to navigation Jump to search

In this page, you will learn how to load any weapon script you want for your weapons, by editing C++ code in your Source SDK project.

Introducing

Most Source Source Engine games use weapon script files to configure most of their parameters, this solution allows users to customize weapons without code editing or adding new classes, including damage, viewmodel, player animations, etc. The core problem is that users can't load any script they want, because weapons are hardcoded to load weapon script files with name equivalent to weapon's classname parameter, this doesn't allow to customize weapons individually or use your own weapon settings on the server.

This can be partially workaround by hooking CBaseCombatWeapon::Precache method, renaming weapon classname before it executes and rename back to previous value (so the weapon can spawn), or by doing the following command sequence: ent_create weapon_crowbar targetname "mycustomweapon" classname "weapon_pipewrench"; ent_fire mycustomweapon addoutput "classname weapon_crowbar" 0.01. The problem with this workaround is that the custom script is loaded only on server, meaning the client values are unaffected, this causes many prediction errors and client-only values such as icons are not loaded at all, including if the hook trick is not used in single player, the weapon will revert back all original values after a save/restore mechanism. This also doesn't allow the player to own multiple weapons of the same class and causes lots of issues with in game logic (for example, players can't select such weapons via HUD bucket and will get prediction errors).

The following text will solve all the issues by adding custom weapon script loading natively via editing and compiling Source SDK source code. The whole idea is simple, use a new keyvalue, named weaponscriptname, instead of using the classname and rework some parts to make as less changes as possible, thus adapting to the system in which the developers took into account only the hardcoded weapon script system. The system should replace classnames with new keyvalues and use classnames only when needed in those rare cases. The system should include ability to change weapon's script at any time via ChangeScript input and notify client side about it. The system should allow NPC spawn with overridden weapon via additionalequipment keyvalue, the weapon classname and scripts name should be split by commad (for example weapon_smg1,weapon_mp5). The system should allow to own multiple weapons with the same classname if they have different script names to allow unlimited custom weapons.

Note.pngNote:This guide tested with Half-Life 2: Deathmatch Half-Life 2: Deathmatch, Half-Life 2 Half-Life 2 and episodes. Not guaranteed to compile or work with Team Fortress 2 Team Fortress 2.

Editing the code

basecombatweapon_shared.cpp

Go to src/game/shared/basecombatweapon_shared.cpp, find the line with DEFINE_FIELD( m_iszName, FIELD_STRING ) and add the fields m_iNeedsUpdate and m_iszWeaponScript:

	DEFINE_FIELD( m_iszName, FIELD_STRING ),
	DEFINE_AUTO_ARRAY( m_iszWeaponScript, FIELD_CHARACTER ),
	DEFINE_FIELD( m_iNeedsUpdate, FIELD_INTEGER ),

Find data table BEGIN_NETWORK_TABLE(CBaseCombatWeapon, DT_BaseCombatWeapon). For table with send properties, add SendPropInt(SENDINFO(m_iNeedsUpdate), 0, SPROP_UNSIGNED), after line with SendPropModelIndex( SENDINFO(m_iWorldModelIndex) ),, add SendPropString(SENDINFO(m_iszWeaponScript)), after line with SendPropEHandle( SENDINFO(m_hOwner) ),. For table with received properties, add RecvPropInt(RECVINFO(m_iNeedsUpdate)), after line with RecvPropInt( RECVINFO(m_iWorldModelIndex)),, add RecvPropString(RECVINFO(m_iszWeaponScript)), after line with RecvPropEHandle( RECVINFO(m_hOwner ), RecvProxy_WeaponOwner ),.

Find data table BEGIN_NETWORK_TABLE_NOBASE( CBaseCombatWeapon, DT_LocalWeaponData ) and add SendPropInt( SENDINFO( m_iNeedsUpdate ) ), after line with SendPropInt( SENDINFO( m_bFlipViewModel ) ), to add m_iNeedsUpdate to the table. Find the line with RecvPropInt( RECVINFO( m_nViewModelIndex ) ), and add RecvPropInt( RECVINFO(m_iNeedsUpdate)), after it.

After the code for CBaseCombatWeapon::ActivityOverride( Activity baseAct, bool *pRequired ) method, add new code for the input and two new console variables. The console variable named sv_weapon_clips_reset_mode will store the value that will affect the way input will reset primary and secondary ammo in weapon clips: no reset if 0, always set max clip value if 1, set max clip value if more than max clip value if 2. The console variable named sv_weapon_vm_anim_reset_mode will be used to determine if we want to use logic from the Deploy() method for the deploy animation, or just set an idle animation. The code for ChangeScript input will call SetCustomWeaponScriptName(const char* pszNewScript) and put given script. The code for SetCustomWeaponScriptName(const char* pszNewScript) will change the weapon's script, if the input parameter isn't empty and owner (if exists) doesn't already own weapon with such script, this method will change the value for m_iszWeaponScript, tell the client to update weapon data by increasing the value from m_iNeedsUpdate by 1, call Precache() on server for this weapon, play deploy or idle animation on viewmodel, reset values for used ammo clips depending on the console variable value. If an owner exists and the owner is a player, it will reset the viewmodel (to trigger instant viewmodel change) and reset viewmodel information to fix mismatched sequence indexes, if this weapon has no owner, or owner is an NPC, it will update world model animation data, if no owner at all, this method will reset collision instead. This is the needed code:

ConVar	sv_weapon_clips_reset_mode("sv_weapon_clips_reset_mode", "1", 
	FCVAR_REPLICATED, 
	"Sets the way to reset clips:\n0 - No reset at all.\n1 - Set max clip value.\n2 - Set max clip value if more then max clip.",
	true, 0.0f, true, 2.0f
);

ConVar	sv_weapon_vm_anim_reset_mode("sv_weapon_vm_anim_reset_mode", "0",
	FCVAR_REPLICATED,
	"Animation to set after weapon script change, 0 to use idle animation, 1 to use deploy animation.",
	true, 0, true, 1
);

#ifndef CLIENT_DLL
//-----------------------------------------------------------------------------
// Purpose: Input to change the weapon script name and re-Precache
//-----------------------------------------------------------------------------
void CBaseCombatWeapon::InputChangeScript(inputdata_t& inputdata)
{
	SetCustomWeaponScriptName(inputdata.value.String());
}

//-----------------------------------------------------------------------------
// Purpose: Changes weapon script by name
//-----------------------------------------------------------------------------
void CBaseCombatWeapon::SetCustomWeaponScriptName(const char* pszNewScript)
{
	//don't update if empty or already used this script
	if (!pszNewScript || !pszNewScript[0] || FClassnameIs(this, pszNewScript))
		return;

	//don't update if other weapon that owns my owner uses the same script
	if (GetOwner() && GetOwner()->Weapon_OwnsThisType(pszNewScript))
		return;

	//copy new data for networked and stored
	Q_strncpy(m_iszWeaponScript.GetForModify(), pszNewScript, MAX_WEAPON_STRING);

	//finish reload before update
	if (m_bInReload)
	{
		FinishReload();
	}

	m_iNeedsUpdate++; //trigger client update

	Precache(); //update with new script

	if (GetOwner())
	{
		if (GetOwner()->IsPlayer() == false)
		{
			//update model data for npc
			SetModel(GetWorldModel());
		}
		else
		{
			//this updates wpn's vm at the same time as wpn's script, instead of waiting 2-6 seconds
			if (GetOwner()->GetActiveWeapon() == this)
				SetViewModel();

			SetModel(GetViewModel()); //this fixes wrong sequence nums (DOESN'T AFFECT WORLD MODEL)

			//use deploy anim if we want
			if (GetOwner()->GetActiveWeapon() == this)
			{
				if (sv_weapon_vm_anim_reset_mode.GetBool())
				{
					Deploy();
				}
				else
				{
					SendWeaponAnim(ACT_VM_IDLE);
				}
			}
		}
	}

	//if i have no owner - reset collsion model with bbox + check if my new wm has collision
	//NOTE: no need if owned by NPC or plr as they update collision when drop weapons
	else
	{
		SetModel(GetWorldModel());
		VPhysicsDestroyObject();

		if (!VPhysicsInitNormal(SOLID_BBOX, GetSolidFlags() | FSOLID_TRIGGER, false))
		{
			SetMoveType(MOVETYPE_NONE);
			SetSolid(SOLID_BBOX);
			AddSolidFlags(FSOLID_TRIGGER);
		}
	}

	//we don't want to reset clips at all, return
	if (sv_weapon_clips_reset_mode.GetInt() == 0)
		return;

	//we want to set max clipw vals
	if (sv_weapon_clips_reset_mode.GetInt() == 1)
	{
		if (UsesClipsForAmmo1())
			m_iClip1 = GetMaxClip1();

		if (UsesClipsForAmmo2())
			m_iClip2 = GetMaxClip2();
		
		return;
	}

	//we want to set max clip only if this weapon has more ammo in clips than max
	if (sv_weapon_clips_reset_mode.GetInt() == 2)
	{
		if (UsesClipsForAmmo1() && m_iClip1 > GetMaxClip1())
			m_iClip1 = GetMaxClip1();

		if (UsesClipsForAmmo2() && m_iClip2 > GetMaxClip2())
			m_iClip2 = GetMaxClip2();
		
		return;
	}
}
#endif

Find line with DEFINE_INPUTFUNC( FIELD_VOID, "HideWeapon", InputHideWeapon ), and add new input after this line:

	DEFINE_INPUTFUNC( FIELD_VOID, "HideWeapon", InputHideWeapon ),
	DEFINE_INPUTFUNC( FIELD_STRING, "ChangeScript", InputChangeScript ),

Go to the CBaseCombatWeapon::Precache method, replace Warning( "Error reading weapon data file for: %s\n", GetClassname() ); with Warning( "Error reading weapon data file for classname \"%s\" with script \"%s\".\n", STRING(m_iClassname), GetClassname()); for more clear information if something goes wrong during weapon script loading.

After the end of the Precache() code, add new code for overridden methods from CBaseEntity.

The code for GetClassname() will replace the classname with a custom script name (you still can get real classname by getting m_iClassname directly. Because most of the code accesses weapon classname for various tasks, such a trick with replacement allows us to not rework tones of the code and keep the system flexible. Some code may access classname value on weapons directly, this needs to be replaced in certain parts (see below), except for the save & restore mechanism (otherwise weapons will disappear after a save load). This code also uses original classname fallback in case if the field is empty, so weapons without a custom script will use the classname instead, just like how it works in the original Source SDK.

The code for ClassMatches(const char* pszClassOrWildcard) will run when used FClassnameIs(CBaseEntity *pEntity, const char *szClassname) util (used to compare classnames). This method is a copy of what runs the original ClassMatches(const char* pszClassOrWildcard) from CBaseEntity, but it's needed to run overridden GetClassname() version (mainly due to how FClassnameIs(CBaseEntity *pEntity, const char *szClassname) is used).

The code for ClassMatches(string_t nameStr) will use return value from ClassMatches(const char* pszClassOrWildcard), so we don't copy paste the code again. This method is an alternative version of the past method, but takes string_t object instead.

The code for KeyValue(const char* szKeyName, const char* szValue) used to get and set the script value that will be used on entity spawn.

This is the needed code:

//-----------------------------------------------------------------------------
// Purpose: Returns weaponscriptname to make weapons of the same classname working properly with the rest of the code.
//-----------------------------------------------------------------------------
const char* CBaseCombatWeapon::GetClassname()
{
	if (m_iszWeaponScript.Get()[0] != '\0')
	{
		return m_iszWeaponScript.Get();
	}

	return BaseClass::GetClassname();
}

//-----------------------------------------------------------------------------
// Purpose: This is used by FClassnameIs to compare classname strings. This needs to be here in this way, so the classname replaced with script.
//-----------------------------------------------------------------------------
bool CBaseCombatWeapon::ClassMatches(const char* pszClassOrWildcard)
{
	string_t stStringToCompare = MAKE_STRING(GetClassname());

	if (IDENT_STRINGS(stStringToCompare, pszClassOrWildcard))
		return true;

	if (stStringToCompare == NULL_STRING)
		return (!pszClassOrWildcard || *pszClassOrWildcard == 0 || *pszClassOrWildcard == '*');

	const char* pszNameToMatch = STRING(stStringToCompare);

	// If the pointers are identical, we're identical
	if (pszNameToMatch == pszClassOrWildcard)
		return true;

	while (*pszNameToMatch && *pszClassOrWildcard)
	{
		unsigned char cName = *pszNameToMatch;
		unsigned char cQuery = *pszClassOrWildcard;
		// simple ascii case conversion
		if (cName == cQuery)
			;
		else if (cName - 'A' <= (unsigned char)'Z' - 'A' && cName - 'A' + 'a' == cQuery)
			;
		else if (cName - 'a' <= (unsigned char)'z' - 'a' && cName - 'a' + 'A' == cQuery)
			;
		else
			break;
		++pszNameToMatch;
		++pszClassOrWildcard;
	}

	if (*pszClassOrWildcard == 0 && *pszNameToMatch == 0)
		return true;

	// @TODO (toml 03-18-03): Perhaps support real wildcards. Right now, only thing supported is trailing *
	if (*pszClassOrWildcard == '*')
		return true;

	return false;
}

#if !defined( CLIENT_DLL )
//-----------------------------------------------------------------------------
// Purpose: This is used by FClassnameIs to compare classname strings, we replace it with script name. This is string_t version.
//-----------------------------------------------------------------------------
bool CBaseCombatWeapon::ClassMatches(string_t nameStr)
{
	return ClassMatches(nameStr.ToCStr());
}
#endif

//-----------------------------------------------------------------------------
// Purpose: Write and store new script val in case if kv is "weaponscriptname" 
//-----------------------------------------------------------------------------
bool CBaseCombatWeapon::KeyValue(const char* szKeyName, const char* szValue)
{
	if (FStrEq(szKeyName, "weaponscriptname"))
	{
		if (szValue[0] != '\0') //if not empty - use val, else get real classname
		{
			Q_strncpy(m_iszWeaponScript.GetForModify(), szValue, MAX_WEAPON_STRING);
		}

		return true;
	}
	return BaseClass::KeyValue(szKeyName, szValue);
}

Find CBaseCombatWeapon::GetName() method, it is reraly used as an alternative method to get string identical to classname. Replace the old code for it with the new code that returns script name if exists:

//-----------------------------------------------------------------------------
// Purpose: Get classname from wpn data or use script name directly if not empty.
//-----------------------------------------------------------------------------
const char *CBaseCombatWeapon::GetName( void ) const
{
	if (m_iszWeaponScript.Get()[0] != '\0')
		return m_iszWeaponScript.Get();

	return GetWpnData().szClassName;
}

basecombatweapon_shared.h

Go to src/game/shared/basecombatweapon_shared.h, find the line with virtual void Spawn( void );, add after it the new methods:

	virtual const char*     GetClassname();
	virtual bool			ClassMatches( const char* pszClassOrWildcard );
	virtual bool			KeyValue( const char *szKeyName, const char *szValue ); // override to set weapon script name

#if !defined( CLIENT_DLL )
	virtual bool			ClassMatches( string_t nameStr );
	virtual void			SetCustomWeaponScriptName(const char* pszNewScript);
	void					InputChangeScript(inputdata_t& inputdata); // Input to change the weapon script name and re-Precache
#endif

Go to line with string_t m_iszName, add after it new variables:

	CNetworkString( m_iszWeaponScript, MAX_WEAPON_STRING ); //networked weapon script name
	CNetworkVar(int, m_iNeedsUpdate);	//mark for client in case if weapon script update is wanted
#if defined ( CLIENT_DLL )
	int m_iOldNeedsUpdate = 0;			//client's variable to compare with networked and decide if update is needed
#endif

c_basecombatweapon.cpp

Go to src/game/client/c_basecombatweapon.cpp, find method C_BaseCombatWeapon::OnDataChanged. Because the client doesn't get a proper script name in time when a weapon is created for the first time, the weapon should update parameters on the client once weapon data on it is ready. It's important to fix prediction issues caused by using a default script name on client and not loading client-side only weapon parameters. To solve this, before line with UpdateVisibility(), call the Precache() method. Another issue is that ChangeScript will update weapon data only on the server, leading to the client getting prediction errors. We should call Precache(), this is why m_iNeedsUpdate and m_iOldNeedsUpdate was added. The solution is simple, just check if m_iNeedsUpdate is not equal to m_iOldNeedsUpdate, if true - call Precache() on the client and set equal value for m_iOldNeedsUpdate, we also should run the same code for ammo value update in singleplayer (not needed in multiplayer), otherwise called Precache() will reset client's ammo value to default, leading to visual inconsistency. For the Source SDK multiplayer branch, find this part of code:

	if ( updateType == DATA_UPDATE_CREATED )
	{
		UpdateVisibility();
	}

Replace with:

	if ( updateType == DATA_UPDATE_CREATED )
	{
		Precache(); //cache weapon on client again, otherwise the client will always use default script name while server not
		UpdateVisibility();
	}

	//update script in case if wanted, but not when restored (or it will cause hud issues)
	if (m_iOldNeedsUpdate != m_iNeedsUpdate && !m_bJustRestored)
	{
		Precache();
		m_iOldNeedsUpdate = m_iNeedsUpdate; //assign new value to prevent updates on client when we don't want

		//don't reset local ammo value in mp, needed only for sp
		if (gpGlobals->maxClients > 1)
			return;

		ConVarRef resetmode("sv_weapon_clips_reset_mode");

		//we don't want to reset clips at all, return
		if (!resetmode.IsValid() || resetmode.GetInt() == 0)
			return;

		//we want to set max clips vals
		if (resetmode.GetInt() == 1)
		{
			if (UsesClipsForAmmo1())
				m_iClip1 = GetMaxClip1();

			if (UsesClipsForAmmo2())
				m_iClip2 = GetMaxClip2();
			
			return;
		}

		//we want to set max clip only if this weapon has more ammo in clips than max
		if (resetmode.GetInt() == 2)
		{
			if (UsesClipsForAmmo1() && m_iClip1 > GetMaxClip1())
				m_iClip1 = GetMaxClip1();

			if (UsesClipsForAmmo2() && m_iClip2 > GetMaxClip2())
				m_iClip2 = GetMaxClip2();
			
			return;
		}
	}

For Source SDK singleplayer, just add the new code at the end of the method and remove UpdateVisibility(); line.

weapon_selection.cpp

Under certain conditions, attempting to select an active weapon via the HUD bucket will cause HUD flicker and the viewmodel to disappear. The fix is simple, don't select wanted weapon if it is already active. Go to src/game/client/weapon_selection.cpp, find the following code:

void CBaseHudWeaponSelection::SetWeaponSelected( void )
{
	Assert( GetSelectedWeapon() );
	// Mark selection so that it's placed into next CUserCmd created
	input->MakeWeaponSelection( GetSelectedWeapon() );
}

Replace with this code:

void CBaseHudWeaponSelection::SetWeaponSelected( void )
{
	Assert( GetSelectedWeapon() );

	//Mark selection so that it's placed into next CUserCmd created if isn't active (or we'll get prediction glitch with custom scripts)
	if(GetSelectedWeapon() != GetActiveWeapon())
		input->MakeWeaponSelection( GetSelectedWeapon() );
}

hud_ammo.cpp

To make both the primary and secondary ammo bar update properly, we need to trigger changes in the same places where the code checks if the current active weapon is the previous one. Go to src/game/client/hl2/hud_ammo.cpp, go to the line with CHudTexture *m_iconPrimaryAmmo;, add m_iOldNeedsUpdate integer:

	CHudTexture *m_iconPrimaryAmmo;
	int		m_iOldNeedsUpdate; //needs update tracking

Go constructor for CHudAmmo, find line with hudlcd->SetGlobalStat( "(weapon_name)", "" );, add m_iOldNeedsUpdate = -1; after it.

Add -1 setting in Init() method as well, find line with m_iconPrimaryAmmo = NULL; and add m_iOldNeedsUpdate = -1; after it.

Add -1 setting in Reset() method as well, find line with m_iAmmo2 = 0; and add m_iOldNeedsUpdate = -1; after it. Find method UpdatePlayerAmmo( C_BasePlayer *player ), go to line with if (wpn == m_hCurrentActiveWeapon), add compare for m_iOldNeedsUpdate with weapon's m_iOldNeedsUpdate from pointer to the weapon with -1 as a fallback if pointer in null, replace the line with the following code:

	int iNeedsUpdate = wpn ? wpn->m_iOldNeedsUpdate : -1;

	if (wpn == m_hCurrentActiveWeapon && m_iOldNeedsUpdate == iNeedsUpdate)

Also add m_iOldNeedsUpdate = iNeedsUpdate; at the end of the method to reset the variable value.

Find the line with SetHiddenBits( HIDEHUD_HEALTH | HIDEHUD_WEAPONSELECTION | HIDEHUD_PLAYERDEAD | HIDEHUD_NEEDSUIT );, add m_iOldNeedsUpdate = -1; before it. Add the same code before the line with wchar_t *tempString = g_pVGuiLocalize->Find("#Valve_Hud_AMMO_ALT"); to set -1 value. Find the line with SetAlpha( 0 );, add m_iOldNeedsUpdate = -1; after it.

Find line with if ( m_hCurrentActiveWeapon != wpn ), replace:

		if ( m_hCurrentActiveWeapon != wpn )
		{
			if ( wpn->UsesSecondaryAmmo() )
			{
				// we've changed to a weapon that uses secondary ammo
				g_pClientMode->GetViewportAnimationController()->StartAnimationSequence("WeaponUsesSecondaryAmmo");
			}
			else 
			{
				// we've changed away from a weapon that uses secondary ammo
				g_pClientMode->GetViewportAnimationController()->StartAnimationSequence("WeaponDoesNotUseSecondaryAmmo");
			}
			m_hCurrentActiveWeapon = wpn;
			
			// Get the icon we should be displaying
			m_iconSecondaryAmmo = gWR.GetAmmoIconFromWeapon( m_hCurrentActiveWeapon->GetSecondaryAmmoType() );
		}
	}

With:

		int iNeedsUpdate = wpn ? wpn->m_iOldNeedsUpdate : -1;

		if (m_hCurrentActiveWeapon != wpn || m_iOldNeedsUpdate != iNeedsUpdate)
		{
			if (wpn && wpn->UsesSecondaryAmmo())
			{
				g_pClientMode->GetViewportAnimationController()->StartAnimationSequence("WeaponUsesSecondaryAmmo");
			}
			else 
			{
				g_pClientMode->GetViewportAnimationController()->StartAnimationSequence("WeaponDoesNotUseSecondaryAmmo");
			}
			m_hCurrentActiveWeapon = wpn;
		}

		// Always update the icon if weapon is valid
		if (wpn && wpn->UsesSecondaryAmmo())
		{
			m_iconSecondaryAmmo = gWR.GetAmmoIconFromWeapon(wpn->GetSecondaryAmmoType());
		}
		else
		{
			m_iconSecondaryAmmo = nullptr;
		}

		// Update m_iOldNeedsUpdate
		m_iOldNeedsUpdate = iNeedsUpdate;
	}

Find the line with CHudTexture *m_iconSecondaryAmmo;, add an m_iOldNeedsUpdate integer variable after it:

	CHudTexture *m_iconSecondaryAmmo;
	int		m_iOldNeedsUpdate; //needs update tracking

gameweaponmanager.cpp

To make the game_weapon_manager entity remove weapons by script instead of classname for more flexability, go to src/game/server/gameweaponmanager.cpp, replace all lines if ( g_Managers[i]->m_iszWeaponName == pWeapon->m_iClassname ) with if ( FClassnameIs( pWeapon, g_Managers[i]->m_iszWeaponName.ToCStr() ) ). Go to line 216, replace Assert( pEntity->m_iClassname == m_iszWeaponName ); with Assert( FClassnameIs(pEntity, m_iszWeaponName.ToCStr() ) ); for the same goal.

npc_combine.cpp

The Combine soldiers use CNPC_Combine::HasShotgun() method to define by classname if they have a shotgun. To define the weapon by script name, go to src/game/server/hl2/npc_combine.cpp, find the method, replace if( GetActiveWeapon() && GetActiveWeapon()->m_iClassname == s_iszShotgunClassname ) with if ( GetActiveWeapon() && FClassnameIs( GetActiveWeapon(), s_iszShotgunClassname.ToCStr() ) ).

ai_basenpc_squad.cpp

The method CAI_BaseNPC::NumWeaponsInSquad( const char *pszWeaponClassname ) used to define amount of weapons in the squad. To make it working with custom weapon scripts, go to file src/game/server/ai_basenpc_squad.cpp, replace line if( GetActiveWeapon() && GetActiveWeapon()->m_iClassname == iszWeaponClassname ) with if( GetActiveWeapon() && FClassnameIs( GetActiveWeapon(), iszWeaponClassname.ToCStr() ) ).

Also find line if( pSquadmate->GetActiveWeapon() && pSquadmate->GetActiveWeapon()->m_iClassname == iszWeaponClassname ), replace with if( pSquadmate->GetActiveWeapon() && FClassnameIs(pSquadmate->GetActiveWeapon(), iszWeaponClassname.ToCStr() ) ).

baseentity.h

Some methods from CBaseEntity need to become virtual methods, so other classes (including CBaseCombatWeapon) can run their own method versions, their are ClassMatches( string_t nameStr ), ClassMatches( const char* pszClassOrWildcard ) and GetClassname(). Go to src/game/server/baseentity.h, find definition for the methods, add "virtual " at their line start.

basecombatweapon.cpp

The weapon respawn mechanism in Source SDK creates entirely new weapon entity, the new weapon should be a copy of original weapon. The respawn mechanism doesn't transfer custom script names, this is why a respawned weapon uses a value equivalent to classname, so it will not use a custom script name. To fix this, go to src/game/server/basecombatweapon.cpp, find method CBaseCombatWeapon::Respawn, add 3 new lines of code in body of if that checks validity of new weapon. The first a sets new keyvalue, the second calls the Precache() method, so the server is not using default script name while client doesn't. The third line of code is workaround to fix incorrect bounding box size, caused by loading the original weapon script name before the code called Precache() with proper script name. Also GetClassname() should be replaced with STRING(m_iClassname) for the CBaseEntity::Create() method to spawn a weapon with existing classname. Replace:

	CBaseEntity *pNewWeapon = CBaseEntity::Create( GetClassname(), g_pGameRules->VecWeaponRespawnSpot( this ), GetLocalAngles(), GetOwnerEntity() );

	if ( pNewWeapon )
	{
		pNewWeapon->AddEffects( EF_NODRAW );// invisible for now
		pNewWeapon->SetTouch( NULL );// no touch
		pNewWeapon->SetThink( &CBaseCombatWeapon::AttemptToMaterialize );

		UTIL_DropToFloor( this, MASK_SOLID );

		// not a typo! We want to know when the weapon the player just picked up should respawn! This new entity we created is the replacement,
		// but when it should respawn is based on conditions belonging to the weapon that was taken.
		pNewWeapon->SetNextThink( gpGlobals->curtime + g_pGameRules->FlWeaponRespawnTime( this ) );
	}

With:

    CBaseEntity *pNewWeapon = CBaseEntity::Create( STRING(m_iClassname), g_pGameRules->VecWeaponRespawnSpot( this ), GetLocalAngles(), GetOwnerEntity() );

	if ( pNewWeapon )
	{
		pNewWeapon->KeyValue("weaponscriptname", GetClassname()); // pass along the script name
		pNewWeapon->Precache(); // precache it here to avoid prediction errors
		pNewWeapon->SetModel(GetWorldModel()); // fix bbox for world model
		pNewWeapon->AddEffects( EF_NODRAW );// invisible for now
		pNewWeapon->SetTouch( NULL );// no touch
		pNewWeapon->SetThink( &CBaseCombatWeapon::AttemptToMaterialize );

		UTIL_DropToFloor( this, MASK_SOLID );

		// not a typo! We want to know when the weapon the player just picked up should respawn! This new entity we created is the replacement,
		// but when it should respawn is based on conditions belonging to the weapon that was taken.
		pNewWeapon->SetNextThink( gpGlobals->curtime + g_pGameRules->FlWeaponRespawnTime( this ) );
	}

ai_basenpc.cpp

Certain lines in the file needs to be replaced with new to make the code compitable with custom loading script system. Go to src/game/server/ai_basenpc.cpp, find line with if ( pInteraction->iszMyWeapon != NULL_STRING && GetActiveWeapon()->m_iClassname != pInteraction->iszMyWeapon ), replace with if (pInteraction->iszMyWeapon != NULL_STRING && FClassnameIs(GetActiveWeapon(), pInteraction->iszMyWeapon.ToCStr())), find line with if ( pInteraction->iszTheirWeapon != NULL_STRING && pNPC->GetActiveWeapon()->m_iClassname != pInteraction->iszTheirWeapon ), replace with if (pInteraction->iszTheirWeapon != NULL_STRING && FClassnameIs(GetActiveWeapon(), pInteraction->iszTheirWeapon.ToCStr()).

To add script override support during NPC spawn via additionalequipment keyvalue, split by commad (for example weapon_smg1,weapon_mp5), find line with if (CapabilitiesGet() & bits_CAP_USE_WEAPONS), replace the following code:

	if (CapabilitiesGet() & bits_CAP_USE_WEAPONS)
	{	// Does this npc spawn with a weapon
		if ( m_spawnEquipment != NULL_STRING && strcmp(STRING(m_spawnEquipment), "0"))
		{
			CBaseCombatWeapon *pWeapon = Weapon_Create( STRING(m_spawnEquipment) );
			if ( pWeapon )
			{
				// If I have a name, make my weapon match it with "_weapon" appended
				if ( GetEntityName() != NULL_STRING )
				{
					pWeapon->SetName( AllocPooledString(UTIL_VarArgs("%s_weapon", STRING(GetEntityName()))) );
				}

				if ( GetEffects() & EF_NOSHADOW )
				{
					// BUGBUG: if this NPC drops this weapon it will forevermore have no shadow
					pWeapon->AddEffects( EF_NOSHADOW );
				}

				Weapon_Equip( pWeapon );
			}
		}
	}

With:

	if (CapabilitiesGet() & bits_CAP_USE_WEAPONS)
	{	// Does this npc spawn with a weapon
		if ( m_spawnEquipment != NULL_STRING && strcmp(STRING(m_spawnEquipment), "0"))
		{
			CBaseCombatWeapon *pWeapon;
			char szWeaponName[MAX_WEAPON_STRING] = "";
			char szScriptName[MAX_WEAPON_STRING] = "";

			//we can have a custom script name for the weapon on npc spawn, split by a comma
			const char* pComma = strchr(m_spawnEquipment.ToCStr(), ',');
			if (pComma)
			{
				size_t weaponLen = pComma - m_spawnEquipment.ToCStr();
				if (weaponLen > 0 && weaponLen < sizeof(szWeaponName))
				{
					V_strncpy(szWeaponName, m_spawnEquipment.ToCStr(), weaponLen + 1);
				}
				V_strncpy(szScriptName, pComma + 1, sizeof(szScriptName));

				m_spawnEquipment = MAKE_STRING(szWeaponName);
				pWeapon = CBaseEntity::Create(szWeaponName, Vector(0, 0, 0), QAngle(0, 0, 0), this);
			}
			else
				pWeapon = Weapon_Create( STRING(m_spawnEquipment) );
				
			if ( pWeapon )
			{
				// If I have a name, make my weapon match it with "_weapon" appended
				if ( GetEntityName() != NULL_STRING )
				{
					pWeapon->SetName( AllocPooledString(UTIL_VarArgs("%s_weapon", STRING(GetEntityName()))) );
				}

				if ( GetEffects() & EF_NOSHADOW )
				{
					// BUGBUG: if this NPC drops this weapon it will forevermore have no shadow
					pWeapon->AddEffects( EF_NOSHADOW );
				}

				//load script if not empty
				if (szScriptName[0] != '\0')
					pCombatWeapon->SetCustomWeaponScriptName(szScriptName);

				Weapon_Equip( pWeapon );
			}
		}
	}

multiplay_gamerules.cpp

Used m_iClassname directly in CMultiplayRules::DeathNotice( CBasePlayer *pVictim, const CTakeDamageInfo &info ) to get weapon classname, this needs a fix to work with a custom weapon script. Go to src/game/shared/multiplay_gamerules.cpp, replace lines killer_weapon_name = STRING( pInflictor->m_iClassname ); with killer_weapon_name = pInflictor->GetClassname(); so the method can use weapon script.

hl2mp/weapon_physcannon.cpp

In Half-Life 2: Deathmatch Half-Life 2: Deathmatch, owning multiple gravity guns will cause claws working only for the last picked up gravity gun, this is because all gravity guns share the same viewmodel entity, so gravity guns with different states cause conflicts. To fix this issue, go to src/game/shared/hl2mp/weapon_physcannon.cpp, find the CWeaponPhysCannon::OpenElements() method, add a check to compare active gravity gun with this and decide if it should override claws position. Replace:

void CWeaponPhysCannon::OpenElements( void )
{
	if ( m_bOpen )
		return;

	WeaponSound( SPECIAL2 );

	CBasePlayer *pOwner = ToBasePlayer( GetOwner() );

	if ( pOwner == NULL )
		return;

	SendWeaponAnim( ACT_VM_IDLE );

	m_bOpen = true;

	DoEffect( EFFECT_READY );

#ifdef CLIENT_DLL
	// Element prediction 
	m_ElementParameter.InitFromCurrent( 1.0f, 0.2f, INTERP_SPLINE );
	m_bOldOpen = true;
#endif
}

With:

void CWeaponPhysCannon::OpenElements( void )
{
	CBasePlayer* pOwner = ToBasePlayer(GetOwner());

	if (pOwner == NULL)
		return;

	//prevent this physcannon from overriding claws position if is not active
	if (pOwner->GetActiveWeapon() != this)
		return;

	if ( m_bOpen )
		return;

	WeaponSound( SPECIAL2 );

	SendWeaponAnim( ACT_VM_IDLE );

	m_bOpen = true;

	DoEffect( EFFECT_READY );

#ifdef CLIENT_DLL
	// Element prediction 
	m_ElementParameter.InitFromCurrent( 1.0f, 0.2f, INTERP_SPLINE );
	m_bOldOpen = true;
#endif
}

Find CWeaponPhysCannon::CloseElements(), add the same check. Replace:

void CWeaponPhysCannon::CloseElements( void )
{
	if ( m_bOpen == false )
		return;

	WeaponSound( MELEE_HIT );

	CBasePlayer *pOwner = ToBasePlayer( GetOwner() );

	if ( pOwner == NULL )
		return;

	SendWeaponAnim( ACT_VM_IDLE );

	m_bOpen = false;

	if ( GetMotorSound() )
	{
		(CSoundEnvelopeController::GetController()).SoundChangeVolume( GetMotorSound(), 0.0f, 1.0f );
		(CSoundEnvelopeController::GetController()).SoundChangePitch( GetMotorSound(), 50, 1.0f );
	}
	
	DoEffect( EFFECT_CLOSED );

#ifdef CLIENT_DLL
	// Element prediction 
	m_ElementParameter.InitFromCurrent( 0.0f, 0.5f, INTERP_SPLINE );
	m_bOldOpen = false;
#endif
}

With:

void CWeaponPhysCannon::CloseElements( void )
{
	CBasePlayer* pOwner = ToBasePlayer(GetOwner());

	if (pOwner == NULL)
		return;

	//prevent this physcannon from overriding claws position if is not active
	if (pOwner->GetActiveWeapon() != this)
		return;

	if ( m_bOpen == false )
		return;

	WeaponSound( MELEE_HIT );

	SendWeaponAnim( ACT_VM_IDLE );

	m_bOpen = false;

	if ( GetMotorSound() )
	{
		(CSoundEnvelopeController::GetController()).SoundChangeVolume( GetMotorSound(), 0.0f, 1.0f );
		(CSoundEnvelopeController::GetController()).SoundChangePitch( GetMotorSound(), 50, 1.0f );
	}
	
	DoEffect( EFFECT_CLOSED );

#ifdef CLIENT_DLL
	// Element prediction 
	m_ElementParameter.InitFromCurrent( 0.0f, 0.5f, INTERP_SPLINE );
	m_bOldOpen = false;
#endif
}

Find CWeaponPhysCannon::UpdateElementPosition(), add the same check. Replace:

void CWeaponPhysCannon::UpdateElementPosition( void )
{
	CBasePlayer *pOwner = ToBasePlayer( GetOwner() );

	float flElementPosition = m_ElementParameter.Interp( gpGlobals->curtime );

	if ( ShouldDrawUsingViewModel() )
	{
		if ( pOwner != NULL )	
		{
			CBaseViewModel *vm = pOwner->GetViewModel();
			
			if ( vm != NULL )
			{
				vm->SetPoseParameter( "active", flElementPosition );
			}
		}
	}
	else
	{
		SetPoseParameter( "active", flElementPosition );
	}
}

With:

void CWeaponPhysCannon::UpdateElementPosition( void )
{
	CBasePlayer *pOwner = ToBasePlayer( GetOwner() );

	//prevent this physcannon from overriding claws position if is not active
	if (pOwner && pOwner->GetActiveWeapon() != this)
		return;

	float flElementPosition = m_ElementParameter.Interp( gpGlobals->curtime );

	if ( ShouldDrawUsingViewModel() )
	{
		if ( pOwner != NULL )	
		{
			CBaseViewModel *vm = pOwner->GetViewModel(m_nViewModelIndex);
			
			if ( vm != NULL )
			{
				vm->SetPoseParameter( "active", flElementPosition );
			}
		}
	}
	else
	{
		SetPoseParameter( "active", flElementPosition );
	}
}

Picking up a gravity gun from the ground also causes misplaced glow and end cap sprites in Half-Life 2: Deathmatch Half-Life 2: Deathmatch, this is because the world and view models use different attachment point indexes. When the gravity gun appears as a physics object, it initialize all the sprites once, because it has no player owner at the moment, it also has no viewmodel to use, meaning it will use world model attachment point indexes as a reference. This is not related to the custom weapon script loading system, but because we can have multiple gravity guns now, this needs to be fixed by setting attachment points for the sprites every time method StartEffects() is called. Find the method, find:

    //Create the glow sprites
	for ( int i = PHYSCANNON_GLOW1; i < (PHYSCANNON_GLOW1+NUM_GLOW_SPRITES); i++ )
	{
		if ( m_Parameters[i].GetMaterial() != NULL )
			continue;

Replace with:

	//Create the glow sprites
	for ( int i = PHYSCANNON_GLOW1; i < (PHYSCANNON_GLOW1+NUM_GLOW_SPRITES); i++ )
	{
		if (m_Parameters[i].GetMaterial() != NULL)
		{
			//reattach the sprite in case to fix misplaced sprites on vm when weapon picked up
			if (ShouldDrawUsingViewModel())
			{
				m_Parameters[i].SetAttachment(LookupAttachment(attachNamesGlow[i - PHYSCANNON_GLOW1]));
			}
			else
			{
				m_Parameters[i].SetAttachment(LookupAttachment(attachNamesGlowThirdPerson[i - PHYSCANNON_GLOW1]));
			}

			continue;
		}

Also find:

	//Create the glow sprites
	for (int i = PHYSCANNON_ENDCAP1; i < (PHYSCANNON_ENDCAP1 + NUM_ENDCAP_SPRITES); i++)
	{
		if (m_Parameters[i].GetMaterial() != NULL)
			continue;

Replace with:

	//Create the glow sprites
	for (int i = PHYSCANNON_ENDCAP1; i < (PHYSCANNON_ENDCAP1 + NUM_ENDCAP_SPRITES); i++)
	{
		//reattach the sprite in case to fix misplaced sprites on vm when weapon picked ups
		if (m_Parameters[i].GetMaterial() != NULL)
		{
			m_Parameters[i].SetAttachment(LookupAttachment(attachNamesEndCap[i - PHYSCANNON_ENDCAP1]));
			continue;
		}

npc_citizen17.cpp

There is a hack used for citizen Matt to replace the crowbar with his pipe. With the custom script system, the hack is broken, causing a crowbar to show instead of a pipe, so we need to replace the hack with another workaround. Go to src/game/server/hl2/npc_citizen17.cpp, find the CNPC_Citizen::FixupMattWeapon() method. Instead of trying to fix the hack, just use the current weapon script system, so we need to fire the ChangeScript input. The pipe is also not designed to be picked up by the player, we can use Deny player pickup (reserve for NPC) (2) spawnflag for this. The flag needs to be used only when the owner is dead, because weapons don't save flags when dropped by NPCs, this is why the input/output system is used. Replace:

void CNPC_Citizen::FixupMattWeapon()
{
	CBaseCombatWeapon *pWeapon = GetActiveWeapon();
	if ( pWeapon && pWeapon->ClassMatches( "weapon_crowbar" ) && NameMatches( "matt" ) )
	{
		Weapon_Drop( pWeapon );
		UTIL_Remove( pWeapon );
		pWeapon = (CBaseCombatWeapon *)CREATE_UNSAVED_ENTITY( CMattsPipe, "weapon_crowbar" );
		pWeapon->SetName( AllocPooledString( "matt_weapon" ) );
		DispatchSpawn( pWeapon );

#ifdef DEBUG
		extern bool g_bReceivedChainedActivate;
		g_bReceivedChainedActivate = false;
#endif
		pWeapon->Activate();
		Weapon_Equip( pWeapon );
	}
}

With:

void CNPC_Citizen::FixupMattWeapon()
{
	CBaseCombatWeapon *pWeapon = GetActiveWeapon();
	if ( pWeapon && pWeapon->ClassMatches( "weapon_crowbar" ) && NameMatches( "matt" ) )
	{
		variant_t tmpVar;

		tmpVar.SetString(MAKE_STRING("weapon_mattpipe"));
		pWeapon->AcceptInput("ChangeScript", this, this, tmpVar, 0);

		//weapon doesn't save the prevent pick up flag when dropped, set the flag when needed (death in this case)
		tmpVar.SetString(MAKE_STRING("OnDeath matt_weapon:AddOutput:spawnflags 2:0.00:1"));
		AcceptInput("AddOutput", this, this, tmpVar, 0);
#ifdef DEBUG
		extern bool g_bReceivedChainedActivate;
		g_bReceivedChainedActivate = false;
#endif
	}
}

You can also remove the following code (because it's not used anymore):

class CMattsPipe : public CWeaponCrowbar
{
	DECLARE_CLASS( CMattsPipe, CWeaponCrowbar );

	const char *GetWorldModel() const	{ return "models/props_canal/mattpipe.mdl"; }
	void SetPickupTouch( void )	{	/* do nothing */ }
};

Note that the new code uses the weapon_mattpipe weapon script, so you need to add the script in the scripts folder. Here is the needed custom weapon script:

// Crowbar

WeaponData
{
	// Weapon data is loaded by both the Game and Client DLLs.
	"printname"	"#HL2_Crowbar"
	"viewmodel"				"models/weapons/v_crowbar.mdl"
	"playermodel"			"models/props_canal/mattpipe.mdl"
	"anim_prefix"			"crowbar"
	"bucket"				"0"
	"bucket_position"		"4"
	"bucket_360"				"2"
	"bucket_position_360"		"0"

	"clip_size"				"-1"
	"primary_ammo"			"None"
	"secondary_ammo"		"None"

	"weight"				"0"
	"item_flags"			"0"

	// Sounds for the weapon. There is a max of 16 sounds per category (i.e. max 16 "single_shot" sounds)
	SoundData
	{
		"single_shot"		"Weapon_Crowbar.Single"
		"melee_hit"			"Weapon_Crowbar.Melee_Hit"
		"melee_hit_world"	"Weapon_Crowbar.Melee_HitWorld"
	}

	// Weapon Sprite data is loaded by the Client DLL.
	TextureData
	{
		"weapon"
		{
				"font"		"WeaponIcons"
				"character"	"c"
		}
		"weapon_s"
		{	
				"font"		"WeaponIconsSelected"
				"character"	"c"
		}
		"ammo"
		{
				"font"		"WeaponIcons"
				"character"	"c"
		}
		"crosshair"
		{
				"font"		"Crosshairs"
				"character"	"Q"
		}
		"autoaim"
		{
			"file"		"sprites/crosshairs"
			"x"			"0"
			"y"			"48"
			"width"		"24"
			"height"	"24"
		}
	}
}

The result

Now you can use weapons with custom scripts, this is very useful for servers, if owners want to use their own weapon values, or for a singleplayer campaign, they can't access the server or client or don't want to add new weapon classes (for example because knife and crowbar are very similar). Players can own and use multiple weapons of the same class with different scripts, allowing you to add an unlimited amount of new weapons for your mod and maps, without the need to add new weapon classes. NPCs can own and appear with overridden weapons. Besides the new system is very flexible, it is also dynamic, because you can switch weapon's script with no issues at any moment on fly with some options, without deleting and giving new weapon.

The only downside is that the client have to download weapon script files from the server and use identical code in the files, otherwise this can lead to prediction errors, or even crashes. But this is still much easier then making all the fields networked and transfer them to client, especially if used fields with custom types.