1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
|
//========= Copyright Valve Corporation, All rights reserved. ============//
#ifndef TF_GC_SERVER_H
#define TF_GC_SERVER_H
#ifdef _WIN32
#pragma once
#endif
#if !defined( _X360 ) && !defined( NO_STEAM )
#include "steam/steam_api.h"
#include "steam/steam_gameserver.h"
#endif
//#include "tf_gc_common.h"
#include "gcsdk/gcclientsdk.h"
#include "playergroup.h"
//#include "dota_gamerules.h"
#include "gc_clientsystem.h"
#include "tf_gcmessages.h"
#include "GameEventListener.h"
#include "rtime.h"
#include "tf_shareddefs.h"
class CTFGSLobby;
class CTFParty;
//enum EDOTA_Uploading_Match_Stats
//{
// EDOTA_MATCH_STATS_IDLE,
// EDOTA_MATCH_STATS_UPLOADING,
// EDOTA_MATCH_STATS_UPLOAD_COMPLETE
//};
#ifdef ENABLE_GC_MATCHMAKING
class CMvMVictoryInfo
{
public:
int m_nLobbyId;
CUtlString m_sChallengeName;
#ifdef USE_MVM_TOUR
CUtlString m_sMannUpTourOfDuty;
#endif // USE_MVM_TOUR
CUtlVector<uint64> m_vPlayerIds;
CUtlVector<bool> m_vSquadSurplus;
RTime32 m_tEventTime;
void Init ( CTFGSLobby *pLobby );
};
class CMatchInfo
{
friend class CTFGCServerSystem;
public:
CMatchInfo( const CTFGSLobby *pLobby );
~CMatchInfo();
uint64 m_nMatchID;
uint64 m_nLobbyID;
EMatchGroup m_eMatchGroup;
uint32 m_uLobbyFlags;
uint32 m_uAverageRank;
RTime32 m_rtMatchCreated;
uint32 m_unEventTeamStatus;
bool m_bFirstPersonActive;
int m_nBotsAdded;
bool m_bServerCreated;
struct DailyStatsRankBucket_t
{
uint32 nRank;
uint32 nRecords;
uint32 nAvgScore;
uint32 nStDevScore;
uint32 nAvgKills;
uint32 nStDevKills;
uint32 nAvgDamage;
uint32 nStDevDamage;
uint32 nAvgHealing;
uint32 nStDevHealing;
uint32 nAvgSupport;
uint32 nStDevSupport;
};
struct PlayerMatchData_t
{
friend class CTFGCServerSystem;
friend class CMatchInfo;
PlayerMatchData_t( CSteamID steamID, const CTFLobbyMember *pMemberData )
: steamID( steamID )
, uPartyID( pMemberData->party_id() )
, eGCTeam( pMemberData->team() )
, bDropped( false )
, bConnected( false )
, rtJoinedMatch( CRTime::RTime32TimeCur() )
, nVoteKickAttempts( 0 )
, nDisconnectedSeconds( 0 )
, nScoreMedal( 0 )
, nKillsMedal( 0 )
, nDamageMedal( 0 )
, nHealingMedal( 0 )
, nSupportMedal( 0 )
, bLateJoin( false )
, nScore( 0 )
, bPlayed( false )
, unMMSkillRating( 0u )
, nDrilloRatingDelta( 0 )
, unClassesPlayed( 0u )
, rtLastActiveEvent( CRTime::RTime32TimeCur() )
, bAlwaysSafeToLeave( false )
, bEverConnected( false )
, bDropWasAbandon( false )
, eDropReason( TFMatchLeaveReason_UNSPECIFIED )
, nConnectingButNotActiveIndex( 0 )
, m_mapXPAccumulation( DefLessFunc( CMsgTFXPSource::XPSourceType ) )
{}
PlayerMatchData_t( const PlayerMatchData_t& rhs );
CSteamID steamID;
uint64 uPartyID;
TF_GC_TEAM eGCTeam;
// If true, this player was dropped from the match and is not part of the active lobby. This is important for
// cases where the GC connection is lost and the lobby state is stale.
bool bDropped;
bool bConnected;
// Timestamp player joined the match at. Not guaranteed to be the same instant the match was created, depending
// on how the GC does things.
RTime32 rtJoinedMatch;
uint32 nVoteKickAttempts;
// Number of cumulative seconds the player has been absent, *not* including the initial connect timeout. Used
// to determine when to award an abandon. We may do odd things like "comp" you some seconds on a second, later,
// disconnect, so this shouldn't be used for stats purposes.
int nDisconnectedSeconds;
int nScoreMedal;
int nKillsMedal;
int nDamageMedal;
int nHealingMedal;
int nSupportMedal;
bool bLateJoin;
int nScore;
bool bPlayed;
// This is a single-value skill rating given to each player by the GC
uint32 unMMSkillRating;
// This is the older drillo rating system that was done on the server. It is still sent up to the GC as the
// input to the drillo backend there. If we want to keep this long-term it should be moved to be a fully-gc
// backend like glicko
int nDrilloRatingDelta;
uint32 unClassesPlayed;
const CMsgTFXPSourceBreakdown& GetXPSources() const { return m_XPBreakdown; }
// Override releasing this player from obligation to this match beyond the normal abandon rules. Used by MvM mode
// for marking everyone who completes a wave as allowed to drop without penalty, for instance.
void MarkAlwaysSafeToLeave() { bAlwaysSafeToLeave = true; }
bool BDropWasAbandon() { return bDropped && bDropWasAbandon; }
TFMatchLeaveReason GetDropReason() { return bDropped ? eDropReason : TFMatchLeaveReason_UNSPECIFIED; }
RTime32 GetLastActiveEventTime( void ) { return rtLastActiveEvent; }
MM_PlayerConnectionState_t GetConnectionState() const;
void UpdateClassesPlayed( int nClass );
struct XPBonusPool_t
{
XPBonusPool_t()
: m_flMultiplier( 1.f )
, m_nBonusPoolRemaining( 0 )
{}
CMsgTFXPSource_XPSourceType m_eType;
// Only give up to this amount
int m_nBonusPoolRemaining;
// Give at this rate
float m_flMultiplier;
};
private:
void OnConnected( int nEntindex );
void OnActive();
// Last time the player changed between active (fully loaded in) and not-active. A player is active if
// ( bConnected && !nConnectingButNotActiveIndex )
RTime32 rtLastActiveEvent;
bool bAlwaysSafeToLeave;
bool bEverConnected;
// If dropped - was it an abandon and what was the reason.
bool bDropWasAbandon;
TFMatchLeaveReason eDropReason;
// Track the janky source-engine state between ClientConnect (when we allow them in) and ClientActive
int nConnectingButNotActiveIndex;
// XP accumulation for a player
CMsgTFXPSourceBreakdown m_XPBreakdown;
// The breakdown stores ints, but we need float precision or else we're going to round off a
// significant amount of XP as the match plays on.
CUtlMap< CMsgTFXPSource::XPSourceType, float > m_mapXPAccumulation;
CUtlVector< XPBonusPool_t > m_vecXPBonusPools;
};
enum RankStatType_t
{
RankStat_Invalid = -1,
RankStat_Score = 0,
RankStat_Kills,
RankStat_Damage,
RankStat_Healing,
RankStat_Support,
};
void SetDailyRankData( DailyStatsRankBucket_t vecRankData );
bool RequestGCRankData( void );
bool CalculatePlayerMatchRankData( void );
bool CalculateMatchSkillRatingAdjustments( int iWinningTeam );
const CMatchInfo::PlayerMatchData_t* GetMatchDataForPlayer( CSteamID steamID ) const;
CMatchInfo::PlayerMatchData_t* GetMatchDataForPlayer( CSteamID steamID );
// For iterating over all players. Index is relative to GetNumTotalMatchPlayers
CMatchInfo::PlayerMatchData_t* GetMatchDataForPlayer( int nPlayer );
// This is the total number of players we have match data for -- it may include dropped players not part of the
// match any longer.
int GetNumTotalMatchPlayers() const;
// Number of players still active in the match.
int GetNumActiveMatchPlayers() const;
// Number of players still active in the match for a specific team
int GetNumActiveMatchPlayersForTeam( int nTeam ) const;
// Total skill rating for a team
int GetTotalSkillRatingForTeam( int nTeam ) const;
// Subset of active match players who are currently connected
int GetNumConnectedMatchPlayers() const;
// Indicates that this is a stale match that is ending. In cases such as rolling matches, we never "end" a match,
// just roll into the next one, since "ended" matches indicate that we've terminated our relationship with the
// players/GC.
bool BMatchTerminated() const { return m_bMatchEnded; }
// Indicates we've sent a result for this match. The match may still be active if we're intending to use it to start
// a rolling match or other post-game activities.
bool BSentResult() const { return m_bSentResult; }
// The canonical size of this type of match. Can be passed from the GC and override the match description size.
uint32 GetCanonicalMatchSize() const;
const char *GetMatchMap() const { return m_strMapName.Length() ? m_strMapName.Get() : NULL; }
// Rewards the player with XP based on the count scaled by the XP per unit of that type.
// nCount here is the raw occurances of the action (ie. Points scored, Gold Medals Scored)
void GiveXPRewardToPlayerForAction( CSteamID steamID, CMsgTFXPSource::XPSourceType eType, int nCount );
// Directly assign the value
void GiveXPDirectly( CSteamID steamID, CMsgTFXPSource::XPSourceType eType, int nAmount, bool bCanAwardBonusXP = true );
// Give an XP bonus that increases
void GiveXPBonus( CSteamID steamID,
CMsgTFXPSource_XPSourceType eType,
float flMultipler,
int nBonusPool );
// Is this player allowed to leave the match without incurring an abandon right now
// TODO(JohnS): This should not go from true to false due to race conditions (players clicks DC, sees no warning,
// races with server deciding its unsafe again), but does for MvM late join. The GS-initiated late
// join rework would make it possible to fix that (once it enters too-low-to-latejoin state it stays
// there)
bool BPlayerSafeToLeaveMatch( CSteamID steamID );
protected:
int GetRankForStat( RankStatType_t statType, int nRankIndex, uint32 nValue );
float NormalDistributionCDF( float flValue, float flMu, float flSigma );
private:
CMatchInfo();
CMatchInfo( const CMatchInfo &otherinfo );
// Track a new player participating in our match
void AddPlayer( CSteamID steamID, const CTFLobbyMember *pMemberData, bool bIsLateJoin, int nEntindex, bool bActive );
// Or with an existing player to copy from (e.g. old match)
void AddPlayer( const PlayerMatchData_t &oldPlayer, int nEntIndex, bool bActive );
// Mark a player as dropped from the match
void DropPlayer( CSteamID steamID, TFMatchLeaveReason eReason, bool bWasAbandon );
void SetEnded() { m_bMatchEnded = true; }
CUtlVector < DailyStatsRankBucket_t > m_vDailyStatsRankData;
CUtlVector < PlayerMatchData_t* > m_vMatchRankData;
CUtlString m_strMapName;
bool m_bMatchEnded;
bool m_bSentResult;
// Canonical size for this match type, override passed from GC.
uint32 m_nGCMatchSize;
float m_flBronzePercentile;
float m_flSilverPercentile;
float m_flGoldPercentile;
};
class CTFGCServerSystem : public CGCClientSystem, public GCSDK::ISharedObjectListener, public IServerGCLobby, public CGameEventListener
{
DECLARE_CLASS_GAMEROOT( CTFGCServerSystem, CGCClientSystem );
// Messages that need to do callbacks
friend class ReliableMsgNewMatchForLobby;
friend class ReliableMsgChangeMatchPlayerTeams;
public:
CTFGCServerSystem( void );
~CTFGCServerSystem( void );
// CAutoGameSystemPerFrame
virtual bool Init() OVERRIDE;
virtual void LevelInitPreEntity() OVERRIDE;
virtual void LevelShutdownPostEntity() OVERRIDE;
virtual void Shutdown() OVERRIDE;
virtual void PreClientUpdate() OVERRIDE;
// uint8 FindItemID( CTF_Item *pItem );
void MatchSignOut();
// const char *GetMatchStartTimeString();
// void GameRules_State_Enter( DOTA_GameState newState );
void SetHibernation( bool bHibernating );
bool ShouldHideServer();
// ISharedObjectListener
virtual void SOCreated( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent ) OVERRIDE;
virtual void PreSOUpdate( const CSteamID & steamIDOwner, GCSDK::ESOCacheEvent eEvent ) OVERRIDE { /* do nothing */ }
virtual void SOUpdated( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent ) OVERRIDE;
virtual void PostSOUpdate( const CSteamID & steamIDOwner, GCSDK::ESOCacheEvent eEvent ) OVERRIDE { /* do nothing */ }
virtual void SODestroyed( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent ) OVERRIDE;
virtual void SOCacheSubscribed( const CSteamID & steamIDOwner, GCSDK::ESOCacheEvent eEvent ) OVERRIDE { }
virtual void SOCacheUnsubscribed( const CSteamID & steamIDOwner, GCSDK::ESOCacheEvent eEvent ) OVERRIDE { }
void DumpLobby();
// IServerGCLobby methods
virtual bool HasLobby() const;
virtual bool SteamIDAllowedToConnect(const CSteamID &steamId) const;
virtual void UpdateServerDetails(void);
virtual bool ShouldHibernate();
// IGameEventListener2
virtual void FireGameEvent( IGameEvent *event ) OVERRIDE;
CTFParty* GetPartyForPlayer( CSteamID steamID ) const;
CMatchInfo *GetMatch() { return m_pMatchInfo; }
const CMatchInfo *GetMatch() const { return m_pMatchInfo; }
// Verbose accessor helpers
//
// Get match only if it is live
CMatchInfo *GetLiveMatch() { return ( m_pMatchInfo && !m_pMatchInfo->m_bMatchEnded ) ? m_pMatchInfo : NULL; }
const CMatchInfo *GetLiveMatch() const { return const_cast<CTFGCServerSystem*>(this)->GetLiveMatch(); }
// Get a player only if there is a live match and they are still in the match (not dropped)
CMatchInfo::PlayerMatchData_t *GetLiveMatchPlayer( CSteamID steamID );
const CMatchInfo::PlayerMatchData_t *GetLiveMatchPlayer( CSteamID steamID ) const ;
int GetTeamForLobbyMember( const CSteamID &steamId ) const;
// bool IsLobbyMemberBroadcaster( const CSteamID &steamId ) const;
// ELanguage GetBroadcasterLanguage( const CSteamID &steamId ) const;
float GetFirstConnectTimeForLobbyMember( const CSteamID &steamId ) const;
int GetVoteKickAttemptsByLobbyMember( const CSteamID &steamID ) const;
void IncrementVoteKickAttemptsByLobbyMember( const CSteamID &steamID );
//
// EDOTA_Uploading_Match_Stats UploadingMatchStats() { return m_nUploadingMatchStats; }
// void OnStatsSubmitted( uint32 unMatchID, int32 nReplaySalt );
// uint32 GetLastMatchID() { return m_unLastMatchID; }
// int32 GetLastReplaySalt() { return m_nLastReplaySalt; }
void ClientActive( CSteamID steamIDClient );
void ClientConnected( CSteamID steamIDPlayer, edict_t *pEntity );
void ClientDisconnected( CSteamID steamIDClient );
inline bool IsMMServerModeActive() const { return m_bMMServerMode; }
void MMServerModeChanged();
// void SetRelayedGameServerSteamID( const CSteamID &steamID ) { m_relayedGameServerSteamID = steamID; }
// void SetParentRelayCount( int nParentRelayCount ) { m_nParentRelayCount = nParentRelayCount; }
float GetTimeLastConnectedToGC( void ) { return m_timeLastConnectedToGC; }
void EndManagedMatch( bool bKickPlayersToParties = false );
// Sends match results. Expects the managed match be ended.
/// MvM game rules processing lets us the players have won
void SendMvMVictoryResult();
// Takes ownership of matchResultMsg
void SendCompetitiveMatchResult( GCSDK::CProtoBufMsg< CMsgGC_Match_Result > *matchResultMsg );
// If the GC has confirmed we are in the pool for late joins. GetTimeRequestedLateJoin() can be compared with
// BLateJoinEligible() to reason about delays in the GC making us available for late join.
bool BLateJoinEligible();
double GetTimeRequestedLateJoin() { return m_flTimeRequestedLateJoin; }
// Eject a player from the match, kicking them if they are still present, with given reason.
bool EjectMatchPlayer( CSteamID steamID, TFMatchLeaveReason eReason );
void MatchPlayerVoteKicked( CSteamID steamID );
const MapDef_t* GetNextMapVoteByIndex( int nIndex ) const;
// Changing Match Player Teams
//
// Is game logic allowed to perform team reassignments for this match mode?
bool CanChangeMatchPlayerTeams();
// When game logic changes the team of a match player, this should be called immediately. It is invalid to call this
// if CanChangeMatchPlayerTeams is false.
void ChangeMatchPlayerTeam( CSteamID steamID, TF_GC_TEAM eTeam );
// Multi-player version of the above, less GC traffic when multiple reassignments occur at once.
struct PlayerTeamPair_t { CSteamID steamID; TF_GC_TEAM eTeam; };
template< typename ANY_ALLOCATOR >
void ChangeMatchPlayerTeams( const CUtlVector< PlayerTeamPair_t, ANY_ALLOCATOR > &vecNewTeams );
// Rolling Matches
//
// Some match types let us keep a lobby and roll into a new match. This may not always be allowed depending on GC
// state.
//
// Upon calling RequestNewMatchForLobby, a timer starts, after which the new match is launched in
// LaunchNewMatchForLobby. During this period our match object is the old match, but the lobby may have been updated
// to reflect the new match.
//
// !! Currently if the GC is out of contact, we will speculatively continue with a new match. There are rare cases
// where the GC will return and decline this request, in which case the resulting match will be unofficial and
// not recorded. It should be trivial to add a bool to prevent such speculative matches should it be necessary.
bool CanRequestNewMatchForLobby();
void RequestNewMatchForLobby( const MapDef_t* pNewMap );
// If the match is in a bogus state and has no useful resolution, terminate it and submit a minidump. This usually
// just means reboot the match server.
void AbortInvalidMatchState();
protected:
// CGCClientSystem
virtual void PreInitGC() OVERRIDE;
virtual void PostInitGC() OVERRIDE;
private:
void SendPlayerLeftMatch( CSteamID steamID, TFMatchLeaveReason eReason, bool bAbandoned );
// Send a kick-lobby message for a stale or unexpected lobby
void SendRejectLobby();
// Kick a player that is no longer present in the match and should not be here.
// Returns true if they were present
bool KickRemovedMatchPlayer( CSteamID steamIDClient );
// The lobby object should only be looked at by this class and may be out of date when the GC reboots and similar --
// most users should use the canonical match state in GetMatch()
const CTFGSLobby *GetLobby() const;
CTFGSLobby *GetLobby();
// Accepts a reservation request from the GC, adding this player to our reserved list, and, for MM mode, to the
// match.
void AcceptGCReservation( CSteamID steamID, const CTFLobbyMember *pMemberData, bool bIsLateJoin, int nEntindex, bool bActive );
// Rolling Matches (private)
//
// If we requested a new match for our existing lobby. We don't actually launch the new match for this timer, but
// the GC may get back to us before (or after!) that period, so this tracks that we're not currently in sync with
// our lobby object. See RequestNewMatchForLobby / LaunchNewMatchForLobby and the "Team Assignments" section of the
// big comment at the top of tf_gc_server.cpp
bool BPendingNewMatch() const { return m_flWaitingForNewMatchTime != 0.f; }
// Called at the end of the m_flWaitingForNewMatchTime period to actually create the new match. We could let the
// caller finish the launch process by changing this timer to a bool and making this public.
void LaunchNewMatchForLobby();
// Callbacks from the GC
void ChangeMatchPlayerTeamsResponse( bool bSuccess );
void NewMatchForLobbyResponse( bool bSuccess );
// Static callbacks that are just forwarding to us
static void ChangeMatchPlayerTeamsResponseCallback( GCSDK::CProtoBufMsg<CMsgGCChangeMatchPlayerTeamsResponse>& msg );
static void NewMatchForLobbyResponseCallback( GCSDK::CProtoBufMsg<CMsgGCNewMatchForLobbyResponse>& msg );
bool m_bSetupSchema;
RTime32 m_unGameStartTime;
float m_timeLastSendGameServerInfoAndConnectedPlayers;
ServerMatchmakingState m_eLastGameServerUpdateState;
TF_MatchmakingMode m_eLastGameServerUpdateMatchmakingMode;
CUtlString m_sLastGameServerUpdateMap;
CUtlString m_sLastGameServerUpdateTags;
int m_nLastGameServerUpdateBotCount;
int m_nLastGameServerUpdateMaxHumans;
int m_nLastGameServerUpdateSlotsFree;
uint32 m_nLastGameServerUpdateLobbyMMVersion;
// EDOTA_Uploading_Match_Stats m_nUploadingMatchStats;
// uint32 m_unLastMatchID;
// int32 m_nLastReplaySalt;
CSteamID m_ourSteamID;
CSteamID m_relayedGameServerSteamID;
int m_nParentRelayCount;
bool m_bMMServerMode;
double m_flTimeBecameEmptyWithLobby;
double m_flTimeRequestedLateJoin;
bool m_bLateJoinEligible;
int m_iSavedVisibleMaxPlayers;
bool m_bOverridingVisibleMaxPlayers;
bool m_bWaitingForNewMatchID;
float m_flWaitingForNewMatchTime;
CMvMVictoryInfo m_mvmVictoryInfo;
// Check for match players who have been disconnected for long enough to warrant an abandon and do so.
void MatchPlayerAbandonThink();
void SetMatchPlayerDropped( CSteamID steamID, TFMatchLeaveReason eReason );
void UpdateConnectedPlayersAndServerInfo( CMsgGameServerMatchmakingStatus_Event event, bool bForceSendServerInfo );
CMatchInfo *m_pMatchInfo;
float m_timeLastConnectedToGC;
// DOTAGameVersion m_GameVersion;
};
CTFGCServerSystem *GTFGCClientSystem();
#endif // #ifdef ENABLE_GC_MATCHMAKING
#endif // TF_GC_SERVER_H
|