Adding a New Weapon to Your TF2 Mod

From Valve Developer Community
Jump to navigation Jump to search
Icon-under construction-blue.png
This is a draft page. It is a work in progress open to editing by anyone.
Remember to check for any notes left by the tagger at this article's talk page.

This tutorial outlines how to add a new weapon for your TF2 mod. For the purposes of this tutorial, we will create a new scattergun for the scout.

This tutorial assumes you have an unmodified TF2 mod up and running, and you are able to compile and test your changes. You do not need to know how to write C++, but this tutorial will modify the code slightly to allow loading in a custom item schema.

Tip.pngTip:Your mod directory (<mod_dir>) is the folder which contains gameinfo.txt.

Introduction

To add weapons to your mod, you must define them in your item schema. The item schema (items_game.txt) is a JSON file that contains metadata for all items in Team Fortress 2. You can add items in one of two ways:

  1. Directly modify the existing items_game.txt file located at {{file|steam/steamapps/common/Team Fortress 2/tf/scripts/items/items_game.txt, or
  2. Create your own item schema and adjust the C++ code to load your custom schema instead of the default one.

The second approach is preferred, as the official items_game.txt file is extremely large (over 262,000 lines) and difficult to manage. Keeping your custom weapons in a separate file makes your mod more organized and maintainable. We will add a directive at the top of our item schema to include items_game.txt as a base, so the mod retains all existing TF items.


For this tutorial, we will create a new scattergun for the Scout called "The Blood Letter". It will act just like the stock scattergun but with a 20% damage penalty, and players shot by the weapon will bleed for 5 seconds. In the item schema, we will define The Blood Letter's name and description, make it inherit behaviour from the scattergun, and give it two attributes: bleeding duration and damage penalty.

Step 1: Add Your Weapon Strings to the Localization File

First, we need to define some Open the localization file 🖿<mod_dir>/resource/mod_tf_english.txt and add the following strings:

Note.pngNote:If you changed the name of your mod, this file must be renamed to <MOD_NAME>_english.txt. Otherwise, an error in the console will appear at startup stating the localization file cannot be found.
"lang"
{
"Language" "English"
"Tokens"
{
	"TF_MyStrings"				"Your strings go in this file."
	"TF_BloodLetter"			"The Blood Letter"
	"TF_BloodLetterDesc"		"This is the description for the Blood Letter."
}
}

Here, we create two tokens, "TF_BloodLetter" and "TF_BloodLetterDesc" and assign strings to them. You are free to name your tokens however you wish. We will use them inside the item schema by prefixing them with an '#' (i.e. #TF_BloodLetter).

Step 2: Define the Weapon in the Item Schema

Create a file named 🖿<mod_dir>/scripts/items/items_mod_tf.txt. You may give it any name you wish, just remember what you call it, as you will need to refer to it later in the C++ code. For the purposes of this tutorial, it will be called items_mod_tf.txt. This will be our item schema.

Open this file in a text editor, and add the following code:

#base items_game.txt

"items_game"
{
	"attributes"
	{
	}
	
	"items"
	{
	}
}

Next, we will define our weapon inside the "items" object:

	"items"
	{
		"21000"
		{
			"name"				"Bloodletter"
			"mod_tf_item"		"1"
			"prefab"			"weapon_scattergun"
			"item_name"			"#TF_BloodLetter"
			"item_description"	"#TF_BloodLetterDesc"
			"item_logname"		"Bloodletter"
			"item_quality"		"unique"
			"attributes"
			{
				"bleeding duration"
				{
					"attribute_class" 	"bleeding_duration"
					"value"				"5"
				}
				"damage penalty"
				{
					"attribute_class"	"mult_dmg"
					"value"				"0.80"
				}
			}
		}
	}

This is a rundown of what each keyvalue does:

  • "21000" - This is the item ID. Check items_game.txt to ensure you don't collide with any existing item IDs. 21000 is a good starting point.
  • "name" - This is the internal name used by the engine.
  • "mod_tf_item" - Indicates this is a mod-specific weapon. Will be used in the C++ code below.
  • "prefab" - The weapon/prefab to inherit from. We want to inherit from the Scattergun, so we select "weapon_scattergun". This prefab is defined on line 20246 of items_game.txt, and all keyvalues defined in this prefab also apply to our weapon, unless our weapon overrides it.
  • "item_name" - This is our weapon's display name, the one which will appear in the UI. #TF_BloodLetter was defined in the Localization file.
  • "item_description" - The description text.
  • "item_logname" - The weapon name as it appears in the console.
  • "item_quality" - The Item Quality. All possible quality types are defined on line 14 of items_game.txt.
  • "attributes" - This is an object containing all the weapon's attributes. A list of all possible attributes can be found here. The Blood Letter has a 5 second bleed and 20% damage penalty attribute. The attribute_class must be set correctly depending on the attribute name.

Step 3: Modify the Code to Read from the New Item Schema

Currently, the code reads from items_game.txt only. We will change the code to read our item schema.

Open Visual Studio (or your preferred text editor), and edit the following files.

Tip.pngTip:Use Ctrl+T in Visual Studio to search for files and Ctrl+G to goto line numbers.


econ_item_schema.h

On line 1603, add:

	bool			m_bModItem;

On line 1283, add:

	bool		IsModItem(void) const				{ return m_bModItem; }

On line 2615, add:

	typedef CUtlMap<int, CEconItemDefinition*, int>	ModItemDefinitionMap_t;
	const ModItemDefinitionMap_t& GetModItemDefinitionMap() const { return m_mapModItems; }

On line 2930, after BaseItemDefinitionMap_t m_mapBaseItems; add:

	ModItemDefinitionMap_t								m_mapModItems;


econ_item_schema.cpp

On line 2309, add:

m_bModItem(false),

On line 3181, add:

	m_bModItem = m_pKVItem->GetInt("mod_tf_item", 0) != 0;	// "mod_tf_item" matches the key we added in the item schema

On line 3810, add:

,	m_mapModItems(DefLessFunc(int))

On line 4305, add:

	m_mapModItems.Purge();

Around line 4425, inside BInitTextBuffer, comment out the if statement and replace with:

	//if ( m_pKVRawDefinition->LoadFromBuffer( NULL, buffer ) )
	// load the custom item schema instead. This, in turn, still loads the base schema (first line of our item schema is '#base items_game.txt').
	if ( m_pKVRawDefinition->LoadFromFile(g_pFullFileSystem, "scripts/items/items_mod_tf.txt", "GAME"))	// IMPORTANT: make sure the path matches the name of your item schema!
	{
		...

On line 5282, add:

	m_mapModItems.Purge();

On line 5353, add:

				if (pItemDef->IsModItem())
				{
					m_mapModItems.Insert(nItemIndex, pItemDef);
				}


tf_item_inventory.h

On line 199, add:

	CEconItemView*		AddModItem(int id);

On line 224, add:

	CUtlVector<CEconItemView*>	m_pModLoadoutItems;

On line 219, add:

	int                 GetModItemCount()			{ return m_pModLoadoutItems.Count(); }
	CEconItemView*		GetModItem(int iIndex)		{ return m_pModLoadoutItems[iIndex]; }


tf_item_inventory.cpp

On line 220, inside CTFInventoryManager::~CTFInventoryManager, add:

	m_pModLoadoutItems.PurgeAndDeleteElements();

Around line 232, write the implementation for CTFInventoryManager::AddModItem:

//-----------------------------------------------------------------------------
// Purpose: Generate Mod Items in backpack	
//-----------------------------------------------------------------------------
	CEconItemView* CTFInventoryManager::AddModItem( int id )
	{
		CEconItemView* pItemView = new CEconItemView;
		CEconItem* pItem = new CEconItem;
		pItem->m_ulID = id;
		pItem->m_unAccountID = 0;
		pItem->m_unDefIndex = id;
		pItemView->Init(id, AE_USE_SCRIPT_VALUE, AE_USE_SCRIPT_VALUE, false);
		pItemView->SetItemID(id);
		pItemView->SetNonSOEconItem(pItem);
		m_pModLoadoutItems.AddToTail(pItemView);
		return pItemView;
	}

On line 256, inside CTFInventoryManager::GenerateBaseItems, add:

	m_pModLoadoutItems.PurgeAndDeleteElements();

On line 272, at the end of CTFInventoryManager::GenerateBaseItems, add:

	// Add mod items
	const CEconItemSchema::BaseItemDefinitionMap_t& mapItemsMod = GetItemSchema()->GetModItemDefinitionMap();
	iStart = 0;
	if (mapItemsMod.Count() != 0)
	{
		for (int it = iStart; it != mapItemsMod.InvalidIndex(); it = mapItemsMod.NextInorder(it))
			AddModItem(mapItemsMod[it]->GetDefinitionIndex());
		Msg("Loaded %i mod items.\n", mapItemsMod.Count());
	}

On line 297, between CEconItemView *pItem = m_LocalInventory.GetInventoryItemByItemID( iItemID ) and if (!pItem), add:

	if (iItemID < 100000)
	{
		int count = TFInventoryManager()->GetModItemCount();
		for (int i = 0; i < count; i++)
		{
			pItem = TFInventoryManager()->GetModItem(i);
			if (pItem && pItem->GetItemDefIndex() == iItemID)
				break;
		}
	}

On line 369, inside CTFInventoryManager::GetAllUsableItemsForSlot, after the for loop, add:

	iCount = m_pModLoadoutItems.Count();
	for (int i = 0; i < iCount; i++)
	{
		CEconItemView* pItem = m_pModLoadoutItems[i];
		CTFItemDefinition* pItemData = pItem->GetStaticData();
		if (!bIsAccountIndex && !pItemData->CanBeUsedByClass(iClass))
			continue;
		if (iSlot >= 0 && pItem->GetStaticData()->GetLoadoutSlot(iClass) != iSlot)
			continue;
		pList->AddToTail(pItem);
	}

Around line 1095, replace CTFPlayerInventory::EquipLocal with:

void CTFPlayerInventory::EquipLocal(uint64 ulItemID, equipped_class_t unClass, equipped_slot_t unSlot)
{
	// These interactions normally result from a round-trip with the GC.
	// We will never get those messages, so we do everything locally.

	// Unequip whatever was previously in the slot.
	itemid_t ulPreviousItem = m_LoadoutItems[unClass][unSlot];
	if (ulPreviousItem != 0 && ulPreviousItem < 100000)
	{
		int count = TFInventoryManager()->GetModItemCount();
		for (int i = 0; i < count; i++)
		{
			CEconItemView* pItem = TFInventoryManager()->GetModItem(i);
			if (pItem && pItem->GetItemDefIndex() == ulPreviousItem)
				pItem->GetSOCData()->UnequipFromClass(unClass);
		}
		CEconItemView* pPreviousItem = GetInventoryItemByItemID(ulPreviousItem);
		if (pPreviousItem) {
			pPreviousItem->GetSOCData()->UnequipFromClass(unClass);
		}
	}
	else
	{
		CEconItemView* pPreviousItem = GetInventoryItemByItemID(ulPreviousItem);
		if (pPreviousItem)
			pPreviousItem->GetSOCData()->UnequipFromClass(unClass);
	}

	// Equip the new item and add it to our loadout.
	if (ulItemID < 100000)
	{
		int count = TFInventoryManager()->GetModItemCount();
		CEconItemView* pItem;
		for (int i = 0; i < count; i++)
		{
			pItem = TFInventoryManager()->GetModItem(i);
			if (pItem && pItem->GetItemDefIndex() == ulItemID)
			{
				pItem->GetSOCData()->Equip(unClass, unSlot);
				break;
			}
		}
		if (!pItem)
		{
			pItem = TFInventoryManager()->AddModItem(ulItemID);
			if (pItem && pItem->GetItemDefIndex() == ulItemID)
				pItem->GetSOCData()->Equip(unClass, unSlot);
		}
	}
	else
	{
		CEconItemView* pItem = GetInventoryItemByItemID(ulItemID);
		if (pItem)
			pItem->GetSOCData()->Equip(unClass, unSlot);
	}

	m_LoadoutItems[unClass][unSlot] = ulItemID;

#ifdef CLIENT_DLL
	int activePreset = m_ActivePreset[unClass];
	m_PresetItems[activePreset][unClass][unSlot] = ulItemID;

	GTFGCClientSystem()->LocalInventoryChanged();
#endif
}

Around line 1552, inside CTFPlayerInventory::GetItemInLoadout, add to the end:

			// To protect against users lying to the backend about the position of their items,
			// we need to validate their position on the server when we retrieve them.
			if ( pItem && AreSlotsConsideredIdentical( pItem->GetStaticData()->GetEquipType(), pItem->GetStaticData()->GetLoadoutSlot( iClass ), iSlot ) )	// keep this line
				return pItem;	// keep this line

			if (m_LoadoutItems[iClass][iSlot] < 100000)
			{
				int count = TFInventoryManager()->GetModItemCount();
				for (int i = 0; i < count; i++)
				{
					CEconItemView* pItem = TFInventoryManager()->GetModItem(i);
					if (pItem && pItem->GetItemDefIndex() == m_LoadoutItems[iClass][iSlot])
					{
						if (pItem && AreSlotsConsideredIdentical(pItem->GetStaticData()->GetEquipType(), pItem->GetStaticData()->GetLoadoutSlot(iClass), iSlot))
							return pItem;
					}
				}
				return TFInventoryManager()->AddModItem(m_LoadoutItems[iClass][iSlot]);
			}

		}
	}
	return TFInventoryManager()->GetBaseItemForClass( iClass, iSlot );
}


tf_gc_server.cpp

Around line 4391, inside CTFGCServerSystem::SDK_ApplyLocalLoadout, at the end of the method, comment out and add:

			pTFInventory->EquipLocal(uItemId, iClass, iSlot); 

			/*
			CEconItem* pItem = (CEconItem*) pItemCache->FindSharedObject(soIndex);
			if (pItem) {
				pTFInventory->EquipLocal(uItemId, iClass, iSlot);
			}
			else {
				Warning("Failed to find item %llu in shared object, but client says it should be equipped by [%i] in slot [%i].\n", uItemId, iClass, iSlot);
			}
			*/
}

Around line 3605, move the #endif // USE_MVM_TOUR derective just before the end of CTFGCServerSystem::SendMvMVictoryResult (around line 3618)

...
#ifdef USE_MVM_TOUR
		if ( !m_mvmVictoryInfo.m_sMannUpTourOfDuty.IsEmpty() )
		{
			msg.set_tour_name_mannup( m_mvmVictoryInfo.m_sMannUpTourOfDuty );
		}
//#endif // USE_MVM_TOUR	<--- COMMENT OUT
		msg.set_lobby_id( m_mvmVictoryInfo.m_nLobbyId );
		msg.set_event_time( m_mvmVictoryInfo.m_tEventTime );

		FOR_EACH_VEC( m_mvmVictoryInfo.m_vPlayerIds, iMember )
		{
			CMsgMvMVictory_Player *pMsgPlayer = msg.add_players();
			pMsgPlayer->set_steam_id( m_mvmVictoryInfo.m_vPlayerIds[ iMember ]);
			pMsgPlayer->set_squad_surplus( m_mvmVictoryInfo.m_vSquadSurplus[ iMember ] );
		}

		ReliableMsgQueue().Enqueue( pReliable );
	}
#endif // USE_MVM_TOUR	<--- MOVE HERE
}
...


loadout_preset_panel.cpp

Finally, on line 239, add this check inside CLoadoutPresetPanel::UpdatePresetButtonStates():

	if (!steamapicontext->SteamUser())
		return;

Step 4. Compile and Test Your Mod

Compile the code (F6 in Visual Studio) and then run your mod.

Open the laodout for the Scout and see if you can equip your weapon in the primary slot.

Create a server and test your weapon in game. You can test your weapon with bots by typing bot add in the console.

Helpful Resources