The Visual Studio IDE has many different panels. Two of these, the solution explorer and the class view, can help us to navigate through the project. If they aren't visible as default, enable them by selecting the missing panel in the view menu.
In the solution explorer the actual files (plus its extension) of the project are listed in a tree form. The class view on the other hand shows us all the classes that exist in the project (note that these are automatically parsed out by Visual Studio, even when we are editing the source code). We will mainly use the "Class View" in this document, unless mentioned differently.
The class view
After loading the project file of the sourcemod, the class view shows us two sub-projects. The tree of the sub-project can be expanded by clicking on the plus ("+") sign, showing us all the classes that belong to this sub-project. Double-clicking on the class name opens the file which contains the class declaration (often an .h-file) while double-clicking on a member function or constructor takes you to the implementation file of the class (mostly a .cpp-file).
A note about adding members to a class: the IDE has a handy way to add member functions and variables by right-clicking in the class view and selecting "Add function" or "Add variable". This however, is not recommended when working on your SourceMod project. Doing this will mostly change options in your project file, resulting in very strange compiler errors.
The project is divided into two sub-projects, client and hl. All classes that belong to first project are classes which implement functions for the client side (for example HUD elements). Classes of hl contain functions for the server side (like the player). There is also a special group of classes, also known as network entities. These classes are present in both sub-projects. Examples of such classes are the weapon classes.
Also note that while we are modding the Half-Life 2 Deathmatch, single player classes and files are also included in our project (like specific rules, single player NPC's and AI), but not used.
In our project we want to remove all violence; i.e. remove weapons, health bar, crosshair, ammo bar and weapons selection bar. One way of doing this, is to edit all the corresponding classes. For example to disable weapons selection, we can edit the VisibleInWeaponSelection function in CBaseCombatWeapon in such a way that it will always return false. But we will not do this. Instead, we want do all of the violence removal at one spot (the CHL2MP_Player class), so that we can to toggle it on and off easily later on.
Step by step removal of violence:
void CHL2MP_Player::SetWeaponEnabled(bool enable) { if (enable) { //Weapons on GiveDefaultItems(); m_Local.m_iHideHUD = 0; m_bViolence = true; //Immortality off! UpdateClientData(); //Update HUD } else { //No weapons please RemoveAllItems(true); m_Local.m_iHideHUD = 0; m_Local.m_iHideHUD |= HIDEHUD_HEALTH; m_Local.m_iHideHUD |= HIDEHUD_CROSSHAIR; m_bViolence = false; //player will be immortal UpdateClientData(); //Update HUD. } }Add SetWeaponsEnabled(false) in the Spawn function of CHL2MP_Player, so every player starts without weapons.
There is a small problem when a player isn't holding any weapon. The player model does not have an animation for a weaponless player. There seems to be no solution for this, then to give the player something to hold. In our case, we have chosen to give every player a stun-stick when violence is disabled. The stun-stick is the most harmless looking object of all available holdable weapons. We just have to disable its original functionality.
The classes of the weapons are not directly accessible from within the CHLMP_Player, so this time we do need to change settings of the weapon directly in its class. Open up the solutions explorer this time, browse to the HL2MP folder in the client project, then to the weapons folder and double-click on weapons_stunstick.cpp. Almost every weapon is derived from the CBaseCombatWeapon (mostly not directly). Therefore, these weapons all have both functions PrimaryAttack and SecondaryAttack derived from their parents. We just need to overwrite the ones in the stun-stick class to always return immediately (and thus doing nothing when an attack is done with this weapon).
A logical entity can be used to add functionality to your game, by communicating to other (non-)logical entities. These logical entities have to be triggered in order to do what they are supposing to do. The entity itself will not visible to the player. Logical entities can be dragged into your map in the Hammer level editor. Entities have inputs and outputs, which can be chained together to do certain things. For example: we have a trigger_proximiy entity and a game_text entity on our map. We then can chain an output of trigger_proximity, OnStartTouch, to an input of the game_text entity, like Display to show a message on the screen when we have triggered the proximity (by touching it).
OnStartTouch output is chained to the SetWeapons input of WeaponsDisabler
A logical entity can be created in our Mod as follows. First of all, all these logical entities which dynamically change the game rules are put together in one .cpp-file. This file is called maprules.cpp, so open up the solution explorer again and browse to the Source Files folder in the hl sub-project.
The best thing to do when experimenting with logical entities is trying to clone an existing entity to discover how it is implemented. A good one to clone or use as a skeleton to create your own entities is CGameText. This class corresponds to the game_text entity that we discussed earlier.
Important are the following lines:
//… LINK_ENTITY_TO_CLASS(your_entity_name, CWeaponsEnabler); BEGIN_DATADESC( CWeaponsEnabler ) DEFINE_KEYFIELD( iEnable, FIELD_INTEGER, "enableWeapons" ), DEFINE_INPUTFUNC( FIELD_VOID, "SetWeapons", InputHandler ), END_DATADESC() //…
The first line sets the "connection" between the class and the entity. The DEFINE_KEYFIELD defines that the value, which is filled in the "enableWeapon" field in the Hammer editor, will be copied into the int variable iEnable.
DEFINE_INPUTFUNC defines an input of the entity. When this input is triggered by an output of another entity, the function "InputHandler" of this class will be called. The remainder should be self-explanatory. Check CGameText or CWeaponsEnabler (Appendix B.1) for more information.
When you have finished creating or editing your entity, you also have to create a .fgd file. This file has to be loaded in the Hammer editor (Tools -> options -> Game Configurations Tab -> Game Data Files) before you can use your entity. The .fgd contains an almost identical mapping of the fields and the input of the entity. Check Appendix A.3 for an example of a .fgd file, which we created for the vu_weapons_enabler entity (this entity will be discussed in the following paragraph).
This logical entity will disable or enable weapons of the player who triggered it. This entity is almost identical to CGameText, but without all the unnecessary options. It does have a field "enableWeapons", which in fact is just a boolean. The entity will call the SetWeaponsEnabled function (defined earlier) of the CHL2MP_player class to enable or disable weapons. The player who has triggered the entity can be found by calling ToBasePlayer(pActivator).
The entity is called "WeaponsDisabler" and has a field called "Weapons enable/disable" (name of this field is defined in a .fgd file.
See Appendix A1 for the complete source code.
See Appendix A2 for the complete source code.
In maprules.cpp:
class CWeaponsEnabler : public CRulePointEntity { public: DECLARE_CLASS( CWeaponsEnabler, CRulePointEntity ); bool KeyValue( const char *szKeyName, const char *szValue ); DECLARE_DATADESC(); void InputHandler( inputdata_t &inputdata ); void SetWeapons( CBaseEntity *pActivator ); void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) { SetWeapons( pActivator ); } private: int iEnable; }; LINK_ENTITY_TO_CLASS( vu_weapons_enabler, CWeaponsEnabler ); BEGIN_DATADESC( CWeaponsEnabler ) DEFINE_KEYFIELD( iEnable, FIELD_INTEGER, "enableWeapons" ), DEFINE_INPUTFUNC( FIELD_VOID, "SetWeapons", InputHandler ), END_DATADESC() bool CWeaponsEnabler::KeyValue( const char *szKeyName, const char *szValue ) { return BaseClass::KeyValue( szKeyName, szValue ); } void CWeaponsEnabler::InputHandler( inputdata_t &inputdata ) { SetWeapons( inputdata.pActivator ); } void CWeaponsEnabler::SetWeapons( CBaseEntity *pActivator ) { if ( !CanFireForActivator( pActivator ) ) return; // If we're in singleplayer, show the message to the player. if ( gpGlobals->maxClients == 1 ) { //Do nothing } else if ( pActivator && pActivator->IsNetClient() ) { // Otherwise show the message to the player that triggered us. if (iEnable == 1) { //True is selected UTIL_DoHudPicture(ToBasePlayer( pActivator ), 0, false); ToBasePlayer( pActivator )->SetWeaponsEnabled(true); ClientPrint(ToBasePlayer( pActivator ), HUD_PRINTCENTER, "FIGHT!!"); } else { //Shoud the puzzlehud be enabled when weapond are disabled? UTIL_DoHudPicture(ToBasePlayer( pActivator ), 0, true); ToBasePlayer( pActivator )->SetWeaponsEnabled(false); } } }
hud_monitor.h:
#include "hudelement.h" #includeclass CHudMonitor : public CHudElement, public vgui::Panel { DECLARE_CLASS_SIMPLE(CHudMonitor, vgui::Panel); public: CHudMonitor(const char *pElementName); void MsgFunc_HudMonitor( bf_read &msg); void DrawPart(int iIndex, int x, int y, int x2, int y2); void Init(); private: static const int OFFSET = 10; static const int PARTS_COUNT = 10; int m_nTextureID[PARTS_COUNT]; int m_nBlinker[PARTS_COUNT]; bool m_nPartToDraw[PARTS_COUNT]; protected: virtual void Paint(); };
hud_monitor.cpp:
#include "hud.h" #include "cbase.h" #include "hud_monitor.h" #include "iclientmode.h" #include "hud_macros.h" #include "vgui_controls/controls.h" #include "vgui/ISurface.h" #include "tier0/memdbgon.h" CHudMonitor::CHudMonitor(const char *pElementName) : CHudElement(pElementName), BaseClass(NULL, "HudMonitor") { vgui::Panel *pParent = g_pClientMode->GetViewport(); SetParent(pParent); for (int i=0; i<11; i++) { m_nTextureID[i] = vgui::surface()->CreateNewTextureID(); } vgui::surface()->DrawSetTextureFile(m_nTextureID[1], "hud_monitor/p1", true, true); vgui::surface()->DrawSetTextureFile(m_nTextureID[2], "hud_monitor/p2", true, true); vgui::surface()->DrawSetTextureFile(m_nTextureID[3], "hud_monitor/p3", true, true); vgui::surface()->DrawSetTextureFile(m_nTextureID[4], "hud_monitor/p4", true, true); vgui::surface()->DrawSetTextureFile(m_nTextureID[5], "hud_monitor/p5", true, true); vgui::surface()->DrawSetTextureFile(m_nTextureID[6], "hud_monitor/p6", true, true); vgui::surface()->DrawSetTextureFile(m_nTextureID[7], "hud_monitor/p7", true, true); vgui::surface()->DrawSetTextureFile(m_nTextureID[8], "hud_monitor/p8", true, true); vgui::surface()->DrawSetTextureFile(m_nTextureID[9], "hud_monitor/p9", true, true); SetHiddenBits(0);//HIDEHUD_PLAYERDEAD | HIDEHUD_NEEDSUIT); }; DECLARE_HUDELEMENT(CHudMonitor); DECLARE_HUD_MESSAGE( CHudMonitor, HudMonitor ); void CHudMonitor::Init() { HOOK_HUD_MESSAGE(CHudMonitor, HudMonitor); //At the start, there is always an empty hud //Initially, draw nothing for (int i=0; i240 } } void CHudMonitor::MsgFunc_HudMonitor( bf_read &msg) { int nMessage; bool bDraw; nMessage = msg.ReadByte(); bDraw = msg.ReadOneBit(); if (nMessage < 10 && nMessage > 0) { //1, 2, .., 9 if (bDraw) { m_nPartToDraw[nMessage] = true; m_nBlinker[nMessage] = 0; } else { m_nPartToDraw[nMessage] = false; } } else if (nMessage == 0) { //Disable whole hud with SetHiddenBits(HIDEHUD_PLAYERDEAD | HIDEHUD_NEEDSUIT); if (bDraw) { //Show this HUD SetHiddenBits(0); } else { //Hide HUD //Put this HUD in the group of the HIDEHUD_VEHICLE_CROSSHAIR //As soon as you turn this off in hl2mp_player, this one turns off as well SetHiddenBits(HIDEHUD_VEHICLE_CROSSHAIR); } } else { //nMessage >= 10 //Handle other messages } } void CHudMonitor::DrawPart(int iIndex, int x, int y, int x2, int y2) { vgui::surface()->DrawSetTexture(m_nTextureID[iIndex]); vgui::surface()->DrawTexturedRect(x, y, x2, y2); if (m_nBlinker[iIndex] < 40) { vgui::surface()->DrawSetColor(255, 255, 50, 50); vgui::surface()->DrawFilledRect(x, y, x2, y2); m_nBlinker[iIndex]++; } else if (m_nBlinker[iIndex] < 80 ) { m_nBlinker[iIndex]++; } else if (m_nBlinker[iIndex] >= 80 && m_nBlinker[iIndex] < 120) { vgui::surface()->DrawSetColor(255, 255, 50, 50); vgui::surface()->DrawFilledRect(x, y, x2, y2); m_nBlinker[iIndex]++; } else if (m_nBlinker[iIndex] >= 120) { //do nothing; } } void CHudMonitor::Paint() { int nWide = this->GetWide(); int nTall = this->GetTall(); int x1, y1, x2, y2; /* ----------- | 1 | 2 | 3 | ----------- | 4 | 5 | 6 | ----------- | 7 | 8 | 9 | ----------- */ x1 = x2 = y1 = y2 = 0; for (int i=0; i 0) { if (m_nPartToDraw[i]) { DrawPart(i, x1, y1, x2, y2); } else { vgui::surface()->DrawSetColor(255, 255, 50, 50); vgui::surface()->DrawOutlinedRect(x1, y1, x2, y2); } } //Always draw background (wireframe of the whole picture) //On top: vgui::surface()->DrawSetColor(255, 255, 50, 50); vgui::surface()->DrawOutlinedRect(OFFSET, OFFSET, nWide-OFFSET, nTall-OFFSET); } }
In util.cpp:
void UTIL_DoHudPicture( CBasePlayer *pToPlayer, int nMessage, bool bDraw ) { CRecipientFilter filter; if( pToPlayer ) { filter.AddRecipient( pToPlayer ); } else { filter.AddAllPlayers(); } filter.MakeReliable(); UserMessageBegin( filter, "HudMonitor" ); WRITE_BYTE(nMessage); WRITE_BOOL(bDraw); MessageEnd(); }
Add to hl2_usermessages.cpp:
void RegisterUserMessages( void ) { usermessages->Register( "Geiger", 1 ); usermessages->Register( "Train", 1 ); usermessages->Register( "HudText", -1 ); usermessages->Register( "SayText", -1 ); usermessages->Register( "TextMsg", -1 ); usermessages->Register( "HudMsg", -1 ); usermessages->Register( "HudMonitor", -1 ); //Add this one <-- ... ...
@include "base.fgd" @PointClass base(Targetname) = vu_weapons_enabler : "Logical entity which can enable/disable weapons." [ enableWeapons(choices) : "Weapon enable/disable" : 1 : "Select whether you want to enable/disable weapons." = [ 1 : "true" 2 : "false" ] input SetWeapons(void) : "Triggers input." ] @PointClass base(Targetname) = vu_puzzle_setter : "Logical entity which can enable/disable parts of the puzzle-like HUD." [ puzzlePartIndex(choices) : "Index of the puzzle-part" : 1 : "1 maps to the first picture top-left, 9 to the bottom-right." = [ 1: "1 (top-left)" 2: "2 (top-middle)" 3: "3 (top-right)" 4: "4 (center-left)" 5: "5 (center-middle)" 6: "6 (center-right)" 7: "7 (bottom-left)" 8: "8 (bottom-middle)" 9: "9 (bottom-right)" ] enablePart(choices) : "Visible/Invisible" : 1 : "Do you want it to be visible?" = [ 1 : "true" 2 : "false" ] input SetPuzzlePart(void) : "Triggers input." ]