Dynamic Weapon Spawns (Advanced)

From Valve Developer Community
Jump to: navigation, search
Icon-broom.png
This article or section should be converted to third person to conform to wiki standards.


Edit
This page has not been translated into your language or even into English, so we have nothing to show you. You can still view the page in other languages if it is translated into them.
If you have already created language subpages, you need to purge the main page in order to display them.

{{ }}


Introduction

This article expands on Dynamic_Weapon_Spawns. The mechanic used was to have a model entity that spawned a set of weapons depending on the values of six ConVars, all of this at the beginning of the round.

Each ConVar would represent an slot of a Weapon Set. Weapon Sets may not just spawn weapons but other items like ammo boxes, armor or health.

Objectives

  • Using a resource file with the contents of Weapon Sets: weaponsets.txt
  • Acquiring the ability to modify, switch, load or save Weapon Sets at any time during the round using ConCommands with their respective AutoComplete functions.
  • Using a Point_Entity CWeaponSetEnt to spawn other entities: Weapons or other items like armor or health.
  • Allowing Inputs/Outputs to be applied on the spawned items. i.e. must allow OnPlayerPickUp and it must triggered when the player picks up the spawned weapon.
  • Acquiring the ability to set the "charge" for the items to spawn (ammo, amount of health, armor to recover, etc).
  • Acquiring the ability to force the spawning of a specific item regardless of the weapon set configuration file contents.
  • Including 6 special spawn rules:
    • <none> which spawns nothing
    • <random>
    • <random-wpn> spawns a random weapon
    • <random-amm> spawns ammo for a random weapon
    • <allweaps-old> divides the items available in tiers, like High, Medium and Lower tier Weapons and only spawns certain tier depending of the slot number.
    • <allweaps-new> uses the same tier division but spawns more than one tier per slot, each tier has a different chance of being spawned at each slot.
  • If the set doesn't have enough slots defined to cover the needs of a map then the values wrap around the Weapon Set. (I.e. a set with only 2 slots and a map entity that requires slot 7, after wrapping 7 around 2 it would end up using slot 1).

The resource file weaponsets.txt

"WeaponSets"
{
	"WeaponSet"
	{
		"Name"			"Pistols"
		"Slots"
		{
			"1"		"357"
			"2"		"ammo_pistol_large"
			"3"		"pistol"
			"4"		"pistol"
			"5"		"ammo_pistol"
		}
	}
	"WeaponSet"
	{
		"Slots"
		{
			"1"		"357"			//only 1 slot
		}
		"Name"			"357s"			//order doesn't matter, Name is last
	}
	"WeaponSet"
	{
		"Name"			"Automatics"
		"Slots"
		{
			"1"		"ar2"
			"2"		"ammo_ar2_secondary"
			"5"		"ammo_ar2"		//there's no 4
			"3"		"smg1"			//3 is after 5
			"6"		"ammo_smg1"
			"7"		"<none>"		//same effect as not listing it
			"8"		"ammo_smg1_grenade"
		}
	}
}

The file goes in the mod folder, next to maplist.txt. In the example above it defines three Weapon Sets: Pistols, 357s and Automatics.

  • 'Pistols' has five slots 1-5 with different items on each slot.
  • 357s only has one slot that spawn the weapon 357.
  • Automatics has eight slots (because the maximum slot is 8), they are listed in disorder and slot 4 is missing, missing slots will spawn "<none>" (nothing).

At map startup the first set in the list loads, in this case Pistols. This set may be modified by using console commands.

Something to note is that each set has a different number of slots, but they should be able to be used on the same map, this map having been constructed to use 'n' different slots. In the case of a map created to use up to slot 7:

  • When being played with Pistols.
    • entities using slot 1 spawn "357".
    • slot 2 spawns "ammo_pistol_large".
    • ...
    • slot 5 spawns "ammo_pistol".
    • there is no slot 6! It will wrap around 5 -> it will use slot 1.
    • slot 7 uses slot 2.
  • When being played with 357s since there's only one slot, after wrapping around, all entities will spawn slot 1, "357".
  • When being played with Automatics, there will be a slot for each entity.
    • 4 is undefined so it will spawn nothing.
    • 7 on the other hand is defined as nothing.
    • slot 8 from the set will stay unused because the map only uses up to slot 7.


Server admins may edit the configuration file and have 'n' slots on any given set, they just have to add a line with the slot number they want. Since level designers will also be able to enter any slot number (some crazy value like 9999 is possible and will work) this may end in confusion. It's the role of the dev team to ship their MOD with base weaponset configurations showing a pattern, the following is an example:

slot 1:		High Power Weapon
slot 2:		High Power Ammo
slot 3:		High Power (Secondary) Ammo

slot 4-5-6:	Medium Power Weapon	- Ammo - (Secondary) Ammo
slot 7-8-9:	Low Power Weapon	- Ammo - (Secondary) Ammo
slot 10:	Armor
slot 11:	Health
slot 12-13-14:	High Power Weapon	- Ammo - (Secondary) Ammo
slot 15-16-17:	Medium Power Weapon	- Ammo - (Secondary) Ammo
slot 18-19-20:	Low Power Weapon	- Ammo - (Secondary) Ammo

The entity FGD definition

The FGD entry to use as a base for the coding.

@include "hl2mp.fgd"

//@PointClass base(Weapon, EnableDisable) iconsprite("editor/item_weaponset.vmt") = item_weaponset :     "A weapon spawn point that uses weapon sets"
@PointClass base(Weapon, EnableDisable) studio("models/items/357ammo.mdl") = item_weaponset : "A weapon spawn point that uses weapon sets"
[
	spawnitem(choices) : "Spawn what" : -20 : "What item should spawn? Unless 'Use Weapon Set' is selected the attribute <weaponsetslot> won't be considered." =
	[
		-20:	"Use Weapon Set"
		-5:	"All Weapons Classic"
		-4:	"All Weapons Modern"
		-3:	"True Random"
		-2:	"Spawn Random Weapons"
		-1:	"Spawn Random Ammo"
		0:	"Spawn Nothing"
		1:	"weapon_ar2"
		2:	"weapon_pistol"
		3:	"weapon_smg1"
		4:	"weapon_357"
		5:	"weapon_xbow"
		6:	"weapon_shotgun"
		7:	"weapon_ml"
		8:	"weapon_stunstick"
		9:	"item_grenade (1)"
		10:	"weapon_slam (1)"
		11:	"item_ammo_ar2 (20)"
		12:	"item_ammo_ar2_secondary (1)"
		13:	"item_ammo_pistol (20)"
		14:	"item_ammo_smg1 (45)"
		15:	"item_ammo_357 (6)"
		16:	"item_xbow_bolt (1)"
		17:	"item_box_buckshot (20)"
		18:	"item_ml_grenade (1)"
		19:	"item_smg1_grenade (1)"
		20:	"item_ammo_pistol_large (100)"
		21:	"item_ammo_smg1_large (225)"
		22:	"item_ammo_ar2_large (100)"
		23:	"item_ammo_357_large (20)"
		24:	"item_health (1)"
		25:	"item_battery (100)"
	]

	weaponsetslot(integer)	: "Weapon Set slot" : 1 : "What Weapon Set slot will this entity use,  random slot if 0. Needs <spawnitem> to be 'Use Weapon Set'. Sets defined in weaponsets.txt."

	howtogiveammo(choices) : "How To Give Ammo" : 0 : "Consider health, armor or whatever the item gives as 'ammo', 'PrimaryAmmo' if it is the only thing they give.\n" +
	"0: Default - If 'Box of Rounds' always gives 10 ammo then give 10 ammo.\n" +
	"1: Absolute - If <ammovalue> says to give 1 ammo give 1 ammo despite the default being 10.\n" +
	"2: Relative - If <ammovalue> says to give 0.5 ammo (50%), a pistol that takes up to 50 rounds and 20 in the magazine will receive 50+20 = 70 * 0.5 = 35 rounds.\n" +
	"   In the case of health we can say the recipient is the player, so he would get 50% health recovered. Same for Armor, and other items." =
	[
		0:	"0-Use Default for item"
		1:	"1-Use Ammo Value as Absolute Value"
		2:	"2-Use Ammo Value as Relative Value (% of Max)"
		3:	"3-Use Custom Ammo String"
	]

	ammovalue(string)	: "Ammo Value" : "" : "Read as integer or float. Only affects 'Primary Ammo'."

	customammostring(string) : "Custom Ammo String" : ""
			: "For items giving two types of ammo, weapons, with different values." +
			" #<GiveIndex> <value> #<GiveIndex> <value>... i.e. primary set as Default" +
			" and Secondary set as Relative 50%: '#0 0 #2 0.5'"

	//spawnflags(Flags) = [ ] the base class weapon has a flag Start constrained
]
base(Weapon)
By using Weapon as the base then this entity inherits the inputs and outputs and Name and Orientation parameters. It also gets the Start Constrained flag.
base(EnableDisable)
By using EnableDisable as the base then this entity inherits the inputs and outputs required for enabling / disabling this entity.
spawnitem(choices)
If "Use Weapon Set" is chosen it will use the weapon set loaded from weaponset.txt using the value of weaponsetslot. If other option is chosen it will just use the selected option without taking into consideration weapon sets.
weaponsetslot(integer)
What slot from the set to read from? The level designer has to decide the slot to use: 1, 2, 3, etc. Will only work if spawnitem has the value "Use Weapon Set".


The last two define what item to spawn, the following help modify the charge of the item. From them, customammostring is the only thing level designers need, however it is a bit too obscure to understand it initially. howtogiveammo and ammovalue are used to try to make this transition a bit easier, it may prove just to add to the confusion... why are there so many options? what does this do?

Feel free to remove customammostring if you prefer to make things as simple as possible, or to remove the other two if you want to force level designers to use the more powerful tool.


Note.pngNote:In the following explanation, Primary ammo is used to define the first type of ammo held by an item, Secondary ammo as the second type of ammo held by an item. The secondary ammo of the SMG1 is the smg1 grenade, however, an ammo box holding smg1 grenades (and nothing else) has no secondary ammo, it only holds primary ammo, smg1 grenades.


howtogiveammo(choices) / ammovalue(string)
These exist so level designers can use this entity without having to deal with the Custom Ammo String, however they are not necessary as the custom ammo string can do the same and more.
  • ammovalue holds a number, depending on the value of howtogiveammo it will be read as integer or float.
  • howtogiveammo usage:
    • 0: Default - If Box of Rounds always gives 10 ammo then give 10 ammo. This option doesn't use ammovalue or the custom string.
    • 1: Absolute - If ammovalue is 1 ammo give 1 ammo despite the default being 10.
    • 2: %Max-Ammo - If the pistol that will receive the ammo takes up to 500 rounds and 50 on its magazine and ammovalue is 0.5 (50%) then give 500+50 = 550 * 0.5 = 275 rounds. A value of 0 means 0%, 1 means 100%. A value of 3 would mean 300% (triple) but it would be unnecessary because something can only hold up to 100% of its maximum charge.
    • 3: Use Custom String - Do not use ammovalue and instead parse customammostring.


customammostring(string)
It can be used for items that store 'n' types of ammo, provided such items exist in the mod. When using index 0 for Default the ammo value used doesn't really matter, so another 0 as the value would be prefered.
The syntax for the string is as follows:
#<GiveIndex> <Value> #<GiveIndex> <Value> ...
Note the #s and spaces, replace the <param>s by the values to be used.
  • <GiveIndex> is the index value of each howtogiveammo option (Default: 0, Absolute: 1, Relative: 2, Custom String: Can't, being used now!).
  • <Value> represents the value of ammovalue.
The first #<Give> <Value> pair sets the PRIMARY ammo, the second sets the SECONDARY ammo. After some SDK modifications, the third pair would set the TERTIARY ammo and so on, that makes the custom string more powerful than howtogiveammo and ammovalue. which can only set the primary ammo.


Set ar2's primary ammo to 1 bullet and secondary ammo to 50% of max (50% of 3 = 1 rounded down).

spawnitem(choices)		= 1: "weapon_ar2"
weaponsetslot(integer)		= blank, not using weapon sets

//Set both primary and secondary ammo, we need the custom string.
howtogiveammo(choices)		= 3: 3-Take values from Custom Ammo String
ammovalue(string)		= blank, using custom string instead

//Absolute 1 bullet, Relative 50%
customammostring(string)	= #1 1 #2 0.5

Use the 100th slot of Weapon Sets if possible. Don't care about the secondary ammo if any, but the primary ammo must hold 50 rounds of ammo.

spawnitem(choices)		= -20: "Use Weapon Set"
weaponsetslot(integer)		= 100

//Set the primary ammo
howtogiveammo(choices)		= 1: 1-Use ammovalue as Absolute Value
ammovalue(string)		= 50
customammostring(string)	= blank, not using it


In any case, ammo can be set to zero, useless for ammo boxes but works with weapons.

The code

Main structures

class CWeaponSetEnt;	//Map Entity, can't belong to a namespace because of macros
			//defining their own inside
class CItem_ModAmmoCrate;	//Another entity, this is an ammo box that holds any type
				//of ammo, I use it to make my life simple.
				//You can use hl2mp's ammo box entities, after adding the
				//ability to modify the amount of ammo they give.

namespace WeaponSetH { //Namespace, just to keep things in order
	struct ItemDef; //Holds constants to make things easier to read
	struct Item;	//Holds data about items
	class CWeaponSet; //Static class that does all the coordination

	struct AllWeapChance;	//Holds data of slots and tiers associated to them
				//to be used by <allweaps-old> and <allweaps-new>
};

Obviously they are using a namespace, so just pretend I used namespace WeaponSetH in the following code blocks.

  • CWeaponSetEnt will use the entity named "item_weaponset"
  • CItem_ModAmmoCrate will use the entity named "item_modammocrate"
Blank image.pngTodo: Change the "ModAmmoCrate" to some better name... MOD-ified is fine for the prototype but not for final code.


ItemDef

ItemDef is just a bunch of integer constants in a struct instead of a enumeration. Why a struct? because the constants do not belong to a single, continuous, enumeration. Also, it allows the use of ItemDef:: to quickly access the constants.

The SPAWN constants are used as itemIndexes (IDs). Despite the un-optimization we could use string indexes over integers (itemNicks), but we require the integers to obtain the Ammo Index (some hl2mp var, see hl2mp_gamerules.cpp::GetAmmoDef, more on this later) and also a way to map the indexes the Hammer entity will use to Item instances (see FGD::spawnitem).

The TIER constants define Item Tiers, the way they are defined it only makes possible to link an item to only one tier unless we used arrays. To allow items to belong to multiple tiers without complicating things, you could make them bit flags and did the proper modifications to the couple of functions that uses this.

Three more constants, they are utility constants, see the code-comments to understand a bit about them.


Item

We know that item/entity definitions (Weapons, Ammo Boxes, Batteries, etc) will not change in the middle of the game. So, after initialization they will only be used to get data from by the use of getter functions.

//Contains an item definition
struct Item {
	int itemIndex;	//This Item Index is according to WeaponSetH::ItemDef SPAWN constants
	int ammoCount;	//Base ammo count, used for Ammo Boxes
	char* entityName;	//The entity name associated to this item
	char* itemNick;	//A nick, ent names are long, this way names are easier to users
	int itemTier;	//specific for All Weapons, but may be used for other things
};
//const int itemListSize;	defined in next block
//WeaponSetH::Item itemList[];	defined in next block

WeaponSetH::Item* GetItemByItemIndex(int index);	//ItemIndex is a member of the struct
WeaponSetH::Item* GetItemByNick(const char* itemNick);	//ItemNick is another member
WeaponSetH::Item* GetItemByArrayIndex(int index);	//The array index comes from
							//itemList[] above

//'slot' = -1 means no slot, 'spawnType' will be taken as a forced item.
//If 'spawnType' equals SPAWN_SET then return whatever the set defines for 'slot'
WeaponSetH::Item* GetBoundItem(int spawnType, int slot = -1);

//Return an item that is valid to spawn (no <set>, <random>, <allweaps-old or new>)
//Process if not spawnable.
WeaponSetH::Item* GetSpawnableItem(int spawnType, int slot);

WeaponSetH::Item* GetItemInAllWeaponsClassicMode(int slot); //Process <allweaps-old>
WeaponSetH::Item* GetItemInAllWeaponsModernMode(int slot); //Process <allweaps-new>

//Wraps an integer value around another, is used to wrap high slot
//numbers around the ones present on the weapon set
int WrapValueAround(int wrapThis, int aroundThis);


If you check the code (weaponset.h), itemList[] and itemListSize are the way they are because it's easier to do maintenance to that initialization matrix. A struct that only defined members can be initialized that way, they are called Aggregates (Initializing Aggregates).

If you added a new weapon/item to your mod and had to include it on the list you would just add a line and compile. The itemList would initialize and itemListSize recalculate. You might have to add a SPAWN and/or TIER constant to WeaponSetH::ItemDef as well.

// {itemType, ammoCount, entityName, nickName, itemTier },
WeaponSetH::Item itemList[] = {
   {ItemDef::SPAWN_ALLWEAPONS_CLASSIC,	0,
		"",			"<allweaps-old>",	ItemDef::TIER_NOSPAWNABLE	},

   {ItemDef::SPAWN_NOTHING,		0,
		"",			"<none>",		ItemDef::TIER_NOTHING		},

   {ItemDef::SPAWN_PISTOL,		0,
		"weapon_pistol",	"pistol",		ItemDef::TIER_LOWWEAPON		},

   {ItemDef::SPAWN_AMMO_PISTOL,	20,
		"item_mod_ammocrate",	"ammo_pistol",		ItemDef::TIER_LOWAMMO		},

   {ItemDef::SPAWN_HEALTH,		25,
		"item_health",		"health",		ItemDef::TIER_OTHERS		},

   {ItemDef::SPAWN_ARMOR,		100,
		"item_battery",		"armor",		ItemDef::TIER_OTHERS_ARMOR	},
};
const int itemListSize = sizeof(WeaponSetH::itemList) / sizeof(WeaponSetH::Item);

As for the getters they return a pointer to a member of the itemList array. Now, in case of failure they would return NULL, but I set them to return the Item NOTHING (which would spawn nothing). Whether this is right or not to you it is just a matter or adding NULL checks where appropriate.

AllWeapChance

AllWeapons is just about game mechanics, including it or not won't prevent you from having Dynamic Weapon Spawns. You can skip this if you want.

Ok remember the structure of a Weapon Set?, A weapon set has a name and numbered slots with items. Since AllWeapons are a special case we make a structure for it, it has slots but doesn't require a name, instead it has probabilities.

struct AllWeapChance {
	int slot;
	int itemTier;
	bool classicChance;	//classic(old), belongs here or not. All items part
				//of the Tiers of this slot here have equal chance of spawning
	float modernChance;	//modern(new) Actual chances of spawning the Tier
};
//AllWeapChance allWeapChanceList[];	see next block
//const int allweapbasestructNumSlots;	see next block
//const int allweapchanceListSize;	see next block

AllWeaps classic mechanics

Let's pretend we define that according to the base structure for <allweaps-old> any "item_weaponset" entity marked as slot 1 would be able to spawn items with Tier TIER_HIGHWEAPON and TIER_MEDWEAPON. There would be entries for this struct with slot = 1, the appropriate itemTiers and classicChance = true.

Now during spawn a random number is generated. We have two Tiers competing for this slot, so we first collect all the Items assigned to these Tiers, and after that all the Items collected compete with equal chances, i.e. there are 4 items assigned to TIER_HIGHWEAPON and 6 to TIER_MEDWEAPON, so each item has 0.1 chance of spawning.

AllWeaps modern mechanics

Now we have the same case (HIGH and MED at slot 1). Since we are defining Modern chances we don't care about AllWeapChance::classicChance, instead we will set AllWeapChance::modernChance. The sum of modernChances for a single slot should be 1.

Here we generate a random number too. We FIRST select a Tier depending on their modernChance value, AFTER-THAT we must select between the many items that can be spawned at that Tier, they all compete with equal chances.

I.e. at slot 1, TIER_HIGHWEAPON which contains 2 items has a modernChance of 0.8, TIER_MEDWEAPON contains 4 items and a chance of 0.2. Doing he math, the overall chance for an item of HIGHWEAPON to be selected would be 0.4 and the chances for an item of MEDWEAPON 0.05.

Initialization

During initialization, if you miss an entry for a TIER then the both chances will be 0, good shortcut.

// {slot, tier, classicChance, modernChance },
AllWeapChance allWeapChanceList[] = {
	{1,		ItemDef::TIER_HIGHWEAPON,	true,	0.80f},
	{1,		ItemDef::TIER_MEDWEAPON,	false,	0.15f},
	{1,		ItemDef::TIER_LOWWEAPON,	false,	0.05f},
	//etc up to slot 20
};
const int allweapbasestructNumSlots = 20; //Equal to the max slot of the base struct, 20
const int allweapchanceListSize = sizeof(WeaponSetH::allWeapChanceList)
		/ sizeof(WeaponSetH::AllWeapChance); //calculates the size of the array


CItem_ModAmmoCrate

Ok back to Dynamic Spawns, this is a class that prevents some boring editing of Valve classes. You can find them at items_world.cpp.

This entity will hold any kind of ammo as defined in AmmoDef. Read items.h, hl2mp_gamerules.cpp, ammodef.h, etc.

#include "items.h" //required for CItem

class CItem_ModAmmoCrate : public CItem {
	
	DECLARE_CLASS(CItem_ModAmmoCrate, CItem);
	DECLARE_DATADESC();
	
	int m_iAmmoType;	//Ammo Type, an ammo index/ID used by hl2mp
	int m_iAmmoAmount;	//Amount of ammo to give when picked up
				//The default ammo crates don't have this.
	
public:
	void Spawn(void);
	void Precache(void);
	bool MyTouch(CBasePlayer *pPlayer);
	void SetAmmoType(int type);	//setters
	void SetAmmoAmount(int ammo);	
};
BEGIN_DATADESC( CItem_ModAmmoCrate )
	DEFINE_KEYFIELD( m_iAmmoType, FIELD_INTEGER, "AmmoType" ),
	DEFINE_KEYFIELD( m_iAmmoAmount, FIELD_INTEGER, "AmmoAmount" ),
END_DATADESC()

LINK_ENTITY_TO_CLASS(item_mod_ammocrate, CItem_ModAmmoCrate);

//Different from items_world.cpp::ITEM_GiveAmmo() in that it gives a forced value
//CItem_ModAmmoCrate::m_iAmmoAmount and doesn't make a query depending on Ammo Type.
int ITEM_GiveForcedAmmo(CBasePlayer *pPlayer, float flCount,
		   const char *pszAmmoName, bool bSuppressSound = false);

This entity isn't included in the FGD since it is more of a utility entity and by using item_weaponset you rule out every item entity from Hammer (ideally you would remove them all from the FGD).


CWeaponSet

What this class has to store is a collection of WeaponSets, which have a Name and a collection of Slots. Each Slot containing an index and an Item. Now instead recreating the structure the solution presented only uses KeyValues.

The functions are designed so it is possible to be applied not only to the current Weapon Set but to any set loaded. The current ConCommands do not make use of this ability. The defaulting to NULL on most arguments means "use current set".

There are extra functions that aren't listed here, they are not used (like NewWeaponSet()) or overloaded functions.

class WeaponSetH::CWeaponSet {
public:
	class EntList;	//container util class
	static EntList *wsEnts;	//weaponset-Entities references CWeaponSetEnt entities created
	
	//Pointer to the root of the KeyValues structure loaded from weaponsets.txt
	static KeyValues *weaponSetFile;

	//Pointer to the root of the WeaponSet selected right now
	static KeyValues *currentSet;

	//Since this is a static class it uses a refCount to call Init, and delete pointers
	static int refCountOfMe;

	CWeaponSet();
	~CWeaponSet();

	static void Init(void);

	//Here we got what we wanted, load, save, modify, add, remove and rename
	static void LoadAllWeaponSetsFromFile(KeyValues** loadHere = NULL);
	static void SaveAllWeaponSetsToFile(void);

	static bool RenameWeaponSet(const char* from, const char* to);
	static bool AddSlotToWeaponSet(WeaponSetH::Item* item, const char* setName = NULL);
	static bool RemoveSlotFromWeaponSet(int slot, const char* setName = NULL);
	static bool ModifySlotOfWeaponSet(int slot, WeaponSetH::Item* item,
								const char* setName = NULL);


	static int GetNumberOfSets(void); //Number of weapon sets loaded

	//Given a Weapon Set and slot get the Item assigned to them
	static WeaponSetH::Item* GetItemForSlotOnWeaponSet(int slot, const char* setName =  NULL);

	//The index is the order in which they appear on the KeyValues, starts at 1
	static KeyValues* GetWeaponSetByIndex(int index);
	static KeyValues* GetWeaponSetByName(const char* setName,
					KeyValues* fromHere =  weaponSetFile);

	static bool SetCurrentWeaponSet(const char* setName);
	static void SetFirstAvailableAsCurrentSet(void); //When we load the weapon sets
							//select the first weapon set available

	//Functions to keep track of CWeaponSetEnt using (EntList)wsEnt member
	static void RegisterNewEntity(CWeaponSetEnt* registerMe);
	static void UnRegisterEntity(CWeaponSetEnt* unregisterMe);
	static void NotifyCurrentSetModificationToEntities();
};

CWeaponSet::EntList

This is a class written because of personal distrust towards CUtl_Vector, it is a normal linked list. It's purpose is initialise several CWeaponSetEnt (the actual entities) during a round, since CWeaponSet has to be able to notify them of changes to the weapon set then it needs a list with references to them.

Blank image.pngTodo: Should these be pointers or EHandles? Haven't had problems with it though.
//container util class, just keeps a reference, doesn't do anything else
class EntList {
	EntList* next;
	CWeaponSetEnt* ent;

public:
	EntList();
	EntList(CWeaponSetEnt *element);
	EntList(CWeaponSetEnt *element, EntList *next);
	~EntList() { ent = NULL; delete next; } //doesn't delete CWeaponSetEnt

	CWeaponSetEnt* add(CWeaponSetEnt* element);
	CWeaponSetEnt* add(CWeaponSetEnt* element, int index);
	CWeaponSetEnt* remove(CWeaponSetEnt* element);
	CWeaponSetEnt* remove(int index);
	CWeaponSetEnt* get(int index);
	int size();
};


CWeaponSetEnt

This class represends the Hammer entity Level Designers are going to use.

//Logical entities are Point Entities that exist on the server
class CWeaponSetEnt : public CLogicalEntity {
	DECLARE_CLASS(CWeaponSetEnt, CLogicalEntity);
	DECLARE_DATADESC();

	//These appear on the FGD so you know what they do
	int m_iSpawnItem;
	int m_iWeaponSlot;
	
	int m_iHowToGiveAmmo;
	string_t m_sAmmoValues;
	string_t m_sCustomAmmoString;

	//For internal use, not on the Hammer entity
	WeaponSetH::Item *m_pItemUsed;	//points to the last Item used, that way,
					//if this entity gets notified of a change
					//to the current weapon set it will do nothing if
					//the bound item is the same as the one already spawned

	EHANDLE m_hlastEntitySpawned;	//Required to keep track of the entity spawned
 					//This way we can call UTIL_Remove when notified of change
 					//It will remove the last entity and spawn the new one
public:
	//Related to m_iHowToGiveAmmo
	enum HowToGiveAmmo {
		GIVE_AMMO_DEFAULT = 0,
		GIVE_AMMO_ABSOLUTE = 1,
		GIVE_AMMO_RELATIVETOMAX = 2,
		GIVE_AMMO_CUSTOMSTRING = 3,
	};

	void Spawn(void);

	void WeaponSetHasChanged();	//This function will be called by CWeaponSet
					//when the current set changes. Calls InitSpawnNewItem

	//Will start a Think to SpawnNewItem. delayed=false means near immediate spawn
	//delayed=true waits ConVar sv_hl2mp_weapon_respawn_time
	void InitSpawnNewItem(bool delayed);	

	//Will decide, depending on what item it must spawn, on a Spawn* function
	void SpawnNewItem(void);

	//Haven't implemented a function to spawn batteries or health
	//implemented functions are:
	void SpawnCItem(WeaponSetH::Item*); //for CItem_ModAmmoCrate
	void SpawnCBaseCombatWeapon(WeaponSetH::Item*); //for any CBaseCombatWeapon

	//Receives a constant HowToGiveAmmo (see enum above)
	//A default value of ammo (primary, secondary)
	//A max-carry value of ammo (based on the player's max, not the particular entity)
	//The return values for how much ammo to set are stored into defaultAmmo[]
	void AmmoToSet(int howToGiveAmmo, int defaultAmmo[2], int maxAmmo[2]);


	//Internal Input to connect Level Designer events placed
	//on this entity to the events on the actual items to spawn
	void InputPickedUp(inputdata_t &inputData);	

private:
	//Outputs to be fired
	COutputEvent	m_OnPlayerPickUp;
	COutputEvent	m_OnNPCPickUp;
	COutputEvent	m_OnPlayerUse;
};
BEGIN_DATADESC( CWeaponSetEnt )
	DEFINE_KEYFIELD( m_iForceWeapon, FIELD_INTEGER, "forceitem" ),	
	DEFINE_KEYFIELD( m_iWeaponSlot, FIELD_INTEGER, "weaponsetslot" ),
	DEFINE_KEYFIELD( m_iHowToGiveAmmo, FIELD_INTEGER, "howtogiveammo" ),
	DEFINE_KEYFIELD( m_sAmmoValues, FIELD_STRING, "ammovalues" ),

	//Since it's for internal use no need to have it linked to Hammer
	//That means not defined in FGD
	//However it has to be defined here so we can use it
	DEFINE_INPUTFUNC(FIELD_STRING, "PickedUp", InputPickedUp ),

	// Links our output member to the output name used by Hammer
	DEFINE_OUTPUT( m_OnPlayerPickUp, "OnPlayerPickUp" ),
	DEFINE_OUTPUT( m_OnNPCPickUp, "OnNPCPickUp" ),
	DEFINE_OUTPUT( m_OnPlayerUse, "OnPlayerUse" ),
END_DATADESC()

LINK_ENTITY_TO_CLASS(item_weaponset, CWeaponSetEnt);

Something to note here, the Spawn function of this class is the one in charge of calling CWeaponSet::Init() by creating an instance of CWeaponSet. That means weaponsets.txt doesn't get loaded until we start a map.

Blank image.pngTodo: The creation of CWeaponSet instance should be at "main()". Then point that out at SDK Modifications.

ConCommands

weaponset_modslot <itemSlot> <itemNick>
Modifies existing slot of current set.
weaponset_load <setname>
Reload Weapon Set <setname> Information from weaponsets.txt, overwrites unsaved changes done to sets during the session. If param is "<all>" then load all weapon sets.
Blank image.pngTodo: Game crashes after the command finishes.
weaponset_save <setname>
Save weapon sets <setname>, saves modifications done during the session to weaponsets.txt. If param is "<all>" it saves all the weapon sets.
Blank image.pngTodo: Game crashes if the param isn't "<all>"
weaponset_use <setname>
Switch to <setname> weapon set.
weaponset_renameto <setnewname>
Rename current weapon set to <setnewname>.
weaponset_addslot <itemnick>
Add a new slot to current weapon set and assign <itemnick> to it.
weaponset_removeslot <slotindex>
Remove existing slot with index <slotindex>.

AutoComplete

"bow" Autocomplete

Someone new to the mod would use help weaponset_modslot and while he would have the idea of the parameters, printing weaponset_modslot <itemSlot> <itemNick> alone wouldn't help. That's why an AutoComplete function is necessary.

The autocomplete answers of weaponset_modslot are smart ones due to the nature of strstr() looking within a string. For example while typing "bow", which is not a valid value, it would show the correct one: "xbow" and also "ammo_xbow" as another option.

static int WeaponSetModSlotAutoComplete ( char const *partial,
	char commands[ COMMAND_COMPLETION_MAXITEMS ][ COMMAND_COMPLETION_ITEM_LENGTH ] ) {

The constants here are global.

	//1. Split by spaces
	char arg[4][COMMAND_COMPLETION_ITEM_LENGTH] = {'\0'};

Init the string with '\0's so after using sscanf the string can be tested against "". It could be replaced by arg[i][0] = '\0'; Since COMMAND_etc is a global restriction we enforce it too :)

	int argsReceived = sscanf(partial, "%s %s %s %s", arg[0], arg[1], arg[2], arg[3]);

The command structure is weaponset_modslot <slot> <itemnick>, that's 3 args, we use four just to know when the user has typed a fourth argument (or more). In that case the program should tell him that's not a correct string. sscanf will return the number of arguments actually read, we will use that number to know what to print on screen.

		arg[0][COMMAND_COMPLETION_ITEM_LENGTH-1] = '\0';
		arg[1][COMMAND_COMPLETION_ITEM_LENGTH-1] = '\0';
		arg[2][COMMAND_COMPLETION_ITEM_LENGTH-1] = '\0';
		arg[3][COMMAND_COMPLETION_ITEM_LENGTH-1] = '\0';

If the user entered a very long parameter then it will overflow the buffer, we close the string just in case.

	const char *lastChar = &partial[strlen(partial)-1];
		if(FStrEq(lastChar, " ")) ++argsReceived; //and the last char is " "

When the user enters "weaponset_modslot <something>" argsReceived will be 2 and we should be autocompleting arg[1]. However when they enter "weaponset_modslot <something> " (<-it has a space) we should be autocompleting arg[2] but argsReceived will still be 2. So if the last char is " " we add one to argsReceived.

	//So we are autocompleting the first argument
	if(argsReceived <= 2) {
		//do stuff, here we initialize maxSlotNo

		//copy a string to commands[0] while enforcing the restriction on length
		Q_snprintf(commands[0], COMMAND_COMPLETION_ITEM_LENGTH,
			"weaponset_modslot <slotindex maxvalue: %d> <itemnick>", maxSlotNo);
		return 1; //we are only returning one result
	};

 	//So they managed to enter the first argument <slot>, get it
	int slot = atoi(arg[1]);

	//Too many arguments, tell him he is wrong by returning the "correct" command entered so far
	//if slot is not a number it will show zero there forcing the user to check why
	//the value is different than what he is typing
	if(argsReceived >= 4) {
		Q_snprintf(commands[0], COMMAND_COMPLETION_ITEM_LENGTH,
					"weaponset_modslot %d %s", slot, arg[2]);
		return 1;
	};

	//So we are completing <itemnick>
	//if(argsReceived == 3)
	int j = WeaponSetH::itemListSize; //first get how many items we have
	int n = 0; //that's different than how many we are going to return, right now, none

	WeaponSetH::Item *it;
	for(int i = 0; i < j; i++) { //navigate through all the items
		it = WeaponSetH::GetItemByArrayIndex(i);

		if(!FStrEq(arg[2], "")) {
			char *c = Q_strstr(  it->itemNick,  arg[2] );
			if(!c) continue;
		};

If the user has typed nothing on this argument yet (has typed "<arg0> <arg1> " <-space) then skip this and just add all the itemnicks to the list. However, if he has entered something already, if the itemnick entered isn't a substring of the item then just go to the next item. Otherwise add to the results.

		Q_snprintf(commands[n], COMMAND_COMPLETION_ITEM_LENGTH,
				"weaponset_modslot %d %s", slot, it->itemNick);
		n++;
	}
    return n; // finally return the number of entries found
}

SDK problems and modifications

The SDK as is gives problems to complete the objectives wanted.

  • The ammo counts for weapons, modified at runtime, do not seem to work.
  • If you go the regular way, then the item spawner spawns an item.
    • If the weapon set data changes it must delete the item and spawn the new one. Easy if you keep a pointer to the entity spawned.
    • If a player picks up the item then it must respawn.
      Ideally simple, since there was no change the item spawner would just wait there until the item respawned on its own. Problem. Weapons have it that if a player picks them up the instance is the one carried by the player, so, to respawn a weapon they create a new instance and copy over the data, this doesn't happen with Ammo Boxes or other things they just go invisible and reappear again after some time.
      Now, weapons and CItem not only have AbsLocation and the relative one, they have OriginalLocation which has no way to set outside the instance (no setters), when an item is spawned it is dropped to the ground and then a Think function waits until the item stops moving (falling, bouncing, etc). That position is set as OriginalLocation. Now, the falling doesn't happen if the item is set with the flag NO_RESPAWN, so they keep OriginalLocation = 0,0,0.
      Also, at ::Materialize() they don't get added to the list of Entities Placed By Level Designer, what is this? entities added to this list teleport back to their original position if moved far away. The ones that aren't, do not teleport back.
      The creation process goes like this: CreateNoSpawn -> Spawn -> InitFallThink -wait timer-> ThinkFall -not moving-> Materialize
      While the problem may not be clear yet... Since you want to be able to do the first point you always want to have a pointer to the "spawned" weapon. If we have a weapon respawn by creating a copy of itself then we just lost that pointer: The solution would be not to let Weapons respawn by themselves. Also, since we are trying not to have a thousand switches to check if they are ammo or weapons or whatever, we just add the flag NO_RESPAWN to every item.
      Oh! We used NO_RESPAWN, so the weapon will be constrained, you fix that by creating a vpsychicsobject for it. Next problem, when you move it it will not return to place: You fix it by adding it manually to the relocate list. Next problem, it won't do the right thing!
      Since OriginalLocation was never modified (0,0,0) then it will relocate, be sure of that, it's current position is not 0,0,0! It will teleport and disappear from where it should be. To actually fix all of this then we must make sure NO_RESPAWN does what it should do, prevent respawning, not prevent falling, relocating, or moving in general.
  • When respawned, items do not make the "respawn" sound.
    While it doesn't matter usually. The items aren't respawning now, they are being spawned again and again by the item respawner. So you never get to hear the sound except when an item teleports (that is done separately somewhere else). I decided to play it manually, you might want to do a modification and add it to the Spawn code.
  • Problem: When I use UTIL_Remove then ::Create() the new item stays constrained in place.
    UTIL_Remove doesn't remove that frame, it waits a bit. So, you mark old item for deletion, it stays there, then new item appears, they share the same physical space, error. It doesn't need a SDK modification, it just goes to explain why we use a 0,1 Think despite we want to spawn without delay. The Immediate Remove function could be of use too.


BaseCombatCharacter.cpp

void CBaseCombatCharacter::Weapon_Equip( CBaseCombatWeapon *pWeapon )

Weapon_Equip is called when the player doesn't have a weapon and just picked it up. It means he does not only picks up ammo from the weapon but the weapon instance itself.

If you check the CBaseCombatWeapon::Spawn() code at spawn, the weapon clips, if any, and the ammo counts are set to their default values of ammo. On the un-modified code of this function ::Weapon_Equip(), the default values are used again to modify the clips and ammo... again. That means that even after spawning the weapon and modifying the ammo held by the weapon it would not count since the ammo counts are reset at equip.

So we have to modify this function so that doesn't happen. In theory when the weapon is on ground we really don't care about clips we just care about total ammo. So, as a standard:

  • PrimaryAmmoCount stores the primary ammo of the weapon. (or Secondary, etc)
  • Ammo does not equal weapon. If you attempt to equip a new weapon with zero ammo it should not fail.
  • If the weapon is being equipped and the Clip is 0 then do an instant reload (fill the clip) then give remaining ammo. This works with ammo set manually by the level designer as clips are set to zero by the spawner.
  • If the weapon on ground has a Clip different than 0 or -1 and we are equipping it, the weapon should start with that clip size. This works when the level designer doesn't set manually the ammo, as the player will receive the default ammo set at ::Spawn(), or the ammo held by the dropped weapon.
  • If the player already has the weapon then the clip should be added to the total ammo. And then the ammo given to the player.

After the first for() there's this line:

	(...)
 	// Weapon is now on my team
	pWeapon->ChangeTeam( GetTeamNumber() );

After that line we add/modify:

//We are setting the ammo at spawn, why should we set it again? Modify this.
//CWeaponSetEnt will set clips to 0. It will be Primary/Secondary ammo the ones keeping the counts
// MOD
	int	primaryGiven	= pWeapon->GetPrimaryAmmoCount();
	int secondaryGiven	= pWeapon->GetSecondaryAmmoCount();

	// If default ammo given is greater than clip
	// size, fill clips and give extra ammo
	if((pWeapon->UsesClipsForAmmo1()) && (pWeapon->m_iClip1 == 0)) {
		int iGive = 0;
		if (primaryGiven >  pWeapon->GetMaxClip1() )
			iGive = pWeapon->GetMaxClip1();
		else
			iGive = primaryGiven;

		pWeapon->m_iClip1 = iGive;
		primaryGiven -= iGive;
	};
	if((pWeapon->UsesClipsForAmmo2()) && (pWeapon->m_iClip2 == 0)) {
		int iGive = 0;
		if (secondaryGiven >  pWeapon->GetMaxClip2() )
			iGive = pWeapon->GetMaxClip2();
		else
			iGive = secondaryGiven;

		pWeapon->m_iClip2 = iGive;
		secondaryGiven -= iGive;
	};
	int takenPrimary   = GiveAmmo( primaryGiven, pWeapon->m_iPrimaryAmmoType); 
	int takenSecondary = GiveAmmo( secondaryGiven, pWeapon->m_iSecondaryAmmoType); 

	pWeapon->SetPrimaryAmmoCount( pWeapon->GetPrimaryAmmoCount() - takenPrimary );
	pWeapon->SetSecondaryAmmoCount( pWeapon->GetSecondaryAmmoCount() - takenSecondary );
// End MOD

Then it gets followed by the regular contents starting at:

	pWeapon->Equip( this );

	// Players don't automatically holster their current weapon
	if ( IsPlayer() == false )
	(...)


bool CBaseCombatCharacter::Weapon_EquipAmmoOnly( CBaseCombatWeapon *pWeapon )

When we already have the weapon this function is called just to give ammo. We modify it too. Following the standard above, since the user already has it then we must not modify the current weapon's clip, just give ammo.

bool CBaseCombatCharacter::Weapon_EquipAmmoOnly( CBaseCombatWeapon *pWeapon )
{
	// Check for duplicates
	for (int i=0;i<MAX_WEAPONS;i++) 
	{
		if ( m_hMyWeapons[i].Get()
			&& FClassnameIs(m_hMyWeapons[i], pWeapon->GetClassname()) )
		{
			// MOD
			// Just give the ammo
			int	primaryGiven	= pWeapon->GetPrimaryAmmoCount();
			int secondaryGiven	= pWeapon->GetSecondaryAmmoCount();
			if( pWeapon->UsesClipsForAmmo1() ) primaryGiven += pWeapon->m_iClip1;
			if( pWeapon->UsesClipsForAmmo2() ) secondaryGiven += pWeapon->m_iClip2;

			int takenPrimary 
					= GiveAmmo( primaryGiven, pWeapon->m_iPrimaryAmmoType); 
			int takenSecondary
					= GiveAmmo( secondaryGiven, pWeapon->m_iSecondaryAmmoType); 

			//Only succeed if we get to take ammo from the weapon
 			//or if it was set to zero ammo on purpose
			if(takenPrimary == 0 && takenSecondary == 0) {
				if(primaryGiven > 0 || secondaryGiven > 0) return false;
			};

			//suceeded, modify counts
			if( pWeapon->UsesClipsForAmmo1() ) pWeapon->m_iClip1 = 0;
			if( pWeapon->UsesClipsForAmmo2() ) pWeapon->m_iClip2 = 0;
			pWeapon->SetPrimaryAmmoCount(
 					pWeapon->GetPrimaryAmmoCount() - takenPrimary );
			pWeapon->SetSecondaryAmmoCount(
 					pWeapon->GetSecondaryAmmoCount() - takenSecondary );

			// End MOD
			return true;
		}
	}

	return false;
}

hl2mp_player.cpp

If you make a map and place a 357 set to only have 1 bullet and then you go and pick it up it will have 7. This "problem" is because every player is given 6x 357 bullets when they spawn along with the other default items.

Now, when you pick up the 357 it does have just 1 bullet on the clip (and 6 on your bullet stash) so this is not an actual bug. The same happens with the shotgun. With the following modification you spawn with the suit, gravity gun and nothing else.

//Comment whatever you don't what. 
void CHL2MP_Player::GiveDefaultItems( void )
{
	EquipSuit();
/* MOD
	CBasePlayer::GiveAmmo( 255,	"Pistol");
	CBasePlayer::GiveAmmo( 45,	"SMG1");
	CBasePlayer::GiveAmmo( 1,	"grenade" );
	CBasePlayer::GiveAmmo( 6,	"Buckshot");
	CBasePlayer::GiveAmmo( 6,	"357" );

	if ( GetPlayerModelType() == PLAYER_SOUNDS_METROPOLICE
		|| GetPlayerModelType() ==  PLAYER_SOUNDS_COMBINESOLDIER )
	{
		GiveNamedItem( "weapon_stunstick" );
	}
	else if ( GetPlayerModelType() == PLAYER_SOUNDS_CITIZEN )
	{
		GiveNamedItem( "weapon_crowbar" );
	}
	
	GiveNamedItem( "weapon_pistol" );
	GiveNamedItem( "weapon_smg1" );
	GiveNamedItem( "weapon_frag" );
   End MOD - The grav gun is below
*/
(...)
}

basecombatweapon.cpp

It is explained above, we don't want these NO_RESPAWN checks, these should be CONSTRAINED and DO_NOT_RELOCATE checks.

void CBaseCombatWeapon::Materialize( void )
{
	(...)
		RemoveEffects( EF_NODRAW );
		DoMuzzleFlash();
	}
#ifdef HL2MP
	// MOD 'ed out
	//if ( HasSpawnFlags( SF_NORESPAWN ) == false )
	{
		VPhysicsInitNormal( SOLID_BBOX, GetSolidFlags() | FSOLID_TRIGGER, false );
		SetMoveType( MOVETYPE_VPHYSICS );

		HL2MPRules()->AddLevelDesignerPlacedObject( this );
	}
	// End MOD
	(...)
}

weapon_hl2mpbase.cpp

Here we modify two functions, Materialize() first

void CWeaponHL2MPBase::	Materialize( void )
{ 
	if ( IsEffectActive( EF_NODRAW ) )
	{
		// changing from invisible state to visible.
		EmitSound( "AlyxEmp.Charge" );
		
		RemoveEffects( EF_NODRAW );
		DoMuzzleFlash();
	}

 	// MOD'ed out one thing is no respawn and another no teleport back to original place!!!
 	//if ( HasSpawnFlags( SF_NORESPAWN ) == false ) commented
	{
		VPhysicsInitNormal( SOLID_BBOX, GetSolidFlags() | FSOLID_TRIGGER, false );
		SetMoveType( MOVETYPE_VPHYSICS );

		HL2MPRules()->AddLevelDesignerPlacedObject( this );
	}

	//if ( HasSpawnFlags( SF_NORESPAWN ) == false ) commented
	{
		if ( GetOriginalSpawnOrigin() == vec3_origin )
		{
			m_vOriginalSpawnOrigin = GetAbsOrigin();
			m_vOriginalSpawnAngles = GetAbsAngles();
		}
	}

	SetPickupTouch();

	SetThink (NULL);
}

Now, reordering the logic of FallInit:

void CWeaponHL2MPBase::FallInit( void )
{
#ifndef CLIENT_DLL
	SetModel( GetWorldModel() );
	VPhysicsDestroyObject();

	// Constrained start?
	if ( HasSpawnFlags( SF_WEAPON_START_CONSTRAINED ) ) {
		//Constrain the weapon in place
		IPhysicsObject *pReferenceObject, *pAttachedObject;
		pReferenceObject = g_PhysWorldObject;
		pAttachedObject = VPhysicsGetObject();

		if ( pReferenceObject && pAttachedObject ) {
			constraint_fixedparams_t fixed;
			fixed.Defaults();
			fixed.InitWithCurrentObjectState( pReferenceObject, pAttachedObject );
					
			fixed.constraint.forceLimit	= lbs2kg( 10000 );
			fixed.constraint.torqueLimit = lbs2kg( 10000 );

			IPhysicsConstraint *pConstraint = GetConstraint();
			pConstraint = physenv->CreateFixedConstraint( pReferenceObject,
								pAttachedObject, NULL, fixed );
			pConstraint->SetGameData( (void *) this );
		};
	} else	{
		SetMoveType( MOVETYPE_NONE );
		SetSolid( SOLID_BBOX );
		AddSolidFlags( FSOLID_TRIGGER );

 		//any difference?
		if ( HasSpawnFlags( SF_NORESPAWN ) == false ) UTIL_DropToFloor( this, MASK_SOLID );
		else VPhysicsInitNormal( SOLID_BBOX, GetSolidFlags() | FSOLID_TRIGGER, false );
	}

	SetPickupTouch();
	SetThink( &CBaseCombatWeapon::FallThink );
	SetNextThink( gpGlobals->curtime + 0.1f );
#endif
}


basecombatweapon_shared.cpp

Note.pngNote:Remember to also add the prototypes to the .h

Clips have no setters, now we need them to set them to zero. Another way would be to modify ::Spawn() so it doesn't set them to the default values but zero giving those defaults to Primary/Secondary ammo. However, you will still need setters if you want to improve on this tutorial and let the user define ammo specifically on clips.

// MOD : Add Functions

void CBaseCombatWeapon::SetClip1(int amount) {
	if(UsesClipsForAmmo1()) m_iClip1 = min(max(0, amount), GetMaxClip1());
}
void CBaseCombatWeapon::SetClip2(int amount) {
	if(UsesClipsForAmmo2()) m_iClip2 = min(max(0, amount), GetMaxClip2());
}

// End MOD

KeyValues.cpp

Note.pngNote:Remember to also add the prototypes to the .h

Well, KeyValues doesn't use the best names for functions, SetString doesn't set the string value of the caller, it creates a child node with that value. Also the "remove key from tree" doesn't really work a standard way.Either remove the key alone and reconnect the tree, or remove the whole subtree. Instead it kills all the direct child nodes (depth 1), etc.

KeyValues cache the value they hold in the latest used format. Let's say you called GetString() on a Key holding an integer, the integer will be transformed to string, the key will cache this new type (and lose the integer one) and return the value you wanted. This on the idea that the next time you want to retrieve the value you will want it again as string and no conversion will be required.

The GetMy* versions (the ones presented below) do not share this idea, no particular reason, so you can add it if you like. This way GetMyString() will always return a copy of the string value that has to be deleted.

Here some utility functions that solve standard needs.

/**
 * Long MOD 
 */
//-----------------------------------------------------------------------------
//Purpose: Will Kill all my tree. Will kill myself if alsoKillCaller is true.
//Will detach me from  my parent and brothers first if parent != NULL
//-----------------------------------------------------------------------------
void KeyValues::KillMyTree(KeyValues* parent, bool alsoKillCaller) {
	if(parent) parent->RemoveSubKey(this); //Left by father and brothers
	RecursiveDelete(alsoKillCaller);
}
//-----------------------------------------------------------------------------
// Purpose: Will Kill all my tree. Won't detach from peers or parent.
//-----------------------------------------------------------------------------
void KeyValues::RecursiveDelete(bool alsoKillCaller) {
 	KeyValues *dat;
	KeyValues *datPrev = NULL;
	
	dat = m_pSub;
	while(true) {
		if(datPrev != NULL) datPrev->RecursiveDelete(true);
		
		datPrev = dat;
		if((dat == NULL) && (datPrev == NULL)) break;

		dat = dat->m_pPeer;
	}

	if(alsoKillCaller) deleteThis();
}
//Returns a copy, don't forget to delete it
char* KeyValues::GetMyString(void) {
	// convert the data to string form then return it
	char *answer = NULL;
	char buf[64];
	int len = 0;
	switch(m_iDataType) {
		case TYPE_FLOAT:
			Q_snprintf( buf, sizeof( buf ), "%f", m_flValue );
		// allocate memory for the new value and copy it in
			len = Q_strlen(buf);
			answer = new char[len + 1];
			Q_memcpy( answer, buf, len+1 );
			break;
		case TYPE_INT:
			Q_snprintf( buf, sizeof( buf ), "%d", m_iValue );
		// allocate memory for the new value and copy it in
			len = Q_strlen(buf);
			answer = new char[len + 1];
			Q_memcpy( answer, buf, len+1 );
			break;
		case TYPE_PTR:
			Q_snprintf( buf, sizeof( buf ), "%d", m_iValue );
			len = Q_strlen(buf);
			answer = new char[len + 1];
			Q_memcpy( answer, buf, len+1 );
			break;
	case TYPE_WSTRING:
		{
#ifdef _WIN32
			// convert the string to char *, and return it
			static char buf[512];
			::WideCharToMultiByte(CP_UTF8, 0, m_wsValue, -1, buf, 512, NULL, NULL);
			len = Q_strlen(buf);
			answer = new char[len + 1];
			Q_memcpy( answer, buf, len+1 );
#endif
			break;
		}
	case TYPE_STRING:
		len = strlen(m_sValue);
		answer = new char[len+1];
		Q_memcpy(answer, m_sValue, len+1);
		break;
	default:
		break;
	};
	
	return answer;
}
int KeyValues::GetMyInt(void) {
	switch(m_iDataType) {
		case TYPE_STRING:
			return atoi(m_sValue);
		case TYPE_WSTRING:
#ifdef _WIN32
			return _wtoi(m_wsValue);
#else
			DevMsg( "TODO: implement _wtoi\n");
			return 0;
#endif
		case TYPE_FLOAT:
			return (int)m_flValue;
		case TYPE_INT:
		case TYPE_PTR:
		default:
			return m_iValue;
	};
}
float KeyValues::GetMyFloat(void) {
	switch ( m_iDataType )	{
		case TYPE_STRING:
			return (float)atof(m_sValue);
		case TYPE_WSTRING:
			return 0.0f;		// no wtof
		case TYPE_FLOAT:
	 		return m_flValue;
		case TYPE_INT:
			return (float)m_iValue;
		case TYPE_PTR:
		default:
			return 0.0f;
	};
	return -1.0f;
}
void* KeyValues::GetMyPtr() {
	switch (m_iDataType) {
		case TYPE_PTR:
			return m_pValue;

		case TYPE_WSTRING:
		case TYPE_STRING:
		case TYPE_FLOAT:
		case TYPE_INT:
		default:
			return NULL;
	}
}
//Will make a copy of the contents of *newValue, don't forget to delete *newValue
void KeyValues::SetMyString(const char* newValue) {
	// delete the old value
	delete [] this->m_sValue;
	// make sure we're not storing the WSTRING  - as we're converting over to STRING
	delete [] this->m_wsValue;
	m_wsValue = NULL;

	if (!newValue)	newValue = "";

	// allocate memory for the new value and copy it in
	int len = Q_strlen( newValue );
	m_sValue = new char[len + 1];
	Q_memcpy( m_sValue, newValue, len+1 );

	m_iDataType = TYPE_STRING;
}
void KeyValues::SetMyInt(int newValue) {
	m_iValue = newValue;
	m_iDataType = TYPE_INT;
}
void KeyValues::SetMyPtr(void *newValue) {
	m_pValue = newValue;
	m_iDataType = TYPE_PTR;
}


//-----------------------------------------------------------------------------
// Purpose: Set the float value of a keyName. 
//-----------------------------------------------------------------------------
void KeyValues::SetMyFloat(float newValue) {
	m_flValue = newValue;
	m_iDataType = TYPE_FLOAT;
}

int KeyValues::NumberOfChilds(void) {
	KeyValues *dat;
	int i = 0;

	for(dat = m_pSub; dat; dat = dat->m_pPeer) {
		++i;
	}

	return i;
}

//If Child Keys have Integer names, return the max of them, min is 1
int KeyValues::MaxIntNameOnChilds(void){
	int ID = 1;

	// search for any key with higher values
	for (KeyValues *dat = m_pSub; dat != NULL; dat = dat->m_pPeer) {
		// case-insensitive string compare
		int val = atoi(dat->GetName());
		if (ID < val) ID = val;
	}

	return ID;
}
/**
 *	End MOD
 */

Getting it to work in your MOD - code download

For those that only want to copy paste: To have the code working on your mod you just have to add the code files to the server dll project.

  • Go through each SDK Modification.
  • Get the code *.rar here here.
  • Add the contents of the FGD proposed to the mod's fgd (preferably the bottom).
  • Edit the item list at spawnitem(choices) to reflect the mod's options.
    • Start Hammer, check the entity item_weaponset, and make a small test map.
  • Place the .h and .cpp files in the dlls folder of the code (inside the mod's folder). Then just add them to the server dll solution, preferably under a new folder "item".
  • Using the modified FGD as a template, modify the SPAWN constants at Item_t so they are linked to the indexes used in the modified FGD.
  • Now go and modify the initialization of itemList, here you link each item's entity-name to FGD item indexes and to the nicknames to be used at weaponset.txt.
  • Place weaponsets.txt in the mod's folder, next to maplist.txt.
  • Edit the sets to suit your mod (change the item nicknames to spawn on each slot) and to suit your test map.
  • Place your test map into your maps' folder, compile the server dll, run the map, check it works, test the console commands.

How to improve

These could be considered todos.

  • Use bit flags for tiers so Items can belong to more than one.
  • To increase function readability you could use your own structure instead using KeyValues. If you go that way you only need KeyValues at Load and Save.
  • maplist.txt not only having a map list but also default sets to start with on each map.
  • Entity flags: So many flags that could be added.
    • Start constrained.
    • Do Not Respawn. Different than...:
    • Do Not Relocate. After being moved from original position do not teleport the item back.
    • Respawns at original spot. Defaults yes. To be mixed with Do Not Relocate, if unchecked, it would respawn at whatever position it was when the player picked it up.
    • Hide while dropping to floor. Right now it spawns at whatever position even mid-air and then falls to the ground where it sets its "original position". Hide the item while doing this.
  • Abstract weapon code so their functions aren't Clip1(), Clip2(), SetPrimaryAmmoCount() etc, but SetClip(index, amount), and the like. That way working with simple weapons like a knife to complex ones like Judge Dredd's gun is possible without much pain everywhere else.
  • Having done the last point do the same with the ammo box so it can hold more than one type of ammo.
  • VGUI to edit weapon sets.
  • vote_weaponset <setname> How does it exactly work depends on the game.
  • VGUI to vote_weaponset :)

See also