You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1875 lines
55 KiB
1875 lines
55 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: Contains the implementation of game rules for multiplayer. |
|
// |
|
// $NoKeywords: $ |
|
//=============================================================================// |
|
|
|
#include "cbase.h" |
|
#include "cdll_int.h" |
|
#include "multiplay_gamerules.h" |
|
#include "viewport_panel_names.h" |
|
#include "gameeventdefs.h" |
|
#include <KeyValues.h> |
|
#include "filesystem.h" |
|
#include "mp_shareddefs.h" |
|
#include "utlbuffer.h" |
|
|
|
#ifdef CLIENT_DLL |
|
|
|
#else |
|
|
|
#include "eventqueue.h" |
|
#include "player.h" |
|
#include "basecombatweapon.h" |
|
#include "gamerules.h" |
|
#include "game.h" |
|
#include "items.h" |
|
#include "entitylist.h" |
|
#include "in_buttons.h" |
|
#include <ctype.h> |
|
#include "voice_gamemgr.h" |
|
#include "iscorer.h" |
|
#include "hltvdirector.h" |
|
#include "AI_Criteria.h" |
|
#include "sceneentity.h" |
|
#include "basemultiplayerplayer.h" |
|
#include "team.h" |
|
#include "usermessages.h" |
|
#include "tier0/icommandline.h" |
|
|
|
#ifdef NEXT_BOT |
|
#include "NextBotManager.h" |
|
#endif |
|
|
|
// TODO Why did we add this to the base class guys. |
|
#if defined ( TF_DLL ) || defined ( TF_CLIENT_DLL ) |
|
#include "player_vs_environment/tf_population_manager.h" |
|
#endif |
|
|
|
#endif |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
|
|
REGISTER_GAMERULES_CLASS( CMultiplayRules ); |
|
|
|
ConVar mp_chattime( |
|
"mp_chattime", |
|
"10", |
|
FCVAR_REPLICATED, |
|
"amount of time players can chat after the game is over", |
|
true, 1, |
|
true, 120 ); |
|
|
|
#ifdef GAME_DLL |
|
void MPTimeLimitCallback( IConVar *var, const char *pOldString, float flOldValue ) |
|
{ |
|
if ( mp_timelimit.GetInt() < 0 ) |
|
{ |
|
mp_timelimit.SetValue( 0 ); |
|
} |
|
|
|
if ( MultiplayRules() ) |
|
{ |
|
MultiplayRules()->HandleTimeLimitChange(); |
|
} |
|
} |
|
#endif |
|
|
|
ConVar mp_timelimit( "mp_timelimit", "0", FCVAR_NOTIFY|FCVAR_REPLICATED, "game time per map in minutes" |
|
#ifdef GAME_DLL |
|
, MPTimeLimitCallback |
|
#endif |
|
); |
|
|
|
ConVar fraglimit( "mp_fraglimit","0", FCVAR_NOTIFY|FCVAR_REPLICATED, "The number of kills at which the map ends"); |
|
|
|
ConVar mp_show_voice_icons( "mp_show_voice_icons", "1", FCVAR_REPLICATED, "Show overhead player voice icons when players are speaking.\n" ); |
|
|
|
#ifdef GAME_DLL |
|
|
|
ConVar tv_delaymapchange( "tv_delaymapchange", "0", FCVAR_NONE, "Delays map change until broadcast is complete" ); |
|
ConVar tv_delaymapchange_protect( "tv_delaymapchange_protect", "1", FCVAR_NONE, "Protect against doing a manual map change if HLTV is broadcasting and has not caught up with a major game event such as round_end" ); |
|
|
|
ConVar mp_restartgame( "mp_restartgame", "0", FCVAR_GAMEDLL, "If non-zero, game will restart in the specified number of seconds" ); |
|
ConVar mp_restartgame_immediate( "mp_restartgame_immediate", "0", FCVAR_GAMEDLL, "If non-zero, game will restart immediately" ); |
|
|
|
ConVar mp_mapcycle_empty_timeout_seconds( "mp_mapcycle_empty_timeout_seconds", "0", FCVAR_REPLICATED, "If nonzero, server will cycle to the next map if it has been empty on the current map for N seconds"); |
|
|
|
void cc_SkipNextMapInCycle() |
|
{ |
|
if ( !UTIL_IsCommandIssuedByServerAdmin() ) |
|
return; |
|
|
|
if ( MultiplayRules() ) |
|
{ |
|
MultiplayRules()->SkipNextMapInCycle(); |
|
} |
|
} |
|
|
|
void cc_GotoNextMapInCycle() |
|
{ |
|
if ( !UTIL_IsCommandIssuedByServerAdmin() ) |
|
return; |
|
|
|
if ( MultiplayRules() ) |
|
{ |
|
MultiplayRules()->ChangeLevel(); |
|
} |
|
} |
|
|
|
ConCommand skip_next_map( "skip_next_map", cc_SkipNextMapInCycle, "Skips the next map in the map rotation for the server." ); |
|
ConCommand changelevel_next( "changelevel_next", cc_GotoNextMapInCycle, "Immediately changes to the next map in the map rotation for the server." ); |
|
|
|
#ifndef TF_DLL // TF overrides the default value of this convar |
|
ConVar mp_waitingforplayers_time( "mp_waitingforplayers_time", "0", FCVAR_GAMEDLL, "WaitingForPlayers time length in seconds" ); |
|
#endif |
|
|
|
ConVar mp_waitingforplayers_restart( "mp_waitingforplayers_restart", "0", FCVAR_GAMEDLL, "Set to 1 to start or restart the WaitingForPlayers period." ); |
|
ConVar mp_waitingforplayers_cancel( "mp_waitingforplayers_cancel", "0", FCVAR_GAMEDLL, "Set to 1 to end the WaitingForPlayers period." ); |
|
ConVar mp_clan_readyrestart( "mp_clan_readyrestart", "0", FCVAR_GAMEDLL, "If non-zero, game will restart once someone from each team gives the ready signal" ); |
|
ConVar mp_clan_ready_signal( "mp_clan_ready_signal", "ready", FCVAR_GAMEDLL, "Text that team leader from each team must speak for the match to begin" ); |
|
|
|
ConVar nextlevel( "nextlevel", |
|
"", |
|
FCVAR_GAMEDLL | FCVAR_NOTIFY, |
|
#if defined( CSTRIKE_DLL ) || defined( TF_DLL ) |
|
"If set to a valid map name, will trigger a changelevel to the specified map at the end of the round" ); |
|
#else |
|
"If set to a valid map name, will change to this map during the next changelevel" ); |
|
#endif // CSTRIKE_DLL || TF_DLL |
|
|
|
#endif |
|
|
|
#ifndef CLIENT_DLL |
|
int CMultiplayRules::m_nMapCycleTimeStamp = 0; |
|
int CMultiplayRules::m_nMapCycleindex = 0; |
|
CUtlVector<char*> CMultiplayRules::m_MapList; |
|
#endif |
|
|
|
//========================================================= |
|
//========================================================= |
|
bool CMultiplayRules::IsMultiplayer( void ) |
|
{ |
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
int CMultiplayRules::Damage_GetTimeBased( void ) |
|
{ |
|
int iDamage = ( DMG_PARALYZE | DMG_NERVEGAS | DMG_POISON | DMG_RADIATION | DMG_DROWNRECOVER | DMG_ACID | DMG_SLOWBURN ); |
|
return iDamage; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
int CMultiplayRules::Damage_GetShouldGibCorpse( void ) |
|
{ |
|
int iDamage = ( DMG_CRUSH | DMG_FALL | DMG_BLAST | DMG_SONIC | DMG_CLUB ); |
|
return iDamage; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
int CMultiplayRules::Damage_GetShowOnHud( void ) |
|
{ |
|
int iDamage = ( DMG_POISON | DMG_ACID | DMG_DROWN | DMG_BURN | DMG_SLOWBURN | DMG_NERVEGAS | DMG_RADIATION | DMG_SHOCK ); |
|
return iDamage; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
int CMultiplayRules::Damage_GetNoPhysicsForce( void ) |
|
{ |
|
int iTimeBasedDamage = Damage_GetTimeBased(); |
|
int iDamage = ( DMG_FALL | DMG_BURN | DMG_PLASMA | DMG_DROWN | iTimeBasedDamage | DMG_CRUSH | DMG_PHYSGUN | DMG_PREVENT_PHYSICS_FORCE ); |
|
return iDamage; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
int CMultiplayRules::Damage_GetShouldNotBleed( void ) |
|
{ |
|
int iDamage = ( DMG_POISON | DMG_ACID ); |
|
return iDamage; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : iDmgType - |
|
// Output : Returns true on success, false on failure. |
|
//----------------------------------------------------------------------------- |
|
bool CMultiplayRules::Damage_IsTimeBased( int iDmgType ) |
|
{ |
|
// Damage types that are time-based. |
|
return ( ( iDmgType & ( DMG_PARALYZE | DMG_NERVEGAS | DMG_POISON | DMG_RADIATION | DMG_DROWNRECOVER | DMG_ACID | DMG_SLOWBURN ) ) != 0 ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : iDmgType - |
|
// Output : Returns true on success, false on failure. |
|
//----------------------------------------------------------------------------- |
|
bool CMultiplayRules::Damage_ShouldGibCorpse( int iDmgType ) |
|
{ |
|
// Damage types that gib the corpse. |
|
return ( ( iDmgType & ( DMG_CRUSH | DMG_FALL | DMG_BLAST | DMG_SONIC | DMG_CLUB ) ) != 0 ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : iDmgType - |
|
// Output : Returns true on success, false on failure. |
|
//----------------------------------------------------------------------------- |
|
bool CMultiplayRules::Damage_ShowOnHUD( int iDmgType ) |
|
{ |
|
// Damage types that have client HUD art. |
|
return ( ( iDmgType & ( DMG_POISON | DMG_ACID | DMG_DROWN | DMG_BURN | DMG_SLOWBURN | DMG_NERVEGAS | DMG_RADIATION | DMG_SHOCK ) ) != 0 ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : iDmgType - |
|
// Output : Returns true on success, false on failure. |
|
//----------------------------------------------------------------------------- |
|
bool CMultiplayRules::Damage_NoPhysicsForce( int iDmgType ) |
|
{ |
|
// Damage types that don't have to supply a physics force & position. |
|
int iTimeBasedDamage = Damage_GetTimeBased(); |
|
return ( ( iDmgType & ( DMG_FALL | DMG_BURN | DMG_PLASMA | DMG_DROWN | iTimeBasedDamage | DMG_CRUSH | DMG_PHYSGUN | DMG_PREVENT_PHYSICS_FORCE ) ) != 0 ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : iDmgType - |
|
// Output : Returns true on success, false on failure. |
|
//----------------------------------------------------------------------------- |
|
bool CMultiplayRules::Damage_ShouldNotBleed( int iDmgType ) |
|
{ |
|
// Damage types that don't make the player bleed. |
|
return ( ( iDmgType & ( DMG_POISON | DMG_ACID ) ) != 0 ); |
|
} |
|
|
|
//********************************************************* |
|
// Rules for the half-life multiplayer game. |
|
//********************************************************* |
|
CMultiplayRules::CMultiplayRules() |
|
{ |
|
#ifndef CLIENT_DLL |
|
m_flTimeLastMapChangeOrPlayerWasConnected = 0.0f; |
|
|
|
RefreshSkillData( true ); |
|
|
|
// 11/8/98 |
|
// Modified by YWB: Server .cfg file is now a cvar, so that |
|
// server ops can run multiple game servers, with different server .cfg files, |
|
// from a single installed directory. |
|
// Mapcyclefile is already a cvar. |
|
|
|
// 3/31/99 |
|
// Added lservercfg file cvar, since listen and dedicated servers should not |
|
// share a single config file. (sjb) |
|
if ( engine->IsDedicatedServer() ) |
|
{ |
|
// dedicated server |
|
const char *cfgfile = servercfgfile.GetString(); |
|
|
|
if ( cfgfile && cfgfile[0] ) |
|
{ |
|
char szCommand[MAX_PATH]; |
|
|
|
Log( "Executing dedicated server config file %s\n", cfgfile ); |
|
Q_snprintf( szCommand,sizeof(szCommand), "exec %s\n", cfgfile ); |
|
engine->ServerCommand( szCommand ); |
|
} |
|
} |
|
else |
|
{ |
|
// listen server |
|
const char *cfgfile = lservercfgfile.GetString(); |
|
|
|
if ( cfgfile && cfgfile[0] ) |
|
{ |
|
char szCommand[MAX_PATH]; |
|
|
|
Log( "Executing listen server config file %s\n", cfgfile ); |
|
Q_snprintf( szCommand,sizeof(szCommand), "exec %s\n", cfgfile ); |
|
engine->ServerCommand( szCommand ); |
|
} |
|
} |
|
|
|
nextlevel.SetValue( "" ); |
|
LoadMapCycleFile(); |
|
|
|
#endif |
|
|
|
LoadVoiceCommandScript(); |
|
} |
|
|
|
bool CMultiplayRules::Init() |
|
{ |
|
#ifdef GAME_DLL |
|
|
|
// Initialize the custom response rule dictionaries. |
|
InitCustomResponseRulesDicts(); |
|
|
|
#endif |
|
|
|
return BaseClass::Init(); |
|
} |
|
|
|
|
|
#ifdef CLIENT_DLL |
|
|
|
|
|
#else |
|
|
|
extern bool g_fGameOver; |
|
|
|
#define ITEM_RESPAWN_TIME 30 |
|
#define WEAPON_RESPAWN_TIME 20 |
|
#define AMMO_RESPAWN_TIME 20 |
|
|
|
//========================================================= |
|
//========================================================= |
|
void CMultiplayRules::RefreshSkillData( bool forceUpdate ) |
|
{ |
|
// load all default values |
|
BaseClass::RefreshSkillData( forceUpdate ); |
|
|
|
// override some values for multiplay. |
|
|
|
// suitcharger |
|
#ifndef TF_DLL |
|
//============================================================================= |
|
// HPE_BEGIN: |
|
// [menglish] CS doesn't have the suitcharger either |
|
//============================================================================= |
|
#ifndef CSTRIKE_DLL |
|
ConVarRef suitcharger( "sk_suitcharger" ); |
|
suitcharger.SetValue( 30 ); |
|
#endif |
|
//============================================================================= |
|
// HPE_END |
|
//============================================================================= |
|
#endif |
|
} |
|
|
|
|
|
//========================================================= |
|
//========================================================= |
|
void CMultiplayRules::Think ( void ) |
|
{ |
|
BaseClass::Think(); |
|
|
|
///// Check game rules ///// |
|
|
|
if ( g_fGameOver ) // someone else quit the game already |
|
{ |
|
ChangeLevel(); // intermission is over |
|
return; |
|
} |
|
|
|
float flTimeLimit = mp_timelimit.GetFloat() * 60; |
|
float flFragLimit = fraglimit.GetFloat(); |
|
|
|
if ( flTimeLimit != 0 && gpGlobals->curtime >= flTimeLimit ) |
|
{ |
|
GoToIntermission(); |
|
return; |
|
} |
|
|
|
if ( flFragLimit ) |
|
{ |
|
// check if any player is over the frag limit |
|
for ( int i = 1; i <= gpGlobals->maxClients; i++ ) |
|
{ |
|
CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); |
|
|
|
if ( pPlayer && pPlayer->FragCount() >= flFragLimit ) |
|
{ |
|
GoToIntermission(); |
|
return; |
|
} |
|
} |
|
} |
|
} |
|
|
|
//========================================================= |
|
//========================================================= |
|
void CMultiplayRules::FrameUpdatePostEntityThink() |
|
{ |
|
BaseClass::FrameUpdatePostEntityThink(); |
|
|
|
float flNow = Plat_FloatTime(); |
|
|
|
// Update time when client was last connected |
|
if ( m_flTimeLastMapChangeOrPlayerWasConnected <= 0.0f ) |
|
{ |
|
m_flTimeLastMapChangeOrPlayerWasConnected = flNow; |
|
} |
|
else |
|
{ |
|
for( int iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ ) |
|
{ |
|
player_info_t pi; |
|
if ( !engine->GetPlayerInfo( iPlayerIndex, &pi ) ) |
|
continue; |
|
#if defined( REPLAY_ENABLED ) |
|
if ( pi.ishltv || pi.isreplay || pi.fakeplayer ) |
|
#else |
|
if ( pi.ishltv || pi.fakeplayer ) |
|
#endif |
|
continue; |
|
|
|
m_flTimeLastMapChangeOrPlayerWasConnected = flNow; |
|
break; |
|
} |
|
} |
|
|
|
// Check if we should cycle the map because we've been empty |
|
// for long enough |
|
if ( mp_mapcycle_empty_timeout_seconds.GetInt() > 0 ) |
|
{ |
|
int iIdleSeconds = (int)( flNow - m_flTimeLastMapChangeOrPlayerWasConnected ); |
|
if ( iIdleSeconds >= mp_mapcycle_empty_timeout_seconds.GetInt() ) |
|
{ |
|
|
|
Log( "Server has been empty for %d seconds on this map, cycling map as per mp_mapcycle_empty_timeout_seconds\n", iIdleSeconds ); |
|
ChangeLevel(); |
|
} |
|
} |
|
} |
|
|
|
|
|
//========================================================= |
|
//========================================================= |
|
bool CMultiplayRules::IsDeathmatch( void ) |
|
{ |
|
return true; |
|
} |
|
|
|
//========================================================= |
|
//========================================================= |
|
bool CMultiplayRules::IsCoOp( void ) |
|
{ |
|
return false; |
|
} |
|
|
|
//========================================================= |
|
//========================================================= |
|
bool CMultiplayRules::FShouldSwitchWeapon( CBasePlayer *pPlayer, CBaseCombatWeapon *pWeapon ) |
|
{ |
|
if ( !pPlayer->Weapon_CanSwitchTo( pWeapon ) ) |
|
{ |
|
// Can't switch weapons for some reason. |
|
return false; |
|
} |
|
|
|
if ( !pPlayer->GetActiveWeapon() ) |
|
{ |
|
// Player doesn't have an active item, might as well switch. |
|
return true; |
|
} |
|
|
|
if ( !pWeapon->AllowsAutoSwitchTo() ) |
|
{ |
|
// The given weapon should not be auto switched to from another weapon. |
|
return false; |
|
} |
|
|
|
if ( !pPlayer->GetActiveWeapon()->AllowsAutoSwitchFrom() ) |
|
{ |
|
// The active weapon does not allow autoswitching away from it. |
|
return false; |
|
} |
|
|
|
if ( pWeapon->GetWeight() > pPlayer->GetActiveWeapon()->GetWeight() ) |
|
{ |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Returns the weapon in the player's inventory that would be better than |
|
// the given weapon. |
|
//----------------------------------------------------------------------------- |
|
CBaseCombatWeapon *CMultiplayRules::GetNextBestWeapon( CBaseCombatCharacter *pPlayer, CBaseCombatWeapon *pCurrentWeapon ) |
|
{ |
|
CBaseCombatWeapon *pCheck; |
|
CBaseCombatWeapon *pBest;// this will be used in the event that we don't find a weapon in the same category. |
|
|
|
int iCurrentWeight = -1; |
|
int iBestWeight = -1;// no weapon lower than -1 can be autoswitched to |
|
pBest = NULL; |
|
|
|
// If I have a weapon, make sure I'm allowed to holster it |
|
if ( pCurrentWeapon ) |
|
{ |
|
if ( !pCurrentWeapon->AllowsAutoSwitchFrom() || !pCurrentWeapon->CanHolster() ) |
|
{ |
|
// Either this weapon doesn't allow autoswitching away from it or I |
|
// can't put this weapon away right now, so I can't switch. |
|
return NULL; |
|
} |
|
|
|
iCurrentWeight = pCurrentWeapon->GetWeight(); |
|
} |
|
|
|
for ( int i = 0 ; i < pPlayer->WeaponCount(); ++i ) |
|
{ |
|
pCheck = pPlayer->GetWeapon( i ); |
|
if ( !pCheck ) |
|
continue; |
|
|
|
// If we have an active weapon and this weapon doesn't allow autoswitching away |
|
// from another weapon, skip it. |
|
if ( pCurrentWeapon && !pCheck->AllowsAutoSwitchTo() ) |
|
continue; |
|
|
|
if ( pCheck->GetWeight() > -1 && pCheck->GetWeight() == iCurrentWeight && pCheck != pCurrentWeapon ) |
|
{ |
|
// this weapon is from the same category. |
|
if ( pCheck->HasAnyAmmo() ) |
|
{ |
|
if ( pPlayer->Weapon_CanSwitchTo( pCheck ) ) |
|
{ |
|
return pCheck; |
|
} |
|
} |
|
} |
|
else if ( pCheck->GetWeight() > iBestWeight && pCheck != pCurrentWeapon )// don't reselect the weapon we're trying to get rid of |
|
{ |
|
//Msg( "Considering %s\n", STRING( pCheck->GetClassname() ); |
|
// we keep updating the 'best' weapon just in case we can't find a weapon of the same weight |
|
// that the player was using. This will end up leaving the player with his heaviest-weighted |
|
// weapon. |
|
if ( pCheck->HasAnyAmmo() ) |
|
{ |
|
// if this weapon is useable, flag it as the best |
|
iBestWeight = pCheck->GetWeight(); |
|
pBest = pCheck; |
|
} |
|
} |
|
} |
|
|
|
// if we make it here, we've checked all the weapons and found no useable |
|
// weapon in the same catagory as the current weapon. |
|
|
|
// if pBest is null, we didn't find ANYTHING. Shouldn't be possible- should always |
|
// at least get the crowbar, but ya never know. |
|
return pBest; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Output : Returns true on success, false on failure. |
|
//----------------------------------------------------------------------------- |
|
bool CMultiplayRules::SwitchToNextBestWeapon( CBaseCombatCharacter *pPlayer, CBaseCombatWeapon *pCurrentWeapon ) |
|
{ |
|
CBaseCombatWeapon *pWeapon = GetNextBestWeapon( pPlayer, pCurrentWeapon ); |
|
|
|
if ( pWeapon != NULL ) |
|
return pPlayer->Weapon_Switch( pWeapon ); |
|
|
|
return false; |
|
} |
|
|
|
//========================================================= |
|
//========================================================= |
|
bool CMultiplayRules::ClientConnected( edict_t *pEntity, const char *pszName, const char *pszAddress, char *reject, int maxrejectlen ) |
|
{ |
|
GetVoiceGameMgr()->ClientConnected( pEntity ); |
|
return true; |
|
} |
|
|
|
void CMultiplayRules::InitHUD( CBasePlayer *pl ) |
|
{ |
|
} |
|
|
|
//========================================================= |
|
//========================================================= |
|
void CMultiplayRules::ClientDisconnected( edict_t *pClient ) |
|
{ |
|
if ( pClient ) |
|
{ |
|
CBasePlayer *pPlayer = (CBasePlayer *)CBaseEntity::Instance( pClient ); |
|
|
|
if ( pPlayer ) |
|
{ |
|
FireTargets( "game_playerleave", pPlayer, pPlayer, USE_TOGGLE, 0 ); |
|
|
|
pPlayer->RemoveAllItems( true );// destroy all of the players weapons and items |
|
|
|
// Kill off view model entities |
|
pPlayer->DestroyViewModels(); |
|
|
|
pPlayer->SetConnected( PlayerDisconnected ); |
|
} |
|
} |
|
} |
|
|
|
//========================================================= |
|
//========================================================= |
|
float CMultiplayRules::FlPlayerFallDamage( CBasePlayer *pPlayer ) |
|
{ |
|
int iFallDamage = (int)falldamage.GetFloat(); |
|
|
|
switch ( iFallDamage ) |
|
{ |
|
case 1://progressive |
|
pPlayer->m_Local.m_flFallVelocity -= PLAYER_MAX_SAFE_FALL_SPEED; |
|
return pPlayer->m_Local.m_flFallVelocity * DAMAGE_FOR_FALL_SPEED; |
|
break; |
|
default: |
|
case 0:// fixed |
|
return 10; |
|
break; |
|
} |
|
} |
|
|
|
//========================================================= |
|
//========================================================= |
|
bool CMultiplayRules::AllowDamage( CBaseEntity *pVictim, const CTakeDamageInfo &info ) |
|
{ |
|
return true; |
|
} |
|
|
|
//========================================================= |
|
//========================================================= |
|
bool CMultiplayRules::FPlayerCanTakeDamage( CBasePlayer *pPlayer, CBaseEntity *pAttacker, const CTakeDamageInfo &info ) |
|
{ |
|
return true; |
|
} |
|
|
|
//========================================================= |
|
//========================================================= |
|
void CMultiplayRules::PlayerThink( CBasePlayer *pPlayer ) |
|
{ |
|
if ( g_fGameOver ) |
|
{ |
|
// clear attack/use commands from player |
|
pPlayer->m_afButtonPressed = 0; |
|
pPlayer->m_nButtons = 0; |
|
pPlayer->m_afButtonReleased = 0; |
|
} |
|
} |
|
|
|
//========================================================= |
|
//========================================================= |
|
void CMultiplayRules::PlayerSpawn( CBasePlayer *pPlayer ) |
|
{ |
|
bool addDefault; |
|
CBaseEntity *pWeaponEntity = NULL; |
|
|
|
pPlayer->EquipSuit(); |
|
|
|
addDefault = true; |
|
|
|
while ( (pWeaponEntity = gEntList.FindEntityByClassname( pWeaponEntity, "game_player_equip" )) != NULL) |
|
{ |
|
pWeaponEntity->Touch( pPlayer ); |
|
addDefault = false; |
|
} |
|
} |
|
|
|
//========================================================= |
|
//========================================================= |
|
bool CMultiplayRules::FPlayerCanRespawn( CBasePlayer *pPlayer ) |
|
{ |
|
return true; |
|
} |
|
|
|
//========================================================= |
|
//========================================================= |
|
float CMultiplayRules::FlPlayerSpawnTime( CBasePlayer *pPlayer ) |
|
{ |
|
return gpGlobals->curtime;//now! |
|
} |
|
|
|
bool CMultiplayRules::AllowAutoTargetCrosshair( void ) |
|
{ |
|
return ( aimcrosshair.GetInt() != 0 ); |
|
} |
|
|
|
//========================================================= |
|
// IPointsForKill - how many points awarded to anyone |
|
// that kills this player? |
|
//========================================================= |
|
int CMultiplayRules::IPointsForKill( CBasePlayer *pAttacker, CBasePlayer *pKilled ) |
|
{ |
|
return 1; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
CBasePlayer *CMultiplayRules::GetDeathScorer( CBaseEntity *pKiller, CBaseEntity *pInflictor ) |
|
{ |
|
if ( pKiller) |
|
{ |
|
if ( pKiller->Classify() == CLASS_PLAYER ) |
|
return (CBasePlayer*)pKiller; |
|
|
|
// Killing entity might be specifying a scorer player |
|
IScorer *pScorerInterface = dynamic_cast<IScorer*>( pKiller ); |
|
if ( pScorerInterface ) |
|
{ |
|
CBasePlayer *pPlayer = pScorerInterface->GetScorer(); |
|
if ( pPlayer ) |
|
return pPlayer; |
|
} |
|
|
|
// Inflicting entity might be specifying a scoring player |
|
pScorerInterface = dynamic_cast<IScorer*>( pInflictor ); |
|
if ( pScorerInterface ) |
|
{ |
|
CBasePlayer *pPlayer = pScorerInterface->GetScorer(); |
|
if ( pPlayer ) |
|
return pPlayer; |
|
} |
|
} |
|
|
|
return NULL; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Returns player who should receive credit for kill |
|
//----------------------------------------------------------------------------- |
|
CBasePlayer *CMultiplayRules::GetDeathScorer( CBaseEntity *pKiller, CBaseEntity *pInflictor, CBaseEntity *pVictim ) |
|
{ |
|
// if this method not overridden by subclass, just call our default implementation |
|
return GetDeathScorer( pKiller, pInflictor ); |
|
} |
|
|
|
//========================================================= |
|
// PlayerKilled - someone/something killed this player |
|
//========================================================= |
|
void CMultiplayRules::PlayerKilled( CBasePlayer *pVictim, const CTakeDamageInfo &info ) |
|
{ |
|
DeathNotice( pVictim, info ); |
|
|
|
// Find the killer & the scorer |
|
CBaseEntity *pInflictor = info.GetInflictor(); |
|
CBaseEntity *pKiller = info.GetAttacker(); |
|
CBasePlayer *pScorer = GetDeathScorer( pKiller, pInflictor, pVictim ); |
|
|
|
pVictim->IncrementDeathCount( 1 ); |
|
|
|
// dvsents2: uncomment when removing all FireTargets |
|
// variant_t value; |
|
// g_EventQueue.AddEvent( "game_playerdie", "Use", value, 0, pVictim, pVictim ); |
|
FireTargets( "game_playerdie", pVictim, pVictim, USE_TOGGLE, 0 ); |
|
|
|
// Did the player kill himself? |
|
if ( pVictim == pScorer ) |
|
{ |
|
if ( UseSuicidePenalty() ) |
|
{ |
|
// Players lose a frag for killing themselves |
|
pVictim->IncrementFragCount( -1 ); |
|
} |
|
} |
|
else if ( pScorer ) |
|
{ |
|
// if a player dies in a deathmatch game and the killer is a client, award the killer some points |
|
pScorer->IncrementFragCount( IPointsForKill( pScorer, pVictim ) ); |
|
|
|
// Allow the scorer to immediately paint a decal |
|
pScorer->AllowImmediateDecalPainting(); |
|
|
|
// dvsents2: uncomment when removing all FireTargets |
|
//variant_t value; |
|
//g_EventQueue.AddEvent( "game_playerkill", "Use", value, 0, pScorer, pScorer ); |
|
FireTargets( "game_playerkill", pScorer, pScorer, USE_TOGGLE, 0 ); |
|
} |
|
else |
|
{ |
|
if ( UseSuicidePenalty() ) |
|
{ |
|
// Players lose a frag for letting the world kill them |
|
pVictim->IncrementFragCount( -1 ); |
|
} |
|
} |
|
} |
|
|
|
//========================================================= |
|
// Deathnotice. |
|
//========================================================= |
|
void CMultiplayRules::DeathNotice( CBasePlayer *pVictim, const CTakeDamageInfo &info ) |
|
{ |
|
// Work out what killed the player, and send a message to all clients about it |
|
const char *killer_weapon_name = "world"; // by default, the player is killed by the world |
|
int killer_ID = 0; |
|
|
|
// Find the killer & the scorer |
|
CBaseEntity *pInflictor = info.GetInflictor(); |
|
CBaseEntity *pKiller = info.GetAttacker(); |
|
CBasePlayer *pScorer = GetDeathScorer( pKiller, pInflictor, pVictim ); |
|
|
|
// Custom damage type? |
|
if ( info.GetDamageCustom() ) |
|
{ |
|
killer_weapon_name = GetDamageCustomString( info ); |
|
if ( pScorer ) |
|
{ |
|
killer_ID = pScorer->GetUserID(); |
|
} |
|
} |
|
else |
|
{ |
|
// Is the killer a client? |
|
if ( pScorer ) |
|
{ |
|
killer_ID = pScorer->GetUserID(); |
|
|
|
if ( pInflictor ) |
|
{ |
|
if ( pInflictor == pScorer ) |
|
{ |
|
// If the inflictor is the killer, then it must be their current weapon doing the damage |
|
if ( pScorer->GetActiveWeapon() ) |
|
{ |
|
#ifdef HL1MP_DLL |
|
killer_weapon_name = pScorer->GetActiveWeapon()->GetClassname(); |
|
#else |
|
killer_weapon_name = pScorer->GetActiveWeapon()->GetDeathNoticeName(); |
|
#endif |
|
} |
|
} |
|
else |
|
{ |
|
killer_weapon_name = STRING( pInflictor->m_iClassname ); // it's just that easy |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
killer_weapon_name = STRING( pInflictor->m_iClassname ); |
|
} |
|
|
|
// strip the NPC_* or weapon_* from the inflictor's classname |
|
if ( strncmp( killer_weapon_name, "weapon_", 7 ) == 0 ) |
|
{ |
|
killer_weapon_name += 7; |
|
} |
|
else if ( strncmp( killer_weapon_name, "NPC_", 4 ) == 0 ) |
|
{ |
|
killer_weapon_name += 4; |
|
} |
|
else if ( strncmp( killer_weapon_name, "func_", 5 ) == 0 ) |
|
{ |
|
killer_weapon_name += 5; |
|
} |
|
} |
|
|
|
IGameEvent * event = gameeventmanager->CreateEvent( "player_death" ); |
|
if ( event ) |
|
{ |
|
event->SetInt("userid", pVictim->GetUserID() ); |
|
event->SetInt("attacker", killer_ID ); |
|
event->SetInt("customkill", info.GetDamageCustom() ); |
|
event->SetInt("priority", 7 ); // HLTV event priority, not transmitted |
|
#ifdef HL1MP_DLL |
|
event->SetString("weapon", killer_weapon_name ); |
|
#endif |
|
gameeventmanager->FireEvent( event ); |
|
} |
|
|
|
} |
|
|
|
//========================================================= |
|
// FlWeaponRespawnTime - what is the time in the future |
|
// at which this weapon may spawn? |
|
//========================================================= |
|
float CMultiplayRules::FlWeaponRespawnTime( CBaseCombatWeapon *pWeapon ) |
|
{ |
|
if ( weaponstay.GetInt() > 0 ) |
|
{ |
|
// make sure it's only certain weapons |
|
if ( !(pWeapon->GetWeaponFlags() & ITEM_FLAG_LIMITINWORLD) ) |
|
{ |
|
return gpGlobals->curtime + 0; // weapon respawns almost instantly |
|
} |
|
} |
|
|
|
return gpGlobals->curtime + WEAPON_RESPAWN_TIME; |
|
} |
|
|
|
// when we are within this close to running out of entities, items |
|
// marked with the ITEM_FLAG_LIMITINWORLD will delay their respawn |
|
#define ENTITY_INTOLERANCE 100 |
|
|
|
//========================================================= |
|
// FlWeaponRespawnTime - Returns 0 if the weapon can respawn |
|
// now, otherwise it returns the time at which it can try |
|
// to spawn again. |
|
//========================================================= |
|
float CMultiplayRules::FlWeaponTryRespawn( CBaseCombatWeapon *pWeapon ) |
|
{ |
|
if ( pWeapon && (pWeapon->GetWeaponFlags() & ITEM_FLAG_LIMITINWORLD) ) |
|
{ |
|
if ( gEntList.NumberOfEntities() < (gpGlobals->maxEntities - ENTITY_INTOLERANCE) ) |
|
return 0; |
|
|
|
// we're past the entity tolerance level, so delay the respawn |
|
return FlWeaponRespawnTime( pWeapon ); |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
//========================================================= |
|
// VecWeaponRespawnSpot - where should this weapon spawn? |
|
// Some game variations may choose to randomize spawn locations |
|
//========================================================= |
|
Vector CMultiplayRules::VecWeaponRespawnSpot( CBaseCombatWeapon *pWeapon ) |
|
{ |
|
return pWeapon->GetAbsOrigin(); |
|
} |
|
|
|
//========================================================= |
|
// WeaponShouldRespawn - any conditions inhibiting the |
|
// respawning of this weapon? |
|
//========================================================= |
|
int CMultiplayRules::WeaponShouldRespawn( CBaseCombatWeapon *pWeapon ) |
|
{ |
|
if ( pWeapon->HasSpawnFlags( SF_NORESPAWN ) ) |
|
{ |
|
return GR_WEAPON_RESPAWN_NO; |
|
} |
|
|
|
return GR_WEAPON_RESPAWN_YES; |
|
} |
|
|
|
//========================================================= |
|
// CanHaveWeapon - returns false if the player is not allowed |
|
// to pick up this weapon |
|
//========================================================= |
|
bool CMultiplayRules::CanHavePlayerItem( CBasePlayer *pPlayer, CBaseCombatWeapon *pItem ) |
|
{ |
|
if ( weaponstay.GetInt() > 0 ) |
|
{ |
|
if ( pItem->GetWeaponFlags() & ITEM_FLAG_LIMITINWORLD ) |
|
return BaseClass::CanHavePlayerItem( pPlayer, pItem ); |
|
|
|
// check if the player already has this weapon |
|
for ( int i = 0 ; i < pPlayer->WeaponCount() ; i++ ) |
|
{ |
|
if ( pPlayer->GetWeapon(i) == pItem ) |
|
{ |
|
return false; |
|
} |
|
} |
|
} |
|
|
|
return BaseClass::CanHavePlayerItem( pPlayer, pItem ); |
|
} |
|
|
|
//========================================================= |
|
//========================================================= |
|
bool CMultiplayRules::CanHaveItem( CBasePlayer *pPlayer, CItem *pItem ) |
|
{ |
|
return true; |
|
} |
|
|
|
//========================================================= |
|
//========================================================= |
|
void CMultiplayRules::PlayerGotItem( CBasePlayer *pPlayer, CItem *pItem ) |
|
{ |
|
} |
|
|
|
//========================================================= |
|
//========================================================= |
|
int CMultiplayRules::ItemShouldRespawn( CItem *pItem ) |
|
{ |
|
if ( pItem->HasSpawnFlags( SF_NORESPAWN ) ) |
|
{ |
|
return GR_ITEM_RESPAWN_NO; |
|
} |
|
|
|
return GR_ITEM_RESPAWN_YES; |
|
} |
|
|
|
|
|
//========================================================= |
|
// At what time in the future may this Item respawn? |
|
//========================================================= |
|
float CMultiplayRules::FlItemRespawnTime( CItem *pItem ) |
|
{ |
|
return gpGlobals->curtime + ITEM_RESPAWN_TIME; |
|
} |
|
|
|
//========================================================= |
|
// Where should this item respawn? |
|
// Some game variations may choose to randomize spawn locations |
|
//========================================================= |
|
Vector CMultiplayRules::VecItemRespawnSpot( CItem *pItem ) |
|
{ |
|
return pItem->GetAbsOrigin(); |
|
} |
|
|
|
//========================================================= |
|
// What angles should this item use to respawn? |
|
//========================================================= |
|
QAngle CMultiplayRules::VecItemRespawnAngles( CItem *pItem ) |
|
{ |
|
return pItem->GetAbsAngles(); |
|
} |
|
|
|
//========================================================= |
|
//========================================================= |
|
void CMultiplayRules::PlayerGotAmmo( CBaseCombatCharacter *pPlayer, char *szName, int iCount ) |
|
{ |
|
} |
|
|
|
//========================================================= |
|
//========================================================= |
|
bool CMultiplayRules::IsAllowedToSpawn( CBaseEntity *pEntity ) |
|
{ |
|
// if ( pEntity->GetFlags() & FL_NPC ) |
|
// return false; |
|
|
|
return true; |
|
} |
|
|
|
|
|
//========================================================= |
|
//========================================================= |
|
float CMultiplayRules::FlHealthChargerRechargeTime( void ) |
|
{ |
|
return 60; |
|
} |
|
|
|
|
|
float CMultiplayRules::FlHEVChargerRechargeTime( void ) |
|
{ |
|
return 30; |
|
} |
|
|
|
//========================================================= |
|
//========================================================= |
|
int CMultiplayRules::DeadPlayerWeapons( CBasePlayer *pPlayer ) |
|
{ |
|
return GR_PLR_DROP_GUN_ACTIVE; |
|
} |
|
|
|
//========================================================= |
|
//========================================================= |
|
int CMultiplayRules::DeadPlayerAmmo( CBasePlayer *pPlayer ) |
|
{ |
|
return GR_PLR_DROP_AMMO_ACTIVE; |
|
} |
|
|
|
CBaseEntity *CMultiplayRules::GetPlayerSpawnSpot( CBasePlayer *pPlayer ) |
|
{ |
|
CBaseEntity *pentSpawnSpot = BaseClass::GetPlayerSpawnSpot( pPlayer ); |
|
|
|
//!! replace this with an Event |
|
/* |
|
if ( IsMultiplayer() && pentSpawnSpot->m_target ) |
|
{ |
|
FireTargets( STRING(pentSpawnSpot->m_target), pPlayer, pPlayer, USE_TOGGLE, 0 ); // dvsents2: what is this code supposed to do? |
|
} |
|
*/ |
|
|
|
return pentSpawnSpot; |
|
} |
|
|
|
|
|
//========================================================= |
|
//========================================================= |
|
bool CMultiplayRules::PlayerCanHearChat( CBasePlayer *pListener, CBasePlayer *pSpeaker ) |
|
{ |
|
return ( PlayerRelationship( pListener, pSpeaker ) == GR_TEAMMATE ); |
|
} |
|
|
|
int CMultiplayRules::PlayerRelationship( CBaseEntity *pPlayer, CBaseEntity *pTarget ) |
|
{ |
|
// half life deathmatch has only enemies |
|
return GR_NOTTEAMMATE; |
|
} |
|
|
|
bool CMultiplayRules::PlayFootstepSounds( CBasePlayer *pl ) |
|
{ |
|
if ( footsteps.GetInt() == 0 ) |
|
return false; |
|
|
|
if ( pl->IsOnLadder() || pl->GetAbsVelocity().Length2D() > 220 ) |
|
return true; // only make step sounds in multiplayer if the player is moving fast enough |
|
|
|
return false; |
|
} |
|
|
|
bool CMultiplayRules::FAllowFlashlight( void ) |
|
{ |
|
return flashlight.GetInt() != 0; |
|
} |
|
|
|
//========================================================= |
|
//========================================================= |
|
bool CMultiplayRules::FAllowNPCs( void ) |
|
{ |
|
return true; // E3 hack |
|
return ( allowNPCs.GetInt() != 0 ); |
|
} |
|
|
|
//========================================================= |
|
//======== CMultiplayRules private functions =========== |
|
|
|
void CMultiplayRules::GoToIntermission( void ) |
|
{ |
|
if ( g_fGameOver ) |
|
return; |
|
|
|
g_fGameOver = true; |
|
|
|
float flWaitTime = mp_chattime.GetInt(); |
|
|
|
if ( tv_delaymapchange.GetBool() ) |
|
{ |
|
if ( HLTVDirector()->IsActive() ) |
|
flWaitTime = MAX( flWaitTime, HLTVDirector()->GetDelay() ); |
|
} |
|
|
|
m_flIntermissionEndTime = gpGlobals->curtime + flWaitTime; |
|
|
|
for ( int i = 1; i <= MAX_PLAYERS; i++ ) |
|
{ |
|
CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); |
|
|
|
if ( !pPlayer ) |
|
continue; |
|
|
|
pPlayer->ShowViewPortPanel( PANEL_SCOREBOARD ); |
|
} |
|
} |
|
|
|
// Strip ' ' and '\n' characters from string. |
|
static void StripWhitespaceChars( char *szBuffer ) |
|
{ |
|
char *szOut = szBuffer; |
|
|
|
for ( char *szIn = szOut; *szIn; szIn++ ) |
|
{ |
|
if ( *szIn != ' ' && *szIn != '\r' ) |
|
*szOut++ = *szIn; |
|
} |
|
*szOut = '\0'; |
|
} |
|
|
|
void CMultiplayRules::GetNextLevelName( char *pszNextMap, int bufsize, bool bRandom /* = false */ ) |
|
{ |
|
char mapcfile[MAX_PATH]; |
|
DetermineMapCycleFilename( mapcfile, sizeof(mapcfile), false ); |
|
|
|
// Check the time of the mapcycle file and re-populate the list of level names if the file has been modified |
|
const int nMapCycleTimeStamp = filesystem->GetPathTime( mapcfile, "GAME" ); |
|
|
|
if ( 0 == nMapCycleTimeStamp ) |
|
{ |
|
// Map cycle file does not exist, make a list containing only the current map |
|
char *szCurrentMapName = new char[MAX_MAP_NAME]; |
|
Q_strncpy( szCurrentMapName, STRING(gpGlobals->mapname), MAX_MAP_NAME ); |
|
m_MapList.AddToTail( szCurrentMapName ); |
|
} |
|
else |
|
{ |
|
// If map cycle file has changed or this is the first time through ... |
|
if ( m_nMapCycleTimeStamp != nMapCycleTimeStamp ) |
|
{ |
|
// Reload |
|
LoadMapCycleFile(); |
|
} |
|
} |
|
|
|
// If somehow we have no maps in the list then add the current one |
|
if ( 0 == m_MapList.Count() ) |
|
{ |
|
char *szDefaultMapName = new char[MAX_MAP_NAME]; |
|
Q_strncpy( szDefaultMapName, STRING(gpGlobals->mapname), MAX_MAP_NAME ); |
|
m_MapList.AddToTail( szDefaultMapName ); |
|
} |
|
|
|
if ( bRandom ) |
|
{ |
|
m_nMapCycleindex = RandomInt( 0, m_MapList.Count() - 1 ); |
|
} |
|
|
|
// Here's the return value |
|
Q_strncpy( pszNextMap, m_MapList[m_nMapCycleindex], bufsize); |
|
} |
|
|
|
void CMultiplayRules::DetermineMapCycleFilename( char *pszResult, int nSizeResult, bool bForceSpew ) |
|
{ |
|
static char szLastResult[ MAX_PATH ]; |
|
|
|
const char *pszVar = mapcyclefile.GetString(); |
|
if ( *pszVar == '\0' ) |
|
{ |
|
if ( bForceSpew || V_stricmp( szLastResult, "__novar") ) |
|
{ |
|
Msg( "mapcyclefile convar not set.\n" ); |
|
V_strcpy_safe( szLastResult, "__novar" ); |
|
} |
|
*pszResult = '\0'; |
|
return; |
|
} |
|
|
|
char szRecommendedName[ MAX_PATH ]; |
|
V_sprintf_safe( szRecommendedName, "cfg/%s", pszVar ); |
|
|
|
// First, look for a mapcycle file in the cfg directory, which is preferred |
|
V_strncpy( pszResult, szRecommendedName, nSizeResult ); |
|
if ( filesystem->FileExists( pszResult, "GAME" ) ) |
|
{ |
|
if ( bForceSpew || V_stricmp( szLastResult, pszResult) ) |
|
{ |
|
Msg( "Using map cycle file '%s'.\n", pszResult ); |
|
V_strcpy_safe( szLastResult, pszResult ); |
|
} |
|
return; |
|
} |
|
|
|
// Nope? Try the root. |
|
V_strncpy( pszResult, pszVar, nSizeResult ); |
|
if ( filesystem->FileExists( pszResult, "GAME" ) ) |
|
{ |
|
if ( bForceSpew || V_stricmp( szLastResult, pszResult) ) |
|
{ |
|
Msg( "Using map cycle file '%s'. ('%s' was not found.)\n", pszResult, szRecommendedName ); |
|
V_strcpy_safe( szLastResult, pszResult ); |
|
} |
|
return; |
|
} |
|
|
|
// Nope? Use the default. |
|
if ( !V_stricmp( pszVar, "mapcycle.txt" ) ) |
|
{ |
|
V_strncpy( pszResult, "cfg/mapcycle_default.txt", nSizeResult ); |
|
if ( filesystem->FileExists( pszResult, "GAME" ) ) |
|
{ |
|
if ( bForceSpew || V_stricmp( szLastResult, pszResult) ) |
|
{ |
|
Msg( "Using map cycle file '%s'. ('%s' was not found.)\n", pszResult, szRecommendedName ); |
|
V_strcpy_safe( szLastResult, pszResult ); |
|
} |
|
return; |
|
} |
|
} |
|
|
|
// Failed |
|
*pszResult = '\0'; |
|
if ( bForceSpew || V_stricmp( szLastResult, "__notfound") ) |
|
{ |
|
Msg( "Map cycle file '%s' was not found.\n", szRecommendedName ); |
|
V_strcpy_safe( szLastResult, "__notfound" ); |
|
} |
|
} |
|
|
|
void CMultiplayRules::LoadMapCycleFileIntoVector( const char *pszMapCycleFile, CUtlVector<char *> &mapList ) |
|
{ |
|
CMultiplayRules::RawLoadMapCycleFileIntoVector( pszMapCycleFile, mapList ); |
|
} |
|
|
|
void CMultiplayRules::RawLoadMapCycleFileIntoVector( const char *pszMapCycleFile, CUtlVector<char *> &mapList ) |
|
{ |
|
CUtlBuffer buf; |
|
if ( !filesystem->ReadFile( pszMapCycleFile, "GAME", buf ) ) |
|
return; |
|
buf.PutChar( 0 ); |
|
V_SplitString( (char*)buf.Base(), "\n", mapList ); |
|
|
|
for ( int i = 0; i < mapList.Count(); i++ ) |
|
{ |
|
bool bIgnore = false; |
|
|
|
// Strip out ' ' and '\r' chars. |
|
StripWhitespaceChars( mapList[i] ); |
|
|
|
if ( !Q_strncmp( mapList[i], "//", 2 ) || mapList[i][0] == '\0' ) |
|
{ |
|
bIgnore = true; |
|
} |
|
|
|
if ( bIgnore ) |
|
{ |
|
delete [] mapList[i]; |
|
mapList.Remove( i ); |
|
--i; |
|
} |
|
} |
|
} |
|
|
|
void CMultiplayRules::FreeMapCycleFileVector( CUtlVector<char *> &mapList ) |
|
{ |
|
// Clear out existing map list. Not using Purge() or PurgeAndDeleteAll() because they won't delete [] each element. |
|
for ( int i = 0; i < mapList.Count(); i++ ) |
|
{ |
|
delete [] mapList[i]; |
|
} |
|
|
|
mapList.RemoveAll(); |
|
} |
|
|
|
bool CMultiplayRules::IsManualMapChangeOkay( const char **pszReason ) |
|
{ |
|
if ( HLTVDirector()->IsActive() && ( HLTVDirector()->GetDelay() >= HLTV_MIN_DIRECTOR_DELAY ) ) |
|
{ |
|
if ( tv_delaymapchange.GetBool() && tv_delaymapchange_protect.GetBool() ) |
|
{ |
|
float flLastEvent = GetLastMajorEventTime(); |
|
if ( flLastEvent > -1 ) |
|
{ |
|
if ( flLastEvent > ( gpGlobals->curtime - ( HLTVDirector()->GetDelay() + 3 ) ) ) // +3 second delay to prevent instant change after a major event |
|
{ |
|
*pszReason = "\n***WARNING*** Map change blocked. HLTV is broadcasting and has not caught up to the last major game event yet.\nYou can disable this check by setting the value of the server convar \"tv_delaymapchange_protect\" to 0.\n"; |
|
return false; |
|
} |
|
} |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
bool CMultiplayRules::IsMapInMapCycle( const char *pszName ) |
|
{ |
|
for ( int i = 0; i < m_MapList.Count(); i++ ) |
|
{ |
|
if ( V_stricmp( pszName, m_MapList[i] ) == 0 ) |
|
{ |
|
return true; |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
void CMultiplayRules::ChangeLevel( void ) |
|
{ |
|
char szNextMap[MAX_MAP_NAME]; |
|
|
|
if ( nextlevel.GetString() && *nextlevel.GetString() ) |
|
{ |
|
Q_strncpy( szNextMap, nextlevel.GetString(), sizeof( szNextMap ) ); |
|
} |
|
else |
|
{ |
|
GetNextLevelName( szNextMap, sizeof(szNextMap) ); |
|
IncrementMapCycleIndex(); |
|
} |
|
|
|
ChangeLevelToMap( szNextMap ); |
|
} |
|
|
|
void CMultiplayRules::LoadMapCycleFile( void ) |
|
{ |
|
int nOldCycleIndex = m_nMapCycleindex; |
|
m_nMapCycleindex = 0; |
|
|
|
char mapcfile[MAX_PATH]; |
|
DetermineMapCycleFilename( mapcfile, sizeof(mapcfile), false ); |
|
|
|
FreeMapCycleFileVector( m_MapList ); |
|
|
|
const int nMapCycleTimeStamp = filesystem->GetPathTime( mapcfile, "GAME" ); |
|
m_nMapCycleTimeStamp = nMapCycleTimeStamp; |
|
|
|
// Repopulate map list from mapcycle file |
|
LoadMapCycleFileIntoVector( mapcfile, m_MapList ); |
|
|
|
// Load server's mapcycle into network string table for client-side voting |
|
if ( g_pStringTableServerMapCycle ) |
|
{ |
|
CUtlString sFileList; |
|
for ( int i = 0; i < m_MapList.Count(); i++ ) |
|
{ |
|
sFileList += m_MapList[i]; |
|
sFileList += '\n'; |
|
} |
|
|
|
g_pStringTableServerMapCycle->AddString( CBaseEntity::IsServer(), "ServerMapCycle", sFileList.Length() + 1, sFileList.String() ); |
|
} |
|
|
|
#if defined ( TF_DLL ) || defined ( TF_CLIENT_DLL ) |
|
if ( g_pStringTableServerPopFiles ) |
|
{ |
|
// Search for all pop files that are prefixed with the current map name |
|
CUtlString sFileList; |
|
|
|
CUtlVector< CUtlString > defaultPopFiles; |
|
CPopulationManager::FindDefaultPopulationFileShortNames( defaultPopFiles ); |
|
|
|
FOR_EACH_VEC( defaultPopFiles, idx ) |
|
{ |
|
sFileList += defaultPopFiles[ idx ]; |
|
sFileList += "\n"; |
|
} |
|
|
|
if ( sFileList.Length() > 0 ) |
|
{ |
|
g_pStringTableServerPopFiles->AddString( CBaseEntity::IsServer(), "ServerPopFiles", sFileList.Length() + 1, sFileList.String() ); |
|
} |
|
} |
|
|
|
if ( g_pStringTableServerMapCycleMvM ) |
|
{ |
|
ConVarRef tf_mvm_missioncyclefile( "tf_mvm_missioncyclefile" ); |
|
KeyValues *pKV = new KeyValues( tf_mvm_missioncyclefile.GetString() ); |
|
if ( pKV->LoadFromFile( g_pFullFileSystem, tf_mvm_missioncyclefile.GetString(), "MOD" ) ) |
|
{ |
|
CUtlVector<CUtlString> mapList; |
|
|
|
// Parse the maps and send a list to each client for vote options |
|
int iMaxCat = pKV->GetInt( "categories", 0 ); |
|
for ( int iCat = 1; iCat <= iMaxCat; iCat++ ) |
|
{ |
|
KeyValues *pCategory = pKV->FindKey( UTIL_VarArgs( "%d", iCat ), false ); |
|
if ( pCategory ) |
|
{ |
|
int iMapCount = pCategory->GetInt( "count", 0 ); |
|
for ( int iMap = 1; iMap <= iMapCount; ++iMap ) |
|
{ |
|
KeyValues *pMission = pCategory->FindKey( UTIL_VarArgs( "%d", iMap ), false ); |
|
if ( pMission ) |
|
{ |
|
const char *pszMap = pMission->GetString( "map", "" ); |
|
int iIdx = mapList.Find( pszMap ); |
|
if ( !mapList.IsValidIndex( iIdx ) ) |
|
{ |
|
mapList.AddToTail( pszMap ); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
if ( mapList.Count() ) |
|
{ |
|
CUtlString sFileList; |
|
for ( int i = 0; i < mapList.Count(); i++ ) |
|
{ |
|
sFileList += mapList[i]; |
|
sFileList += '\n'; |
|
} |
|
|
|
g_pStringTableServerMapCycleMvM->AddString( CBaseEntity::IsServer(), "ServerMapCycleMvM", sFileList.Length() + 1, sFileList.String() ); |
|
} |
|
|
|
pKV->deleteThis(); |
|
} |
|
} |
|
#endif |
|
|
|
// If the current map is in the same location in the new map cycle, keep that index. This gives better behavior |
|
// when reloading a map cycle that has the current map in it multiple times. |
|
int nOldPreviousMap = ( nOldCycleIndex == 0 ) ? ( m_MapList.Count() - 1 ) : ( nOldCycleIndex - 1 ); |
|
if ( nOldCycleIndex >= 0 && nOldCycleIndex < m_MapList.Count() && |
|
nOldPreviousMap >= 0 && nOldPreviousMap < m_MapList.Count() && |
|
V_strcmp( STRING( gpGlobals->mapname ), m_MapList[ nOldPreviousMap ] ) == 0 ) |
|
{ |
|
// The old index is still valid, and falls after our current map in the new cycle, use it |
|
m_nMapCycleindex = nOldCycleIndex; |
|
} |
|
else |
|
{ |
|
// Otherwise, if the current map selection is in the list, set m_nMapCycleindex to the map that follows it. |
|
for ( int i = 0; i < m_MapList.Count(); i++ ) |
|
{ |
|
if ( V_strcmp( STRING( gpGlobals->mapname ), m_MapList[i] ) == 0 ) |
|
{ |
|
m_nMapCycleindex = i; |
|
IncrementMapCycleIndex(); |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
|
|
void CMultiplayRules::ChangeLevelToMap( const char *pszMap ) |
|
{ |
|
g_fGameOver = true; |
|
m_flTimeLastMapChangeOrPlayerWasConnected = 0.0f; |
|
Msg( "CHANGE LEVEL: %s\n", pszMap ); |
|
engine->ChangeLevel( pszMap, NULL ); |
|
} |
|
|
|
|
|
#endif |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Shared script resource of voice menu commands and hud strings |
|
//----------------------------------------------------------------------------- |
|
void CMultiplayRules::LoadVoiceCommandScript( void ) |
|
{ |
|
KeyValues *pKV = new KeyValues( "VoiceCommands" ); |
|
|
|
if ( pKV->LoadFromFile( filesystem, "scripts/voicecommands.txt", "GAME" ) ) |
|
{ |
|
for ( KeyValues *menu = pKV->GetFirstSubKey(); menu != NULL; menu = menu->GetNextKey() ) |
|
{ |
|
int iMenuIndex = m_VoiceCommandMenus.AddToTail(); |
|
|
|
int iNumItems = 0; |
|
|
|
// for each subkey of this menu, add a menu item |
|
for ( KeyValues *menuitem = menu->GetFirstSubKey(); menuitem != NULL; menuitem = menuitem->GetNextKey() ) |
|
{ |
|
iNumItems++; |
|
|
|
if ( iNumItems > 9 ) |
|
{ |
|
Warning( "Trying to load more than 9 menu items in voicecommands.txt, extras ignored" ); |
|
continue; |
|
} |
|
|
|
VoiceCommandMenuItem_t item; |
|
|
|
#ifndef CLIENT_DLL |
|
int iConcept = GetMPConceptIndexFromString( menuitem->GetString( "concept", "" ) ); |
|
if ( iConcept == MP_CONCEPT_NONE ) |
|
{ |
|
Warning( "Voicecommand script attempting to use unknown concept. Need to define new concepts in code. ( %s )\n", menuitem->GetString( "concept", "" ) ); |
|
} |
|
item.m_iConcept = iConcept; |
|
|
|
item.m_bShowSubtitle = ( menuitem->GetInt( "show_subtitle", 0 ) > 0 ); |
|
item.m_bDistanceBasedSubtitle = ( menuitem->GetInt( "distance_check_subtitle", 0 ) > 0 ); |
|
|
|
Q_strncpy( item.m_szGestureActivity, menuitem->GetString( "activity", "" ), sizeof( item.m_szGestureActivity ) ); |
|
#else |
|
Q_strncpy( item.m_szSubtitle, menuitem->GetString( "subtitle", "" ), MAX_VOICE_COMMAND_SUBTITLE ); |
|
Q_strncpy( item.m_szMenuLabel, menuitem->GetString( "menu_label", "" ), MAX_VOICE_COMMAND_SUBTITLE ); |
|
|
|
#endif |
|
m_VoiceCommandMenus.Element( iMenuIndex ).AddToTail( item ); |
|
} |
|
} |
|
} |
|
|
|
pKV->deleteThis(); |
|
} |
|
|
|
#ifndef CLIENT_DLL |
|
|
|
void CMultiplayRules::SkipNextMapInCycle() |
|
{ |
|
char szSkippedMap[MAX_MAP_NAME]; |
|
char szNextMap[MAX_MAP_NAME]; |
|
|
|
GetNextLevelName( szSkippedMap, sizeof( szSkippedMap ) ); |
|
IncrementMapCycleIndex(); |
|
GetNextLevelName( szNextMap, sizeof( szNextMap ) ); |
|
|
|
Msg( "Skipping: %s\tNext map: %s\n", szSkippedMap, szNextMap ); |
|
|
|
if ( nextlevel.GetString() && *nextlevel.GetString() ) |
|
{ |
|
Msg( "Warning! \"nextlevel\" is set to \"%s\" and will override the next map to be played.\n", nextlevel.GetString() ); |
|
} |
|
} |
|
|
|
void CMultiplayRules::IncrementMapCycleIndex() |
|
{ |
|
// Reset index if we've passed the end of the map list |
|
if ( ++m_nMapCycleindex >= m_MapList.Count() ) |
|
{ |
|
m_nMapCycleindex = 0; |
|
} |
|
} |
|
|
|
bool CMultiplayRules::ClientCommand( CBaseEntity *pEdict, const CCommand &args ) |
|
{ |
|
CBasePlayer *pPlayer = ToBasePlayer( pEdict ); |
|
|
|
const char *pcmd = args[0]; |
|
if ( FStrEq( pcmd, "voicemenu" ) ) |
|
{ |
|
if ( args.ArgC() < 3 ) |
|
return true; |
|
|
|
CBaseMultiplayerPlayer *pMultiPlayerPlayer = dynamic_cast< CBaseMultiplayerPlayer * >( pPlayer ); |
|
|
|
if ( pMultiPlayerPlayer ) |
|
{ |
|
int iMenu = atoi( args[1] ); |
|
int iItem = atoi( args[2] ); |
|
|
|
VoiceCommand( pMultiPlayerPlayer, iMenu, iItem ); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
return BaseClass::ClientCommand( pEdict, args ); |
|
} |
|
|
|
void CMultiplayRules::ClientCommandKeyValues( edict_t *pEntity, KeyValues *pKeyValues ) |
|
{ |
|
CBaseMultiplayerPlayer *pPlayer = dynamic_cast< CBaseMultiplayerPlayer * >( CBaseEntity::Instance( pEntity ) ); |
|
|
|
if ( !pPlayer ) |
|
return; |
|
|
|
char const *pszCommand = pKeyValues->GetName(); |
|
if ( pszCommand && pszCommand[0] ) |
|
{ |
|
if ( FStrEq( pszCommand, "AchievementEarned" ) ) |
|
{ |
|
if ( pPlayer->ShouldAnnounceAchievement() ) |
|
{ |
|
int nAchievementID = pKeyValues->GetInt( "achievementID" ); |
|
|
|
IGameEvent * event = gameeventmanager->CreateEvent( "achievement_earned" ); |
|
if ( event ) |
|
{ |
|
event->SetInt( "player", pPlayer->entindex() ); |
|
event->SetInt( "achievement", nAchievementID ); |
|
gameeventmanager->FireEvent( event ); |
|
} |
|
|
|
pPlayer->OnAchievementEarned( nAchievementID ); |
|
} |
|
} |
|
} |
|
} |
|
|
|
VoiceCommandMenuItem_t *CMultiplayRules::VoiceCommand( CBaseMultiplayerPlayer *pPlayer, int iMenu, int iItem ) |
|
{ |
|
// have the player speak the concept that is in a particular menu slot |
|
if ( !pPlayer ) |
|
return NULL; |
|
|
|
if ( iMenu < 0 || iMenu >= m_VoiceCommandMenus.Count() ) |
|
return NULL; |
|
|
|
if ( iItem < 0 || iItem >= m_VoiceCommandMenus.Element( iMenu ).Count() ) |
|
return NULL; |
|
|
|
VoiceCommandMenuItem_t *pItem = &m_VoiceCommandMenus.Element( iMenu ).Element( iItem ); |
|
|
|
Assert( pItem ); |
|
|
|
char szResponse[AI_Response::MAX_RESPONSE_NAME]; |
|
|
|
if ( pPlayer->CanSpeakVoiceCommand() ) |
|
{ |
|
CMultiplayer_Expresser *pExpresser = pPlayer->GetMultiplayerExpresser(); |
|
Assert( pExpresser ); |
|
pExpresser->AllowMultipleScenes(); |
|
|
|
if ( pPlayer->SpeakConceptIfAllowed( pItem->m_iConcept, NULL, szResponse, AI_Response::MAX_RESPONSE_NAME ) ) |
|
{ |
|
// show a subtitle if we need to |
|
if ( pItem->m_bShowSubtitle ) |
|
{ |
|
CRecipientFilter filter; |
|
|
|
if ( pItem->m_bDistanceBasedSubtitle ) |
|
{ |
|
filter.AddRecipientsByPAS( pPlayer->WorldSpaceCenter() ); |
|
|
|
// further reduce the range to a certain radius |
|
int i; |
|
for ( i = filter.GetRecipientCount()-1; i >= 0; i-- ) |
|
{ |
|
int index = filter.GetRecipientIndex(i); |
|
|
|
CBasePlayer *pListener = UTIL_PlayerByIndex( index ); |
|
|
|
if ( pListener && pListener != pPlayer ) |
|
{ |
|
float flDist = ( pListener->WorldSpaceCenter() - pPlayer->WorldSpaceCenter() ).Length2D(); |
|
|
|
if ( flDist > VOICE_COMMAND_MAX_SUBTITLE_DIST ) |
|
filter.RemoveRecipientByPlayerIndex( index ); |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
filter.AddAllPlayers(); |
|
} |
|
|
|
// if we aren't a disguised spy |
|
if ( !pPlayer->ShouldShowVoiceSubtitleToEnemy() ) |
|
{ |
|
// remove players on other teams |
|
filter.RemoveRecipientsNotOnTeam( pPlayer->GetTeam() ); |
|
} |
|
|
|
// Register this event in the mod-specific usermessages .cpp file if you hit this assert |
|
Assert( usermessages->LookupUserMessage( "VoiceSubtitle" ) != -1 ); |
|
|
|
// Send a subtitle to anyone in the PAS |
|
UserMessageBegin( filter, "VoiceSubtitle" ); |
|
WRITE_BYTE( pPlayer->entindex() ); |
|
WRITE_BYTE( iMenu ); |
|
WRITE_BYTE( iItem ); |
|
MessageEnd(); |
|
} |
|
|
|
pPlayer->NoteSpokeVoiceCommand( szResponse ); |
|
|
|
#ifdef NEXT_BOT |
|
// let bots react to player's voice commands |
|
CUtlVector< INextBot * > botVector; |
|
TheNextBots().CollectAllBots( &botVector ); |
|
|
|
for( int i=0; i<botVector.Count(); ++i ) |
|
{ |
|
botVector[i]->OnActorEmoted( pPlayer, pItem->m_iConcept ); |
|
} |
|
#endif |
|
} |
|
else |
|
{ |
|
pItem = NULL; |
|
} |
|
|
|
pExpresser->DisallowMultipleScenes(); |
|
return pItem; |
|
} |
|
|
|
return NULL; |
|
} |
|
|
|
bool CMultiplayRules::IsLoadingBugBaitReport() |
|
{ |
|
return ( !engine->IsDedicatedServer()&& CommandLine()->CheckParm( "-bugbait" ) && sv_cheats->GetBool() ); |
|
} |
|
|
|
void CMultiplayRules::HaveAllPlayersSpeakConceptIfAllowed( int iConcept, int iTeam /* = TEAM_UNASSIGNED */, const char *modifiers /* = NULL */ ) |
|
{ |
|
CBaseMultiplayerPlayer *pPlayer; |
|
for ( int i = 1; i <= gpGlobals->maxClients; i++ ) |
|
{ |
|
pPlayer = ToBaseMultiplayerPlayer( UTIL_PlayerByIndex( i ) ); |
|
|
|
if ( !pPlayer ) |
|
continue; |
|
|
|
if ( iTeam != TEAM_UNASSIGNED ) |
|
{ |
|
if ( pPlayer->GetTeamNumber() != iTeam ) |
|
continue; |
|
} |
|
|
|
pPlayer->SpeakConceptIfAllowed( iConcept, modifiers ); |
|
} |
|
} |
|
|
|
void CMultiplayRules::RandomPlayersSpeakConceptIfAllowed( int iConcept, int iNumRandomPlayer /*= 1*/, int iTeam /*= TEAM_UNASSIGNED*/, const char *modifiers /*= NULL*/ ) |
|
{ |
|
CUtlVector< CBaseMultiplayerPlayer* > speakCandidates; |
|
|
|
CBaseMultiplayerPlayer *pPlayer; |
|
for ( int i = 1; i <= gpGlobals->maxClients; i++ ) |
|
{ |
|
pPlayer = ToBaseMultiplayerPlayer( UTIL_PlayerByIndex( i ) ); |
|
|
|
if ( !pPlayer ) |
|
continue; |
|
|
|
if ( iTeam != TEAM_UNASSIGNED ) |
|
{ |
|
if ( pPlayer->GetTeamNumber() != iTeam ) |
|
continue; |
|
} |
|
|
|
speakCandidates.AddToTail( pPlayer ); |
|
} |
|
|
|
int iSpeaker = iNumRandomPlayer; |
|
while ( iSpeaker > 0 && speakCandidates.Count() > 0 ) |
|
{ |
|
int iRandomSpeaker = RandomInt( 0, speakCandidates.Count() - 1 ); |
|
speakCandidates[ iRandomSpeaker ]->SpeakConceptIfAllowed( iConcept, modifiers ); |
|
speakCandidates.FastRemove( iRandomSpeaker ); |
|
iSpeaker--; |
|
} |
|
} |
|
|
|
void CMultiplayRules::ClientSettingsChanged( CBasePlayer *pPlayer ) |
|
{ |
|
// NVNT see if this user is still or has began using a haptic device |
|
const char *pszHH = engine->GetClientConVarValue( pPlayer->entindex(), "hap_HasDevice" ); |
|
if( pszHH ) |
|
{ |
|
int iHH = atoi( pszHH ); |
|
pPlayer->SetHaptics( iHH != 0 ); |
|
} |
|
|
|
} |
|
void CMultiplayRules::GetTaggedConVarList( KeyValues *pCvarTagList ) |
|
{ |
|
BaseClass::GetTaggedConVarList( pCvarTagList ); |
|
|
|
// sv_gravity |
|
KeyValues *pGravity = new KeyValues( "sv_gravity" ); |
|
pGravity->SetString( "convar", "sv_gravity" ); |
|
pGravity->SetString( "tag", "gravity" ); |
|
|
|
pCvarTagList->AddSubKey( pGravity ); |
|
|
|
// sv_alltalk |
|
KeyValues *pAllTalk = new KeyValues( "sv_alltalk" ); |
|
pAllTalk->SetString( "convar", "sv_alltalk" ); |
|
pAllTalk->SetString( "tag", "alltalk" ); |
|
|
|
pCvarTagList->AddSubKey( pAllTalk ); |
|
} |
|
|
|
#else |
|
|
|
const char *CMultiplayRules::GetVoiceCommandSubtitle( int iMenu, int iItem ) |
|
{ |
|
Assert( iMenu >= 0 && iMenu < m_VoiceCommandMenus.Count() ); |
|
if ( iMenu < 0 || iMenu >= m_VoiceCommandMenus.Count() ) |
|
return ""; |
|
|
|
Assert( iItem >= 0 && iItem < m_VoiceCommandMenus.Element( iMenu ).Count() ); |
|
if ( iItem < 0 || iItem >= m_VoiceCommandMenus.Element( iMenu ).Count() ) |
|
return ""; |
|
|
|
VoiceCommandMenuItem_t *pItem = &m_VoiceCommandMenus.Element( iMenu ).Element( iItem ); |
|
|
|
Assert( pItem ); |
|
|
|
return pItem->m_szSubtitle; |
|
} |
|
|
|
// Returns false if no such menu is declared or if it's an empty menu |
|
bool CMultiplayRules::GetVoiceMenuLabels( int iMenu, KeyValues *pKV ) |
|
{ |
|
Assert( iMenu >= 0 && iMenu < m_VoiceCommandMenus.Count() ); |
|
if ( iMenu < 0 || iMenu >= m_VoiceCommandMenus.Count() ) |
|
return false; |
|
|
|
int iNumItems = m_VoiceCommandMenus.Element( iMenu ).Count(); |
|
|
|
for ( int i=0; i<iNumItems; i++ ) |
|
{ |
|
VoiceCommandMenuItem_t *pItem = &m_VoiceCommandMenus.Element( iMenu ).Element( i ); |
|
|
|
KeyValues *pLabelKV = new KeyValues( pItem->m_szMenuLabel ); |
|
|
|
pKV->AddSubKey( pLabelKV ); |
|
} |
|
|
|
return iNumItems > 0; |
|
} |
|
|
|
#endif
|
|
|