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.
3848 lines
141 KiB
3848 lines
141 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
#include "cbase.h" |
|
|
|
#include "tf_gc_server.h" |
|
#include "gcsdk/gcsdk_auto.h" |
|
#include "tf_gcmessages.h" |
|
#include "tf_player.h" |
|
#include "rtime.h" |
|
// XXX(JohnS): Eventually, we want to send a smaller lobby object to clients. For now, they use the CTFGSLobby, which is |
|
// in shared code for that reason. |
|
#include "tf_lobby_server.h" |
|
#include "tf_gamerules.h" |
|
#include "eiface.h" |
|
#include "cdll_int.h" |
|
#include "econ_item_inventory.h" |
|
#include "gameinterface.h" |
|
#include "client.h" |
|
#include "tier1/convar.h" |
|
#include "tf_matchmaking_shared.h" |
|
#include "tf_quickplay_shared.h" |
|
#include "tf_mann_vs_machine_stats.h" |
|
#include "tf_objective_resource.h" |
|
#include "tf_player.h" |
|
#include "tf_voteissues.h" |
|
#include "player_vs_environment/tf_population_manager.h" |
|
#include "quest_objective_manager.h" |
|
#include "player_resource.h" |
|
#include "tf_player_resource.h" |
|
#include "tf_gamestats.h" |
|
#include "tf_player.h" |
|
#include "tf_match_description.h" |
|
#include "util.h" |
|
#include "tier1/utlqueue.h" |
|
#include "tf_player_resource.h" |
|
#include "tf_gc_shared.h" |
|
#include "tf_party.h" |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
using namespace GCSDK; |
|
|
|
// How many minutes before we assume something is FUBAR and reboot if we're empty and waiting for the GC to acknowledge us. |
|
|
|
// With valid match data: wait a while. GC could be having trouble, or connectivity issues, and we want to hold on to |
|
// the results for it to come back up. After three hours, assume its us. |
|
const int k_InvalidState_Timeout_With_Match = 60 * 2; |
|
const int k_InvalidState_Timeout_Without_Match = 5; |
|
|
|
#ifdef ENABLE_GC_MATCHMAKING |
|
|
|
/*********************************************************************************************************************** |
|
////////////////////////////////////////////////////////////\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ |
|
|
|
XXX(JohnS) NOTE The current state of the matchmaking flow through this class is a bit of a mess. Have been |
|
incrementally cleaning things up, but be careful. |
|
|
|
UpdateConnectedPlayersAndServerInfo() |
|
This is the heug god function that sync's our state with the GC's state via the Lobby shared object: |
|
- Our actual connected players |
|
|
|
- m_pMatchInfo (via GetMatch()) - this represents our match in progress, and should generally mirror the GC, but |
|
*MIGHT NOT*. For instance, when the GC is unavailable this object is locked, and when the GC returns we may be |
|
desync'd. This function is in charge of managing that. Outside code should simply look at the MatchInfo object |
|
and trust that it is the state of the match. |
|
|
|
- m_vecReservationExpiryTime - this should be merged into MatchInfo eventually, but is an array of active |
|
reservations and when they expire. This isn't in MatchInfo because in some modes we operate with reservations |
|
but without running a proper Match. When we're running a match, anyone in this vector should be in the MatchInfo |
|
|
|
- CTFGSLobby - This is the shared object from the server that represents the match we are hosting. However, it is |
|
*NOT* the article of record on the match. This is due to matches being designed to be resilient to GC connection |
|
loss. Essentially, only this function should be looking at CTFGSLobby and negotiating the state of the actual |
|
match it believes itself to have in MatchInfo. |
|
|
|
== Gameserver / GC Authority |
|
- GC forms matches, adds players to matches, passes them to servers |
|
- Servers run matches to completion, have authority on abandons/etc. regardless of GC state |
|
- Servers pass result, including any abandons, to GC. Message is queued if GC is unavailable. |
|
- GC takes match results and does ELO calculation and any stats/etc. |
|
- GC can request players be kicked from matches or matches be canceled |
|
- If more players are needed |
|
- Gameserver requests GC attention with appropriate flag (6v6: Stalled, waiting on complete match, 12v12: |
|
Non-full match) |
|
- GC adds players to lobby, making them part of the match |
|
- If server state is poor (hypothetically: lag, too many abandons, abnormal something or other) |
|
- Game server sends KickLobby to terminate match, sends failed match result |
|
- If GC is unavailable |
|
- Game server still carries out duties, may decide to make changes like end match instead of request late joins |
|
if it decides GC wont be able to provide them. |
|
|
|
== Match Start |
|
- GC creates a lobby and hands it to us. UpdateConnectedPlayers tick initializes a MatchInfo struct as |
|
appropriate, accepts players. |
|
|
|
== Adding Players |
|
- The GC adds players to the lobby (so, when GC down, matches cannot gain players) |
|
- UpdateConnectedPlayersAndServerInfo ensures that makes sense (it should, though, we no longer have legacy match |
|
types where the GC adds players we shouldn't accept) |
|
- UpdateConnectedPlayers calls AcceptGCReservation, player is added to match and put in reservation list |
|
|
|
== Dropping Players |
|
- Case 1: Player is not present, but is in the lobby (GC *might* be down, doesn't matter) |
|
- Player marked missing in MatchInfo by UpdateConnectedPlayers tick |
|
- After a grace period, player marked dropped, as an abandoner in MatchInfo |
|
- PlayerLeftMatch message is sent to tell the GC about their leaving. |
|
- Case 2: Player is dropped from GC lobby |
|
- UpdateConnectedPlayers assumes GC kicked them, marks them dropped from match and kicks them. |
|
- TODO: Ideally there'd be a KickThisGuy GC message, and we'd respond with PlayerLeftMatch, rather than the GC |
|
unilaterally dropping people like this. |
|
- Case 3: Votekicked |
|
- PlayerLeftMatch is sent, from server |
|
- All cases: |
|
- A reliable GC message player-abandoned (or was kicked or never joined) message queued to reconcile this with |
|
the lobby state, but if GC is unavailable it will be informed when it returns. |
|
- Player is marked dropped in MatchInfo |
|
|
|
== Team Assignments |
|
- The GC delivers an initial team assignment for each player added to the match. This team assignment does not |
|
change when game teams change sides, see TFGameRules::GameTeamToLobbyTeam and its inverse to map these to game |
|
logic teams (vs TF_GC_TEAM objects) |
|
|
|
- All other team changes have to be initiated by a game server message, in modes that allow it, to prevent |
|
race-conditions. |
|
|
|
- The NewMatchForLobby message expects the GC to shuffle our teams. We prevent races by not issuing other team |
|
change messages while this message is pending. If we time out waiting for the GC, some modes may start a |
|
speculative server-created match (expecting the GC to come back and respond to that message positively). In |
|
this case, we queue a ChangeMatchPlayerTeams message to stomp any assignments back to our known state, |
|
allowing us to ignore the temporary de-sync (queued messages always get processed in sequence) |
|
|
|
- The ChangeMatchPlayerTeams message allows the gameserver to change match player teams mid-game in match modes |
|
that allow it. The game server is in charge of not queuing this message in parallel with NewMatchForLobby |
|
above, or handling the potential race. |
|
|
|
- When processing either of these messages, the GC cancels any players that are awaiting acceptance by the |
|
game-server, and re-tries if necessary. This prevents team changes from racing with player-joins which may |
|
have been predicated on differing team layouts. |
|
- The game server does not accept pending players or send any heartbeats until any queued messages have been |
|
responded to. See Queued Messages below. |
|
|
|
== Match End |
|
- Match result message provides canonical record of match, is queued to send to GC when available. |
|
- GameServerKickingLobby message dissolves live match if GC is available/tracking it. Queued similarly. |
|
- ** This can happen before or after the match result. |
|
- In MvM, we send potentially multiple victory messages per match -- they can cycle missions and keep winning. |
|
- As of right now, in competitive, we end the match coincident with sending a match result. |
|
- Match ended doesn't necessarily kick players, so a dead/finished match will stick around on our end until |
|
everyone Disconnects, (or the game logic kicks them, e.g. MatchInfo->BEnded + a timeout) |
|
- Ended matches have queued a message to dissolve their lobby, though, so further GC interaction with the match is |
|
not possible, and players are allowed to leave (since they're now allowed to be put in a new match by the GC) |
|
|
|
== Queued Messages And Match State And Race Conditions |
|
- Since queued messages are sent in order until confirmed, the GC will always see (eventually) a coherent |
|
story. For instance: |
|
- PlayerLeftMatch - GC marks this player as leaving match |
|
- KickingLobby - GC marks match as finished, result pending |
|
- MatchResult (minus the two players who left) - GC finishes match accounting, marks match complete, missing |
|
players are already noted as leavers so their absence from the result is expected. |
|
- While messages are queued, we do not run the UpdateConnectedPlayers() think. This prevents having to worry about |
|
a fractal of potential edge cases -- we don't look at updated lobby data or send heartbeats while anything we're |
|
trying to tell the GC hasn't been confirmed. This also means we won't send a heartbeat until all such actions |
|
have been confirmed. |
|
- GC message handlers for queued messages do have to handle possible races -- if the GC sends us players while |
|
we're sending a "Reassign Player Team" message, this behavior means we'll stubbornly wait for a response to |
|
the team message before acknowledging any players, allowing the GC to easily resolve the race (in this case, |
|
by canceling or retrying any attempted add-player-match actions) |
|
|
|
== Gameserver Crashes |
|
- If GC is available, it handles it, otherwise, match is lost. Gameservers don't currently try to persist this |
|
state. |
|
|
|
== Match empties out |
|
- If the match is still going, it should reach ended as everyone in it gets timed out as an abandon. |
|
- If the GC is around, it will revoke the lobby once we inform it everyone has dropped. |
|
- Once the match is marked ended, and the GC concurs and deletes the lobby, we delete MatchInfo |
|
- If the GC is not around, we hang out on the completed match state until it is. We can't exactly take new |
|
matches in the mean time. (but, see k_InvalidState_Timeout_With_Match) |
|
|
|
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\//////////////////////////////////////////////////////////// |
|
***********************************************************************************************************************/ |
|
|
|
static const char g_pszIdleKickString[] = "#TF_Idle_kicked"; |
|
|
|
//ConVar dota_force_upload_match_stats( "dota_force_upload_match_stats", "0", FCVAR_CHEAT, "If enabled, server will upload match stats even when there aren't human players on each side" ); |
|
//extern ConVar dota_force_bot_cycle; |
|
extern CServerGameDLL g_ServerGameDLL; |
|
|
|
// How long a player can be missing from a MM match before they are dropped and given an abandon. Set to -1 to disable. |
|
ConVar tf_mm_player_disconnect_time_before_abandon( "tf_mm_player_disconnect_time_before_abandon", "180", FCVAR_DEVELOPMENTONLY ); |
|
// How quickly we should forgive a match player's disconnected time after they return. At a ratio of 10, 30 minutes of |
|
// connected time would cancel out 3 minutes of disconnected type. Set to 0 to disable. |
|
ConVar tf_mm_player_disconnect_time_forgive_ratio( "tf_mm_player_disconnect_time_forgive_ratio", "10", FCVAR_DEVELOPMENTONLY ); |
|
// Any disconnect, no matter for how long, should count as this many seconds of disconnected time. This is because the |
|
// act of reconnecting can be more disruptive than just the absense -- ready-up timers reset, the game may |
|
// pause/unpause, etc.. |
|
// |
|
// Currently at 90 -- two rapid rejoins in a row, even with instant loading, will eat up your DC allowance. Note that |
|
// if you take at least 90s to rejoin/load anyway, this would have no effect. |
|
ConVar tf_mm_player_disconnect_time_minimum_penalty( "tf_mm_player_disconnect_time_minimum_penalty", "90", FCVAR_DEVELOPMENTONLY ); |
|
|
|
ConVar tf_mm_next_map_result_hold_time( "tf_mm_next_map_result_hold_time", "7" ); |
|
|
|
ConVar tf_mvm_allow_abandon_after_seconds( "tf_mvm_allow_abandon_after_seconds", "600", FCVAR_DEVELOPMENTONLY ); |
|
ConVar tf_mvm_allow_abandon_below_players( "tf_mvm_allow_abandon_below_players", "5", FCVAR_DEVELOPMENTONLY ); |
|
|
|
ConVar tf_allow_server_hibernation( "tf_allow_server_hibernation", "1", FCVAR_NONE, "Allow the server to hibernate when empty." ); |
|
|
|
#ifdef STAGING_ONLY |
|
ConVar tf_debug_xp_changes( "tf_debug_xp_changes", "0" ); |
|
#endif |
|
|
|
//DEFINE_LOGGING_CHANNEL_NO_TAGS( LOG_CONSOLE, "Console" ); |
|
|
|
static CTFGCServerSystem s_TFGCServerSystem; |
|
CTFGCServerSystem *GTFGCClientSystem() { return &s_TFGCServerSystem; } |
|
|
|
//bool g_bServerReceivedGCWelcome = false; |
|
int g_gcServerVersion = 0; // Version from the GC |
|
|
|
static bool g_bWarnedAboutMaxplayersInMVM = false; |
|
|
|
extern ConVar tf_mm_servermode; |
|
extern ConVar tf_mm_trusted; |
|
extern ConVar tf_mm_strict; |
|
|
|
// Some reliable messages don't know the matchID yet when they are queued, but we should have it by time they send. This |
|
// helper takes their current match ID and returns the one they should use, for use in OnPrepare(). |
|
// |
|
// Returns the current match's ID if: |
|
// - The msg's match ID is 0, and we now have a match ID |
|
// |
|
// Calls AbortInvalidMatchState if: |
|
// - The msg's match ID is not zero, and different from the current match |
|
// - Or they're both zero and we're in a match group that requires match IDs. |
|
// |
|
// NOTE We always wait for all pending messages before accepting new matches, so the above should hold unless something |
|
// got badly confused. Only matches with bServerCreated start without knowing their match ID, and should know it |
|
// by time any message that needs it gets sent. (a previous message in queue should be requesting it) |
|
static uint64 ReliableMsgCheckUpdateMatchID( uint64 nMsgMatchID ) |
|
{ |
|
uint64 nCurrentMatchID = GTFGCClientSystem()->GetMatch()->m_nMatchID; |
|
Assert( !nMsgMatchID || nMsgMatchID == nCurrentMatchID ); |
|
|
|
// If we were queued for a match we didn't know the ID of yet, we can now glom it |
|
if ( nCurrentMatchID && nMsgMatchID == 0 ) |
|
{ |
|
return nCurrentMatchID; |
|
} |
|
else if ( nCurrentMatchID != nMsgMatchID ) |
|
{ |
|
// Something is bad |
|
GTFGCClientSystem()->AbortInvalidMatchState(); |
|
} |
|
else if ( !nCurrentMatchID && !nMsgMatchID ) |
|
{ |
|
auto *pMatchDesc = GetMatchGroupDescription( GTFGCClientSystem()->GetMatch()->m_eMatchGroup ); |
|
if ( !pMatchDesc || pMatchDesc->BRequiresMatchID() ) |
|
{ |
|
GTFGCClientSystem()->AbortInvalidMatchState(); |
|
} |
|
} |
|
|
|
return nMsgMatchID; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Reliable messages |
|
//----------------------------------------------------------------------------- |
|
class ReliableMsgNewMatchForLobby |
|
: public CJobReliableMessageBase < ReliableMsgNewMatchForLobby, |
|
CMsgGCNewMatchForLobbyRequest, k_EMsgGC_NewMatchForLobbyRequest, |
|
CMsgGCNewMatchForLobbyResponse, k_EMsgGC_NewMatchForLobbyResponse > |
|
{ |
|
public: |
|
void OnReply( Reply_t &msgReply ) |
|
{ GTFGCClientSystem()->NewMatchForLobbyResponse( msgReply.Body().success() ); } |
|
|
|
void OnPrepare() |
|
{ Assert( Msg().Body().current_match_id() == GTFGCClientSystem()->GetMatch()->m_nMatchID ); } |
|
|
|
const char *MsgName() { return "NewMatchForLobby"; } |
|
void InitDebugString( CUtlString &dbgStr ) |
|
{ |
|
dbgStr.Format( "Match %llx, Lobby %llx, Next Map %d", |
|
Msg().Body().current_match_id(), |
|
Msg().Body().lobby_id(), |
|
Msg().Body().next_map_id() ); |
|
} |
|
}; |
|
|
|
//----------------------------------------------------------------------------- |
|
class ReliableMsgChangeMatchPlayerTeams |
|
: public CJobReliableMessageBase < ReliableMsgChangeMatchPlayerTeams, |
|
CMsgGCChangeMatchPlayerTeamsRequest, k_EMsgGC_ChangeMatchPlayerTeamsRequest, |
|
CMsgGCChangeMatchPlayerTeamsResponse, k_EMsgGC_ChangeMatchPlayerTeamsResponse > |
|
{ |
|
public: |
|
void OnReply( Reply_t &msgReply ) |
|
{ GTFGCClientSystem()->ChangeMatchPlayerTeamsResponse( msgReply.Body().success() ); } |
|
|
|
// May have been queued for a pending match |
|
void OnPrepare() { Msg().Body().set_match_id( ReliableMsgCheckUpdateMatchID( Msg().Body().match_id() ) ); } |
|
|
|
const char *MsgName() { return "ChangeMatchPlayerTeams"; } |
|
void InitDebugString( CUtlString &dbgStr ) |
|
{ |
|
dbgStr.Format( "Match %llx, Lobby %llx, %d members", |
|
Msg().Body().match_id(), Msg().Body().lobby_id(), Msg().Body().member_size() ); |
|
} |
|
}; |
|
|
|
//----------------------------------------------------------------------------- |
|
class ReliableMsgMvMVictory |
|
: public CJobReliableMessageBase < ReliableMsgMvMVictory, |
|
CMsgMvMVictory, k_EMsgGCMvMVictory, |
|
CMsgMvMMannUpVictoryReply, k_EMsgGCMvMVictoryReply > |
|
{ |
|
public: |
|
const char *MsgName() { return "MvMVictory"; } |
|
void InitDebugString( CUtlString &dbgStr ) { dbgStr.Format( "Lobby %016llx", Msg().Body().lobby_id() ); } |
|
}; |
|
|
|
//----------------------------------------------------------------------------- |
|
class ReliableMsgGameServerKickingLobby |
|
: public CJobReliableMessageBase < ReliableMsgGameServerKickingLobby, |
|
CMsgGameServerKickingLobby, k_EMsgGCGameServerKickingLobby, |
|
CMsgGameServerKickingLobbyResponse, k_EMsgGCGameServerKickingLobbyResponse > |
|
{ |
|
public: |
|
// May have been queued for a pending match |
|
void OnPrepare() { Msg().Body().set_match_id( ReliableMsgCheckUpdateMatchID( Msg().Body().match_id() ) ); } |
|
const char *MsgName() { return "GameServerKickingLobby"; } |
|
void InitDebugString( CUtlString &dbgStr ) { dbgStr.Format( "Match %llx, Lobby %llx", |
|
Msg().Body().match_id(), Msg().Body().lobby_id() ); } |
|
}; |
|
|
|
//----------------------------------------------------------------------------- |
|
class ReliableMsgPlayerLeftMatch |
|
: public CJobReliableMessageBase < ReliableMsgPlayerLeftMatch, |
|
CMsgPlayerLeftMatch, k_EMsgGCPlayerLeftMatch, |
|
CMsgPlayerLeftMatchResponse, k_EMsgGCPlayerLeftMatchResponse > |
|
{ |
|
public: |
|
// May have been queued for a pending match |
|
void OnPrepare() { Msg().Body().set_match_id( ReliableMsgCheckUpdateMatchID( Msg().Body().match_id() ) ); } |
|
const char *MsgName() { return "PlayerLeftMatch"; } |
|
void InitDebugString( CUtlString &dbgStr ) { dbgStr.Format( "Player %s, Match %llx, Lobby %llx", |
|
CSteamID( Msg().Body().steam_id() ).Render(), |
|
Msg().Body().match_id(), Msg().Body().lobby_id() ); } |
|
}; |
|
|
|
//----------------------------------------------------------------------------- |
|
// Sent for players who where votekicked after leaving the match |
|
// - That is, were being votekicked when they left, it later passed, to resolve the race-condition by posthumously |
|
// upgrading their penalty GC-side) |
|
class ReliableMsgPlayerVoteKickedAfterLeavingMatch |
|
: public CJobReliableMessageBase < ReliableMsgPlayerVoteKickedAfterLeavingMatch, |
|
CMsgPlayerVoteKickedAfterLeavingMatch, k_EMsgGCPlayerVoteKickedAfterLeavingMatch, |
|
CMsgPlayerVoteKickedAfterLeavingMatchResponse, k_EMsgGCPlayerVoteKickedAfterLeavingMatchResponse > |
|
{ |
|
public: |
|
// May have been queued for a pending match |
|
void OnPrepare() { Msg().Body().set_match_id( ReliableMsgCheckUpdateMatchID( Msg().Body().match_id() ) ); } |
|
const char *MsgName() { return "PlayerVoteKickedAfterLeavingMatch"; } |
|
void InitDebugString( CUtlString &dbgStr ) { dbgStr.Format( "Player %s, Match %llx, Lobby %llx", |
|
CSteamID( Msg().Body().steam_id() ).Render(), |
|
Msg().Body().match_id(), Msg().Body().lobby_id() ); } |
|
}; |
|
|
|
//----------------------------------------------------------------------------- |
|
class ReliableMsgMatchResult |
|
: public CJobReliableMessageBase < ReliableMsgMatchResult, |
|
CMsgGC_Match_Result, k_EMsgGC_Match_Result, |
|
CMsgGC_Match_ResultResponse, k_EMsgGC_Match_ResultResponse > |
|
{ |
|
public: |
|
// May have been queued for a pending match |
|
void OnPrepare() { Msg().Body().set_match_id( ReliableMsgCheckUpdateMatchID( Msg().Body().match_id() ) ); } |
|
const char *MsgName() { return "MatchResult"; } |
|
void InitDebugString( CUtlString &dbgStr ) { dbgStr.Format( "Match %016llx", Msg().Body().match_id() ); } |
|
}; |
|
|
|
//----------------------------------------------------------------------------- |
|
// CMvMVictoryInfo |
|
//----------------------------------------------------------------------------- |
|
void CMvMVictoryInfo::Init ( CTFGSLobby *pLobby ) |
|
{ |
|
if ( !pLobby ) |
|
{ |
|
MMLog( "CTFGCServerSystem::MvMVictory() -- no lobby, so not sending results to GC\n" ); |
|
return; |
|
} |
|
|
|
m_nLobbyId = pLobby->GetGroupID(); |
|
m_sChallengeName = pLobby->GetMissionName(); |
|
#ifdef USE_MVM_TOUR |
|
if ( IsMannUpGroup( pLobby->GetMatchGroup() ) ) |
|
{ |
|
const char *pszTourName = pLobby->GetMannUpTourName(); |
|
Assert( pszTourName ); |
|
m_sMannUpTourOfDuty = pszTourName; |
|
} |
|
#endif // USE_MVM_TOUR |
|
m_tEventTime = CRTime::RTime32TimeCur(); |
|
|
|
m_vPlayerIds.RemoveAll(); |
|
m_vSquadSurplus.RemoveAll(); |
|
|
|
for ( int iMember = 0; iMember < pLobby->GetNumMembers(); iMember++ ) |
|
{ |
|
m_vPlayerIds.AddToTail( pLobby->GetMember( iMember ).ConvertToUint64() ); |
|
m_vSquadSurplus.AddToTail( pLobby->GetMemberDetails( iMember )->squad_surplus() ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// CCompetitiveMatchInfo |
|
//----------------------------------------------------------------------------- |
|
CMatchInfo::CMatchInfo( const CTFGSLobby *pLobby ) |
|
: m_nMatchID( pLobby->GetMatchID() ) |
|
, m_nLobbyID( pLobby->GetGroupID() ) |
|
, m_eMatchGroup( pLobby->GetMatchGroup() ) |
|
, m_uLobbyFlags( pLobby->GetFlags() ) |
|
, m_uAverageRank( pLobby->Obj().average_rank() ) |
|
, m_rtMatchCreated( CRTime::RTime32TimeCur() ) |
|
, m_unEventTeamStatus( pLobby->Obj().is_war_match() ) |
|
, m_bFirstPersonActive( false ) |
|
, m_nBotsAdded( 0 ) |
|
, m_bServerCreated( false ) |
|
, m_strMapName( pLobby->GetMapName() ) |
|
, m_bMatchEnded( false ) |
|
, m_bSentResult( false ) |
|
, m_nGCMatchSize( pLobby->Obj().has_fixed_match_size() ? pLobby->Obj().fixed_match_size() : 0 ) |
|
#ifdef STAGING_ONLY |
|
, m_flBronzePercentile( 0.5f ) |
|
, m_flSilverPercentile( 0.65f ) |
|
, m_flGoldPercentile( 0.8f ) |
|
#else |
|
, m_flBronzePercentile( 0.6f ) |
|
, m_flSilverPercentile( 0.75f ) |
|
, m_flGoldPercentile( 0.9f ) |
|
#endif |
|
{ |
|
uint32 nNumCompLevels = GetMatchGroupDescription( k_nMatchGroup_Casual_6v6 )->m_pProgressionDesc->GetNumLevels(); |
|
m_vDailyStatsRankData.EnsureCapacity( nNumCompLevels ); |
|
|
|
RequestGCRankData(); |
|
} |
|
|
|
CMatchInfo::~CMatchInfo() |
|
{ |
|
m_vMatchRankData.PurgeAndDeleteElements(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// |
|
//----------------------------------------------------------------------------- |
|
CMatchInfo::CMatchInfo() |
|
{ |
|
// Don't do this |
|
Assert( 0 ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// |
|
//----------------------------------------------------------------------------- |
|
CMatchInfo::CMatchInfo( const CMatchInfo &otherinfo ) |
|
{ |
|
// Don't do this |
|
Assert( 0 ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// |
|
//----------------------------------------------------------------------------- |
|
CMatchInfo::PlayerMatchData_t::PlayerMatchData_t( const PlayerMatchData_t& rhs ) |
|
: m_mapXPAccumulation( DefLessFunc( CMsgTFXPSource::XPSourceType ) ) |
|
{ |
|
steamID = rhs.steamID; |
|
uPartyID = rhs.uPartyID; |
|
eGCTeam = rhs.eGCTeam; |
|
bDropped = rhs.bDropped; |
|
bConnected = rhs.bConnected; |
|
rtJoinedMatch = CRTime::RTime32TimeCur(); |
|
nVoteKickAttempts = rhs.nVoteKickAttempts; |
|
nDisconnectedSeconds = 0; |
|
nScoreMedal = rhs.nScoreMedal; |
|
nKillsMedal = rhs.nKillsMedal; |
|
nDamageMedal = rhs.nDamageMedal; |
|
nHealingMedal = rhs.nHealingMedal; |
|
nSupportMedal = rhs.nSupportMedal; |
|
bLateJoin = rhs.bLateJoin; |
|
nScore = rhs.nScore; |
|
rtLastActiveEvent = CRTime::RTime32TimeCur(); |
|
bAlwaysSafeToLeave = rhs.bAlwaysSafeToLeave; |
|
bEverConnected = rhs.bEverConnected; |
|
bDropWasAbandon = rhs.bDropWasAbandon; |
|
eDropReason = rhs.eDropReason; |
|
nConnectingButNotActiveIndex = rhs.nConnectingButNotActiveIndex; |
|
bPlayed = false; |
|
unMMSkillRating = rhs.unMMSkillRating; |
|
nDrilloRatingDelta = 0; |
|
unClassesPlayed = 0u; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// |
|
//----------------------------------------------------------------------------- |
|
MM_PlayerConnectionState_t CMatchInfo::PlayerMatchData_t::GetConnectionState() const |
|
{ |
|
if ( bConnected ) |
|
{ |
|
return nConnectingButNotActiveIndex == 0 ? MM_CONNECTED : MM_LOADING; |
|
} |
|
else |
|
{ |
|
return bEverConnected ? MM_DISCONNECTED : MM_CONNECTING; |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// |
|
//----------------------------------------------------------------------------- |
|
void CMatchInfo::PlayerMatchData_t::UpdateClassesPlayed( int nClass ) |
|
{ |
|
Assert( nClass >= TF_FIRST_NORMAL_CLASS && nClass <= TF_LAST_NORMAL_CLASS ); |
|
|
|
unClassesPlayed = unClassesPlayed | ( 1 << nClass ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// |
|
//----------------------------------------------------------------------------- |
|
void CMatchInfo::PlayerMatchData_t::OnConnected( int nEntindex ) |
|
{ |
|
if ( bConnected ) |
|
{ |
|
// This is before steamID validation, so make sure we don't add a path that would reward spoof connections. |
|
Assert( !"Player connecting is marked connected" ); |
|
return; |
|
} |
|
|
|
nConnectingButNotActiveIndex = nEntindex; |
|
|
|
// Mark connected. |
|
bConnected = true; |
|
bEverConnected = true; |
|
|
|
RTime32 now = CRTime::RTime32TimeCur(); |
|
MMLog( "Match player %s reconnected into slot %d, last active %u seconds ago.\n", |
|
steamID.Render(), nConnectingButNotActiveIndex, now - rtLastActiveEvent ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// |
|
//----------------------------------------------------------------------------- |
|
void CMatchInfo::PlayerMatchData_t::OnActive() |
|
{ |
|
nConnectingButNotActiveIndex = 0; |
|
CMatchInfo* pMatch = GTFGCClientSystem()->GetMatch(); |
|
Assert( pMatch); |
|
if ( pMatch && !pMatch->m_bFirstPersonActive ) |
|
{ |
|
MMLog( "Match going active\n" ); |
|
pMatch->m_bFirstPersonActive = true; |
|
} |
|
|
|
// Disconnected seconds for the time since they were last active, including DC'd time and time spent loading. This |
|
// prevents people who crash but rejoin quickly being able to be not-in-game for far longer than intended. Since we |
|
// already marked them connected, the abandon think won't touch them if this accumulation goes over the limit, but |
|
// it will count against them if they drop again. |
|
RTime32 now = CRTime::RTime32TimeCur(); |
|
RTime32 missing = now - rtLastActiveEvent; |
|
// See this convar's comment for why we do this. |
|
RTime32 minimum = (RTime32)Clamp( tf_mm_player_disconnect_time_minimum_penalty.GetInt(), 0, INT_MAX ); |
|
nDisconnectedSeconds += Max( missing, minimum ); |
|
rtLastActiveEvent = now; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Add a rank bucket stats vector |
|
//----------------------------------------------------------------------------- |
|
void CMatchInfo::SetDailyRankData( DailyStatsRankBucket_t vecRankData ) |
|
{ |
|
m_vDailyStatsRankData.AddToTail( vecRankData ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Request the competitive daily stats rollup from the GC |
|
//----------------------------------------------------------------------------- |
|
bool CMatchInfo::RequestGCRankData( void ) |
|
{ |
|
if ( !GetMatchGroupDescription( m_eMatchGroup ) || |
|
!GetMatchGroupDescription( m_eMatchGroup )->m_params.m_bDistributePerformanceMedals ) |
|
{ |
|
return false; |
|
} |
|
|
|
GCSDK::CProtoBufMsg< CMsgGC_DailyCompetitiveStatsRollup > msg( k_EMsgGC_DailyCompetitiveStatsRollup ); |
|
return GCClientSystem()->BSendMessage( msg ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CMatchInfo::AddPlayer( const PlayerMatchData_t &player, int nEntIndex, bool bActive ) |
|
{ |
|
PlayerMatchData_t* pOldPlayerMatchData = GetMatchDataForPlayer( player.steamID ); |
|
if ( pOldPlayerMatchData ) |
|
{ |
|
// Already have data? |
|
if ( pOldPlayerMatchData->bDropped ) |
|
{ |
|
// Returning a player that had dropped from the match. Re-create their entry as a fresh player, so the |
|
// constructor re-does everything. |
|
MMLog( "Player %s re-added to match they previously dropped from, replacing existing entry\n", |
|
player.steamID.Render() ); |
|
m_vMatchRankData.FindAndRemove( pOldPlayerMatchData ); |
|
delete pOldPlayerMatchData; |
|
pOldPlayerMatchData = nullptr; |
|
} |
|
else |
|
{ |
|
// This player is already in the match |
|
Assert( false ); |
|
MMLog( "!! Player %s being added to the match, but they are already present\n", |
|
player.steamID.Render() ); |
|
return; |
|
} |
|
} |
|
|
|
PlayerMatchData_t* pPlayerMatchData = new PlayerMatchData_t( player ); |
|
m_vMatchRankData.AddToTail( pPlayerMatchData ); |
|
|
|
if ( nEntIndex != 0 ) |
|
{ |
|
pPlayerMatchData->OnConnected( nEntIndex ); |
|
} |
|
|
|
if ( bActive ) |
|
{ |
|
pPlayerMatchData->OnActive(); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CMatchInfo::AddPlayer( CSteamID steamID, const CTFLobbyMember *pMemberData, bool bIsLateJoin, int nEntIndex, bool bActive ) |
|
{ |
|
PlayerMatchData_t playerMatchData( steamID, pMemberData ); |
|
playerMatchData.unMMSkillRating = pMemberData->skillrating(); |
|
playerMatchData.bLateJoin = bIsLateJoin; |
|
|
|
AddPlayer( playerMatchData, nEntIndex, bActive ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CMatchInfo::DropPlayer( CSteamID steamID, TFMatchLeaveReason eReason, bool bWasAbandon ) |
|
{ |
|
CMatchInfo::PlayerMatchData_t *pPlayerMatchData = GetMatchDataForPlayer( steamID ); |
|
|
|
AssertMsg( pPlayerMatchData, "If we have competitive match info, this player should be known" ); |
|
|
|
if ( pPlayerMatchData ) |
|
{ |
|
if ( pPlayerMatchData->bDropped ) |
|
{ |
|
MMLog( "!! Double-dropping player %s\n", steamID.Render() ); |
|
Assert( false ); |
|
} |
|
pPlayerMatchData->bDropped = true; |
|
pPlayerMatchData->eDropReason = eReason; |
|
pPlayerMatchData->bDropWasAbandon = bWasAbandon; |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
const CMatchInfo::PlayerMatchData_t* CMatchInfo::GetMatchDataForPlayer( CSteamID steamID ) const |
|
{ |
|
return const_cast<CMatchInfo*>(this)->GetMatchDataForPlayer( steamID ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
CMatchInfo::PlayerMatchData_t* CMatchInfo::GetMatchDataForPlayer( CSteamID steamID ) |
|
{ |
|
FOR_EACH_VEC( m_vMatchRankData, i ) |
|
{ |
|
if ( m_vMatchRankData[i]->steamID == steamID ) |
|
return ( m_vMatchRankData[i] ); |
|
} |
|
|
|
return NULL; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
CMatchInfo::PlayerMatchData_t* CMatchInfo::GetMatchDataForPlayer( int idx ) |
|
{ |
|
return m_vMatchRankData[idx]; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
int CMatchInfo::GetNumTotalMatchPlayers() const |
|
{ |
|
return m_vMatchRankData.Count(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
int CMatchInfo::GetNumActiveMatchPlayers() const |
|
{ |
|
int nActivePlayers = 0; |
|
FOR_EACH_VEC( m_vMatchRankData, idx ) |
|
{ |
|
nActivePlayers += !m_vMatchRankData[idx]->bDropped; |
|
} |
|
return nActivePlayers; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
int CMatchInfo::GetNumActiveMatchPlayersForTeam( int nTeam ) const |
|
{ |
|
int nActivePlayers = 0; |
|
FOR_EACH_VEC( m_vMatchRankData, idx ) |
|
{ |
|
if ( !m_vMatchRankData[idx]->bDropped ) |
|
{ |
|
if ( m_vMatchRankData[idx]->eGCTeam == nTeam ) |
|
{ |
|
nActivePlayers++; |
|
} |
|
} |
|
} |
|
return nActivePlayers; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
int CMatchInfo::GetTotalSkillRatingForTeam( int nTeam ) const |
|
{ |
|
// Re-evaluate this when skillrating might be for other backends |
|
FixmeMMRatingBackendSwapping(); |
|
int nSkillRating = 0; |
|
|
|
FOR_EACH_VEC( m_vMatchRankData, idx ) |
|
{ |
|
if ( !m_vMatchRankData[idx]->bDropped ) |
|
{ |
|
if ( m_vMatchRankData[idx]->eGCTeam == nTeam ) |
|
{ |
|
nSkillRating += m_vMatchRankData[idx]->unMMSkillRating; |
|
} |
|
} |
|
} |
|
|
|
return nSkillRating; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
int CMatchInfo::GetNumConnectedMatchPlayers() const |
|
{ |
|
int nConnectedPlayers = 0; |
|
FOR_EACH_VEC( m_vMatchRankData, idx ) |
|
{ |
|
nConnectedPlayers += ( m_vMatchRankData[idx]->bConnected && !m_vMatchRankData[idx]->bDropped ); |
|
} |
|
return nConnectedPlayers; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
uint32 CMatchInfo::GetCanonicalMatchSize() const |
|
{ |
|
return m_nGCMatchSize ? m_nGCMatchSize : GetMatchGroupDescription( m_eMatchGroup )->GetMatchSize(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CMatchInfo::GiveXPRewardToPlayerForAction( CSteamID steamID, CMsgTFXPSource::XPSourceType eType, int nCount ) |
|
{ |
|
// Needs to be a positive number! |
|
if ( nCount <= 0 ) |
|
return; |
|
GiveXPDirectly( steamID, eType, ceil( (float)nCount * g_XPSourceDefs[ eType ].m_flValueMultiplier ), true ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CMatchInfo::GiveXPDirectly( CSteamID steamID, CMsgTFXPSource::XPSourceType eType, int nAmount, bool bCanAwardBonusXP ) |
|
{ |
|
const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( m_eMatchGroup ); |
|
if ( !pMatchDesc || !pMatchDesc->BUsesXP() || nAmount <= 0 ) |
|
{ |
|
return; |
|
} |
|
|
|
PlayerMatchData_t *pMatchPlayer = GetMatchDataForPlayer( steamID ); |
|
|
|
if ( pMatchPlayer && !pMatchPlayer->bDropped ) |
|
{ |
|
CMsgTFXPSource* pSource = NULL; |
|
|
|
auto idx = pMatchPlayer->m_mapXPAccumulation.Find( eType ); |
|
if ( idx == pMatchPlayer->m_mapXPAccumulation.InvalidIndex() ) |
|
{ |
|
idx = pMatchPlayer->m_mapXPAccumulation.Insert( eType, 0.f ); |
|
} |
|
|
|
// You can only draw from the bonus pool if you GAINED xp |
|
if ( nAmount > 0 && bCanAwardBonusXP ) |
|
{ |
|
FOR_EACH_VEC_BACK( pMatchPlayer->m_vecXPBonusPools, i ) |
|
{ |
|
PlayerMatchData_t::XPBonusPool_t& xpMultiplier = pMatchPlayer->m_vecXPBonusPools[ i ]; |
|
|
|
// We do this so when specifying the multiplier, you can say you want the multiplier to be |
|
int nBonusAmount = ceil( nAmount * xpMultiplier.m_flMultiplier ); |
|
|
|
// If there's a maximum amount to give for this bonus, subtract from the total |
|
// and remove this bonus if the pool is emptied |
|
Assert( xpMultiplier.m_nBonusPoolRemaining > 0 ); |
|
nBonusAmount = Min( nBonusAmount, xpMultiplier.m_nBonusPoolRemaining ); |
|
xpMultiplier.m_nBonusPoolRemaining -= nBonusAmount; |
|
|
|
// Save the type so we can recursively pass it below |
|
CMsgTFXPSource::XPSourceType eBonusType = xpMultiplier.m_eType; |
|
// If there's no more in the pool, then we can remove this from the list |
|
if ( xpMultiplier.m_nBonusPoolRemaining <= 0 ) |
|
{ |
|
// We're going backwards, so this is ok |
|
pMatchPlayer->m_vecXPBonusPools.Remove( i ); |
|
} |
|
|
|
// Give the bonus |
|
GiveXPDirectly( steamID, eBonusType, nBonusAmount, false ); |
|
} |
|
} |
|
|
|
// Accumulate in the map |
|
pMatchPlayer->m_mapXPAccumulation[ idx ] += nAmount; |
|
int nAccum = pMatchPlayer->m_mapXPAccumulation[ idx ]; |
|
// Don't make a XPSource proto object if there's nothing to even report |
|
if ( nAccum == 0 ) |
|
return; |
|
|
|
// Find the type if it exists. |
|
for( int i=0; i < pMatchPlayer->m_XPBreakdown.sources_size(); ++i ) |
|
{ |
|
if ( pMatchPlayer->m_XPBreakdown.sources( i ).type() == eType ) |
|
{ |
|
pSource = pMatchPlayer->m_XPBreakdown.mutable_sources( i ); |
|
break; |
|
} |
|
} |
|
|
|
// Create a new one if we need to |
|
if ( pSource == NULL ) |
|
{ |
|
pSource = pMatchPlayer->m_XPBreakdown.add_sources(); |
|
pSource->set_account_id( steamID.GetAccountID() ); |
|
pSource->set_match_group( m_eMatchGroup ); |
|
pSource->set_type( eType ); |
|
pSource->set_match_id( m_nMatchID ); |
|
pSource->set_amount( 0 ); |
|
} |
|
|
|
#ifdef STAGING_ONLY |
|
if ( tf_debug_xp_changes.GetBool() && nAccum != pSource->amount() ) |
|
{ |
|
CBasePlayer* pPlayer = UTIL_PlayerBySteamID( steamID ); |
|
if ( pPlayer ) |
|
{ |
|
Msg( "%s received %d %s xp\n", pPlayer->GetPlayerName(), |
|
nAccum - pSource->amount(), |
|
CMsgTFXPSource_XPSourceType_descriptor()->value( eType )->name().c_str() ); |
|
} |
|
} |
|
#endif |
|
|
|
// Update the amount |
|
pSource->set_amount( nAccum ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CMatchInfo::GiveXPBonus( CSteamID steamID, |
|
CMsgTFXPSource_XPSourceType eType, |
|
float flMultipler, |
|
int nBonusPool ) |
|
{ |
|
const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( m_eMatchGroup ); |
|
if ( !pMatchDesc || !pMatchDesc->BUsesXP() ) |
|
{ |
|
return; |
|
} |
|
|
|
PlayerMatchData_t *pMatchPlayer = GetMatchDataForPlayer( steamID ); |
|
|
|
if ( pMatchPlayer && !pMatchPlayer->bDropped ) |
|
{ |
|
// Find existing entry if there is one |
|
auto idx = pMatchPlayer->m_vecXPBonusPools.InvalidIndex(); |
|
FOR_EACH_VEC( pMatchPlayer->m_vecXPBonusPools, i ) |
|
{ |
|
// Found it |
|
if( pMatchPlayer->m_vecXPBonusPools[ i ].m_eType == eType ) |
|
{ |
|
idx = i; |
|
break; |
|
} |
|
} |
|
|
|
// Create new entry if we didnt have an existing one |
|
if ( idx == pMatchPlayer->m_vecXPBonusPools.InvalidIndex() ) |
|
{ |
|
idx = pMatchPlayer->m_vecXPBonusPools.AddToTail(); |
|
} |
|
|
|
// Add bonus |
|
PlayerMatchData_t::XPBonusPool_t& currentXPMultiplier = pMatchPlayer->m_vecXPBonusPools[ idx ]; |
|
currentXPMultiplier.m_nBonusPoolRemaining += nBonusPool; |
|
currentXPMultiplier.m_eType = eType; |
|
currentXPMultiplier.m_flMultiplier = Max( currentXPMultiplier.m_flMultiplier, flMultipler ); |
|
} |
|
} |
|
|
|
#ifdef STAGING_ONLY |
|
CON_COMMAND( give_xp_bonus, "Gives the player with the specified name an xp boost. Usage: give_xp_bonus <name> <type> <multiplier> <bonus_pool>" ) |
|
{ |
|
if ( args.ArgC() != 5 ) |
|
{ |
|
Msg( "Incorrect arguments. Usage: give_xp_bonus <name> <type> <multiplier> <bonus_pool>\n" ); |
|
return; |
|
} |
|
|
|
CBasePlayer* pPlayer = UTIL_PlayerByName( args.Arg( 1 ) ); |
|
if ( !pPlayer ) |
|
{ |
|
Msg( "No player named %s\n", args.Arg( 1 ) ); |
|
return; |
|
} |
|
|
|
if ( !GTFGCClientSystem()->GetMatch() ) |
|
{ |
|
Msg( "Not running a match\n" ); |
|
return; |
|
} |
|
|
|
CMsgTFXPSource_XPSourceType nType = (CMsgTFXPSource_XPSourceType)atoi( args.Arg( 2 ) ); |
|
|
|
if ( nType < CMsgTFXPSource_XPSourceType_XPSourceType_MIN |
|
|| nType >= CMsgTFXPSource_XPSourceType_NUM_SOURCE_TYPES ) |
|
{ |
|
Msg( "Type is not a valid type!\n" ); |
|
return; |
|
} |
|
|
|
CSteamID steamID; |
|
pPlayer->GetSteamID( &steamID ); |
|
GTFGCClientSystem()->GetMatch()->GiveXPBonus( steamID, |
|
nType, |
|
atof( args.Arg( 3 ) ), |
|
atoi( args.Arg( 4 ) ) ); |
|
} |
|
#endif |
|
|
|
//----------------------------------------------------------------------------- |
|
bool CMatchInfo::BPlayerSafeToLeaveMatch( CSteamID steamID ) |
|
{ |
|
PlayerMatchData_t *pMatchPlayer = this->GetMatchDataForPlayer( steamID ); |
|
|
|
// Right now, you cannot leave while the match is running |
|
bool bSafe = m_bMatchEnded || !pMatchPlayer || pMatchPlayer->bDropped || pMatchPlayer->bAlwaysSafeToLeave; |
|
|
|
// The match description might have special exceptions |
|
if ( !bSafe && pMatchPlayer ) |
|
{ |
|
bSafe = bSafe || GetMatchGroupDescription( m_eMatchGroup )->BMatchIsSafeToLeaveForPlayer( this, pMatchPlayer ); |
|
} |
|
|
|
return bSafe; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Determine the performance ranking of each player after a competitive match |
|
//----------------------------------------------------------------------------- |
|
bool CMatchInfo::CalculatePlayerMatchRankData( void ) |
|
{ |
|
Assert( TFGameRules() ); |
|
if ( !TFGameRules() ) |
|
return false; |
|
|
|
CTFPlayerResource *pTFResource = dynamic_cast< CTFPlayerResource* >( g_pPlayerResource ); |
|
if ( !pTFResource ) |
|
return false; |
|
|
|
CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch(); |
|
if ( !pMatch ) |
|
return false; |
|
|
|
if ( !m_vDailyStatsRankData.Count() ) |
|
{ |
|
Warning( "CalculatePlayerMatchRankData(): DailyStatsRankData is empty\n" ); |
|
return false; |
|
} |
|
|
|
const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( pMatch->m_eMatchGroup ); |
|
if ( !pMatchDesc || |
|
!pMatchDesc->m_pProgressionDesc || |
|
!pMatchDesc->m_params.m_bDistributePerformanceMedals ) |
|
{ |
|
return false; |
|
} |
|
|
|
CUtlVector < CTFPlayer* > vecPlayers; |
|
CollectHumanPlayers( &vecPlayers ); |
|
FOR_EACH_VEC( vecPlayers, i ) |
|
{ |
|
if ( !vecPlayers[i] ) |
|
continue; |
|
|
|
CSteamID steamID; |
|
if ( !vecPlayers[i]->GetSteamID( &steamID ) ) |
|
continue; |
|
|
|
PlayerStats_t *pStats = CTF_GameStats.FindPlayerStats( vecPlayers[i] ); |
|
CMatchInfo::PlayerMatchData_t *matchData = GetMatchDataForPlayer( steamID ); |
|
if ( !matchData || !pStats ) |
|
{ |
|
Warning( "Missing player data in CalculatePlayerMatchRankData\n" ); |
|
Assert( false ); |
|
continue; |
|
} |
|
|
|
// Get player's competitive rank |
|
FixmeMMRatingBackendSwapping(); // This is assuming we're using primary skill rating for rank |
|
uint32 unRank = pMatchDesc->m_pProgressionDesc->GetLevelForExperience( matchData->unMMSkillRating ).m_nLevelNum; |
|
int nRankIndex = -1; |
|
|
|
// Let's find the typical stats for your rank |
|
FOR_EACH_VEC( m_vDailyStatsRankData, j ) |
|
{ |
|
if ( unRank == m_vDailyStatsRankData[j].nRank ) |
|
{ |
|
#ifndef STAGING_ONLY |
|
if ( m_vDailyStatsRankData[j].nRecords < 10 ) |
|
{ |
|
Warning( "CalculatePlayerMatchRankData(): Too few stat entries (%d) for rank %d\n", m_vDailyStatsRankData[j].nRecords, unRank ); |
|
return false; |
|
} |
|
#endif // !STAGING_ONLY |
|
|
|
nRankIndex = j; |
|
break; |
|
} |
|
} |
|
|
|
uint32 unScoreMedal = GetRankForStat( RankStat_Score, nRankIndex, pTFResource->GetTotalScore( vecPlayers[i]->entindex() ) ); |
|
uint32 unKillsMedal = GetRankForStat( RankStat_Kills, nRankIndex, pStats->statsAccumulated.m_iStat[TFSTAT_KILLS] ); |
|
uint32 unDamageMedal = GetRankForStat( RankStat_Damage, nRankIndex, pStats->statsAccumulated.m_iStat[TFSTAT_DAMAGE] ); |
|
uint32 unHealingMedal = GetRankForStat( RankStat_Healing, nRankIndex, pStats->statsAccumulated.m_iStat[TFSTAT_HEALING] ); |
|
uint32 unSupportMedal = GetRankForStat( RankStat_Support, nRankIndex, TFGameRules()->CalcPlayerSupportScore( &pStats->statsAccumulated, vecPlayers[i]->entindex() ) ); |
|
|
|
matchData->nScoreMedal = unScoreMedal; |
|
matchData->nKillsMedal = unKillsMedal; |
|
matchData->nDamageMedal = unDamageMedal; |
|
matchData->nHealingMedal = unHealingMedal; |
|
matchData->nSupportMedal = unSupportMedal; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// |
|
//----------------------------------------------------------------------------- |
|
bool CMatchInfo::CalculateMatchSkillRatingAdjustments( int iWinningTeam ) |
|
{ |
|
// This is assuming skill rating is drillo,and doing a client-side prediction on it |
|
FixmeMMRatingBackendSwapping(); |
|
if ( !iWinningTeam ) |
|
{ |
|
Log( "CalculateMatchSkillRatingAdjustments(): Invalid team!\n" ); |
|
return false; |
|
} |
|
|
|
EMatchGroup matchGroup = m_eMatchGroup; |
|
if ( !IsLadderGroup( matchGroup ) ) |
|
{ |
|
Assert( false ); |
|
Log( "CalculateMatchSkillRatingAdjustments(): Match %llu has an invalid MatchGroup (%i)\n", m_nMatchID, (int)matchGroup ); |
|
return false; |
|
} |
|
|
|
const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( matchGroup ); |
|
if ( !pMatchDesc || !pMatchDesc->m_pProgressionDesc ) |
|
{ |
|
Log( "CalculateMatchSkillRatingAdjustments(): Match has bogus MatchGroupDescription\n" ); |
|
return false; |
|
} |
|
|
|
CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch(); |
|
if ( !pMatch ) |
|
{ |
|
Log( "CalculateMatchSkillRatingAdjustments(): Match has bogus CMatchInfo\n" ); |
|
return false; |
|
} |
|
|
|
int nWinnerTotal = 0; |
|
int nLoserTotal = 0; |
|
uint32 unWinningPlayers = 0u; |
|
uint32 unLosingPlayers = 0u; |
|
|
|
// Gather data so we can figure out rating adjustments |
|
for ( int i = 0; i < GetNumTotalMatchPlayers(); i++ ) |
|
{ |
|
CMatchInfo::PlayerMatchData_t *pPlayerInfo = GetMatchDataForPlayer( i ); |
|
Assert( pPlayerInfo ); |
|
if ( !pPlayerInfo || pPlayerInfo->bDropped ) |
|
continue; |
|
|
|
if ( TFGameRules()->GetGameTeamForGCTeam( pPlayerInfo->eGCTeam ) == iWinningTeam ) |
|
{ |
|
nWinnerTotal += pPlayerInfo->unMMSkillRating; |
|
++unWinningPlayers; |
|
} |
|
else |
|
{ |
|
nLoserTotal += pPlayerInfo->unMMSkillRating; |
|
++unLosingPlayers; |
|
} |
|
} |
|
|
|
if ( pMatchDesc->m_params.m_bRequireCompleteMatch && ( unWinningPlayers + unLosingPlayers != GetCanonicalMatchSize() ) ) |
|
{ |
|
Assert( false ); |
|
Log( "CalculateMatchSkillRatingAdjustments(): Match %llu has invalid team size(s): %d vs %d\n", |
|
m_nMatchID, unWinningPlayers, unLosingPlayers ); |
|
} |
|
|
|
int nTeamSize = ( pMatch->GetCanonicalMatchSize() % 2 ) ? ( pMatch->GetCanonicalMatchSize() / 2 + 1 ) : ( pMatch->GetCanonicalMatchSize() / 2 ); |
|
int nWinningTeamAverage = (float)nWinnerTotal / Max( nTeamSize, 1 ); |
|
int nLosingTeamAverage = (float)nLoserTotal / Max( nTeamSize, 1 ); |
|
int nRatingDiff = nLosingTeamAverage - nWinningTeamAverage; |
|
|
|
// Determine adjustment based on difference between teams |
|
const int nChange = RemapValClamped( nRatingDiff, /* from */ -(float)k_unDrilloRating_MaxDifference, (float)k_unDrilloRating_MaxDifference, |
|
/* to */ (float)k_nDrilloRating_MinRatingAdjust, (float)k_nDrilloRating_Ladder_MaxRatingAdjust ); |
|
|
|
// Cap loss for low-rated teams, but not low-rated winners. This breaks the loose "sort-of-zero-sum" system we have, but that's ok in the lower range. |
|
const int nLoserChange = ( nLosingTeamAverage <= k_unDrilloRating_Ladder_LowSkill ) ? Min( nChange, k_nDrilloRating_Ladder_MaxLossAdjust_LowRank ) : nChange; |
|
|
|
// Rating delta update |
|
for ( int i = 0; i < GetNumTotalMatchPlayers(); i++ ) |
|
{ |
|
CMatchInfo::PlayerMatchData_t *pPlayerInfo = GetMatchDataForPlayer( i ); |
|
Assert( pPlayerInfo ); |
|
if ( !pPlayerInfo ) |
|
continue; |
|
|
|
int nAmount = nChange; |
|
if ( pPlayerInfo->BDropWasAbandon() ) |
|
{ |
|
// Abandon |
|
nAmount = -k_nDrilloRating_Ladder_MaxRatingAdjust; |
|
if ( m_eMatchGroup == k_nMatchGroup_Ladder_6v6 ) |
|
{ |
|
GiveXPDirectly( pPlayerInfo->steamID, CMsgTFXPSource_XPSourceType::CMsgTFXPSource_XPSourceType_SOURCE_COMPETITIVE_ABANDON, nAmount ); |
|
} |
|
} |
|
else if ( TFGameRules()->GetGameTeamForGCTeam( pPlayerInfo->eGCTeam ) != iWinningTeam ) |
|
{ |
|
// Loss |
|
nAmount = -nLoserChange; |
|
} |
|
|
|
pPlayerInfo->nDrilloRatingDelta = nAmount; |
|
|
|
// Scoreboard |
|
IGameEvent *pEvent = gameeventmanager->CreateEvent( "competitive_stats_update" ); |
|
if ( pEvent ) |
|
{ |
|
CBasePlayer *pPlayer = UTIL_PlayerBySteamID( pPlayerInfo->steamID ); |
|
if ( !pPlayer ) |
|
continue; |
|
|
|
pEvent->SetInt( "index", pPlayer->entindex() ); |
|
pEvent->SetInt( "rating", pPlayerInfo->unMMSkillRating ); |
|
// This is the only place this guy is used. We should eventually have the GC send down results and use that |
|
// instead of running this prediction step here. |
|
pEvent->SetInt( "delta", pPlayerInfo->nDrilloRatingDelta ); |
|
CMatchInfo::PlayerMatchData_t *pMatchRankData = GetMatchDataForPlayer( pPlayerInfo->steamID ); |
|
pEvent->SetInt( "score_rank", pMatchRankData ? pMatchRankData->nScoreMedal : 0 ); // medal won (if any) |
|
pEvent->SetInt( "kills_rank", pMatchRankData ? pMatchRankData->nKillsMedal : 0 ); // |
|
pEvent->SetInt( "damage_rank", pMatchRankData ? pMatchRankData->nDamageMedal : 0 ); // |
|
pEvent->SetInt( "healing_rank", pMatchRankData ? pMatchRankData->nHealingMedal : 0 ); // |
|
pEvent->SetInt( "support_rank", pMatchRankData ? pMatchRankData->nSupportMedal : 0 ); // |
|
gameeventmanager->FireEvent( pEvent ); |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Returns the medal rank (if any) for this stat |
|
//----------------------------------------------------------------------------- |
|
int CMatchInfo::GetRankForStat( RankStatType_t statType, int nRankIndex, uint32 nValue ) |
|
{ |
|
if ( !m_vDailyStatsRankData.IsValidIndex( nRankIndex ) ) |
|
return StatMedal_None; |
|
|
|
// Get match duration, so we can scale values accordingly (total time won't have last round time included yet) |
|
uint16 nMatchDuration = CTF_GameStats.m_currentMap.m_Header.m_iTotalTime + ( gpGlobals->curtime - TFGameRules()->GetRoundStart() ); |
|
|
|
// Assume 9 minute average match duration; TO DO: Use actual values generated from matchresults table |
|
uint16 nAverageMatchDuration = 9 * 60; |
|
|
|
// Adjusted Value |
|
float flStatAdjustment = ( float ) nAverageMatchDuration / ( float ) nMatchDuration; |
|
flStatAdjustment = clamp( flStatAdjustment, 0.33f, 3.0f ); |
|
|
|
nValue = nValue * flStatAdjustment; |
|
|
|
uint32 unStatAvg = m_vDailyStatsRankData[nRankIndex].nAvgScore; |
|
uint32 unStatStdDev = m_vDailyStatsRankData[nRankIndex].nStDevScore; |
|
|
|
switch ( statType ) |
|
{ |
|
case RankStat_Score: |
|
break; |
|
case RankStat_Kills: |
|
unStatAvg = m_vDailyStatsRankData[nRankIndex].nAvgKills; |
|
unStatStdDev = m_vDailyStatsRankData[nRankIndex].nStDevKills; |
|
break; |
|
case RankStat_Damage: |
|
unStatAvg = m_vDailyStatsRankData[nRankIndex].nAvgDamage; |
|
unStatStdDev = m_vDailyStatsRankData[nRankIndex].nStDevDamage; |
|
break; |
|
case RankStat_Healing: |
|
unStatAvg = m_vDailyStatsRankData[nRankIndex].nAvgHealing; |
|
unStatStdDev = m_vDailyStatsRankData[nRankIndex].nStDevHealing; |
|
break; |
|
case RankStat_Support: |
|
unStatAvg = m_vDailyStatsRankData[nRankIndex].nAvgSupport; |
|
unStatStdDev = m_vDailyStatsRankData[nRankIndex].nStDevSupport; |
|
break; |
|
default: |
|
Assert( 0 ); |
|
return 0; |
|
} |
|
|
|
if ( !unStatAvg || !unStatStdDev ) |
|
return 0; |
|
|
|
int nMedalRank = StatMedal_None; |
|
|
|
// Non-zero value? |
|
if ( unStatAvg && unStatStdDev ) |
|
{ |
|
int nDelta = nValue - unStatAvg; |
|
if ( nDelta > 0 ) |
|
{ |
|
float flPercentile = NormalDistributionCDF( (float) nValue, (float) unStatAvg, (float) unStatStdDev ); |
|
|
|
if ( flPercentile >= m_flGoldPercentile ) |
|
{ |
|
nMedalRank = StatMedal_Gold; |
|
} |
|
else if ( flPercentile >= m_flSilverPercentile ) |
|
{ |
|
nMedalRank = StatMedal_Silver; |
|
} |
|
else if ( flPercentile >= m_flBronzePercentile ) |
|
{ |
|
nMedalRank = StatMedal_Bronze; |
|
} |
|
|
|
// TODO: |
|
// - Stat must be "n" std deviations above the match average, too (anti-farming) |
|
// - Match must qualify: |
|
// - Less than "n" minutes |
|
// - At least "x" of "y" players at match end (no leavers?) |
|
} |
|
} |
|
|
|
return clamp( nMedalRank, StatMedal_None, StatMedal_Gold ); |
|
} |
|
|
|
|
|
float CMatchInfo::NormalDistributionCDF( float flValue, float flMu, float flSigma ) |
|
{ |
|
if ( flSigma <= 0.f ) |
|
return 0.5f; |
|
|
|
return 0.5f * ( 1.f + erf( ( flValue - flMu ) / ( flSigma * sqrt( 2.f ) ) ) ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// CGCCompetitiveDailyStatsRollupJob |
|
//----------------------------------------------------------------------------- |
|
class CGCCompetitiveDailyStatsRollupJob : public GCSDK::CGCClientJob |
|
{ |
|
public: |
|
CGCCompetitiveDailyStatsRollupJob( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient ) {} |
|
|
|
virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket ) |
|
{ |
|
GCSDK::CProtoBufMsg< CMsgGC_DailyCompetitiveStatsRollup_Response > msg( pNetPacket ); |
|
|
|
CMatchInfo *pInfo = GTFGCClientSystem()->GetMatch(); |
|
if ( !pInfo ) |
|
return false; |
|
|
|
// Empty rankdata is valid (GC runs checks that might cause this as people reach new ranks) |
|
for ( int i = 0; i < msg.Body().rankdata_size(); i++ ) |
|
{ |
|
CMatchInfo::DailyStatsRankBucket_t rankBucket = { |
|
msg.Body().rankdata( i ).rank(), |
|
msg.Body().rankdata( i ).records(), |
|
msg.Body().rankdata( i ).avg_score(), |
|
msg.Body().rankdata( i ).stdev_score(), |
|
msg.Body().rankdata( i ).avg_kills(), |
|
msg.Body().rankdata( i ).stdev_kills(), |
|
msg.Body().rankdata( i ).avg_damage(), |
|
msg.Body().rankdata( i ).stdev_damage(), |
|
msg.Body().rankdata( i ).avg_healing(), |
|
msg.Body().rankdata( i ).stdev_healing(), |
|
msg.Body().rankdata( i ).avg_support(), |
|
msg.Body().rankdata( i ).stdev_support() |
|
}; |
|
|
|
pInfo->SetDailyRankData( rankBucket ); |
|
} |
|
|
|
return true; |
|
} |
|
}; |
|
GC_REG_JOB( GCSDK::CGCClient, CGCCompetitiveDailyStatsRollupJob, "CGCCompetitiveDailyStatsRollupJob", k_EMsgGC_DailyCompetitiveStatsRollup_Response, k_EServerTypeGCClient ); |
|
|
|
//----------------------------------------------------------------------------- |
|
// CGCVoteSystemVoteKickResponse |
|
//----------------------------------------------------------------------------- |
|
class CGCVoteSystemVoteKickResponse : public GCSDK::CGCClientJob |
|
{ |
|
public: |
|
CGCVoteSystemVoteKickResponse( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient ) {} |
|
|
|
virtual bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket ) |
|
{ |
|
GCSDK::CProtoBufMsg< CMsgGC_VoteKickPlayerRequestResponse > msg( pNetPacket ); |
|
if ( g_voteController ) |
|
{ |
|
g_voteController->GCResponseReceived( msg.Body().allowed() ); |
|
} |
|
|
|
return true; |
|
} |
|
}; |
|
GC_REG_JOB( GCSDK::CGCClient, CGCVoteSystemVoteKickResponse, "CGCVoteSystemVoteKickResponse", k_EMsgGCVoteKickPlayerRequestResponse, k_EServerTypeGCClient ); |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
class CGCKickPlayerFromLobbyJob : public GCSDK::CGCClientJob |
|
{ |
|
public: |
|
CGCKickPlayerFromLobbyJob( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {} |
|
|
|
virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket ) |
|
{ |
|
GCSDK::CProtoBufMsg<CMsgGC_KickPlayerFromLobby> msg( pNetPacket ); |
|
|
|
CSteamID steamID( msg.Body().targetid() ); |
|
if ( steamID.IsValid() ) |
|
{ |
|
GTFGCClientSystem()->EjectMatchPlayer( steamID, TFMatchLeaveReason_ADMIN_KICK ); |
|
} |
|
|
|
return true; |
|
} |
|
}; |
|
GC_REG_JOB( GCSDK::CGCClient, CGCKickPlayerFromLobbyJob, "CGCKickPlayerFromLobbyJob", k_EMsgGC_KickPlayerFromLobby, GCSDK::k_EServerTypeGCClient ); |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
CTFGCServerSystem::CTFGCServerSystem() |
|
: m_flTimeRequestedLateJoin( -1.f ) |
|
, m_bLateJoinEligible( false ) |
|
, m_iSavedVisibleMaxPlayers( -1 ) |
|
, m_bOverridingVisibleMaxPlayers( false ) |
|
, m_bWaitingForNewMatchID( false ) |
|
, m_flWaitingForNewMatchTime( 0.f ) |
|
{ |
|
// replace base GCClientSystem |
|
SetGCClientSystem( this ); |
|
|
|
m_unGameStartTime = 0; |
|
m_bSetupSchema = false; |
|
m_timeLastSendGameServerInfoAndConnectedPlayers = 0; |
|
//m_flUpdateGCGameTime = 0; |
|
//m_nUploadingMatchStats = EDOTA_MATCH_STATS_IDLE; |
|
//m_nParentRelayCount = 0; |
|
//m_nLastUpdateGCServerType = -1; |
|
m_eLastGameServerUpdateState = ServerMatchmakingState_NOT_PARTICIPATING; |
|
m_eLastGameServerUpdateMatchmakingMode = TF_Matchmaking_MVM; |
|
m_nLastGameServerUpdateBotCount = -1; |
|
m_nLastGameServerUpdateMaxHumans = -1; |
|
m_nLastGameServerUpdateSlotsFree = -1; |
|
m_nLastGameServerUpdateLobbyMMVersion = 0; |
|
m_flTimeBecameEmptyWithLobby = 0.0f; |
|
m_timeLastConnectedToGC = 0.f; |
|
m_pMatchInfo = NULL; |
|
|
|
g_bWarnedAboutMaxplayersInMVM = false; |
|
} |
|
|
|
|
|
CTFGCServerSystem::~CTFGCServerSystem( void ) |
|
{ |
|
// Prevent other system from using this pointer after it's destroyed |
|
SetGCClientSystem( NULL ); |
|
|
|
if ( m_pMatchInfo ) |
|
{ |
|
delete m_pMatchInfo; |
|
} |
|
} |
|
|
|
|
|
bool CTFGCServerSystem::Init() |
|
{ |
|
ListenForGameEvent( "player_disconnect" ); |
|
ListenForGameEvent( "player_score_changed" ); |
|
|
|
g_bWarnedAboutMaxplayersInMVM = false; |
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFGCServerSystem::PreInitGC() |
|
{ |
|
BaseClass::PreInitGC(); |
|
|
|
if ( !m_bSetupSchema ) |
|
{ |
|
// REG_SHARED_OBJECT_SUBCLASS( CDOTAHeroStandings ); |
|
// REG_SHARED_OBJECT_SUBCLASS( CDOTAGameAccountClient ); |
|
REG_SHARED_OBJECT_SUBCLASS( CTFGSLobby ); |
|
REG_SHARED_OBJECT_SUBCLASS( CTFParty ); |
|
|
|
m_bSetupSchema = true; |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFGCServerSystem::PostInitGC() |
|
{ |
|
BaseClass::PostInitGC(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFGCServerSystem::LevelShutdownPostEntity() |
|
{ |
|
BaseClass::LevelShutdownPostEntity(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFGCServerSystem::Shutdown() |
|
{ |
|
BaseClass::Shutdown(); |
|
|
|
// Remove listener, if we have one |
|
if ( m_ourSteamID.IsValid() ) |
|
{ |
|
GCClientSystem()->GetGCClient()->RemoveSOCacheListener( m_ourSteamID, this ); |
|
} |
|
} |
|
|
|
void CTFGCServerSystem::LevelInitPreEntity() |
|
{ |
|
BaseClass::LevelInitPreEntity(); |
|
// Assert( m_nUploadingMatchStats != EDOTA_MATCH_STATS_UPLOADING ); |
|
// if ( m_nUploadingMatchStats == EDOTA_MATCH_STATS_UPLOADING ) |
|
// { |
|
// Warning( "Error, changed level while waiting for match stats to upload!\n" ); |
|
// return; |
|
// } |
|
// m_nUploadingMatchStats = EDOTA_MATCH_STATS_IDLE; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFGCServerSystem::ClientActive( CSteamID steamIDClient ) |
|
{ |
|
if ( !steamIDClient.IsValid() || !steamIDClient.BIndividualAccount() ) |
|
{ |
|
if ( !HushAsserts() ) |
|
{ |
|
Assert( steamIDClient.IsValid() ); |
|
Assert( steamIDClient.BIndividualAccount() ); |
|
} |
|
return; |
|
} |
|
|
|
CMatchInfo *pMatch = GetMatch(); |
|
CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch ? pMatch->GetMatchDataForPlayer( steamIDClient ) : NULL; |
|
if ( !pMatchPlayer ) |
|
return; |
|
|
|
pMatchPlayer->OnActive(); |
|
|
|
// Only subscribe to match players' SOCaches. They're the only ones who will have |
|
// parties that we care about. |
|
GetGCClient()->AddSOCacheListener( steamIDClient, this ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFGCServerSystem::ClientConnected( CSteamID steamIDClient, edict_t *pEntity ) |
|
{ |
|
// Note that we won't be notified of players connecting with unknown steamIDs, SteamIDAllowedToConnect() should be |
|
// used to reject those in a strict MM scenario where that is not acceptable. |
|
CMatchInfo *pMatch = GetMatch(); |
|
CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch ? pMatch->GetMatchDataForPlayer( steamIDClient ) : NULL; |
|
if ( !pMatchPlayer ) |
|
return; |
|
|
|
pMatchPlayer->OnConnected( pEntity->m_EdictIndex ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFGCServerSystem::ClientDisconnected( CSteamID steamIDClient ) |
|
{ |
|
if ( !steamIDClient.IsValid() || !steamIDClient.BIndividualAccount() ) |
|
{ |
|
Assert( steamIDClient.IsValid() ); |
|
Assert( steamIDClient.BIndividualAccount() ); |
|
return; |
|
} |
|
|
|
GetGCClient()->RemoveSOCacheListener( steamIDClient, this ); |
|
|
|
// This is here because ClientDisconnected code is not called on gamerules or player |
|
// when the game is in state g_fGameOver. See CServerGameClients::ClientDisconnect. |
|
CBasePlayer* pPlayer = UTIL_PlayerBySteamID( steamIDClient ); |
|
if ( TFGameRules() && pPlayer ) |
|
{ |
|
TFGameRules()->SetPlayerNextMapVote( pPlayer->entindex(), CTFGameRules::USER_NEXT_MAP_VOTE_UNDECIDED ); |
|
} |
|
|
|
CMatchInfo *pMatch = GetMatch(); |
|
CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch ? pMatch->GetMatchDataForPlayer( steamIDClient ) : NULL; |
|
if ( !pMatchPlayer ) |
|
{ |
|
return; |
|
} |
|
|
|
if ( !pMatchPlayer->bConnected ) |
|
{ |
|
Assert( !"Player disconnecting is not marked connected" ); |
|
return; |
|
} |
|
|
|
// Did they disconnect while still loading in? |
|
bool bWasActive = pMatchPlayer->nConnectingButNotActiveIndex == 0; |
|
|
|
RTime32 now = CRTime::RTime32TimeCur(); |
|
// Time spent in the active state. |
|
RTime32 timeSpentActive = bWasActive ? ( now - pMatchPlayer->rtLastActiveEvent ) : 0; |
|
|
|
// Mark disconnected |
|
pMatchPlayer->bConnected = false; |
|
pMatchPlayer->nConnectingButNotActiveIndex = 0; |
|
|
|
// If they were active, they now transitioned to inactive. If they were loading, this value is still the last time |
|
// they went inactive, and shouldn't change. |
|
if ( bWasActive ) |
|
{ pMatchPlayer->rtLastActiveEvent = now; } |
|
|
|
// Optionally forgive some amount of their disconnected seconds accumulation based on how long they were present. |
|
int nForgiveRatio = tf_mm_player_disconnect_time_forgive_ratio.GetInt(); |
|
if ( timeSpentActive > 0 && nForgiveRatio > 0 && pMatchPlayer->nDisconnectedSeconds > 0 ) |
|
{ |
|
double dForgiven = (double)pMatchPlayer->nDisconnectedSeconds - ( (double)timeSpentActive / nForgiveRatio ); |
|
|
|
int nOldVal = pMatchPlayer->nDisconnectedSeconds; |
|
pMatchPlayer->nDisconnectedSeconds = Max( 0, (int)dForgiven ); |
|
|
|
MMLog("Client %s was connected for %u seconds, disconnect timer lowered from %i to %i\n", |
|
steamIDClient.Render(), timeSpentActive, nOldVal, pMatchPlayer->nDisconnectedSeconds ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFGCServerSystem::PreClientUpdate( ) |
|
{ |
|
BaseClass::PreClientUpdate(); |
|
|
|
CRTime::UpdateRealTime(); |
|
|
|
if ( GCClientSystem()->BConnectedtoGC() ) |
|
{ |
|
m_timeLastConnectedToGC = Plat_FloatTime(); |
|
} |
|
|
|
// We want a pause so players can read what the next map is. Once we've waited |
|
// long enough, we're doing a map change regardless of if the GC got back to us |
|
// with a new match ID. |
|
if ( Plat_FloatTime() > m_flWaitingForNewMatchTime |
|
&& m_flWaitingForNewMatchTime != 0.f ) |
|
{ |
|
LaunchNewMatchForLobby(); |
|
} |
|
|
|
// |
|
// Check for updating the caches that we're listening to |
|
// |
|
CSteamID const *pSteamID = engine->GetGameServerSteamID(); |
|
if ( pSteamID && m_ourSteamID != *pSteamID ) |
|
{ |
|
Assert( pSteamID->BGameServerAccount() ); |
|
|
|
// If we were previously listening to somebody else, stop listening. This |
|
// means we were connected, then reconnected and got a different Steam ID, |
|
// and is weird, but possible |
|
if ( m_ourSteamID.IsValid() ) |
|
{ |
|
MMLog( "CTFGCServerSystem - removing listener to old Steam ID %s\n", m_ourSteamID.Render() ); |
|
GCClientSystem()->GetGCClient()->RemoveSOCacheListener( m_ourSteamID, this ); |
|
} |
|
|
|
// Remember our new Steam ID |
|
m_ourSteamID = *pSteamID; |
|
|
|
// And start listening |
|
GCClientSystem()->GetGCClient()->AddSOCacheListener( m_ourSteamID, this ); |
|
} |
|
|
|
MatchPlayerAbandonThink(); |
|
UpdateConnectedPlayersAndServerInfo( CMsgGameServerMatchmakingStatus_Event_None, false ); |
|
|
|
// Check if the game is empty, and we need to shut down our lobby |
|
|
|
CTFGSLobby *pLobby = GetLobby(); |
|
if ( pLobby ) |
|
{ |
|
switch ( pLobby->GetState() ) |
|
{ |
|
case CSOTFGameServerLobby_State_SERVERSETUP: |
|
// We could most definitely be empty here, waiting for players to join! |
|
// Don't kill the server just yet |
|
break; |
|
|
|
case CSOTFGameServerLobby_State_RUN: |
|
break; |
|
|
|
default: |
|
case CSOTFGameServerLobby_State_UNKNOWN: |
|
MMLog( "Lobby in invalid state %d\n", (int)pLobby->GetState() ); |
|
break; |
|
} |
|
} |
|
|
|
// Check for slamming visiblemaxplayers |
|
static ConVarRef sv_visiblemaxplayers( "sv_visiblemaxplayers" ); |
|
if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) |
|
{ |
|
// Abort the server if they don't have enough maxplayers |
|
if ( gpGlobals->maxClients < 32 ) |
|
{ |
|
if( !g_bWarnedAboutMaxplayersInMVM ) |
|
{ |
|
// Prevent this warning from endlessly spamming the console... |
|
g_bWarnedAboutMaxplayersInMVM = true; |
|
Warning( "You must set maxplayers to 32 to host Mann vs. Machine\n" ); |
|
} |
|
|
|
if ( engine->IsDedicatedServer() ) |
|
{ |
|
engine->ServerCommand( "exit\n" ); |
|
} |
|
return; |
|
} |
|
|
|
// This changes what the server browser displays |
|
// update sv_visiblemaxplayers for MvM, count only non-bot spectators |
|
CUtlVector<CTFPlayer *> spectatorVector; |
|
CollectPlayers( &spectatorVector, TEAM_SPECTATOR ); |
|
int spectatorCount = 0; |
|
FOR_EACH_VEC ( spectatorVector, iIndex ) |
|
{ |
|
if ( !spectatorVector[iIndex]->IsBot() && !spectatorVector[iIndex]->IsReplay() && !spectatorVector[iIndex]->IsHLTV() ) |
|
{ |
|
spectatorCount++; |
|
} |
|
} |
|
|
|
int playerCount = kMVM_DefendersTeamSize + spectatorCount; |
|
if ( sv_visiblemaxplayers.GetInt() <= 0 || sv_visiblemaxplayers.GetInt() != playerCount ) |
|
{ |
|
MMLog( "Setting sv_visiblemaxplayers to %d for MvM\n", playerCount ); |
|
|
|
// save off visible players |
|
if ( !m_bOverridingVisibleMaxPlayers ) |
|
{ |
|
m_bOverridingVisibleMaxPlayers = true; |
|
m_iSavedVisibleMaxPlayers = sv_visiblemaxplayers.GetInt(); |
|
} |
|
|
|
sv_visiblemaxplayers.SetValue( playerCount ); |
|
} |
|
} |
|
else |
|
{ |
|
// Not in MvM. Check for restoring sv_visiblemaxplayers |
|
if ( m_bOverridingVisibleMaxPlayers ) |
|
{ |
|
MMLog( "Restoring sv_visiblemaxplayers to %d\n", m_iSavedVisibleMaxPlayers ); |
|
sv_visiblemaxplayers.SetValue( m_iSavedVisibleMaxPlayers ); |
|
m_bOverridingVisibleMaxPlayers = false; |
|
m_iSavedVisibleMaxPlayers = -1; |
|
} |
|
} |
|
|
|
// You may not be in matchmaking if you have a password! |
|
static ConVarRef sv_password( "sv_password" ); |
|
if ( tf_mm_servermode.GetInt() != 0 && *sv_password.GetString() != '\0' ) |
|
{ |
|
Warning( "Setting tf_mm_servermode=0 due to sv_password\n" ); |
|
tf_mm_servermode.SetValue( 0 ); |
|
} |
|
|
|
// TFGameRules()->SetStableMode( IsStableMode() ); |
|
// |
|
// if ( HLTVDirector() && HLTVDirector()->GetHLTVServer() ) |
|
// { |
|
// gcGameTime = Max( 0.0f, TFGameRules()->GetDOTATime() - HLTVDirector()->GetDelay() ); |
|
// } |
|
// else |
|
// { |
|
// gcGameTime = TFGameRules()->GetDOTATime(); |
|
// } |
|
|
|
// // Slam server region to 255 while in PVE mode |
|
// static ConVarRef sv_region( "sv_region" ); |
|
// if ( sv_region.GetInt() != 255 ) |
|
// { |
|
// MMLog( "Setting 'sv_region 255 ' due to tf_mm_servermode\n" ); |
|
// sv_region.SetValue( 255 ); |
|
// } |
|
} |
|
|
|
void CTFGCServerSystem::MatchPlayerAbandonThink() |
|
{ |
|
CMatchInfo *pMatchInfo = GetMatch(); |
|
if ( !pMatchInfo || pMatchInfo->m_bMatchEnded ) |
|
{ return; } |
|
|
|
int nAbandonSeconds = tf_mm_player_disconnect_time_before_abandon.GetInt(); |
|
// Disabled |
|
if ( nAbandonSeconds < 0 ) |
|
{ return; } |
|
|
|
int nPlayers = pMatchInfo->GetNumTotalMatchPlayers(); |
|
bool bDroppedPlayers = false; |
|
for ( int idx = 0; idx < nPlayers; idx++ ) |
|
{ |
|
CMatchInfo::PlayerMatchData_t *pPlayer = pMatchInfo->GetMatchDataForPlayer( idx ); |
|
|
|
// The engine doesn't really tell the game of connected-but-not-active players dropping. Keep an eye on their |
|
// entity being quietly cleaned up and note the disconnect. |
|
if ( pPlayer->nConnectingButNotActiveIndex ) |
|
{ |
|
const CSteamID *pIndexSteamID = engine->GetClientSteamIDByPlayerIndex( pPlayer->nConnectingButNotActiveIndex ); |
|
if ( !pIndexSteamID || *pIndexSteamID != pPlayer->steamID ) |
|
{ |
|
MMLog( "Match player %s dropped before going active\n", pPlayer->steamID.Render() ); |
|
ClientDisconnected( pPlayer->steamID ); |
|
} |
|
} |
|
|
|
if ( !pPlayer->bConnected && !pPlayer->bDropped ) |
|
{ |
|
// nDisconnectedSeconds is accumulated from previous absences, but doesn't include the current disconnect. |
|
int nTimeGone = CRTime::RTime32TimeCur() - pPlayer->rtLastActiveEvent + pPlayer->nDisconnectedSeconds; |
|
if ( nTimeGone > nAbandonSeconds ) |
|
{ |
|
MMLog( "Match player %s has been absent for a combined total of %u seconds, dropping from match\n", |
|
pPlayer->steamID.Render(), nTimeGone ); |
|
SetMatchPlayerDropped( pPlayer->steamID, pPlayer->bEverConnected ? TFMatchLeaveReason_AWOL : TFMatchLeaveReason_NO_SHOW ); |
|
bDroppedPlayers = true; |
|
} |
|
} |
|
} |
|
if ( bDroppedPlayers ) |
|
{ UpdateServerDetails(); } |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
bool CTFGCServerSystem::EjectMatchPlayer( CSteamID steamID, TFMatchLeaveReason eReason ) |
|
{ |
|
CMatchInfo *pMatch = GetLiveMatch(); |
|
CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch ? pMatch->GetMatchDataForPlayer( steamID ) : NULL; |
|
if ( !pMatchPlayer || pMatchPlayer->bDropped ) |
|
{ return false; } |
|
|
|
SetMatchPlayerDropped( steamID, eReason ); |
|
KickRemovedMatchPlayer( steamID ); |
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFGCServerSystem::MatchPlayerVoteKicked( CSteamID steamID ) |
|
{ |
|
bool bEjected = EjectMatchPlayer( steamID, TFMatchLeaveReason_VOTE_KICK ); |
|
if ( bEjected ) |
|
{ |
|
// Was part of our match, handled. |
|
MMLog( "Player %s vote-kicked from live match\n", steamID.Render() ); |
|
return; |
|
} |
|
|
|
// Not part of our match, check if they used to be |
|
CMatchInfo *pMatch = GetLiveMatch(); |
|
if ( !pMatch ) |
|
return; |
|
|
|
CMatchInfo::PlayerMatchData_t *pPlayer = pMatch->GetMatchDataForPlayer( steamID ); |
|
if ( !pPlayer || ( pPlayer && !pPlayer->bDropped ) ) |
|
{ |
|
AssertMsg( !pPlayer || pPlayer->bDropped, |
|
"Player is still part of our match, so EjectMatchPlayer should have succeeded" ); |
|
return; |
|
} |
|
|
|
// Previously in this match, but left before kick arrived. Send this message made just for that occasion, update our |
|
// record to reflect the reason. |
|
MMLog( "Player %s vote-kicked after departing match\n", steamID.Render() ); |
|
pPlayer->eDropReason = TFMatchLeaveReason_VOTE_KICK; |
|
ReliableMsgPlayerVoteKickedAfterLeavingMatch *pReliable = new ReliableMsgPlayerVoteKickedAfterLeavingMatch(); |
|
auto &msg = pReliable->Msg().Body(); |
|
|
|
msg.set_steam_id( steamID.ConvertToUint64() ); |
|
msg.set_lobby_id( pMatch->m_nLobbyID ); |
|
msg.set_match_id( pMatch->m_nMatchID ); |
|
|
|
pReliable->Enqueue(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
bool CTFGCServerSystem::KickRemovedMatchPlayer( CSteamID steamIDClient ) |
|
{ |
|
CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerBySteamID( steamIDClient ) ); |
|
if ( !pPlayer ) |
|
{ return false; } |
|
|
|
MMLog( "Kicking ejected player %s\n", steamIDClient.Render() ); |
|
engine->ServerCommand( UTIL_VarArgs( "kickid %d %s\n", pPlayer->GetUserID(), "#TF_MM_Generic_Kicked" ) ); |
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
bool CTFGCServerSystem::CanChangeMatchPlayerTeams() |
|
{ |
|
// Warning: LaunchNewMatchForLobby is counting on being able to do this, so avoid the temptation to forbid this |
|
// during match-result phase or similar (this is only for is-our-state-consistent-to-allow-this, not |
|
// should-gamerules-be-doing-this, that's on them) |
|
CMatchInfo *pMatch = GetMatch(); |
|
const IMatchGroupDescription* pMatchDesc = pMatch ? GetMatchGroupDescription( pMatch->m_eMatchGroup ) : NULL; |
|
|
|
if ( !pMatch || !pMatchDesc || pMatch->BMatchTerminated() || !pMatchDesc->BCanServerChangeMatchPlayerTeams() ) |
|
{ return false; } |
|
|
|
// If we're waiting to launch a new match, the team change would be for the new match that the GC is about to send |
|
// down, which has new teams. We probably are not intending that since we have no idea what this player's current |
|
// team is. |
|
// |
|
// (See the Team Assignments comment at the start of this file for ordering regarding new matches and team changes.) |
|
if ( BPendingNewMatch() ) |
|
{ return false; } |
|
|
|
return true; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// ChangeMatchPlayerTeams handling |
|
//----------------------------------------------------------------------------- |
|
void CTFGCServerSystem::ChangeMatchPlayerTeam( CSteamID steamID, TF_GC_TEAM eTeam ) |
|
{ |
|
// Helper for single member. |
|
CUtlVectorFixed< PlayerTeamPair_t, 1 > vec; |
|
vec.AddToTail( { steamID, eTeam } ); |
|
ChangeMatchPlayerTeams( vec ); |
|
} |
|
|
|
template< typename ANY_ALLOCATOR > |
|
void CTFGCServerSystem::ChangeMatchPlayerTeams( const CUtlVector< PlayerTeamPair_t, ANY_ALLOCATOR > &vecNewTeams ) |
|
{ |
|
if ( !CanChangeMatchPlayerTeams() ) |
|
{ |
|
// Some match logic is badly out of sync if it thinks it can do this. |
|
MMLog( "!! Game server is attempting to change player teams in an invalidate state\n" ); |
|
AbortInvalidMatchState(); |
|
return; |
|
} |
|
|
|
// Job takes ownership of message |
|
MMLog( "Sending team assignment request to GC:\n" ); |
|
|
|
ReliableMsgChangeMatchPlayerTeams *pReliable = new ReliableMsgChangeMatchPlayerTeams(); |
|
|
|
auto &msg = pReliable->Msg().Body(); |
|
msg.set_match_id( GetMatch()->m_nMatchID ); |
|
msg.set_lobby_id( GetMatch()->m_nLobbyID ); |
|
FOR_EACH_VEC( vecNewTeams, idx ) |
|
{ |
|
const CSteamID &steamID = vecNewTeams[idx].steamID; |
|
const TF_GC_TEAM &eTeam = vecNewTeams[idx].eTeam; |
|
|
|
// Do we know about this guy? |
|
CMatchInfo::PlayerMatchData_t *pPlayer = m_pMatchInfo->GetMatchDataForPlayer( steamID ); |
|
if ( !pPlayer || pPlayer->bDropped ) |
|
{ |
|
MMLog("!! Got team change request for player not in match %s\n", steamID.Render() ); |
|
continue; |
|
} |
|
|
|
MMLog(" %37s -> %d\n", steamID.Render(), eTeam ); |
|
auto *member = msg.add_member(); |
|
member->set_member_id( steamID.ConvertToUint64() ); |
|
member->set_new_team( eTeam ); |
|
|
|
// Reflect change locally immediately, this message should not fail |
|
pPlayer->eGCTeam = eTeam; |
|
} |
|
|
|
pReliable->Enqueue(); |
|
} |
|
|
|
void CTFGCServerSystem::ChangeMatchPlayerTeamsResponse( bool bSuccess ) |
|
{ |
|
if ( !bSuccess && GetLobby() ) |
|
{ |
|
// If the lobby went away prior to the GC responding, it is out of sync and can't do anything meaningful with |
|
// these updates right now, but we still have authority to finish the match and send a result, so just keep |
|
// plugging along. But if we still HAVE the lobby, and the GC said no, something is badly out of sync with this |
|
// match. |
|
MMLog( "!! ChangeMatchPlayerTeams rejected, something is confused\n" ); |
|
AbortInvalidMatchState(); |
|
return; |
|
} |
|
MMLog( "ChangeMatchPlayerTeams acknowledged\n" ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
const MapDef_t* CTFGCServerSystem::GetNextMapVoteByIndex( int nIndex ) const |
|
{ |
|
const CTFGSLobby *pLobby = GetLobby(); |
|
if ( pLobby && nIndex < pLobby->Obj().next_maps_for_vote_size() ) |
|
{ |
|
return GetItemSchema()->GetMasterMapDefByIndex( pLobby->Obj().next_maps_for_vote( nIndex ) ); |
|
} |
|
|
|
Assert( false ); |
|
return GetItemSchema()->GetMasterMapDefByName( "ctf_2fort" ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: GC Msg to request starting a new match for an existing lobby |
|
//----------------------------------------------------------------------------- |
|
void CTFGCServerSystem::NewMatchForLobbyResponse( bool bSuccess ) |
|
{ |
|
// We should be expecting this |
|
if ( !m_bWaitingForNewMatchID ) |
|
{ |
|
MMLog( "!! Got a NewMatchForLobbyResponse when not expecting it\n" ); |
|
AbortInvalidMatchState(); |
|
} |
|
|
|
Assert( TFGameRules() ); |
|
|
|
MMLog( "NewMatchID response recieved -- %s.\n", bSuccess ? "Success!" : "Failed!" ); |
|
|
|
m_bWaitingForNewMatchID = false; |
|
|
|
CMatchInfo *pMatch = GetMatch(); |
|
if ( pMatch && pMatch->m_bServerCreated ) |
|
{ |
|
// We went ahead without a match ID, the new ID should've already arrived in SOUpdated |
|
if ( bSuccess ) |
|
{ |
|
if ( !pMatch || pMatch->m_bServerCreated || !pMatch->m_nMatchID ) |
|
{ |
|
MMLog( "!! Got a NewMatchForLobby response but have not received a new match ID" ); |
|
AbortInvalidMatchState(); |
|
} |
|
} |
|
else |
|
{ |
|
// Failed, but we already have a running speculative match. It is essentially an unofficial match now. |
|
MMLog( "!! NewMatchForLobby responded negatively, this match will likely not be acknowledged by the system.\n" ); |
|
// TODO ROLLING MATCHES: Check that the jobs that will now send MatchID 0 do something salient |
|
} |
|
} |
|
else |
|
{ |
|
// Still waiting to actually kick off the new match. If the response was a failure, we can just abort. |
|
if ( !bSuccess ) |
|
{ |
|
MMLog( "!! NewMatchForLobby responded negatively. We haven't launched the match yet, so just shutting down.\n" ); |
|
if ( TFGameRules() ) |
|
{ |
|
TFGameRules()->KickPlayersNewMatchIDRequestFailed(); |
|
} |
|
else |
|
{ |
|
AbortInvalidMatchState(); |
|
} |
|
} |
|
} |
|
} |
|
|
|
bool CTFGCServerSystem::CanRequestNewMatchForLobby() |
|
{ |
|
// If this is a match that is not in sync with the GC, or it's not even a match, then no |
|
if ( !m_pMatchInfo || !GetLobby() || m_pMatchInfo->BMatchTerminated() ) |
|
{ return false; } |
|
|
|
// If we're waiting on other pending match magic, then no you can't stack them god help your soul. |
|
if ( m_pMatchInfo->m_bServerCreated || m_bWaitingForNewMatchID || m_flWaitingForNewMatchTime != 0.f ) |
|
{ return false; } |
|
|
|
// Match description allow it? |
|
const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( m_pMatchInfo->m_eMatchGroup ); |
|
if ( !pMatchDesc->BCanServerRequestNewMatchForLobby() ) |
|
{ return false; } |
|
|
|
return true; |
|
} |
|
|
|
void CTFGCServerSystem::RequestNewMatchForLobby( const MapDef_t* pNewMap ) |
|
{ |
|
// Wat r u doin |
|
if ( !CanRequestNewMatchForLobby() ) |
|
{ |
|
AbortInvalidMatchState(); |
|
} |
|
|
|
m_flWaitingForNewMatchTime = Plat_FloatTime() + tf_mm_next_map_result_hold_time.GetFloat(); |
|
m_bWaitingForNewMatchID = true; |
|
m_pMatchInfo->m_strMapName = pNewMap->pszMapName; |
|
|
|
ReliableMsgNewMatchForLobby *pReliable = new ReliableMsgNewMatchForLobby(); |
|
auto &msg = pReliable->Msg().Body(); |
|
|
|
msg.set_next_map_id( pNewMap->m_nDefIndex ); |
|
msg.set_lobby_id( GetLobby()->GetGroupID() ); |
|
msg.set_current_match_id( GetMatch()->m_nMatchID ); |
|
MMLog( "Sending request to GC for a new match ID.\n" ); |
|
|
|
pReliable->Enqueue(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CTFGCServerSystem::SetMatchPlayerDropped( CSteamID steamID, TFMatchLeaveReason eReason ) |
|
{ |
|
CMatchInfo *pMatch = GetMatch(); |
|
CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch ? pMatch->GetMatchDataForPlayer( steamID ) : NULL; |
|
Assert( pMatchPlayer ); |
|
if ( !pMatchPlayer ) |
|
{ return; } |
|
|
|
Assert( !pMatchPlayer->bDropped ); |
|
|
|
// Determine if this was an abandon |
|
bool bAbandon = true; |
|
switch ( eReason ) |
|
{ |
|
case TFMatchLeaveReason_VOTE_KICK: |
|
// Vote kicks don't penalize you currently. We need to revisit how these tie in with e.g. abuse reports/etc.. |
|
bAbandon = false; |
|
break; |
|
case TFMatchLeaveReason_NO_SHOW: |
|
case TFMatchLeaveReason_GC_REMOVED: |
|
// For right now, until we have more confidence in our network connectivity and possibly have SDR hooked up, |
|
// we'll give no shows the benefit of the doubt if they never made it to connect. ( If they can't connect an |
|
// give up and click abandon on their end, it will show up as GC_REMOVED ) |
|
bAbandon = pMatchPlayer->bEverConnected; |
|
break; |
|
case TFMatchLeaveReason_ADMIN_KICK: |
|
case TFMatchLeaveReason_AWOL: |
|
case TFMatchLeaveReason_IDLE: |
|
break; |
|
default: AssertMsg( false, "Unhandled TFMatchLeaveReason" ); |
|
} |
|
|
|
bAbandon = bAbandon && !pMatch->BPlayerSafeToLeaveMatch( steamID ); |
|
|
|
/// TODO ROLLING MATCHES: Technically if this happens with a rolling match in queue, we'll drop them from the old |
|
/// match without record of them in the new? |
|
pMatch->DropPlayer( steamID, eReason, bAbandon ); |
|
SendPlayerLeftMatch( steamID, eReason, bAbandon ); |
|
} |
|
|
|
void CTFGCServerSystem::UpdateServerDetails(void) |
|
{ |
|
UpdateConnectedPlayersAndServerInfo( CMsgGameServerMatchmakingStatus_Event_None, false ); |
|
} |
|
|
|
bool CTFGCServerSystem::ShouldHibernate() |
|
{ |
|
// We only hibernate if we're just sitting there with a freshly loaded map |
|
return engine->IsDedicatedServer() && tf_allow_server_hibernation.GetBool() && !GetLobby() && !BPendingReliableMessages() && !m_pMatchInfo; |
|
} |
|
|
|
void CTFGCServerSystem::FireGameEvent( IGameEvent *event ) |
|
{ |
|
// Disconnected from gameserver |
|
if ( !Q_stricmp( event->GetName(), "player_disconnect" ) ) |
|
{ |
|
const char * pszReason = event->GetString( "reason", "" ); |
|
if ( Q_strstr( pszReason, "kick" ) || Q_strstr( pszReason, "Kick" ) || Q_strstr( pszReason, g_pszVoteKickString ) ) |
|
{ |
|
CBasePlayer *pPlayer = UTIL_PlayerByUserId( event->GetInt( "userid", 0 ) ); |
|
if ( !pPlayer ) |
|
return; |
|
|
|
CSteamID steamId; |
|
if ( !pPlayer->GetSteamID( &steamId ) ) |
|
return; |
|
|
|
// Only care if this is a member of a live match |
|
CMatchInfo *pMatch = GetMatch(); |
|
CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch ? pMatch->GetMatchDataForPlayer( steamId ) : NULL; |
|
if ( !pMatch || !pMatchPlayer || pMatch->m_bMatchEnded || pMatchPlayer->bDropped ) |
|
{ return; } |
|
|
|
TFMatchLeaveReason eReason = TFMatchLeaveReason_ADMIN_KICK; |
|
|
|
if ( Q_strstr( pszReason, g_pszIdleKickString ) ) |
|
{ |
|
eReason = TFMatchLeaveReason_IDLE; |
|
} |
|
// kickid %d You have been voted off; |
|
// Vote kicks should not trigger abandon |
|
else if ( Q_strstr( pszReason, g_pszVoteKickString ) ) |
|
{ |
|
eReason = TFMatchLeaveReason_VOTE_KICK; |
|
} |
|
|
|
SetMatchPlayerDropped( steamId, eReason ); |
|
UpdateServerDetails(); |
|
} |
|
} |
|
else if ( FStrEq( event->GetName(), "player_score_changed" ) ) |
|
{ |
|
CMatchInfo *pMatch = GetMatch(); |
|
if ( !pMatch ) |
|
return; |
|
|
|
CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( event->GetInt( "player" ) ) ); |
|
if ( !pPlayer ) |
|
return; |
|
|
|
CSteamID steamId; |
|
if ( !pPlayer->GetSteamID( &steamId ) ) |
|
return; |
|
|
|
const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( pMatch->m_eMatchGroup ); |
|
if ( !pMatchDesc || !pMatchDesc->m_pProgressionDesc ) |
|
return; |
|
|
|
// Add to this player's score XP |
|
pMatch->GiveXPRewardToPlayerForAction( steamId, CMsgTFXPSource_XPSourceType_SOURCE_SCORE, event->GetInt( "delta", 0 ) ); |
|
} |
|
} |
|
|
|
CTFParty* CTFGCServerSystem::GetPartyForPlayer( CSteamID steamID ) const |
|
{ |
|
// Dig up this guy's party |
|
CGCClientSharedObjectCache* pSOCache = const_cast< CTFGCServerSystem* >( this )->GetSOCache( steamID ); |
|
if ( !pSOCache ) |
|
{ |
|
return NULL; |
|
} |
|
|
|
CSharedObjectTypeCache* pPartyTypeCache = pSOCache->FindTypeCache( CTFParty::k_nTypeID ); |
|
if ( !pPartyTypeCache || pPartyTypeCache->GetCount() == 0 ) |
|
{ |
|
return NULL; |
|
} |
|
|
|
return assert_cast< CTFParty* >( pPartyTypeCache->GetObject( 0 ) ); |
|
} |
|
|
|
const CMatchInfo::PlayerMatchData_t *CTFGCServerSystem::GetLiveMatchPlayer( CSteamID steamID ) const |
|
{ |
|
return const_cast<CTFGCServerSystem*>(this)->GetLiveMatchPlayer( steamID ); |
|
} |
|
|
|
CMatchInfo::PlayerMatchData_t *CTFGCServerSystem::GetLiveMatchPlayer( CSteamID steamID ) |
|
{ |
|
CMatchInfo *pMatch = GetMatch(); |
|
if ( !pMatch || pMatch->m_bMatchEnded ) |
|
{ return NULL; } |
|
|
|
CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch->GetMatchDataForPlayer( steamID ); |
|
if ( !pMatchPlayer || pMatchPlayer->bDropped ) |
|
{ return NULL; } |
|
|
|
return pMatchPlayer; |
|
} |
|
|
|
void CTFGCServerSystem::SOCreated( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent ) |
|
{ |
|
// Msg( "CTFGCServerSystem::SOCreated type = %d owner = %s\n", pObject->GetTypeID(), steamIDOwner.Render() ); |
|
|
|
// Lobby handling |
|
if ( pObject->GetTypeID() == CTFGSLobby::k_nTypeID ) |
|
{ |
|
const CTFGSLobby *pConstLobby = static_cast<const CTFGSLobby*>( pObject ); |
|
CTFGSLobby *pLobby = const_cast<CTFGSLobby *>( pConstLobby ); // GROSS |
|
Assert( pLobby == GetLobby() ); // There can be only be one... |
|
|
|
MMLog( "Lobby %016llx instanced on this server in state %s\n", |
|
pLobby->GetGroupID(), CSOTFGameServerLobby_State_Name( pLobby->GetState() ).c_str() ); |
|
|
|
// Check if we need to switch the map or load a pop file. |
|
CMsgGameServerMatchmakingStatus_Event statusEvent = CMsgGameServerMatchmakingStatus_Event_None; |
|
bool bNewLobby = ( pLobby->GetState() == CSOTFGameServerLobby_State_SERVERSETUP ); |
|
|
|
if ( m_bMMServerMode && bNewLobby ) |
|
{ |
|
MMLog( " Map: '%s'\n", pLobby->GetMapName() ); |
|
MMLog( " Mission: '%s'\n", pLobby->GetMissionName() ); |
|
|
|
EMatchGroup eMatchGroup = (EMatchGroup)pLobby->Obj().match_group(); |
|
|
|
// Acknowledge the players that just connected. (This will create |
|
// reservations for the players and let the GC we are expecting the |
|
// players.) |
|
statusEvent = CMsgGameServerMatchmakingStatus_Event_AcknowledgePlayers; |
|
|
|
// Create a record of the match on first connect. |
|
if ( m_pMatchInfo ) |
|
{ |
|
MMLog( "!! Received new anticipated lobby while running existing match. " |
|
"Old match ID [ %llu ] ended [ %u ] " |
|
"New matchID [ %llu ]\n", |
|
m_pMatchInfo->m_nMatchID, m_pMatchInfo->m_bMatchEnded, |
|
pLobby->GetMatchID() ); |
|
Assert( false ); |
|
|
|
delete m_pMatchInfo; |
|
|
|
// In theory the overwritten match will now be forgotten by us, all errant players kicked by the |
|
// UpdateConnectedPlayers tick... |
|
} |
|
|
|
m_pMatchInfo = new CMatchInfo( pLobby ); |
|
GTFGCClientSystem()->DumpLobby(); |
|
|
|
if ( eMatchGroup == EMatchGroup::k_nMatchGroup_Invalid || |
|
!GetMatchGroupDescription( eMatchGroup )->InitServerSettingsForMatch( pConstLobby ) ) |
|
{ |
|
AbortInvalidMatchState(); |
|
} |
|
|
|
// FIXME We should have some version checking like this. |
|
// int engineServerVersion = engine->GetServerVersion(); |
|
// |
|
// // Version checking is enforced if both sides do not report zero as their version |
|
// if ( engineServerVersion && g_gcServerVersion && engineServerVersion != g_gcServerVersion ) |
|
// { |
|
// // If we're out of date exit |
|
// Msg("Version out of date (GC wants %d, we are %d), terminating!\n", g_gcServerVersion, engine->GetServerVersion() ); |
|
// engine->ServerCommand( "quit\n" ); |
|
// } |
|
} |
|
else |
|
{ |
|
// We could've just gotten re-sent this lobby, is it the match we think we're running? If we are running a |
|
// match for a different lobby, something is super wrong |
|
uint64 nExistingMatchID = m_pMatchInfo ? m_pMatchInfo->m_nMatchID : 0; |
|
uint64 nLobbyMatchID = pLobby->Obj().has_match_id() ? pLobby->GetMatchID() : 0; |
|
if ( m_pMatchInfo && nExistingMatchID == nLobbyMatchID ) |
|
{ |
|
MMLog( "GC refreshed lobby for match ID [ %llu ]\n", m_pMatchInfo->m_nMatchID ); |
|
} |
|
else |
|
{ |
|
MMLog( "!! Got assigned a lobby not in server-setup state, or when not accepting lobbies. Rejecting.\n" |
|
"Lobby matchID [ %llu ], existing match [ %llu ]\n", |
|
pLobby->GetMatchID(), m_pMatchInfo ? m_pMatchInfo->m_nMatchID : 0ull ); |
|
|
|
if ( !m_pMatchInfo ) |
|
{ |
|
// Not running a match, don't want this one, just reject the lobby. |
|
// |
|
// This can happen when we crash and are handed a stale lobby upon reboot, rejecting it will |
|
// terminate that match. |
|
SendRejectLobby(); |
|
} |
|
else |
|
{ |
|
// Otherwise, we thought we had a lobby, but the GC sent us a different match? No idea what is going |
|
// on, probably some bad de-sync happened. |
|
// |
|
// No faith we can continue and send authoritative match results about anything. |
|
AbortInvalidMatchState(); |
|
} |
|
} |
|
|
|
} |
|
|
|
UpdateConnectedPlayersAndServerInfo( statusEvent, false ); |
|
} |
|
} |
|
|
|
void CTFGCServerSystem::SOUpdated( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent ) |
|
{ |
|
// Don't care if we're not running a match |
|
CMatchInfo *pMatch = GetMatch(); |
|
if ( !pMatch ) |
|
return; |
|
|
|
// Lobby handling |
|
if ( pObject->GetTypeID() == CTFGSLobby::k_nTypeID ) |
|
{ |
|
const CTFGSLobby *pConstLobby = static_cast<const CTFGSLobby*>( pObject ); |
|
CTFGSLobby *pLobby = const_cast<CTFGSLobby *>( pConstLobby ); // GROSS |
|
Assert( pLobby == GetLobby() ); // There can be only be one... |
|
|
|
bool bNeedsToUpdatePlayerAndServer = false; |
|
// Check if we have new reservations not part of the match |
|
for ( int i = 0; i < pLobby->GetNumMembers(); i++ ) |
|
{ |
|
const CTFLobbyMember *pMemberDetails = pLobby->GetMemberDetails( i ); |
|
Assert( pMemberDetails ); |
|
if ( !pMemberDetails ) |
|
continue; |
|
|
|
CSteamID steamID( pMemberDetails->id() ); |
|
CTFLobbyMember_ConnectState eLobbyState = pLobby->GetMemberConnectState( i ); |
|
|
|
if ( eLobbyState == CTFLobbyMember_ConnectState_RESERVATION_PENDING ) |
|
{ |
|
CMatchInfo::PlayerMatchData_t *pPlayer = pMatch->GetMatchDataForPlayer( pLobby->GetMember( i ) ); |
|
if ( !pPlayer || pPlayer->bDropped ) |
|
{ |
|
// Lobby has a new player we don't think is in our match, force an update to acknowledge them ASAP |
|
bNeedsToUpdatePlayerAndServer = true; |
|
} |
|
} |
|
} |
|
|
|
if ( bNeedsToUpdatePlayerAndServer ) |
|
{ |
|
UpdateConnectedPlayersAndServerInfo( CMsgGameServerMatchmakingStatus_Event_AcknowledgePlayers, true ); |
|
} |
|
|
|
// If we terminated while the new match ID was pending we're still unwinding the incoming messages |
|
bool bNewMatchID = m_pMatchInfo && !m_pMatchInfo->BMatchTerminated() && ( m_pMatchInfo->m_nMatchID != pLobby->GetMatchID() ); |
|
if ( bNewMatchID ) |
|
{ |
|
if ( m_bWaitingForNewMatchID && m_pMatchInfo->m_bServerCreated ) |
|
{ |
|
// We sent a request for a new matchID to put in for the match |
|
// we're running, and it just came back. |
|
MMLog( "Received new matchID for server-created match. " |
|
"New matchID [ %llu ]\n", |
|
pLobby->GetMatchID() ); |
|
m_pMatchInfo->m_nMatchID = pLobby->GetMatchID(); |
|
m_pMatchInfo->m_bServerCreated = false; |
|
} |
|
else if ( m_bWaitingForNewMatchID && m_flWaitingForNewMatchTime != 0.f ) |
|
{ |
|
// We're counting down to launching a new match, and the new match ID arrived. We'll pick it up from the |
|
// lobby in LaunchNewMatchForLobby |
|
MMLog( "Received new matchID while waiting for new matchID. " |
|
"Old match ID [ %llu ] ended [ %u ] " |
|
"New matchID [ %llu ]\n", |
|
m_pMatchInfo->m_nMatchID, m_pMatchInfo->m_bMatchEnded, |
|
pLobby->GetMatchID() ); |
|
} |
|
else if ( !m_bWaitingForNewMatchID && m_flWaitingForNewMatchTime == 0.f ) |
|
{ |
|
// A lobby came in with a match ID that's not what our current |
|
// one is, and we were not expecting this. |
|
// |
|
// Note that we hold on to the stale lobby between NewMatchForLobby and LaunchNewMatchForLobby, so we |
|
// don't panic if the stale lobby updates. The only other way out of that state is terminating the |
|
// match. |
|
MMLog( "Received new matchID when we weren't expecting one! " |
|
"Current matchID [ %llu ] " |
|
"New matchID [ %llu ]\n", |
|
m_pMatchInfo->m_nMatchID, |
|
pLobby->GetMatchID() ); |
|
AbortInvalidMatchState(); |
|
} |
|
} |
|
} |
|
} |
|
|
|
void CTFGCServerSystem::SODestroyed( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent ) |
|
{ |
|
// Lobby handling |
|
if ( pObject->GetTypeID() == CTFGSLobby::k_nTypeID ) |
|
{ |
|
// Lobby is gone! Reset |
|
UpdateConnectedPlayersAndServerInfo( CMsgGameServerMatchmakingStatus_Event_None, true ); |
|
} |
|
} |
|
|
|
const CTFGSLobby *CTFGCServerSystem::GetLobby() const |
|
{ |
|
if ( !m_ourSteamID.IsValid() ) |
|
return NULL; |
|
|
|
GCSDK::CGCClientSharedObjectCache *pSOCache = GCClientSystem()->GetSOCache( m_ourSteamID ); |
|
if ( !pSOCache ) |
|
return NULL; |
|
|
|
CSharedObjectTypeCache *pTypeCache = pSOCache->FindBaseTypeCache( CTFGSLobby::k_nTypeID ); |
|
if ( pTypeCache && pTypeCache->GetCount() > 0 ) |
|
{ |
|
AssertMsg1( pTypeCache->GetCount() == 1, "Server has %d lobby objects in his cache! He should only have 1.", pTypeCache->GetCount() ); |
|
const CTFGSLobby *pLobby = static_cast<CTFGSLobby*>( pTypeCache->GetObject( pTypeCache->GetCount() - 1 ) ); |
|
return pLobby; |
|
} |
|
|
|
return NULL; |
|
} |
|
|
|
CTFGSLobby *CTFGCServerSystem::GetLobby() |
|
{ |
|
// It's safe to un-constify the returned lobby if we're being called through a non-const reference ourselves. |
|
return const_cast< CTFGSLobby * >( ((const CTFGCServerSystem *)this)->GetLobby() ); |
|
} |
|
|
|
void CTFGCServerSystem::DumpLobby() |
|
{ |
|
CTFGSLobby *pLobby = GetLobby(); |
|
if ( !pLobby ) |
|
{ |
|
Msg( "Failed to find lobby shared object\n" ); |
|
return; |
|
} |
|
|
|
pLobby->SpewDebug(); |
|
} |
|
|
|
bool CTFGCServerSystem::HasLobby() const |
|
{ |
|
return GetLobby() != NULL; |
|
} |
|
|
|
void CTFGCServerSystem::SetHibernation( bool bHibernating ) |
|
{ |
|
// !FIXME! Need to get rid of all the hibernation crap. We don't really need it |
|
} |
|
|
|
bool CTFGCServerSystem::ShouldHideServer() |
|
{ |
|
// !NO! Don't set this right now. We'll just pass the "hidden" tag and so the server |
|
// browser wil not list us. |
|
// if ( m_bMMServerMode && tf_mm_strict.GetBool() ) |
|
// return true; |
|
return false; |
|
} |
|
|
|
bool CTFGCServerSystem::SteamIDAllowedToConnect(const CSteamID &steamID) const |
|
{ |
|
// If we're not in strict mode, anybody can join! |
|
if ( !m_bMMServerMode || tf_mm_strict.GetInt() != 1 ) |
|
return true; |
|
|
|
// If we don't have a match, nobody can join |
|
const CMatchInfo *pMatchInfo = GetMatch(); |
|
if ( !pMatchInfo ) |
|
{ |
|
return false; |
|
} |
|
|
|
const CMatchInfo::PlayerMatchData_t *pMatchData = pMatchInfo->GetMatchDataForPlayer( steamID ); |
|
if ( !pMatchData || pMatchData->bDropped ) |
|
{ |
|
// Not in the match or was dropped, reject |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
////----------------------------------------------------------------------------- |
|
//int CTFGCServerSystem::GetTeamForLobbyMember( const CSteamID &steamId ) const |
|
//{ |
|
// const CTFGSLobby *pLobby = GetLobby(); |
|
// if ( !pLobby ) |
|
// { |
|
// return DOTA_TEAM_NOTEAM; |
|
// } |
|
// |
|
// int team = pLobby->GetMemberTeam( steamId ); |
|
// |
|
// switch ( team ) |
|
// { |
|
// case DOTA_GC_TEAM_GOOD_GUYS: |
|
// return DOTA_TEAM_GOODGUYS; |
|
// |
|
// case DOTA_GC_TEAM_BAD_GUYS: |
|
// return DOTA_TEAM_BADGUYS; |
|
// |
|
// case DOTA_GC_TEAM_BROADCASTER: |
|
// case DOTA_GC_TEAM_PLAYER_POOL: |
|
// case DOTA_GC_TEAM_SPECTATOR: |
|
// return TEAM_SPECTATOR; |
|
// } |
|
// |
|
// return DOTA_TEAM_NOTEAM; |
|
//} |
|
// |
|
////----------------------------------------------------------------------------- |
|
//bool CTFGCServerSystem::IsLobbyMemberBroadcaster( const CSteamID &steamId ) const |
|
//{ |
|
// const CTFGSLobby *pLobby = GetLobby(); |
|
// if ( !pLobby ) |
|
// { |
|
// return false; |
|
// } |
|
// |
|
// return pLobby->GetMemberTeam( steamId ) == DOTA_GC_TEAM_BROADCASTER; |
|
//} |
|
// |
|
////----------------------------------------------------------------------------- |
|
//ELanguage CTFGCServerSystem::GetBroadcasterLanguage( const CSteamID &steamId ) const |
|
//{ |
|
// const CTFGSLobby *pLobby = GetLobby(); |
|
// if ( !pLobby ) |
|
// { |
|
// return k_Lang_English; |
|
// } |
|
// |
|
// if ( pLobby->GetMemberTeam( steamId ) != DOTA_GC_TEAM_BROADCASTER ) |
|
// return k_Lang_English; |
|
// |
|
// int index = pLobby->GetMemberIndexBySteamID( steamId ); |
|
// if ( index < 0 ) |
|
// return k_Lang_English; |
|
// |
|
// const CTFLobbyMember* pMember = pLobby->GetMemberDetails( index ); |
|
// switch( pMember->slot() ) |
|
// { |
|
// default: |
|
// case 1: |
|
// return k_Lang_English; |
|
// case 2: |
|
// return k_Lang_German; |
|
// case 3: |
|
// return k_Lang_Simplified_Chinese; |
|
// case 4: |
|
// return k_Lang_Russian; |
|
// } |
|
// |
|
// return k_Lang_English; |
|
//} |
|
|
|
//----------------------------------------------------------------------------- |
|
CON_COMMAND( tf_server_lobby_debug, "Prints server lobby object" ) |
|
{ |
|
GTFGCClientSystem()->DumpLobby(); |
|
} |
|
|
|
ConVar dbg_spew_connected_players_level( "dbg_spew_connected_players_level", "0", FCVAR_NONE, "If enabled, server will spew connected player GC updates\n" ); |
|
|
|
// Inform the GC of any change in the connected players |
|
void CTFGCServerSystem::UpdateConnectedPlayersAndServerInfo( CMsgGameServerMatchmakingStatus_Event event, bool bForceSendMessages ) |
|
{ |
|
VPROF_BUDGET( "CTFGCServerSystem::UpdateConnectedPlayersAndServerInfo", VPROF_BUDGETGROUP_OTHER_NETWORKING ); |
|
|
|
// Don't bother sending if we aren't initialized yet |
|
if ( gpGlobals->maxClients == 0 || TFGameRules() == NULL ) |
|
return; |
|
|
|
/// TODO ROLLING MATCH: Remove event field from this message. We might just ignore some events, and they're not |
|
/// useful. |
|
|
|
// Don't send heartbeats while we're waiting for reliable messages to process, our state is not in sync with what we |
|
// tried to send to the GC, and sending a new heartbeat before pending messages have been responded to isn't |
|
// helpful. |
|
if ( BPendingReliableMessages() ) |
|
{ return; } |
|
|
|
// Or if we're in the waiting period to kick off a new match -- if all pending messages came back, our lobby now |
|
// reflects the requested match, but we haven't actually launched it yet, so heartbeats would not be valid. |
|
if ( m_flWaitingForNewMatchTime != 0.f ) |
|
{ return; } |
|
|
|
const CTFGSLobby *pLobby = GetLobby(); |
|
if ( !pLobby || !m_bMMServerMode ) |
|
{ |
|
Assert( event == CMsgGameServerMatchmakingStatus_Event_None ); |
|
} |
|
|
|
double now = Plat_FloatTime(); |
|
|
|
if ( dbg_spew_connected_players_level.GetInt() >= 4 ) { Msg( "UpdateConnectedPlayers ======================================\n" ); } |
|
|
|
static ConVarRef sv_visiblemaxplayers( "sv_visiblemaxplayers" ); |
|
|
|
CProtoBufMsg<CMsgGameServerMatchmakingStatus> msg( k_EMsgGCGameServerMatchmakingStatus ); |
|
ServerMatchmakingState eGameServerInfoState = ServerMatchmakingState_NOT_PARTICIPATING; |
|
TF_MatchmakingMode eGameServerInfoMatchmakingMode = TF_Matchmaking_INVALID; |
|
CUtlString sGameServerInfoMap; |
|
CUtlString sGameServerInfoTags; |
|
int nBotCountToSend = -1; |
|
float flSendInterval = 60.0f; |
|
int nUnconnectedPlayerReservationRequests = 0; |
|
bool bLobbyIncorrect = false; |
|
CUtlVector<CSteamID> vecFailedLoaders; |
|
TF_GC_GameState gcState = TF_GC_GAMESTATE_DISCONNECT; |
|
|
|
CMatchInfo *pMatch = GetMatch(); |
|
const IMatchGroupDescription* pMatchDesc = pMatch ? GetMatchGroupDescription( pMatch->m_eMatchGroup ) : NULL; |
|
|
|
// Build list of currently connected clients, and classify them according to their role |
|
struct Reservation_t |
|
{ |
|
CSteamID m_steamID; |
|
int m_nEntindex; |
|
bool m_bActive; |
|
}; |
|
CUtlVector< Reservation_t > vecReservationRequests; |
|
CUtlVector<CSteamID> vecConnectedPlayers; |
|
int nAdminSlots = 0; |
|
int nAdHocPlayers = 0; |
|
int nMatchPlayers = 0; |
|
int nBots = 0; |
|
for ( int i = 1; i <= gpGlobals->maxClients; i++ ) |
|
{ |
|
const CSteamID *pPlayerSteamID = engine->GetClientSteamIDByPlayerIndex( i ); |
|
|
|
// Filter out non-players |
|
player_info_t sPlayerInfo; |
|
bool bActive = false; |
|
if ( engine->GetPlayerInfo( i, &sPlayerInfo ) ) |
|
{ |
|
if ( sPlayerInfo.ishltv || sPlayerInfo.isreplay ) |
|
{ |
|
++nAdminSlots; |
|
continue; |
|
} |
|
if ( sPlayerInfo.fakeplayer ) |
|
{ |
|
++nBots; |
|
continue; |
|
} |
|
|
|
if ( pPlayerSteamID == NULL || !pPlayerSteamID->IsValid() ) |
|
{ |
|
// This can occur in lan-mode |
|
Warning( "Player with no steam ID, counting as ad-hoc\n" ); |
|
} |
|
|
|
bActive = true; |
|
} |
|
else |
|
{ |
|
// Client not "active", but might be connected. |
|
// this happens during changelevel |
|
if ( pPlayerSteamID == NULL || !pPlayerSteamID->IsValid() ) |
|
{ |
|
continue; |
|
} |
|
|
|
// Connected, but not active. |
|
bActive = false; |
|
|
|
// Shove in a dummy name or debug spew |
|
V_strcpy_safe( sPlayerInfo.name, pPlayerSteamID->Render() ); |
|
} |
|
|
|
// Some kind of player, add them to match players or ad-hoc |
|
CSteamID playerSteamID; |
|
if ( pPlayerSteamID && pPlayerSteamID->IsValid() ) |
|
playerSteamID = *pPlayerSteamID; |
|
|
|
CMatchInfo::PlayerMatchData_t *pMatchPlayer = ( pMatch && playerSteamID.IsValid() ) \ |
|
? pMatch->GetMatchDataForPlayer( playerSteamID ) \ |
|
: NULL; |
|
bool bMatchPlayer = pMatchPlayer && !pMatchPlayer->bDropped; |
|
if ( bMatchPlayer ) |
|
{ ++nMatchPlayers; } |
|
else |
|
{ ++nAdHocPlayers; } |
|
|
|
if ( dbg_spew_connected_players_level.GetInt() >= 4 ) { Msg( " Client[%d]: %s '%s':\n", i, playerSteamID.Render(), sPlayerInfo.name ); } |
|
|
|
// |
|
// !! In lan mode, this player may not have a steamID. They can't be a lobby member or similar, so the below |
|
// !! code should just assume they're ad-hoc if !playerSteamID.IsValid() |
|
// |
|
if ( playerSteamID.IsValid() ) |
|
vecConnectedPlayers.AddToTail( playerSteamID ); |
|
|
|
// If we don't have a lobby, then we may still be running a match after a GC crash/reboot, in which case the |
|
// lobby might've been lost -- but we're still expected to complete the match on our own authority and report |
|
// the result. |
|
|
|
/// XXX(JohnS): Ideally, in the state where the GC rebooted and the lobby disintegrated, we'd have some way |
|
/// to tell the GC to recreate the lobby on its end when we re-establish, rather than finishing |
|
/// out a phantom match -- it doesn't know the user is still in a match until the match result |
|
/// arrives. However, as we locally track and report the match result and any abandons, the user |
|
/// can't really exploit this state other than potentially alt-F4ing and requeuing faster than |
|
/// their abandon timeout. The GC, however, loses the ability to kick the player from this |
|
/// lobby. (that it no longer knows about) |
|
if ( pLobby ) |
|
{ |
|
|
|
// If he's in the lobby, them count him as a connected player. |
|
// Otherwise, he's an ad-hoc join. |
|
CMsgGameServerMatchmakingStatus_PlayerConnectState sendPlayerConnectState = CMsgGameServerMatchmakingStatus_PlayerConnectState_INVALID; |
|
const CTFLobbyMember *pMember = pLobby->GetMemberDetails( playerSteamID ); |
|
if ( pMember ) |
|
{ |
|
CTFLobbyMember_ConnectState eLobbyState = pMember->connect_state(); |
|
if ( dbg_spew_connected_players_level.GetInt() >= 4 ) |
|
{ |
|
Msg( " '%s' In lobby with state %s\n", sPlayerInfo.name, |
|
CTFLobbyMember_ConnectState_Name( eLobbyState ).c_str() ); |
|
} |
|
switch ( eLobbyState ) |
|
{ |
|
case CTFLobbyMember_ConnectState_RESERVATION_PENDING: |
|
// Check if we have match data for this guy |
|
if ( !bMatchPlayer ) |
|
{ |
|
bLobbyIncorrect = true; |
|
vecReservationRequests.AddToTail( { *pPlayerSteamID, i, bActive } ); |
|
} |
|
|
|
break; |
|
case CTFLobbyMember_ConnectState_RESERVED: |
|
|
|
// Only count them as actually "connected" if they are active. |
|
// We do not count them as "connected", to make sure we treat a |
|
// disconnection before they become "active" as a failure to load, |
|
// but a disconnection after they become active as a "leaver" |
|
if ( bActive ) |
|
{ |
|
sendPlayerConnectState = CMsgGameServerMatchmakingStatus_PlayerConnectState_CONNECTED; |
|
bLobbyIncorrect = true; |
|
} |
|
else |
|
{ |
|
sendPlayerConnectState = CMsgGameServerMatchmakingStatus_PlayerConnectState_RESERVED; |
|
if ( eLobbyState != CTFLobbyMember_ConnectState_RESERVED ) |
|
bLobbyIncorrect = true; |
|
} |
|
break; |
|
|
|
case CTFLobbyMember_ConnectState_CONNECTED: |
|
sendPlayerConnectState = CMsgGameServerMatchmakingStatus_PlayerConnectState_CONNECTED; |
|
|
|
break; |
|
case CTFLobbyMember_ConnectState_DISCONNECTED: |
|
sendPlayerConnectState = CMsgGameServerMatchmakingStatus_PlayerConnectState_CONNECTED; |
|
bLobbyIncorrect = true; |
|
break; |
|
default: |
|
AssertMsg1( false, "Unknown lobby member state %d", eLobbyState ); |
|
break; |
|
} |
|
} |
|
else if ( m_pMatchInfo && !m_pMatchInfo->m_bMatchEnded ) |
|
{ |
|
// Competitive match, player missing from lobby |
|
if ( bMatchPlayer ) |
|
{ |
|
// Player was part of the match, but GC removed them. |
|
MMLog( "Removing match player %s -- dropped from lobby, but still in match and game\n", |
|
playerSteamID.Render() ); |
|
EjectMatchPlayer( playerSteamID, TFMatchLeaveReason_GC_REMOVED ); |
|
nMatchPlayers--; |
|
} |
|
else if ( tf_mm_strict.GetInt() == 1 ) |
|
{ |
|
// A player is present that shouldn't be |
|
MMLog( "!! Unknown player in managed match %s\n", playerSteamID.Render() ); |
|
KickRemovedMatchPlayer( playerSteamID ); |
|
nAdHocPlayers--; |
|
} |
|
} |
|
else |
|
{ |
|
// Not a managed match |
|
if ( dbg_spew_connected_players_level.GetInt() >= 4 ) |
|
{ |
|
Msg( " '%s' Not in lobby, client is ad-hoc join\n", sPlayerInfo.name ); |
|
} |
|
} |
|
|
|
if ( sendPlayerConnectState != CMsgGameServerMatchmakingStatus_PlayerConnectState_INVALID ) |
|
{ |
|
CMsgGameServerMatchmakingStatus_Player *pMsgPlayer = msg.Body().add_players(); |
|
pMsgPlayer->set_steam_id( playerSteamID.ConvertToUint64() ); |
|
pMsgPlayer->set_connect_state( sendPlayerConnectState ); |
|
} |
|
} |
|
} // end For each client |
|
|
|
// |
|
// Now, check match for players that we are tracking but are not connected, and count them in the total and the |
|
// status message |
|
// |
|
if ( pMatch && !pMatch->BMatchTerminated() ) |
|
{ |
|
int nTotalMatch = pMatch->GetNumTotalMatchPlayers(); |
|
for ( int idx = 0; idx < nTotalMatch; idx++ ) |
|
{ |
|
CMatchInfo::PlayerMatchData_t *pPlayer = pMatch->GetMatchDataForPlayer( idx ); |
|
// Don't care if they are now dropped or were handled in the connected players loop above |
|
if ( pPlayer->bDropped || vecConnectedPlayers.Find( pPlayer->steamID ) != vecConnectedPlayers.InvalidIndex() ) |
|
{ continue; } |
|
|
|
if ( pPlayer->bConnected ) |
|
{ |
|
MMLog( "!! Match player %s not present but marked connected\n", pPlayer->steamID.Render() ); |
|
} |
|
|
|
// Note that if the GC lost our lobby (which should only occur due to system failure on the other end), we |
|
// just keep dutifully sending status updates for the players we have as long as we have a match |
|
if ( pLobby && !pLobby->GetMemberDetails( pPlayer->steamID ) ) |
|
{ |
|
// Player was part of the match, but GC removed them. |
|
MMLog( "Removing player %s, not present in match and dropped from lobby\n", |
|
pPlayer->steamID.Render() ); |
|
SetMatchPlayerDropped( pPlayer->steamID, TFMatchLeaveReason_GC_REMOVED ); |
|
} |
|
else |
|
{ |
|
// We are holding a valid reservation. Add this fact to the message, to confirm |
|
// that we are aware of the player. |
|
nMatchPlayers++; |
|
CMsgGameServerMatchmakingStatus_Player *pMsgPlayer = msg.Body().add_players(); |
|
pMsgPlayer->set_steam_id( pPlayer->steamID.ConvertToUint64() ); |
|
if ( dbg_spew_connected_players_level.GetInt() >= 4 ) |
|
{ Msg( " Player[%d]: %s reserved\n", msg.Body().players_size(), pPlayer->steamID.Render() ); } |
|
pMsgPlayer->set_connect_state( CMsgGameServerMatchmakingStatus_PlayerConnectState_RESERVED ); |
|
bLobbyIncorrect = true; |
|
} |
|
} |
|
} |
|
|
|
// |
|
// Scan lobby, and check for lobby player entries that don't match our local state. |
|
// |
|
if ( pLobby ) |
|
{ |
|
if ( dbg_spew_connected_players_level.GetInt() >= 4 ) |
|
{ Msg( "Checking all connected players are marked connected in lobby:\n" ); } |
|
|
|
for ( int i = 0; i < pLobby->GetNumMembers(); i++ ) |
|
{ |
|
const CTFLobbyMember *pMemberDetails = pLobby->GetMemberDetails( i ); |
|
Assert( pMemberDetails ); |
|
if ( !pMemberDetails ) |
|
continue; |
|
CSteamID steamID( pMemberDetails->id() ); |
|
|
|
CTFLobbyMember_ConnectState eLobbyState = pLobby->GetMemberConnectState( i ); |
|
if ( dbg_spew_connected_players_level.GetInt() >= 4 ) |
|
{ Msg( " Lobby member %s is in state %s\n", steamID.Render(), CTFLobbyMember_ConnectState_Name( eLobbyState ).c_str() ); } |
|
|
|
int iConnectedPlayer = vecConnectedPlayers.Find( steamID ); |
|
if ( iConnectedPlayer >= 0 ) |
|
{ continue; } // we handled them earlier |
|
|
|
// Player is not currently connected. Check against what the lobby thinks |
|
switch ( eLobbyState ) |
|
{ |
|
case CTFLobbyMember_ConnectState_RESERVATION_PENDING: |
|
{ |
|
// Check if we already have a reservation for this guy |
|
CMatchInfo::PlayerMatchData_t *pMatchPlayer = GetMatch() ? GetMatch()->GetMatchDataForPlayer( steamID ) : NULL; |
|
if ( GetMatch() && ( !pMatchPlayer || pMatchPlayer->bDropped ) ) |
|
{ |
|
bLobbyIncorrect = true; |
|
vecReservationRequests.AddToTail( { steamID, 0, false } ); |
|
++nUnconnectedPlayerReservationRequests; |
|
} |
|
else |
|
{ |
|
if ( dbg_spew_connected_players_level.GetInt() >= 4 ) |
|
{ Msg( " Player[%d]: %s requested reservation. We already had one.\n", msg.Body().players_size(), steamID.Render() ); } |
|
} |
|
} break; |
|
|
|
case CTFLobbyMember_ConnectState_RESERVED: |
|
// We'll handle it below when we process our reservations |
|
break; |
|
|
|
case CTFLobbyMember_ConnectState_CONNECTED: |
|
if ( dbg_spew_connected_players_level.GetInt() >= 4 ) |
|
{ Msg( " Lobby member %s no longer connected, lobby is incorrect\n", steamID.Render() ); } |
|
bLobbyIncorrect = true; |
|
break; |
|
case CTFLobbyMember_ConnectState_DISCONNECTED: |
|
break; |
|
default: |
|
AssertMsg1( false, "Unknown lobby member state %d", eLobbyState ); |
|
break; |
|
} |
|
} |
|
} |
|
|
|
// Now we've scanned connected players, our match, and the lobby object. Count up the total taken slots, and how |
|
// many slots the match could have (0 if no match) These are slots that are spoken for, not necessarily currently |
|
// connected |
|
// NOTE: These might be updated by accepting reservations or dropping players in the next section |
|
|
|
bool bLiveMatch = pMatch && pMatchDesc && !pMatch->m_bMatchEnded; |
|
// TODO ROLLING MATCHES: Need a check for no-latejoins state for after we've sent a match result? |
|
int nMaxMatchPlayers = bLiveMatch ? pMatch->GetCanonicalMatchSize() : 0; |
|
int nMaxHumans = gpGlobals->maxClients - nAdminSlots; |
|
{ |
|
// Maybe cap visible max humans. Honor the override MvM mode might apply, but if we are accepting arbitrary new |
|
// matches expose the real value we would allow a new potentially non-mvm match to use. |
|
int nLimitVisibleSlots = sv_visiblemaxplayers.GetInt(); |
|
if ( m_bOverridingVisibleMaxPlayers && !bLiveMatch && m_bMMServerMode ) |
|
{ nLimitVisibleSlots = m_iSavedVisibleMaxPlayers; } |
|
// Don't limit visible slots to below the current match |
|
if ( nMaxMatchPlayers > 0 ) |
|
{ nLimitVisibleSlots = Max( nMaxMatchPlayers, nLimitVisibleSlots ); } |
|
if ( nLimitVisibleSlots > 0 ) |
|
{ nMaxHumans = Min( nMaxHumans, nLimitVisibleSlots ); } |
|
} |
|
|
|
int nHumans = nAdHocPlayers + nMatchPlayers; |
|
int nClients = nHumans + nBots + nAdminSlots; |
|
// Maximum nHumans should be allowed to be. Max clients - AdminSlots, capped to visiblemaxplayers |
|
|
|
// If we've never added a player to our match this is the first think |
|
bool bNewMatch = bLiveMatch && pMatch->GetNumTotalMatchPlayers() == 0; |
|
// If our current state allows us to accept new match players |
|
bool bRequestMatchLateJoin = bLiveMatch && \ |
|
nHumans < nMaxMatchPlayers && \ |
|
nClients < gpGlobals->maxClients && \ |
|
pMatchDesc->ShouldRequestLateJoin(); |
|
|
|
// |
|
// Check if the GC is requesting us to make some more reservations, and accepting them would not exceed |
|
// desired match size or engine capabilities. |
|
// |
|
if ( pLobby && vecReservationRequests.Count() && |
|
( bNewMatch || bRequestMatchLateJoin ) && |
|
nUnconnectedPlayerReservationRequests + nHumans <= nMaxMatchPlayers && |
|
nUnconnectedPlayerReservationRequests + nClients <= gpGlobals->maxClients ) |
|
{ |
|
MMLog( "GC is requesting us to reserve %d slots.\n", vecReservationRequests.Count() ); |
|
|
|
// Accept one at a time and check if we can handle more |
|
FOR_EACH_VEC( vecReservationRequests, idx ) |
|
{ |
|
const CTFLobbyMember *pMember = pLobby->GetMemberDetails( vecReservationRequests[ idx ].m_steamID ); |
|
AcceptGCReservation( vecReservationRequests[ idx ].m_steamID, pMember, !bNewMatch, |
|
vecReservationRequests[ idx ].m_nEntindex, vecReservationRequests[ idx ].m_bActive ); |
|
|
|
// Add them to our message for this pass |
|
CMsgGameServerMatchmakingStatus_Player *pMsgPlayer = msg.Body().add_players(); |
|
pMsgPlayer->set_steam_id( vecReservationRequests[ idx ].m_steamID.ConvertToUint64() ); |
|
pMsgPlayer->set_connect_state( CMsgGameServerMatchmakingStatus_PlayerConnectState_RESERVED ); |
|
} |
|
|
|
// We promised more people slots, recompute this |
|
nMatchPlayers += nUnconnectedPlayerReservationRequests; |
|
nHumans += nUnconnectedPlayerReservationRequests; |
|
nClients += nUnconnectedPlayerReservationRequests; |
|
bRequestMatchLateJoin = bRequestMatchLateJoin && \ |
|
nHumans < nMaxMatchPlayers && \ |
|
nClients < gpGlobals->maxClients && \ |
|
pMatchDesc && pMatchDesc->ShouldRequestLateJoin(); |
|
} |
|
else if ( nUnconnectedPlayerReservationRequests ) |
|
{ |
|
MMLog( "Refused %d reservations -- not accepting match players or exceeds capacity\n", |
|
vecReservationRequests.Count() ); |
|
} |
|
|
|
// Check if they think that they are acknowledging some players, make sure |
|
// we would have decided to send a message anyway, even without their event |
|
if ( event == CMsgGameServerMatchmakingStatus_Event_AcknowledgePlayers ) |
|
{ |
|
Assert( bLobbyIncorrect == true ); |
|
} |
|
|
|
// |
|
// Clean up complete match if all players have left and the GC has dissolved the lobby. |
|
// |
|
// Deleting this should clear us up to accept new matches below, |
|
// where our ready-for-match state depends on !pLobby && !pMatch. |
|
// |
|
// Don't clean up if GC hasn't acknowledged dissolution of lobby yet, or we'll have a lobby with no associated |
|
// match to indicate what state it was in. If the GC is MIA to clean-up lobbies that's okay, we can't start a |
|
// new match until it's ready anyway, and the empty-with-lobby below check will kill us if we get stuck in this |
|
// state. |
|
if ( vecConnectedPlayers.Count() == 0 && |
|
m_pMatchInfo && !pLobby && m_pMatchInfo->m_bMatchEnded ) |
|
{ |
|
MMLog( "Cleaning out finished match %llu\n", m_pMatchInfo->m_nMatchID ); |
|
delete m_pMatchInfo; |
|
m_pMatchInfo = NULL; |
|
bLiveMatch = false; |
|
pMatch = NULL; |
|
pMatchDesc = NULL; |
|
} |
|
|
|
// Check if we're empty with a lobby. Ordinarily, we shouldn't linger too long in this state. Either we're in |
|
// the process of timing out everyone as abandoners (which should take a lot less than this timeout) or the GC |
|
// is down. But if that state persists for two hours, assume we're in a bad stuck state and reboot. |
|
if ( pLobby && vecConnectedPlayers.Count() == 0 ) |
|
{ |
|
if ( m_flTimeBecameEmptyWithLobby == 0.0 ) |
|
{ |
|
m_flTimeBecameEmptyWithLobby = now; |
|
} |
|
else |
|
{ |
|
int nSecondsEmptyWithLobby = int( now - m_flTimeBecameEmptyWithLobby ); |
|
int nTimeoutMinutes = ( BPendingReliableMessages() || m_pMatchInfo ) ? k_InvalidState_Timeout_With_Match \ |
|
: k_InvalidState_Timeout_Without_Match; |
|
if ( nSecondsEmptyWithLobby > nTimeoutMinutes*60 ) |
|
{ |
|
MMLog( "**** Server has been empty with a lobby for %d seconds. Quitting\n", nSecondsEmptyWithLobby ); |
|
AbortInvalidMatchState(); |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
m_flTimeBecameEmptyWithLobby = 0.0; |
|
} |
|
|
|
|
|
// Determine game state |
|
gcState = TF_GC_GAMESTATE_GAME_IN_PROGRESS; |
|
switch ( TFGameRules()->State_Get() ) |
|
{ |
|
case GR_STATE_INIT: |
|
gcState = TF_GC_GAMESTATE_STATE_INIT; |
|
break; |
|
|
|
case GR_STATE_PREGAME: |
|
case GR_STATE_STARTGAME: |
|
case GR_STATE_PREROUND: |
|
case GR_STATE_RESTART: |
|
gcState = TF_GC_GAMESTATE_STRATEGY_TIME; |
|
break; |
|
|
|
default: |
|
Assert( false ); |
|
case GR_STATE_RND_RUNNING: |
|
case GR_STATE_BETWEEN_RNDS: |
|
case GR_STATE_BONUS: |
|
break; |
|
|
|
case GR_STATE_TEAM_WIN: |
|
case GR_STATE_STALEMATE: |
|
if ( TFGameRules()->IsMannVsMachineMode() ) |
|
{ |
|
// *Currently* can only end in victory (or dissolves because everyone leaves) |
|
if ( |
|
TFGameRules()->State_Get() == GR_STATE_TEAM_WIN |
|
&& TFGameRules()->GetWinningTeam() == TF_TEAM_PVE_DEFENDERS ) |
|
{ |
|
gcState = TF_GC_GAMESTATE_POST_GAME; |
|
} |
|
} |
|
else if ( TFGameRules()->IsCompetitiveMode() ) |
|
{ |
|
if ( TFGameRules()->State_Get() == GR_STATE_GAME_OVER ) |
|
{ |
|
gcState = TF_GC_GAMESTATE_POST_GAME; |
|
} |
|
} |
|
break; |
|
|
|
case GR_STATE_GAME_OVER: |
|
gcState = TF_GC_GAMESTATE_GAME_IN_PROGRESS; |
|
if ( TFGameRules()->IsMannVsMachineMode() || |
|
TFGameRules()->IsCompetitiveMode() ) // right? |
|
{ |
|
gcState = TF_GC_GAMESTATE_DISCONNECT; |
|
} |
|
break; |
|
} |
|
|
|
// What state are we? |
|
if ( m_bMMServerMode ) |
|
{ |
|
static ConVarRef sv_tags( "sv_tags" ); |
|
eGameServerInfoMatchmakingMode = TF_Matchmaking_LADDER; |
|
nBotCountToSend = -1; |
|
sGameServerInfoMap = STRING( gpGlobals->mapname ); |
|
sGameServerInfoTags = sv_tags.GetString(); |
|
sGameServerInfoTags.Clear(); |
|
|
|
// Set the "map" to the current challenge, if in MvM |
|
if ( TFGameRules()->IsMannVsMachineMode() ) |
|
{ |
|
const char *pszFilenameShort = g_pPopulationManager ? g_pPopulationManager->GetPopulationFilenameShort() : NULL; |
|
if ( pszFilenameShort && pszFilenameShort[0] ) |
|
{ |
|
sGameServerInfoMap = pszFilenameShort; |
|
} |
|
} |
|
|
|
// Determine state |
|
if ( !m_pMatchInfo && !pLobby ) |
|
{ |
|
// No match, lobby, or players, ready for match |
|
if ( BPendingReliableMessages() ) |
|
{ |
|
eGameServerInfoState = ServerMatchmakingState_NOT_PARTICIPATING; |
|
if ( m_eLastGameServerUpdateState != eGameServerInfoState ) |
|
{ MMLog( "No match, but have not finished sending reliable messages, not re-enrolling in MM yet\n" ); } |
|
} |
|
else |
|
{ |
|
eGameServerInfoState = ServerMatchmakingState_EMPTY; |
|
if ( m_eLastGameServerUpdateState != eGameServerInfoState ) |
|
{ MMLog( "No match, but configured for MM, enrolling in matchmaking\n" ); } |
|
} |
|
|
|
// Unless we're not setup with no actual usable slots or have random unknown humans in strict mode |
|
if ( nClients >= gpGlobals->maxClients || nMaxHumans < 1 || |
|
( nHumans && tf_mm_strict.GetInt() == 1 ) ) |
|
{ |
|
eGameServerInfoState = ServerMatchmakingState_NOT_PARTICIPATING; |
|
if ( m_eLastGameServerUpdateState != eGameServerInfoState ) |
|
{ |
|
MMLog( "!! No match, but no usable slots or unexpected clients, cannot enroll in matchmaking. " |
|
"[ nClients %d, maxClients %d, nHumans %d, nMaxHumans %d ]\n", |
|
nClients, gpGlobals->maxClients, nHumans, nMaxHumans ); |
|
} |
|
} |
|
} |
|
else if ( bLiveMatch ) |
|
{ |
|
// Have a running match. |
|
eGameServerInfoState = bRequestMatchLateJoin ? ServerMatchmakingState_ACTIVE_MATCH_REQUESTING_LATE_JOIN \ |
|
: ServerMatchmakingState_ACTIVE_MATCH; |
|
} |
|
else |
|
{ |
|
// We have a match but it isn't live, or we have no match but the GC hasn't torn down the lobby yet ( we |
|
// should have either rejected the lobby in SOCreated or sent a cleanup message when ending the match, but |
|
// our GC connection may be lagged, just stay out of the pool until we reconcile ) |
|
if ( m_eLastGameServerUpdateState != eGameServerInfoState ) |
|
{ MMLog( "Match state is not in sync with GC, remaining out of MM until lobby is cleaned up\n" ); } |
|
eGameServerInfoState = ServerMatchmakingState_NOT_PARTICIPATING; |
|
} |
|
} |
|
|
|
// This is probably not worth the risk / reward right now. We've given instructions |
|
// telling server operators how to avoid this from happening, and it might break something |
|
// // Check if we have a lobby, and they have switched to/from MvM mode, then don't |
|
// // put us in matchmaking for now |
|
// bool bMapIsMvmMap = ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ); |
|
// if ( ( pLobby != NULL ) && ( bMapIsMvmMap != bIsMvmMode ) ) |
|
// { |
|
// eGameServerInfoMatchmakingMode = TF_Matchmaking_INVALID; |
|
// eGameServerInfoState = ServerMatchmakingState_NOT_PARTICIPATING; |
|
// MMLog( "Sending NOT_PARTICIPATING. Is MvM Map: %d, tf_mm_servermode=%d\n", bMapIsMvmMap ? 1 : 0, tf_mm_servermode.GetInt() ); |
|
// } |
|
|
|
int nSlotsFree = nMaxHumans - nHumans; |
|
|
|
// Check if number of slots available is changing. Our urgency to notify the GC about this |
|
// change depends on which direction it is changing! |
|
if ( nSlotsFree < m_nLastGameServerUpdateSlotsFree ) |
|
{ |
|
// We currently have fewer slots available than the GC thinks we do. |
|
// This is an important state change and we need to let the GC know about |
|
// this immediately, otherwise it might ask us to fill reservations we cannot |
|
// satisfy. We want the window for this race condition to be as small as |
|
// possible. |
|
bForceSendMessages = true; |
|
} |
|
else if ( nSlotsFree > m_nLastGameServerUpdateSlotsFree ) |
|
{ |
|
// We have more slots open than the GC thinks we do. We should let the GC |
|
// know relatively soon, but it's really not urgent that we flush this out |
|
// *immediately*. Also, because players come and go frequently (especially |
|
// in PvP), having this timer avoids massive spam if tons of players all decide |
|
// to leave at once. |
|
flSendInterval = Min( flSendInterval, 10.0f ); |
|
} |
|
|
|
// Check if we MUST send a message, no matter how recently we sent the last update. |
|
if ( event == CMsgGameServerMatchmakingStatus_Event_None && |
|
!bForceSendMessages && |
|
( eGameServerInfoState == m_eLastGameServerUpdateState ) && |
|
( eGameServerInfoMatchmakingMode == m_eLastGameServerUpdateMatchmakingMode ) && |
|
// map changes are infrequent, and matter quite a bit, so always send them |
|
Q_stricmp( m_sLastGameServerUpdateMap, sGameServerInfoMap ) == 0 ) |
|
{ |
|
|
|
// No need to send periodic updates if we're not participating and don't think we have a lobby or match at all. |
|
if ( eGameServerInfoState == ServerMatchmakingState_NOT_PARTICIPATING && !pLobby && !m_pMatchInfo ) |
|
return; |
|
|
|
// Check for certain rules changes. When they change, we care about them being |
|
// eventually correct, but it's not urgent |
|
if ( ( Q_stricmp( m_sLastGameServerUpdateTags, sGameServerInfoTags ) != 0 ) || |
|
( nMaxHumans != m_nLastGameServerUpdateMaxHumans ) || |
|
( nBotCountToSend != m_nLastGameServerUpdateBotCount ) ) |
|
{ |
|
flSendInterval = Min( flSendInterval, 20.0f ); |
|
} |
|
|
|
// If lobby is incorrect in an ordinary way (player left, etc), |
|
// flush the change decently quickly |
|
if ( pLobby && bLobbyIncorrect ) |
|
{ |
|
// Send updates more quickly if the GC hasn't acknowledged, but don't DDoS. Ideally the event that made the |
|
// lobby incorrect triggered a Update( bForce = true ); |
|
flSendInterval = Min( flSendInterval, 10.0f ); |
|
} |
|
|
|
if ( now < m_timeLastSendGameServerInfoAndConnectedPlayers + flSendInterval ) |
|
{ return; } |
|
} |
|
|
|
// Fill in info about our connection state |
|
msg.Body().set_server_version( engine->GetServerVersion() ); |
|
msg.Body().set_matchmaking_state( eGameServerInfoState ); |
|
if ( eGameServerInfoState == ServerMatchmakingState_NOT_PARTICIPATING ) |
|
{ |
|
msg.Body().set_match_group( k_nMatchGroup_Invalid ); |
|
if ( dbg_spew_connected_players_level.GetInt() >= 2 ) |
|
{ |
|
MMLog("Sending CMsgGameServerMatchmakingStatus (state=%s)\n", |
|
ServerMatchmakingState_Name( msg.Body().matchmaking_state() ).c_str() ); |
|
} |
|
} |
|
else |
|
{ |
|
static ConVarRef sv_region( "sv_region" ); |
|
msg.Body().set_server_region( sv_region.GetInt() ); |
|
msg.Body().set_server_loadavg( GetCPUUsage() ); |
|
msg.Body().set_server_dedicated( engine->IsDedicatedServer() ); |
|
msg.Body().set_server_trusted( tf_mm_trusted.GetBool() ); |
|
msg.Body().set_matchmaking_mode( eGameServerInfoMatchmakingMode ); |
|
msg.Body().set_map( sGameServerInfoMap ); |
|
msg.Body().set_game_state( gcState ); |
|
if ( pLobby ) |
|
msg.Body().set_lobby_mm_version( pLobby->GetLobbyMMVersion() ); |
|
if ( nBotCountToSend >= 0 ) |
|
msg.Body().set_bot_count( (uint32)nBotCountToSend ); |
|
Assert( nMaxHumans > 0 ); |
|
msg.Body().set_max_players( nMaxHumans ); |
|
Assert( nSlotsFree >= 0 ); |
|
msg.Body().set_slots_free( nSlotsFree ); |
|
msg.Body().set_tags( sGameServerInfoTags ); |
|
msg.Body().set_strict( tf_mm_strict.GetInt() ); |
|
|
|
if ( event != CMsgGameServerMatchmakingStatus_Event_None ) |
|
{ msg.Body().set_event( event ); } |
|
|
|
if ( ( dbg_spew_connected_players_level.GetInt() >= 2 ) || |
|
( event != CMsgGameServerMatchmakingStatus_Event_None && dbg_spew_connected_players_level.GetInt() >= 1 ) ) |
|
{ |
|
MMLog("Sending CMsgGameServerMatchmakingStatus (state=%s, slots_free=%d, event=%s, %s)\n", |
|
ServerMatchmakingState_Name( msg.Body().matchmaking_state() ).c_str(), |
|
msg.Body().slots_free(), |
|
CMsgGameServerMatchmakingStatus_Event_Name( msg.Body().event() ).c_str(), |
|
( tf_mm_trusted.GetBool() ? ", trusted=true" : "" ) |
|
); |
|
} |
|
|
|
if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) |
|
{ |
|
msg.Body().set_mvm_credits_acquired( MannVsMachineStats_GetAcquiredCredits( -1 ) ); |
|
msg.Body().set_mvm_credits_dropped( MannVsMachineStats_GetAcquiredCredits( -1 ) ); |
|
msg.Body().set_mvm_wave( MannVsMachineStats_GetCurrentWave() ); |
|
} |
|
|
|
EMatchGroup eCurrentGroup = k_nMatchGroup_Invalid; |
|
if ( m_pMatchInfo ) |
|
{ |
|
eCurrentGroup = m_pMatchInfo->m_eMatchGroup; |
|
} |
|
|
|
msg.Body().set_match_group( eCurrentGroup ); |
|
} |
|
|
|
// Check if we MUST send a message, no matter how recently we sent the last update. |
|
if ( event == CMsgGameServerMatchmakingStatus_Event_None && |
|
!bForceSendMessages && |
|
( msg.Body().lobby_mm_version() == m_nLastGameServerUpdateLobbyMMVersion ) && |
|
( msg.Body().matchmaking_state() == m_eLastGameServerUpdateState ) && |
|
( msg.Body().matchmaking_mode() == m_eLastGameServerUpdateMatchmakingMode ) && |
|
// map changes are infrequent, and matter quite a bit, so always send them |
|
Q_stricmp( m_sLastGameServerUpdateMap, msg.Body().map().c_str() ) == 0 ) |
|
{ |
|
|
|
// No need to send periodic updates if we're not participating and don't think we have a lobby or match at all. |
|
if ( msg.Body().matchmaking_state() == ServerMatchmakingState_NOT_PARTICIPATING && !pLobby && !m_pMatchInfo ) |
|
return; |
|
|
|
// Check for certain rules changes. When they change, we care about them being |
|
// eventually correct, but it's not urgent |
|
if ( ( Q_stricmp( m_sLastGameServerUpdateTags, msg.Body().tags().c_str() ) != 0 ) || |
|
( msg.Body().max_players() != (uint32)m_nLastGameServerUpdateMaxHumans ) || |
|
( msg.Body().bot_count() != (uint32)m_nLastGameServerUpdateBotCount ) ) |
|
{ |
|
flSendInterval = Min( flSendInterval, 20.0f ); |
|
} |
|
|
|
// If lobby is incorrect in an ordinary way (player left, etc), |
|
// flush the change decently quickly |
|
if ( pLobby && bLobbyIncorrect ) |
|
{ |
|
// Send updates more quickly if the GC hasn't acknowledged, but don't DDoS. Ideally the event that made the |
|
// lobby incorrect triggered a Update( bForce = true ); |
|
flSendInterval = Min( flSendInterval, 10.0f ); |
|
} |
|
|
|
if ( now < m_timeLastSendGameServerInfoAndConnectedPlayers + flSendInterval ) |
|
{ return; } |
|
} |
|
|
|
GCClientSystem()->BSendMessage( msg ); |
|
|
|
// Remember what/when we sent, so we can tell next time if we need to send |
|
m_timeLastSendGameServerInfoAndConnectedPlayers = now; |
|
m_eLastGameServerUpdateMatchmakingMode = msg.Body().matchmaking_mode(); |
|
m_eLastGameServerUpdateState = msg.Body().matchmaking_state(); |
|
m_sLastGameServerUpdateMap = msg.Body().map().c_str(); |
|
m_sLastGameServerUpdateTags = msg.Body().tags().c_str(); |
|
m_nLastGameServerUpdateBotCount = nBotCountToSend; |
|
m_nLastGameServerUpdateMaxHumans = nMaxHumans; |
|
m_nLastGameServerUpdateSlotsFree = nSlotsFree; |
|
m_nLastGameServerUpdateLobbyMMVersion = msg.Body().lobby_mm_version(); |
|
|
|
// Remember when we started requesting late join, so we can compare it to our lobby's late-join state to reason |
|
// about how long we've been waiting. |
|
if ( eGameServerInfoState == ServerMatchmakingState_ACTIVE_MATCH_REQUESTING_LATE_JOIN ) |
|
{ |
|
if ( m_flTimeRequestedLateJoin == -1.f ) |
|
{ |
|
m_flTimeRequestedLateJoin = CRTime::RTime32TimeCur(); |
|
MMLog( "Requested late join for active match\n" ); |
|
} |
|
} |
|
else if ( m_flTimeRequestedLateJoin != -1.f ) |
|
{ |
|
MMLog( "Stopped requesting late join for active match after %.02fs\n", |
|
CRTime::RTime32TimeCur() - m_flTimeRequestedLateJoin ); |
|
m_flTimeRequestedLateJoin = -1.f; |
|
} |
|
|
|
// Only late join eligible when are requesting late join, we have a lobby from the GC, and it has marked itself as |
|
// late join eligible. If we've lost our lobby or it hasn't updated to become eligible, there may be GC connection |
|
// difficulties. |
|
|
|
// We only update this at the end of updates, rather than on the fly, to ensure we don't expose this value prior to |
|
// processing other updates in the lobby object. For instance, the lobby might remove us from late join and give us |
|
// reserved members at the same time, we don't want callers to see one, but not the other. |
|
m_bLateJoinEligible = m_flTimeRequestedLateJoin != -1.f && GetLobby() && GetLobby()->GetLateJoinEligible(); |
|
|
|
} |
|
|
|
|
|
// *************************************************************************************************************** |
|
void CTFGCServerSystem::SendMvMVictoryResult() |
|
{ |
|
// Note that we don't have to have an *ended* match -- MvM code technically allows players to continue in the same |
|
// match and achieve multiple victories. |
|
Assert( m_pMatchInfo ); |
|
|
|
CTFGSLobby *pLobby = GetLobby(); |
|
if ( !pLobby ) |
|
{ |
|
// FIXME - We should be able to submit this even if the GC reboots and loses our lobby state (though it wont |
|
// happen that often, as the GC tries to revive lobby state from memcached) |
|
MMLog( "CTFGCServerSystem::MvMVictory() -- no lobby, so not sending results to GC\n" ); |
|
return; |
|
} |
|
|
|
if ( IsMannUpGroup( pLobby->GetMatchGroup() ) ) |
|
{ |
|
m_mvmVictoryInfo.Init( pLobby ); |
|
|
|
ReliableMsgMvMVictory *pReliable = new ReliableMsgMvMVictory; |
|
|
|
auto &msg = pReliable->Msg().Body(); |
|
|
|
msg.set_mission_name( m_mvmVictoryInfo.m_sChallengeName ); |
|
#ifdef USE_MVM_TOUR |
|
if ( !m_mvmVictoryInfo.m_sMannUpTourOfDuty.IsEmpty() ) |
|
{ |
|
msg.set_tour_name_mannup( m_mvmVictoryInfo.m_sMannUpTourOfDuty ); |
|
} |
|
#endif // USE_MVM_TOUR |
|
msg.set_lobby_id( m_mvmVictoryInfo.m_nLobbyId ); |
|
msg.set_event_time( m_mvmVictoryInfo.m_tEventTime ); |
|
|
|
FOR_EACH_VEC( m_mvmVictoryInfo.m_vPlayerIds, iMember ) |
|
{ |
|
CMsgMvMVictory_Player *pMsgPlayer = msg.add_players(); |
|
pMsgPlayer->set_steam_id( m_mvmVictoryInfo.m_vPlayerIds[ iMember ]); |
|
pMsgPlayer->set_squad_surplus( m_mvmVictoryInfo.m_vSquadSurplus[ iMember ] ); |
|
} |
|
|
|
pReliable->Enqueue(); |
|
} |
|
} |
|
|
|
////----------------------------------------------------------------------------- |
|
//// Purpose: Job for being told when the server GC connection is established |
|
////----------------------------------------------------------------------------- |
|
//class CGCClientJobServerWelcome : public GCSDK::CGCClientJob |
|
//{ |
|
//public: |
|
// CGCClientJobServerWelcome( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient ) { } |
|
// |
|
// virtual bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket ) |
|
// { |
|
// CProtoBufMsg<CMsgServerWelcome> msg( pNetPacket ); |
|
// |
|
// g_bServerReceivedGCWelcome = true; |
|
// |
|
// GTFGCClientSystem()->UpdateGCServerInfo(); |
|
// |
|
// // Validate version |
|
// int engineServerVersion = engine->GetServerVersion(); |
|
// g_gcServerVersion = (int)msg.Body().version(); |
|
// |
|
// // Version checking is enforced if both sides do not report zero as their version |
|
// if ( engineServerVersion && g_gcServerVersion && engineServerVersion != g_gcServerVersion ) |
|
// { |
|
// // If we're out of date exit |
|
// Msg("Version out of date (GC wants %d, we are %d)!\n", g_gcServerVersion, engine->GetServerVersion() ); |
|
// |
|
// // If we hibernating, quit now, otherwise we will quit on hibernation |
|
// if ( g_ServerGameDLL.m_bIsHibernating ) |
|
// { |
|
// engine->ServerCommand( "quit\n" ); |
|
// } |
|
// } |
|
// else |
|
// { |
|
// Msg("GC Connection established for server version %d\n", engine->GetServerVersion() ); |
|
// } |
|
// |
|
// return true; |
|
// } |
|
//}; |
|
//GC_REG_JOB( GCSDK::CGCClient, CGCClientJobServerWelcome, "CGCClientJobServerWelcome", k_EMsgGCServerWelcome, k_EServerTypeGCClient ); |
|
|
|
|
|
//// temp for tracking down machines submitted stats |
|
//#if defined ( _WIN32 ) |
|
//#define WIN32_LEAN_AND_MEAN |
|
//#undef INVALID_HANDLE_VALUE |
|
//#undef DECLARE_HANDLE |
|
//#include <windows.h> |
|
//bool DOTA_GetComputerName( char *pszComputerName, DWORD *length ) |
|
//{ |
|
// return !!GetComputerName( pszComputerName, length ); |
|
//} |
|
//#endif |
|
|
|
// ************************************************************************************************** |
|
void CTFGCServerSystem::SendRejectLobby() |
|
{ |
|
MMLog( "Sending CMsgGameServerKickingLobby to reject stale lobby\n" ); |
|
|
|
ReliableMsgGameServerKickingLobby *pReliable = new ReliableMsgGameServerKickingLobby(); |
|
|
|
auto &msg = pReliable->Msg().Body(); |
|
msg.set_create_party( false ); |
|
if ( GetLobby() ) |
|
{ |
|
msg.set_lobby_id( GetLobby()->GetGroupID() ); |
|
msg.set_lobby_id( GetLobby()->GetMatchID() ); |
|
} |
|
|
|
pReliable->Enqueue(); |
|
} |
|
|
|
// ************************************************************************************************** |
|
void CTFGCServerSystem::EndManagedMatch( bool bKickPlayersToParties ) |
|
{ |
|
CMatchInfo *pMatch = GetMatch(); |
|
// Sanity |
|
AssertMsg( !pMatch || !pMatch->m_bMatchEnded, "Ending an already ended match" ); |
|
if ( !pMatch ) |
|
{ return; } |
|
|
|
pMatch->SetEnded(); |
|
|
|
// Cancel launching the new match. Leave the rest of the state alone, we'll send a NewMatch -> EndMatch and things |
|
// will just work out as responses come in. |
|
m_flWaitingForNewMatchTime = 0.f; |
|
|
|
if ( !m_pMatchInfo->m_bSentResult ) |
|
{ |
|
Warning( "Ending a managed match without sending a result" ); |
|
Assert( false ); |
|
} |
|
|
|
ReliableMsgGameServerKickingLobby *pReliable = new ReliableMsgGameServerKickingLobby(); |
|
auto &msg = pReliable->Msg().Body(); |
|
|
|
if ( bKickPlayersToParties ) |
|
{ |
|
CUtlVector<CSteamID> vecConnectedPlayers; |
|
int total = pMatch->GetNumTotalMatchPlayers(); |
|
|
|
for ( int idx = 0; idx < total; idx++ ) |
|
{ |
|
CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch->GetMatchDataForPlayer( idx ); |
|
if ( !pMatchPlayer->bDropped && pMatchPlayer->bConnected ) |
|
{ |
|
msg.add_connected_players( pMatchPlayer->steamID.ConvertToUint64() ); |
|
} |
|
} |
|
|
|
|
|
if ( msg.connected_players_size() <= 0 ) |
|
{ |
|
bKickPlayersToParties = false; |
|
} |
|
} |
|
|
|
if ( bKickPlayersToParties ) |
|
{ |
|
MMLog( "Sending CMsgGameServerKickingLobby, requesting party with %d connected players\n", msg.connected_players_size() ); |
|
} |
|
else |
|
{ |
|
MMLog( "Sending CMsgGameServerKickingLobby, not requesting party\n" ); |
|
} |
|
|
|
msg.set_create_party( bKickPlayersToParties ); |
|
msg.set_lobby_id( pMatch->m_nLobbyID ); |
|
msg.set_match_id( pMatch->m_nMatchID ); |
|
|
|
pReliable->Enqueue(); |
|
} |
|
|
|
// ************************************************************************************************** |
|
void CTFGCServerSystem::SendPlayerLeftMatch( CSteamID targetPlayer, TFMatchLeaveReason eReason, bool bIsAbandon ) |
|
{ |
|
CMatchInfo *pMatch = GetMatch(); |
|
// Sanity |
|
AssertMsg( pMatch && !pMatch->m_bMatchEnded, "Don't expect to be sending this without a live match" ); |
|
if ( !pMatch ) |
|
{ return; } |
|
|
|
ReliableMsgPlayerLeftMatch *pReliable = new ReliableMsgPlayerLeftMatch(); |
|
auto &msg = pReliable->Msg().Body(); |
|
|
|
msg.set_steam_id( targetPlayer.ConvertToUint64() ); |
|
msg.set_leave_reason( eReason ); |
|
MMLog( "Sending CMsgPlayerLeftMatch with target of %s [ abandon = %d ]\n", targetPlayer.Render(), bIsAbandon ); |
|
|
|
msg.set_lobby_id( pMatch->m_nLobbyID ); |
|
msg.set_match_id( pMatch->m_nMatchID ); |
|
msg.set_was_abandon( bIsAbandon ); |
|
|
|
pReliable->Enqueue(); |
|
} |
|
|
|
// ************************************************************************************************** |
|
void CTFGCServerSystem::SendCompetitiveMatchResult( GCSDK::CProtoBufMsg< CMsgGC_Match_Result > *pMatchResultMsg ) |
|
{ |
|
// We should have matchinfo when completing a ladder match |
|
if ( !m_pMatchInfo ) |
|
{ |
|
Warning( "Sending competitive match results without match info!\n" ); |
|
Assert( false ); |
|
} |
|
|
|
if ( m_pMatchInfo->m_bSentResult ) |
|
{ |
|
Warning( "Sending competitive match results without an ended match\n" ); |
|
Assert( false ); |
|
} |
|
|
|
ReliableMsgMatchResult *pReliable = new ReliableMsgMatchResult; |
|
auto &msg = pReliable->Msg().Body(); |
|
/// XXX(JohnS): With refactor this is now kinda silly. Callers should really just be giving us a CMsgGC_Match_Result |
|
/// instead of the wrapper. |
|
msg.CopyFrom( pMatchResultMsg->Body() ); |
|
pReliable->Enqueue(); |
|
|
|
m_pMatchInfo->m_bSentResult = true; |
|
} |
|
|
|
// ************************************************************************************************** |
|
bool CTFGCServerSystem::BLateJoinEligible() |
|
{ |
|
return m_bLateJoinEligible; |
|
} |
|
|
|
// ************************************************************************************************** |
|
void CTFGCServerSystem::AcceptGCReservation( CSteamID steamID, const CTFLobbyMember *pMemberData, bool bIsLateJoin, int nEntindex, bool bActive ) |
|
{ |
|
if ( m_pMatchInfo ) |
|
{ |
|
// Accepting new player to competitive match, add to match data |
|
MMLog( "New match player %s\n", steamID.Render() ); |
|
m_pMatchInfo->AddPlayer( steamID, pMemberData, bIsLateJoin, nEntindex, bActive ); |
|
} |
|
} |
|
|
|
// ************************************************************************************************** |
|
void CTFGCServerSystem::AbortInvalidMatchState() |
|
{ |
|
// TODO ROLLING MATCHES: SteamAPI_SetMiniDumpComment / SteamAPI_WriteMiniDump |
|
MMLog( "**** MM Server in invalid match state, terminating\n" ); |
|
engine->ServerCommand( "quit\n" ); |
|
} |
|
|
|
// ************************************************************************************************** |
|
void CTFGCServerSystem::MMServerModeChanged() |
|
{ |
|
// Save old boolean state |
|
bool bSaveMMServerMode = m_bMMServerMode; |
|
|
|
// Set new state |
|
m_bMMServerMode = ( tf_mm_servermode.GetInt() != 0 ); |
|
|
|
// Check if logical state is changing; output some text no matter what |
|
if ( m_bMMServerMode ) |
|
{ |
|
if ( bSaveMMServerMode ) |
|
{ |
|
MMLog( "Lobby-based matchmaking is active\n" ); |
|
} |
|
else |
|
{ |
|
MMLog( "Entering lobby-based matchmaking mode\n" ); |
|
} |
|
|
|
if ( tf_mm_strict.GetInt() == 0 ) |
|
{ |
|
MMLog( " Open mode active. Gameserver will show in server browser and accept ad-hoc joins.\n" ); |
|
} |
|
else if ( tf_mm_strict.GetInt() == 1 ) |
|
{ |
|
MMLog( " Strict mode is active. Gameserver will not show in server browser or accept ad-hoc joins.\n" ); |
|
} |
|
else |
|
{ |
|
MMLog( " Server is hidden from server browser list, but will accept ad-hoc joins.\n" ); |
|
} |
|
|
|
if ( tf_mm_trusted.GetInt() != 0 ) |
|
{ |
|
MMLog( " Requested trusted server status.\n" ); |
|
} |
|
|
|
} |
|
else |
|
{ |
|
if ( bSaveMMServerMode ) |
|
{ |
|
MMLog( "Leaving lobby-based matchmaking mode\n" ); |
|
} |
|
else |
|
{ |
|
MMLog( "Lobby-based matchmaking mode not active\n" ); |
|
} |
|
} |
|
|
|
// Force this major change out immediately |
|
UpdateConnectedPlayersAndServerInfo( CMsgGameServerMatchmakingStatus_Event_None, true ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CTFGCServerSystem::LaunchNewMatchForLobby() |
|
{ |
|
/// XXX(JohnS): Technically the lobby might legitimately be gone here -- if we have gotten the NewMatchForLobby |
|
/// response and the GC then croaks, we might be told it lost our lobby, but have the new match |
|
/// assignment and be able to proceed without needing the lobby at all (as in normal cases where the GC |
|
/// loses state after giving us the authority to run a match). |
|
/// |
|
/// Since the match in question hasn't started yet, and this is nearly impossible given the timing |
|
/// window, I'm not doing the work to cache the lobby values we need in here just to let the |
|
/// just-created match survive that edge case. |
|
const CTFGSLobby* pLobby = GetLobby(); |
|
|
|
if ( !pLobby || m_flWaitingForNewMatchTime == 0.f || !m_pMatchInfo || \ |
|
m_pMatchInfo->BMatchTerminated() || m_pMatchInfo->m_bServerCreated ) |
|
{ |
|
// You need to prepare for the switch with RequestNewMatchForLobby first. Should not have gotten here if we have |
|
// a terminated or server created match -- Must still be managed by the GC in order to roll into a new match. |
|
Assert( false ); |
|
MMLog( "!! Attempting to launch a new match for a lobby without valid state\n" ); |
|
AbortInvalidMatchState(); |
|
} |
|
|
|
m_flWaitingForNewMatchTime = 0.f; |
|
|
|
CMatchInfo* pNewMatchInfo = new CMatchInfo( pLobby ); |
|
// The old match info is holding the vote-winning map name |
|
pNewMatchInfo->m_strMapName = m_pMatchInfo->m_strMapName; |
|
EMatchGroup eMatchGroup = pLobby->GetMatchGroup(); |
|
|
|
// We still need a new match ID from the GC. Mark that this new match is |
|
// created by us so that: 1) If we do get a response for a new match ID |
|
// we know what to do with it |
|
// 2) The GC knows to assign it a match ID if it |
|
// gets a match result for it before (1) occurs |
|
if ( m_bWaitingForNewMatchID ) |
|
{ |
|
// Mark that we're going rogue |
|
pNewMatchInfo->m_bServerCreated = true; |
|
pNewMatchInfo->m_nMatchID = 0; // Don't inherit the stale one from the lobby |
|
|
|
if ( !CanChangeMatchPlayerTeams() ) |
|
{ |
|
// Server created speculative matches are counting on the GC approving this when it wakes up, and also |
|
// approving our override of player teams below. If we want a mode that does rolling matches but has no |
|
// authority to override teams, we'd need to just cancel the pending match here instead of using |
|
// m_bServerCreated |
|
AbortInvalidMatchState(); |
|
} |
|
} |
|
|
|
for( int idx = 0; idx < m_pMatchInfo->GetNumTotalMatchPlayers(); idx++ ) |
|
{ |
|
const CMatchInfo::PlayerMatchData_t* pPlayerMatchData = m_pMatchInfo->GetMatchDataForPlayer( idx ); |
|
// We don't need record of dropped players for the new match |
|
if ( pPlayerMatchData->bDropped ) |
|
{ continue; } |
|
|
|
// We stop doing maintenance on lobby->match sync during the pending-new-match period, but we don't want to |
|
// include players who would be dropped on the first think -- we'd have erroneous record that they were |
|
// officially part of the match for some period, when they were not. |
|
// |
|
// XXX(JohnS): Technically, we could create a speculative match, then when the new match ID arrives, some |
|
// members vanished -- those members were never actually part of the lobby from the GC |
|
// perspective. We might need to cull these people on the first post-new-matchID-think if having |
|
// record of them is causing problems. (a bWasEverConfirmedByGC flag?) |
|
if ( !pLobby->GetMemberDetails( pPlayerMatchData->steamID ) ) |
|
{ continue; } |
|
|
|
// AddPlayer needs to know if they are connected/active right now |
|
int nEntIndex = 0; |
|
bool bActive = false; |
|
if ( pPlayerMatchData->bConnected ) |
|
{ |
|
if ( pPlayerMatchData->nConnectingButNotActiveIndex ) |
|
{ |
|
// Connected, not active |
|
bActive = false; |
|
nEntIndex = pPlayerMatchData->nConnectingButNotActiveIndex; |
|
} |
|
else |
|
{ |
|
// Connected and active |
|
bActive = true; |
|
// We could null check this but we'd just use the information to call AbortInvalidMatchState(). |
|
nEntIndex = UTIL_PlayerBySteamID( pPlayerMatchData->steamID )->entindex(); |
|
} |
|
} |
|
|
|
pNewMatchInfo->AddPlayer( *pPlayerMatchData, nEntIndex, bActive ); |
|
} |
|
|
|
delete m_pMatchInfo; |
|
m_pMatchInfo = pNewMatchInfo; |
|
|
|
// If we are going ahead with a server-created match, queue a ChangeMatchPlayerTeams message in sequence with our |
|
// pending new match request -- the GC will process, in order: |
|
// |
|
// - Give us a new match! |
|
// -> Okay here's new match & teams |
|
// - Set everyone's teams to (the previous match teams)! |
|
// -> Okay here's new lobby with teams that match your state |
|
// |
|
// ... And since we don't run UpdateConnectedPlayers() while messages are in queue, by time we run our next |
|
// look-at-the-lobby think, we'll be in sync again. |
|
CUtlVector< PlayerTeamPair_t > vecPlayerTeams; |
|
for( int idx = 0; idx < m_pMatchInfo->GetNumTotalMatchPlayers(); idx++ ) |
|
{ |
|
const CMatchInfo::PlayerMatchData_t *pPlayer = m_pMatchInfo->GetMatchDataForPlayer( idx ); |
|
vecPlayerTeams.AddToTail( { pPlayer->steamID, pPlayer->eGCTeam } ); |
|
} |
|
ChangeMatchPlayerTeams( vecPlayerTeams ); |
|
|
|
GTFGCClientSystem()->DumpLobby(); |
|
|
|
if ( eMatchGroup == EMatchGroup::k_nMatchGroup_Invalid || |
|
!GetMatchGroupDescription( eMatchGroup )->InitServerSettingsForMatch( pLobby ) ) |
|
{ |
|
AbortInvalidMatchState(); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Activate / deactive GC hosting mode |
|
//----------------------------------------------------------------------------- |
|
void OnMMServerModeChanged( IConVar *pConVar, const char *pOldString, float flOldValue ) |
|
{ |
|
GTFGCClientSystem()->MMServerModeChanged(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void OnMMServerModeTrustedChanged( IConVar *pConVar, const char *pOldString, float flOldValue ) |
|
{ |
|
OnMMServerModeChanged( pConVar, pOldString, flOldValue ); |
|
} |
|
|
|
ConVar tf_mm_servermode( "tf_mm_servermode", "0", FCVAR_NOTIFY, |
|
"Activates / deactivates Lobby-based hosting mode.\n" |
|
" 0 = not active\n" |
|
" 1 = Put in matchmaking pool (Lobby will control current map)\n", |
|
true, |
|
0.f, |
|
true, |
|
1.f, |
|
OnMMServerModeChanged ); |
|
|
|
ConVar tf_mm_strict( "tf_mm_strict", "0", FCVAR_NOTIFY, |
|
" 0 = Show in server browser, and allow ad-hoc joins\n" |
|
" 1 = Hide from server browser and only allow joins coordinated through GC matchmaking\n" |
|
" 2 = Hide from server browser, but allow ad-hoc joins\n", |
|
OnMMServerModeChanged ); |
|
|
|
ConVar tf_mm_trusted( "tf_mm_trusted", "0", FCVAR_NOTIFY | FCVAR_HIDDEN, |
|
"Set to 1 on Valve servers to requested trusted status. (Yes, it is authenticated on the backend, and attempts by non-valve servers are logged.)\n", |
|
OnMMServerModeTrustedChanged ); |
|
|
|
#endif // #ifdef ENABLE_GC_MATCHMAKING
|
|
|