|
|
|
In this tutorial, I'll add the necessary framework to have a secondary clip to the two dlls. Then I'll give you an example on how to activate the secondary clip by modifying the MP5.
Note that Valve used an additional Hud element to display the grenade stats in TFC (this HUD element is included with the source code - TFC doesn't have its own client dll). I won't use it, as using it would mean pulling the secondary ammo out of the standard ammo registry, which is too messy. A little client dll coding can't hurt anyway...
Contents:
1. Secondary Clip Framework (Logic Part)
2. Secondary Clip Framework (HUD Part)
3. Example: MP5 with secondary clip
Old code is in yellow, new code is in Red. Code that has been deleted is in Grey.Changed code and reference info is Green. Note that code from a previous chapter will be listed as old code. Line numbers needn't be exact values (especially if you have the Professional SDK). You will see where you have to add new code by looking at the surrounding lines of old code.
This is a lot of code that should be quite self-explanatory, and you needn't really understand it to have secondary clips included to your mod. But if you ask me, you should actually understand all the code of the SDK.
First of all, some new variables and member functions are needed to handle the secondary clip. All of it is in weapons.h. The first thing to change is the ItemInfo, to store the size of the secondary clip. Add the following around line 210:
int iMaxClip; // secondary clip int iMaxClip2; // secondary clip int iId;
A reference to this has to be added to CBasePlayerItem, around line 282:
int iMaxClip( void ) { return ItemInfoArray[ m_iId ].iMaxClip; } // secondary clip int iMaxClip2( void ) { return ItemInfoArray[ m_iId ].iMaxClip2; } // secondary clip int iWeight( void ) { return ItemInfoArray[ m_iId ].iWeight; }
There are several things to be added to CBasePlayerWeapon. Go to line 313:
BOOL AddPrimaryAmmo( int iCount, char *szName, int iMaxClip, int iMaxCarry ); // secondary clip BOOL AddSecondaryAmmo( int iCount, char *szName, int iMaxClip, int iMaxCarry ); // secondary clip virtual void UpdateItemInfo( void ) {}; // updates HUD state
iMaxClip has to be added to the function call (should be clear). Now on line 331:
int DefaultReload( int iClipSize, int iAnim, float fDelay ); // secondary clip int DefaultReloadSecondary(int iClipSize, int iAnim, float flDelay); // secondary clip virtual void ItemPostFrame( void ); // called each frame by the player PostThink
As the name says it, the new member function DefaultReloadSecondary() will be the default convenience function to call when reloading the secondary clip. Go on to line 340 and add:
virtual void Reload( void ) { return; } // do "+RELOAD" // secondary clip virtual void ReloadSecondary( void ) { return; } // secondary clip virtual void WeaponIdle( void ) { return; } // called when no buttons pressed
... should be clear. There are still some variables to define around line 360:
int m_iClientClip; // the last version of m_iClip sent to hud dll // secondary clip int m_iClip2; // same as above for our secondary clip int m_iClientClip2; // secondary clip int m_iClientWeaponState; // the last version of the weapon state sent to hud dll (is current weapon, is on target) int m_fInReload; // Are we in the middle of a reload; // secondary clip int m_fInReload2; // secondary clip int m_iDefaultAmmo;// how much ammo you get when you pick up this weapon as placed by a level designer.
m_iClip2 stores the current secondary clip, while m_iClientClip2 tells us what the client dll currently stores as the secondary clip value. m_fInReload2 will tell us whether we're currently reloading the secondary clip.
Now you have to set up the newly created ItemInfo::iMaxClip2 to the correct value for all your weapons. Go to line 310 in crossbow.cpp and add:
p->iMaxClip = CROSSBOW_MAX_CLIP; // secondary clip p->iMaxClip2 = WEAPON_NOCLIP; // secondary clip p->iSlot = 2;
You have to add these lines to all the weapons' GetItemInfo() member functions. The default weapons' GetItemInfo() are in
Those were the formalities. The actual code has to be added to weapons.cpp, in quite a lot of places. I'm not sure what the best order to tell them would be (from the teacher's point of view :-), so I'll add them in the order they are in the source.
We want to have the secondary clip state saved on game save/load, so add the following around line 452:
DEFINE_FIELD( CBasePlayerWeapon, m_iClip, FIELD_INTEGER ), // secondary clip DEFINE_FIELD( CBasePlayerWeapon, m_iClip2, FIELD_INTEGER ), // secondary clip DEFINE_FIELD( CBasePlayerWeapon, m_iDefaultAmmo, FIELD_INTEGER ),
This will make sure that m_iClip2 will be saved together with the other member variables. Some of the main things are in ItemPostFrame(), which handles keypresses, automatic reloads, idle animations... First of all, make sure that it keeps track of a "secondary reload", around line 640:
m_fInReload = FALSE; } // secondary clip if ((m_fInReload2) && (m_pPlayer->m_flNextAttack <= gpGlobals->time)) { // complete the reload. int j = min( iMaxClip2() - m_iClip2, m_pPlayer->m_rgAmmo[m_iSecondaryAmmoType]); // Add them to the clip m_iClip2 += j; m_pPlayer->m_rgAmmo[m_iSecondaryAmmoType] -= j; m_fInReload2 = FALSE; } // secondary clip
If a reload is in progress, and it's time to finish it, update the ammo values and quit the reload. Modify the code right below, as we might need to check for the secondary clip now, instead of just checking the ammo stats:
if ((m_pPlayer->pev->button & IN_ATTACK2) && (m_flNextSecondaryAttack <= gpGlobals->time)) { // secondary clip if ( (m_iClip2 == 0 && pszAmmo2()) || (iMaxClip2() == -1 && !m_pPlayer->m_rgAmmo[SecondaryAmmoIndex()] ) ) // secondary clip { m_fFireOnEmpty = TRUE; } SecondaryAttack(); m_pPlayer->pev->button &= ~IN_ATTACK2; }
Then you have to change the reload key code completely to allow for secondary reloads. It's around line 677:
PrimaryAttack(); } // secondary clip else if ( m_pPlayer->pev->button & IN_RELOAD && (iMaxClip() != WEAPON_NOCLIP || iMaxClip2() != WEAPON_NOCLIP) && !m_fInReload && !m_fInReload2) { // reload when reload is pressed, or if no buttons are down and weapon is empty. // check for empty clips first, then for partly depleted clips. This may look // a little odd... BOOL fCanReload, fCanReload2; fCanReload = m_pPlayer->m_rgAmmo[PrimaryAmmoIndex()] && iMaxClip() != WEAPON_NOCLIP; fCanReload2 = m_pPlayer->m_rgAmmo[SecondaryAmmoIndex()] && iMaxClip2() != WEAPON_NOCLIP; if (fCanReload && m_iClip == 0) Reload(); else if (fCanReload2 && m_iClip2 == 0) ReloadSecondary(); else if (fCanReload && m_iClip < iMaxClip()) Reload(); else if (fCanReload2 && m_iClip2 < iMaxClip2()) ReloadSecondary(); } // secondary clip else if ( !(m_pPlayer->pev->button & (IN_ATTACK|IN_ATTACK2) ) ) {
Yes, this may look odd in the first place, but it's actually fairly easy: If one of the clips is empty, give priority to it. Otherwise, reload the primary clip first (if necessary).
Now you have to add an automatic secondary reload around line 724:
Reload(); return; } // secondary clip if ( m_iClip2 == 0 && iMaxClip2() >= 0 && !(iFlags() & ITEM_FLAG_NOAUTORELOAD) && m_flNextSecondaryAttack < gpGlobals->time ) { ReloadSecondary(); return; } // secondary clip }
This is pretty much the same as for primary reloads: If the weapon has secondary clips at all, and if the clip's empty, reload.
I rewrote the AddSecondaryAmmo() function to be precisely the same as AddPrimaryAmmo(), except that it's secondary ammo... The original code is around line 910. Replace it with the following:
// secondary clip BOOL CBasePlayerWeapon :: AddSecondaryAmmo( int iCount, char *szName, int iMaxClip, int iMaxCarry ) { int iIdAmmo; if (iMaxClip < 1) { m_iClip2 = -1; iIdAmmo = m_pPlayer->GiveAmmo( iCount, szName, iMaxCarry ); } else if (m_iClip2 == 0) { int i; i = min( m_iClip2 + iCount, iMaxClip ) - m_iClip2; m_iClip2 += i; iIdAmmo = m_pPlayer->GiveAmmo( iCount - i, szName, iMaxCarry ); } else { iIdAmmo = m_pPlayer->GiveAmmo( iCount, szName, iMaxCarry ); // this is old!!! } //m_pPlayer->m_rgAmmo[m_iSecondaryAmmoType] = iMax; // hack for testing if (iIdAmmo > 0) { m_iSecondaryAmmoType = iIdAmmo; EMIT_SOUND(ENT(pev), CHAN_ITEM, "items/9mmclip1.wav", 1, ATTN_NORM); } return iIdAmmo > 0 ? TRUE : FALSE; } // secondary clip
Next thing to change is the code in CanDeploy(), around line 980:
bHasAmmo |= (m_pPlayer->m_rgAmmo[m_iSecondaryAmmoType] != 0); } // secondary clip if (m_iClip > 0 || m_iClip2 > 0) // secondary clip { bHasAmmo |= 1; } if (!bHasAmmo) {
This code makes sure that you can deploy when there's only secondary clip in the weapon. Now define the function DefaultReloadSecondary(), which is very much the same as DefaultReload(). I put it around line 1030:
// secondary clip BOOL CBasePlayerWeapon::DefaultReloadSecondary( int iClipSize, int iAnim, float fDelay ) { if (m_pPlayer->m_rgAmmo[m_iSecondaryAmmoType] <= 0) return FALSE; int j = min(iClipSize - m_iClip2, m_pPlayer->m_rgAmmo[m_iSecondaryAmmoType]); if (j == 0) return FALSE; m_pPlayer->m_flNextAttack = gpGlobals->time + fDelay; //!!UNDONE -- reload sound goes here !!! SendWeaponAnim( iAnim ); m_fInReload2 = TRUE; m_flTimeWeaponIdle = gpGlobals->time + 3; return TRUE; } // secondary clip
Next piece of code to add is in Holster() (which is around line 1088), to make sure that we quit a secondary reload when the weapon is holstered:
m_fInReload = FALSE; // cancel any reload in progress. // secondary clip m_fInReload2 = FALSE; // secondary clip m_pPlayer->pev->viewmodel = 0;
Change function ExtractAmmo() to satisfy the new declaration of AddSecondaryAmmo(). It's around line 1181:
if ( pszAmmo2() != NULL ) { // secondary clip iReturn = pWeapon->AddSecondaryAmmo( 0, (char *)pszAmmo2(), iMaxClip2(), iMaxAmmo2() ); // secondary clip } return iReturn;
And finally, replace ExtractClipAmmo() with the new version (around line 1190):
//========================================================= // called by the new item's class with the existing item as parameter //========================================================= // secondary clip int CBasePlayerWeapon::ExtractClipAmmo( CBasePlayerWeapon *pWeapon ) { int iReturn; if ( m_iClip != WEAPON_NOCLIP ) { iReturn = pWeapon->m_pPlayer->GiveAmmo( m_iClip, (char *)pszAmmo1(), iMaxAmmo1() ); // , &m_iPrimaryAmmoType } if ( m_iClip2 != WEAPON_NOCLIP ) { iReturn = pWeapon->m_pPlayer->GiveAmmo( m_iClip2, (char *)pszAmmo2(), iMaxAmmo2() ); // , &m_iPrimaryAmmoType } return iReturn; } // secondary clip
You could now add a secondary clip to your weapon, but you wouldn't see it on the HUD. So prepare for some client dll coding...
In this chapter, we'll add the secondary clip functionality to the HUD.
The very first thing to do is to modify the "CurWeapon" message. This message is used to send the primary clip state, so it's only consequent that we send the secondary clip state in this message as well. We'll send the secondary clip as a byte, which increases the message length by one. So the first thing to do is to open up player.cpp and modify the message registration around line 3260:
gmsgSelAmmo = REG_USER_MSG("SelAmmo", sizeof(SelAmmo)); // secondary clip gmsgCurWeapon = REG_USER_MSG("CurWeapon", 4); // this message is now 1 byte longer // secondary clip gmsgGeigerRange = REG_USER_MSG("Geiger", 1);
There are three places where this message is sent. The first place is CBasePlayer::RemoveAllItems() around line 790 (still in player.cpp):
// send Selected Weapon Message to our client MESSAGE_BEGIN( MSG_ONE, gmsgCurWeapon, NULL, pev ); WRITE_BYTE(0); WRITE_BYTE(0); WRITE_BYTE(0); // secondary clip WRITE_BYTE(0); // NEW: this is the secondary clip // secondary clip MESSAGE_END(); }
The second place is in CBasePlayer::Killed(), around line 880:
// Tell Ammo Hud that the player is dead MESSAGE_BEGIN( MSG_ONE, gmsgCurWeapon, NULL, pev ); WRITE_BYTE(0); WRITE_BYTE(0XFF); WRITE_BYTE(0xFF); // secondary clip WRITE_BYTE(0xFF); // this is the secondary clip // secondary clip MESSAGE_END();
The last (and actually most important place) is in CBasePlayerWeapon::UpdateClientData() in weapons.cpp around line 840:
else state = 1; } // secondary clip if ( !pPlayer->m_fWeapon || pPlayer->m_pActiveItem != pPlayer->m_pClientActiveItem || m_iClip != m_iClientClip || m_iClip2 != m_iClientClip2 || state != m_iClientWeaponState || pPlayer->m_iFOV != pPlayer->m_iClientFOV ) { MESSAGE_BEGIN( MSG_ONE, gmsgCurWeapon, NULL, pPlayer->pev ); WRITE_BYTE( state ); WRITE_BYTE( m_iId ); WRITE_BYTE( m_iClip ); WRITE_BYTE( m_iClip2 ); // this is NEW MESSAGE_END(); m_iClientClip = m_iClip; m_iClientClip2 = m_iClip2; // this is NEW m_iClientWeaponState = state; pPlayer->m_fWeapon = TRUE; } // secondary clip if ( m_pNext ) m_pNext->UpdateClientData( pPlayer );
First of all, the if-statement has been changed so that the information is resent when secondary clip state changes, too. Code is added to send the new byte of the message and to make sure that we don't send the update message over and over, by setting m_iClientClip2 to m_iClip2.
Everything else that has to be changed is in the client dll. First of all, add the secondary clip state to the WEAPON structure. Open ammo.h and add the following around line 38:
int iClip; // secondary clip int iClip2; // secondary clip int iCount; // # of itesm in plist
The rest that has to be changed is in ammo.cpp. First of all, WeaponsResource::HasAmmo() needs to be updated (around line 64):
if ( p->iMax1 == -1 ) return TRUE; // secondary clip return (p->iAmmoType == -1) || p->iClip > 0 || CountAmmo(p->iAmmoType) || p->iClip2 > 0 || CountAmmo(p->iAmmo2Type) || ( p->iFlags & WEAPON_FLAGS_SELECTONEMPTY ); // secondary clip }
Then we have to add to the message functions on the client side. Go to line 560, and add (in CHudAmmo::MsgFunc_CurWeapon()):
int iClip = READ_CHAR(); // secondary clip int iClip2 = READ_CHAR(); // NEW: secondary clip // secondary clip // detect if we're also on target if ( iState > 1 )
Now add the following to line 595 (still in the same function):
else pWeapon->iClip = iClip; // secondary clip if ( iClip2 < -1 ) pWeapon->iClip2 = abs(iClip2); else pWeapon->iClip2 = iClip2; // secondary clip if ( iState == 0 ) // we're not the current weapon, so update no more return 1;
We also have to add code to CHudAmmo::MsgFunc_WeaponList(), on line 657:
Weapon.iClip = 0; // secondary clip Weapon.iClip2 = 0; // secondary clip gWR.AddWeapon( &Weapon );
The last, but biggest thing to change is the CHudAmmo::Draw() function. Remove all the code at the end of the function (around line 916), which covers secondary ammo, and replace it with the following:
// Draw the ammo Icon int iOffset = (m_pWeapon->rcAmmo.bottom - m_pWeapon->rcAmmo.top)/8; SPR_Set(m_pWeapon->hAmmo, r, g, b); SPR_DrawAdditive(0, x, y - iOffset, &m_pWeapon->rcAmmo); } // secondary clip // Does weapon have any secondary ammo at all? if (m_pWeapon->iAmmo2Type > 0 && (gWR.CountAmmo(pw->iAmmo2Type) || pw->iClip2 >= 0)) { int iIconWidth = m_pWeapon->rcAmmo2.right - m_pWeapon->rcAmmo2.left; y -= gHUD.m_iFontHeight + gHUD.m_iFontHeight/4; if (pw->iClip2 >= 0) { // room for the number and the '|' and the current ammo x = ScreenWidth - (8 * AmmoWidth) - iIconWidth; x = gHUD.DrawHudNumber(x, y, iFlags | DHN_3DIGITS, pw->iClip2, r, g, b); wrect_t rc; rc.top = 0; rc.left = 0; rc.right = AmmoWidth; rc.bottom = 100; int iBarWidth = AmmoWidth/10; x += AmmoWidth/2; UnpackRGB(r,g,b, RGB_YELLOWISH); // draw the | bar FillRGBA(x, y, iBarWidth, gHUD.m_iFontHeight, r, g, b, a); x += iBarWidth + AmmoWidth/2;; // GL Seems to need this ScaleColors(r, g, b, a ); x = gHUD.DrawHudNumber(x, y, iFlags | DHN_3DIGITS, gWR.CountAmmo(pw->iAmmo2Type), r, g, b); } else { // SPR_Draw a bullets only line x = ScreenWidth - 4 * AmmoWidth - iIconWidth; x = gHUD.DrawHudNumber(x, y, iFlags | DHN_3DIGITS, gWR.CountAmmo(pw->iAmmo2Type), r, g, b); } // Draw the ammo Icon int iOffset = (m_pWeapon->rcAmmo2.bottom - m_pWeapon->rcAmmo2.top)/8; SPR_Set(m_pWeapon->hAmmo2, r, g, b); SPR_DrawAdditive(0, x, y - iOffset, &m_pWeapon->rcAmmo2); } // secondary clip return 1; }
This is more or less the same code as for the primary ammo. It draws both ammo and clip states, together with the seperating bar if there is a clip, and only the ammo state if the weapon doesn't have secondary clips. It finally draws the ammo icon.
Ok, that's it. You can now implement secondary clips to your weapon. For an example on how to do this, read the following chapter.
In this chapter, I'll give you a quick example on how to add a secondary clip to a weapon, which is done in three steps. I'll give you the line numbers for the mp5.cpp.
First of all, you have to set the size of the secondary clip. To do this, modify GetItemInfo() (around line 120):
p->iMaxClip = MP5_MAX_CLIP; // secondary clip p->iMaxClip2 = 4; // secondary clip p->iSlot = 2;
Actually, it would be better coding practice to define the clip size in weapons.h by something like:
#define MP5_MAX_CLIP_SECONDARY 4
You'd then use the following in GetItemInfo():
p->iMaxClip2 = MP5_MAX_CLIP_SECONDARY;
The next step is to modify SecondaryAttack(), so that it uses clips. The first things to change start in line 240:
if (m_pPlayer->pev->waterlevel == 3) { PlayEmptySound( ); m_flNextPrimaryAttack = gpGlobals->time + 0.15; return; } // secondary clip if (m_iClip2 <= 0) { PlayEmptySound( ); return; } m_iClip2--; // secondary clip m_pPlayer->m_iWeaponVolume = NORMAL_GUN_VOLUME; m_pPlayer->m_iWeaponFlash = BRIGHT_GUN_FLASH; m_pPlayer->m_iExtraSoundTypes = bits_SOUND_DANGER; m_pPlayer->m_flStopExtraSoundTime = gpGlobals->time + 0.2; // secondary clip // m_pPlayer->m_rgAmmo[m_iSecondaryAmmoType]--; // secondary clip SendWeaponAnim( MP5_LAUNCH );
You still have to change a line to the end of the function (HEV updating code):
m_flTimeWeaponIdle = gpGlobals->time + 5;// idle pretty soon after shooting. // secondary clip if ((m_iClip2 <= 0) && !m_pPlayer->m_rgAmmo[m_iSecondaryAmmoType]) // HEV suit - indicate out of ammo condition m_pPlayer->SetSuitUpdate("!HEV_AMO0", FALSE, 0); // secondary clip m_pPlayer->pev->punchangle.x -= 10; }
The last thing you have to do is to implement reload code. To do this, you have to overwrite the ReloadSecondary() virtual function for your weapon. Add this to line 53:
void Reload( void ); // secondary clip void ReloadSecondary( void ); // secondary clip void WeaponIdle( void );
You have to define this function somewhere. I put it below the normal Reload(), around line 302:
// secondary clip void CMP5::ReloadSecondary( void ) { DefaultReloadSecondary( 4, MP5_RELOAD, 1.5 ); } // secondary clip
This function works in exactly the same way as the normal Reload() function. You might have to adjust it to your needs, but for the easy things this should be enough. The first parameter in the call to DefaultReloadSecondary() is the clip size. This should match the clip size in GetItemInfo().
Note that it uses MP5_RELOAD as animation to play, which is the normal reload animation. This looks a little odd, so you'll want to add an animation to the weapon's view model, add an entry to the animation enum of the weapon (usually found at the beginning of the weapon's source), and use this instead.
If you have any questions, suggestions, bugfixes, creative critizism or you want to give me donations for this great tutorial, mail me or post on the Wavelength forums. Go to Wavelength, click on Forums and then on Coding. You have to register yourself to post there, but visiting it regularly is good anyway. There is a forum on HL Programming Planet that I regularly visit, too, but it has less traffic and less people visit it.
You may use this tutorial for any non-commercial mod development, as long as you give me some credit for it.
(c) 2000 by Nicolai Haehnle aka Prefect