884 lines
21 KiB
C++
884 lines
21 KiB
C++
// XBLive_MM.CPP
|
|
//
|
|
// Wrappers and extra utilities based around the matchsim generated code.
|
|
// Interface is borrowed from the SOF2 code, but modified quite a bit.
|
|
//
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// SERVER SIDE
|
|
|
|
#include "xblive.h"
|
|
#include "xonline.h"
|
|
#include "match.h"
|
|
#include "xboxcommon.h"
|
|
#include "..\server\server.h"
|
|
#include "../ui/ui_local.h"
|
|
#include "../qcommon/cm_local.h"
|
|
#include "../client/client.h"
|
|
|
|
|
|
// All servers are rated from 1 to 4
|
|
#define MAX_QOS 4
|
|
|
|
|
|
// External tools for servers to grab info from when advertising:
|
|
extern vmCvar_t g_friendlyFire;
|
|
extern vmCvar_t g_maxForceRank;
|
|
extern qboolean HasSetSaberOnly(void);
|
|
extern int RE_RegisterShaderNoMip( const char *name );
|
|
|
|
|
|
// Hardcoded list of map names, so that current map can be an index rather than string
|
|
// This needs to stay up-to-date.
|
|
const char *mapArray[] = {
|
|
"mp/ctf1",
|
|
"mp/ctf2",
|
|
"mp/ctf3",
|
|
"mp/ctf4",
|
|
"mp/ctf5",
|
|
"mp/duel1",
|
|
"mp/duel2",
|
|
"mp/duel3",
|
|
"mp/duel4",
|
|
"mp/duel5",
|
|
"mp/duel6",
|
|
"mp/duel7",
|
|
"mp/duel8",
|
|
"mp/duel9",
|
|
"mp/duel10",
|
|
"mp/ffa1",
|
|
"mp/ffa2",
|
|
"mp/ffa3",
|
|
"mp/ffa4",
|
|
"mp/ffa5",
|
|
"mp/siege_hoth",
|
|
"mp/siege_desert",
|
|
"mp/siege_korriban",
|
|
};
|
|
const int MAP_ARRAY_SIZE = sizeof(mapArray) / sizeof(mapArray[0]);
|
|
|
|
// Long names of all the maps. This should be done some other way.
|
|
const char *mapLongArray[] = {
|
|
"@MENUS_IMPERIAL_DROP_ZONE_ABR",
|
|
"@MENUS_HOTH_WASTELAND_ABR",
|
|
"@MENUS_YAVIN_HILLTOPS_ABR",
|
|
"@MENUS_CORUSCANT_STREETS_ABR",
|
|
"@MENUS_FACTORY_ABR",
|
|
"@MENUS_BESPIN_COURTYARD_ABR",
|
|
"@MENUS_GENERATOR_ROOM_ABR",
|
|
"@MENUS_IMPERIAL_SHAFT_ABR",
|
|
"@MENUS_IMPERIAL_CONTROL_ROOM_ABR",
|
|
"@MENUS_TASPIR_LANDING_ABR",
|
|
"@MENUS_YAVIN_TRAINING_AREA_ABR",
|
|
"@MENUS_RANCOR_PIT_ABR",
|
|
"@MENUS_ABANDONED_CITY_ABR",
|
|
"@MENUS_HOTH_CANYON_ABR",
|
|
"@MENUS_VJUN_FUEL_PROCESSING_ABR",
|
|
"@MENUS_VJUN_SENTINEL_ABR",
|
|
"@MENUS_KORRIBAN_TOMBS_ABR",
|
|
"@MENUS_TATOOINE_CITY_ABR",
|
|
"@MENUS_RIFT_SANCTUARY_ABR",
|
|
"@MENUS_TASPIR_ABR",
|
|
"@MENUS_HOTH_ATTACK_LOWER_ABR",
|
|
"@MENUS_DESERT_RESCUE_LOWER_ABR",
|
|
"@MENUS_KORRIBAN_VALLEY_LOWER_ABR",
|
|
};
|
|
|
|
const char *gameTypeArray[] = {
|
|
"@MENUS_FREE_FOR_ALL_ABR",
|
|
"",
|
|
"",
|
|
"@MENUS_DUEL_ABR",
|
|
"@MENUS_POWERDUEL_ABR",
|
|
"",
|
|
"@MENUS_TEAM_FFA_ABR",
|
|
"@MENUS_SIEGE_ABR",
|
|
"@MENUS_CAPTURE_THE_FLAG_ABR",
|
|
"",
|
|
};
|
|
const int GAMETYPE_ARRAY_SIZE = sizeof(gameTypeArray) / sizeof(gameTypeArray[0]);
|
|
|
|
// Our session, assuming that we're a server
|
|
CSession session; // from match.h
|
|
|
|
// Our two query objects, one used for optimatch and quickmatch,
|
|
// the other for joining games via session ID
|
|
COptiMatchQuery query; // from match.h
|
|
CJoinSessionByIDQuery queryByID; // from match.h
|
|
|
|
// Info about the session we're currently in
|
|
XBLClientData_t xbc;
|
|
JoinType joinVia;
|
|
|
|
// The currently selected optimatch game (from the UI's list)
|
|
int optiMatchIndex = 0;
|
|
|
|
// make a wide char string from a char string
|
|
//
|
|
void charToWchar( WCHAR* wc, char* c)
|
|
{
|
|
assert( wc );
|
|
|
|
if(!c)
|
|
{
|
|
wc[0] = 0;
|
|
return;
|
|
}
|
|
|
|
wsprintfW(wc, L"%hs", c);
|
|
}
|
|
|
|
// make a char string from a widechar string
|
|
//
|
|
void wcharToChar( char* c, WCHAR* wc )
|
|
{
|
|
assert(c && wc);
|
|
|
|
sprintf(c, "%ls", wc);
|
|
}
|
|
|
|
|
|
// Easy conversion between map name and index values
|
|
|
|
// Returns MAP_ARRAY_SIZE if map isn't found
|
|
int mapNameToIndex(const char *mapname)
|
|
{
|
|
for ( int index = 0; index < MAP_ARRAY_SIZE; ++index)
|
|
if (!stricmp(mapname, mapArray[index]))
|
|
return index;
|
|
return MAP_ARRAY_SIZE;
|
|
}
|
|
|
|
// Returns pointer to the table, don't stomp. NULL if not valid
|
|
const char *mapIndexToName(int index)
|
|
{
|
|
if (index >= 0 && index < MAP_ARRAY_SIZE)
|
|
return mapArray[index];
|
|
return NULL;
|
|
}
|
|
|
|
// Returns pointer to the table, don't stomp. NULL if not valid
|
|
const char *mapIndexToLongName(int index)
|
|
{
|
|
if (index >= 0 && index < MAP_ARRAY_SIZE)
|
|
return mapLongArray[index];
|
|
return NULL;
|
|
}
|
|
|
|
// Returns pointer to the table, don't stomp. NULL if not valid
|
|
const char *gameTypeIndexToName(int index)
|
|
{
|
|
if (index >= 0 && index < GAMETYPE_ARRAY_SIZE)
|
|
return gameTypeArray[index];
|
|
return NULL;
|
|
}
|
|
|
|
|
|
// increment the player count advertised on this session
|
|
//
|
|
DWORD XBL_MM_AddPlayer( bool usePrivateSlot )
|
|
{
|
|
if (!logged_on || !session.Exists())
|
|
return -1;
|
|
|
|
// SOF2 had a really bad system that checked for server's friends. We do what
|
|
// MS says is good - all invites/joins go into private, everyone else goes public.
|
|
if( usePrivateSlot && session.PrivateOpen )
|
|
{
|
|
session.PrivateFilled++;
|
|
session.PrivateOpen--;
|
|
session.TotalPlayers++;
|
|
}
|
|
else
|
|
{
|
|
// Either joined via matchmaking, or we don't have any more private slots
|
|
// Give them a public slot.
|
|
if ( session.PublicOpen )
|
|
{
|
|
session.PublicFilled++;
|
|
session.PublicOpen--;
|
|
session.TotalPlayers++;
|
|
}
|
|
else
|
|
{
|
|
assert( 0 );
|
|
}
|
|
}
|
|
|
|
session.Update();
|
|
|
|
return 0;
|
|
}
|
|
|
|
// decrement the player count advertised on this session
|
|
//
|
|
DWORD XBL_MM_RemovePlayer( bool usePrivateSlot )
|
|
{
|
|
if(!logged_on || !session.Exists())
|
|
return -1;
|
|
|
|
// The value we're given only tells us if they were eligble for a private slot when
|
|
// they joined, but they may have actually ended up in a public slot. As such, this
|
|
// can get pretty wacky. Oh well.
|
|
if ( usePrivateSlot && session.PrivateFilled )
|
|
{
|
|
session.PrivateFilled--;
|
|
session.PrivateOpen++;
|
|
session.TotalPlayers--;
|
|
}
|
|
else
|
|
{
|
|
if ( session.PublicFilled )
|
|
{
|
|
session.PublicFilled--;
|
|
session.PublicOpen++;
|
|
session.TotalPlayers--;
|
|
}
|
|
else
|
|
{
|
|
assert( 0 );
|
|
}
|
|
}
|
|
|
|
session.Update();
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Is there a public slot available? SV_DirectConnect needs to know so it can reject
|
|
// clients when several people try to connect at once:
|
|
bool XBL_MM_PublicSlotAvailable( void )
|
|
{
|
|
return session.PublicOpen;
|
|
}
|
|
|
|
// SOF2 had some silly two-stage thing. They stored off the session parms here, then used
|
|
// a couple globals to delay advertisement until later. I'm going to try and avoid that
|
|
void XBL_MM_Init_Session()
|
|
{
|
|
// Fill in # of slots. OpenPublic is total slots, minus one for server, minus # reserved for private
|
|
session.PrivateFilled = 0;
|
|
session.PrivateOpen = sv_privateClients->integer;
|
|
session.PublicFilled = (com_dedicated->integer ? 0 : 1); // Non-dedicated server fills a slot
|
|
session.PublicOpen = sv_maxclients->integer - (session.PrivateOpen + session.PublicFilled);
|
|
session.TotalPlayers = session.PublicFilled;
|
|
|
|
// Get current map index, and gametype
|
|
int index = mapNameToIndex( sv_mapname->string );
|
|
if (index == MAP_ARRAY_SIZE)
|
|
{
|
|
Com_Error( ERR_FATAL, "Bad map name: %s\n", sv_mapname->string );
|
|
}
|
|
session.CurrentMap = index;
|
|
session.GameType = sv_gametype->integer;
|
|
|
|
// Copy the host's gamertag to the session name
|
|
XONLINE_USER* pHostAccount = XBL_GetUserInfo( XBL_GetSelectedAccountIndex() );
|
|
WCHAR sessionName[XONLINE_GAMERTAG_SIZE] = { 0 };
|
|
if ( pHostAccount )
|
|
{
|
|
charToWchar( sessionName, pHostAccount->szGamertag );
|
|
}
|
|
else
|
|
{
|
|
charToWchar( sessionName, "unknown" );
|
|
}
|
|
session.SetSessionName( sessionName );
|
|
|
|
// All other game options:
|
|
session.FriendlyFire = g_friendlyFire.integer;
|
|
session.JediMastery = g_maxForceRank.integer;
|
|
session.SaberOnly = HasSetSaberOnly();
|
|
session.Dedicated = com_dedicated->integer;
|
|
|
|
// Actually create the session. If we don't call Process immediately, it explodes
|
|
HRESULT hr = session.Create();
|
|
if (hr != S_OK)
|
|
{
|
|
Com_Error( ERR_DROP, "Failed to create session: 0x%x\n", hr );
|
|
}
|
|
|
|
do
|
|
{
|
|
if( !XBL_PumpLogon() )
|
|
return;
|
|
hr = session.Process();
|
|
} while ( session.IsCreating() );
|
|
|
|
// VVFIXME - Better error handling
|
|
if ( !session.Exists() )
|
|
{
|
|
Com_Error( ERR_DROP, "Failed to create session #2: 0x%x\n", hr );
|
|
}
|
|
|
|
// Fix for a bug. Server was using Notification API before advertising, so for
|
|
// the first few seconds, had the wrong sessionID, and thus couldn't invite.
|
|
// Force an update now:
|
|
XBL_F_CheckJoinableStatus( true );
|
|
}
|
|
|
|
void XBL_MM_Update_Session()
|
|
{
|
|
// VVFIXME - Do we need to ensure that slot counts are right?
|
|
// Our gamertag hasn't changed (I hope) so we leave that alone.
|
|
|
|
// Get current map index, and gametype
|
|
int index = mapNameToIndex( sv_mapname->string );
|
|
if (index == MAP_ARRAY_SIZE)
|
|
{
|
|
Com_Error( ERR_FATAL, "Bad map name: %s\n", sv_mapname->string );
|
|
}
|
|
session.CurrentMap = index;
|
|
session.GameType = sv_gametype->integer;
|
|
|
|
// All other game options:
|
|
session.FriendlyFire = g_friendlyFire.integer;
|
|
session.JediMastery = g_maxForceRank.integer;
|
|
session.SaberOnly = HasSetSaberOnly();
|
|
session.Dedicated = com_dedicated->integer;
|
|
|
|
// Update the advertised session info
|
|
session.Update();
|
|
}
|
|
|
|
// Ensure that our session is being advertised correctly
|
|
// Will update map name after a level change, etc...
|
|
void XBL_MM_Advertise()
|
|
{
|
|
// If not on xboxlive dont post server
|
|
if(!logged_on)
|
|
return;
|
|
|
|
if ( session.Exists() )
|
|
{
|
|
// Our session is already being advertised, update it
|
|
XBL_MM_Update_Session();
|
|
}
|
|
else
|
|
{
|
|
// Brand new session
|
|
XBL_MM_Init_Session();
|
|
}
|
|
}
|
|
|
|
|
|
// Finish off and tidy up match making
|
|
void XBL_MM_Shutdown( bool processLogon )
|
|
{
|
|
// We're killing our session, no matter what
|
|
session.Delete();
|
|
|
|
// XDK code to finish this off, while not letting the logon task expire,
|
|
// except if we're already leaving live, where we can get into a recursive
|
|
// com_error situation:
|
|
HRESULT hr;
|
|
do
|
|
{
|
|
if( processLogon && !XBL_PumpLogon() )
|
|
return;
|
|
hr = session.Process();
|
|
} while ( session.IsDeleting() );
|
|
}
|
|
|
|
|
|
// Search for and join a good online server
|
|
// GameType is optional - X_MATCH_NULL_INTEGER to omit
|
|
bool XBL_MM_QuickMatch(ULONGLONG GameType)
|
|
{
|
|
// Reuse our optimatch query object. VVFIXME - needs to be torn down first?
|
|
|
|
HRESULT hr = query.Query(
|
|
GameType,
|
|
X_MATCH_NULL_INTEGER, // CurrentMap
|
|
0, // MinimumPlayers
|
|
8, // MaximumPlayers
|
|
X_MATCH_NULL_INTEGER, // FriendlyFire
|
|
X_MATCH_NULL_INTEGER, // JediMastery
|
|
X_MATCH_NULL_INTEGER, // SaberOnly
|
|
X_MATCH_NULL_INTEGER); // Dedicated
|
|
if ( FAILED( hr ) )
|
|
{
|
|
UI_xboxErrorPopup( XB_POPUP_MATCHMAKING_ERROR );
|
|
return false;
|
|
}
|
|
// Keep servicing the query until it completes.
|
|
// The logon task must also be serviced in order to remain connected.
|
|
do
|
|
{
|
|
if( !XBL_PumpLogon() )
|
|
return false;
|
|
hr = query.Process();
|
|
} while( query.IsRunning() );
|
|
|
|
if( !query.Succeeded() )
|
|
{
|
|
UI_xboxErrorPopup( XB_POPUP_MATCHMAKING_ERROR );
|
|
return false;
|
|
}
|
|
|
|
// VVFIXME - Need to do probing, and pick best session, not just the first one
|
|
if (!query.Results.Size())
|
|
{
|
|
UI_xboxErrorPopup( XB_POPUP_QUICKMATCH_NO_RESULTS );
|
|
return false;
|
|
}
|
|
|
|
COptiMatchResult &server(query.Results[0]);
|
|
|
|
XBL_MM_SetJoinType( VIA_QUICKMATCH );
|
|
// VVFIXME - Experiment, leave this out so that the screen doesn't get trashed
|
|
// right as we connect?
|
|
// Menus_CloseAll();
|
|
Net_XboxConnect(&server.SessionID, &server.KeyExchangeKey, &server.HostAddress);
|
|
/*
|
|
// warn of lag and join(or not) via ui
|
|
//
|
|
if( cls.globalServers[bestSesh].ping < MINIMUM_QOS )
|
|
{
|
|
joinServerSlot = bestSesh;
|
|
Cvar_Set(CVAR_UI_XBOXREBOOTTITLE, " ");
|
|
Cvar_Set(CVAR_UI_XBOXREBOOTMESSAGE, StringTable_Get(XSTR_GAMEPLAY_AFFECTED_BY_NETWORK));
|
|
//Menus_ActivateByName("ingame_small_bgd");
|
|
Menus_CloseByName("quickmatch_popup");
|
|
Menus_ActivateByName("xblive_slow_warning");
|
|
return false;
|
|
}
|
|
XBL_MM_JoinServer( bestSesh );
|
|
*/
|
|
return true;
|
|
}
|
|
|
|
|
|
// work out the value of a servers QoS
|
|
//
|
|
int getQoSValue( XNQOSINFO* qosInfo )
|
|
{
|
|
if ( !qosInfo )
|
|
return 0; // Sentinel for an as-yet-unknown ping
|
|
|
|
WORD avgPing = qosInfo->wRttMedInMsecs;
|
|
if (avgPing < 150)
|
|
return 3;
|
|
else if (avgPing < 250)
|
|
return 2;
|
|
else
|
|
return 1;
|
|
}
|
|
|
|
|
|
// Find currently running sessions
|
|
int XBL_MM_Find_Session(ULONGLONG GameType, // Optional: X_MATCH_NULL_INTEGER to omit
|
|
const char *mapName, // Optional: "any" to omit
|
|
ULONGLONG MinimumPlayers,
|
|
ULONGLONG MaximumPlayers,
|
|
ULONGLONG FriendlyFire, // Optional: X_MATCH_NULL_INTEGER to omit
|
|
ULONGLONG JediMastery, // Optional: X_MATCH_NULL_INTEGER to omit
|
|
ULONGLONG SaberOnly, // Optional: X_MATCH_NULL_INTEGER to omit
|
|
ULONGLONG Dedicated) // Optional: X_MATCH_NULL_INTEGER to omit
|
|
{
|
|
// Kill off a previous query that's still running
|
|
query.Cancel();
|
|
|
|
ULONGLONG CurrentMap = mapNameToIndex( mapName );
|
|
if( CurrentMap == MAP_ARRAY_SIZE )
|
|
CurrentMap = X_MATCH_NULL_INTEGER;
|
|
|
|
HRESULT hr = query.Query(
|
|
GameType,
|
|
CurrentMap,
|
|
MinimumPlayers,
|
|
MaximumPlayers,
|
|
FriendlyFire,
|
|
JediMastery,
|
|
SaberOnly,
|
|
Dedicated);
|
|
if ( FAILED( hr ) )
|
|
{
|
|
Com_Error( ERR_DROP, "@MENUS_XBOX_LOST_CONNECTION" );
|
|
}
|
|
// Keep servicing the query until it completes.
|
|
// The logon task must also be serviced in order
|
|
// to remain connected.
|
|
do
|
|
{
|
|
if( !XBL_PumpLogon() )
|
|
return 0;
|
|
hr = query.Process();
|
|
} while( query.IsRunning() );
|
|
|
|
if( !query.Succeeded() )
|
|
{
|
|
Com_Error( ERR_DROP, "@MENUS_XBOX_LOST_CONNECTION" );
|
|
}
|
|
|
|
if (!query.Results.Size())
|
|
{
|
|
// VVFIXME - handle search that returns no results
|
|
Com_Printf("No games found in query\n");
|
|
return 0;
|
|
}
|
|
|
|
// The above gets all results, and does initial (can we connect) probing.
|
|
// We wait for that, then begin real probing here, which is done async.
|
|
query.Probe();
|
|
optiMatchIndex = 0;
|
|
|
|
return query.Results.Size();
|
|
}
|
|
|
|
|
|
// Joins the specified server from the optimatch results list
|
|
void XBL_MM_JoinServer( void )
|
|
{
|
|
// Sanity check
|
|
if( optiMatchIndex < 0 || optiMatchIndex >= query.Results.Size() )
|
|
return;
|
|
|
|
COptiMatchResult *res = &query.Results[ optiMatchIndex ];
|
|
|
|
XBL_MM_SetJoinType( VIA_OPTIMATCH );
|
|
Net_XboxConnect( &res->SessionID, &res->KeyExchangeKey, &res->HostAddress );
|
|
|
|
query.Cancel();
|
|
}
|
|
|
|
|
|
// Single function to run a query by session ID. Several other functions
|
|
// use this to get various results (or join servers)
|
|
static HRESULT runSessionIDQuery( const XNKID *sessionID )
|
|
{
|
|
HRESULT hr;
|
|
hr = queryByID.Query( *(ULONGLONG *)sessionID );
|
|
if ( FAILED( hr ) )
|
|
{
|
|
Com_Error( ERR_FATAL, "Session ID query failed with 0x%x\n", hr );
|
|
}
|
|
|
|
// Keep servicing the query until it completes.
|
|
// The logon task must also be serviced in order to remain connected.
|
|
do
|
|
{
|
|
if( !XBL_PumpLogon() )
|
|
return 0;
|
|
hr = queryByID.Process();
|
|
} while( queryByID.IsRunning() );
|
|
|
|
if ( !queryByID.Succeeded() )
|
|
{
|
|
Com_Error( ERR_FATAL, "Session ID query failed #2 with 0x%x\n", hr );
|
|
}
|
|
|
|
return hr;
|
|
}
|
|
|
|
// Returns true if the sessions traffic performs lower than minimum levels for invite
|
|
bool XBL_MM_ThisSessionIsLagging( const XNKID *sessionID )
|
|
{
|
|
// DO the query first...
|
|
runSessionIDQuery( sessionID );
|
|
|
|
// This happens if it's a different title, for example.
|
|
if ( !queryByID.Results.Size() )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Start probing
|
|
queryByID.Probe();
|
|
|
|
// Keep probing, and keep the logon task serviced.
|
|
// VVFIXME - Timeout here?
|
|
HRESULT hr;
|
|
do
|
|
{
|
|
if( !XBL_PumpLogon() )
|
|
return false;
|
|
hr = queryByID.Process();
|
|
} while( queryByID.IsProbing() );
|
|
|
|
return getQoSValue( queryByID.Results[0].pQosInfo ) < MINIMUM_QOS;
|
|
}
|
|
|
|
namespace ui
|
|
{
|
|
extern void Menus_CloseAll( void );
|
|
}
|
|
|
|
// Connnect to a game we have the session ID for
|
|
bool XBL_MM_ConnectViaSessionID( const XNKID *sessionID, bool invited )
|
|
{
|
|
// Do the query...
|
|
runSessionIDQuery( sessionID );
|
|
|
|
// No results? (Should only be one)
|
|
if ( !queryByID.Results.Size() )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
CJoinSessionByIDResult &server(queryByID.Results[0]);
|
|
if (server.PrivateOpen < 1 && server.PublicOpen < 1)
|
|
{
|
|
// No slots available
|
|
return false;
|
|
}
|
|
|
|
// OK. If we're currently hosting a dedicated server, we need to clean things up:
|
|
if( com_dedicated->integer )
|
|
{
|
|
// Marks us as no longer joinable or playing:
|
|
XBL_F_OnClientLeaveSession();
|
|
|
|
// Code copied from "Leave" in ui_main.c
|
|
Cvar_SetValue( "cl_paused", 0 );
|
|
Key_SetCatcher( KEYCATCH_UI );
|
|
Cvar_SetValue( "cl_running", 1 );
|
|
SV_Shutdown( "Server quit\n" );
|
|
|
|
CL_ShutdownUI();
|
|
extern void RE_Shutdown( qboolean destroyWindow );
|
|
RE_Shutdown( qfalse );
|
|
CL_Disconnect( qtrue );
|
|
|
|
Cvar_SetValue( "dedicated", 0 );
|
|
Cvar_SetValue( "ui_dedicated", 0 );
|
|
CL_FlushMemory();
|
|
|
|
ui::Menus_CloseAll();
|
|
}
|
|
else if( cls.state == CA_ACTIVE )
|
|
{
|
|
// If we were already playing, we need to disconnect earlier than normal,
|
|
// to clean up XBL stuff. Don't delete textures, we need to draw the connect screen:
|
|
CL_Disconnect( qtrue, qfalse );
|
|
Net_XboxDisconnect();
|
|
}
|
|
|
|
Net_XboxConnect(&server.SessionID, &server.KeyExchangeKey, &server.HostAddress);
|
|
|
|
return true;
|
|
}
|
|
|
|
// Check if session exists
|
|
bool XBL_MM_IsSessionIDValid(const XNKID *sessionID)
|
|
{
|
|
// Do the query...
|
|
runSessionIDQuery( sessionID );
|
|
|
|
// Did we get any hits?
|
|
return bool( queryByID.Results.Size() );
|
|
}
|
|
|
|
// Remember how we started connecting to a session, in case it turns out to suck
|
|
void XBL_MM_SetJoinType(JoinType way)
|
|
{
|
|
joinVia = way;
|
|
}
|
|
|
|
// Are we eligible for a private slot in the game we're connecting to?
|
|
bool XBL_MM_CanUsePrivateSlot( void )
|
|
{
|
|
return ((joinVia == VIA_FRIEND_JOIN) ||
|
|
(joinVia == VIA_FRIEND_INVITE));
|
|
}
|
|
|
|
extern void UI_JoinSession();
|
|
extern void UI_JoinInvite();
|
|
|
|
// Join a server even after recieving a message saying it was low QoS
|
|
void XBL_MM_JoinLowQOSSever()
|
|
{
|
|
if( joinVia == VIA_FRIEND_JOIN )
|
|
{
|
|
UI_JoinSession();
|
|
}
|
|
|
|
else if( joinVia == VIA_FRIEND_INVITE)
|
|
{
|
|
UI_JoinInvite();
|
|
}
|
|
|
|
/*
|
|
else if( joinVia == VIA_OPTIMATCH )
|
|
{
|
|
XBL_MM_JoinServer( joinServerSlot );
|
|
}
|
|
|
|
else if( joinVia == VIA_QUICKMATCH )
|
|
{
|
|
XBL_MM_JoinServer( joinServerSlot );
|
|
}
|
|
*/
|
|
}
|
|
|
|
// Bail out of joining a server after recieving a message saying it was low QoS
|
|
void XBL_MM_DontJoinLowQOSSever()
|
|
{
|
|
// If we were originally invited, make sure to decline the invitation
|
|
if( joinVia == VIA_FRIEND_INVITE )
|
|
{
|
|
HRESULT result = XBL_F_PerformMenuAction(UI_F_GAMEDECLINE);
|
|
}
|
|
|
|
// All other scenarios require no extra work
|
|
}
|
|
|
|
// run this code every game tick
|
|
// will only be called while logged on
|
|
//
|
|
void XBL_MM_Tick()
|
|
{
|
|
// VVFIXME - SOF2 re-advertised after some crazy timeout.
|
|
|
|
// New version just ticks the session object as well, it does nothing if it's not real
|
|
HRESULT hr = session.Process();
|
|
#ifdef _DEBUG
|
|
if ( FAILED( hr ) )
|
|
Com_Printf("session.Process() failed: %s\n", getXBLErrorName(hr));
|
|
#endif
|
|
|
|
// The only time we need to do async work on the query is when probing for QoS
|
|
if ( query.IsProbing() )
|
|
{
|
|
query.Process();
|
|
}
|
|
}
|
|
|
|
//
|
|
// Big pile of functions that the UI uses to pull system link results for display
|
|
//
|
|
int Syslink_GetNumServers( void )
|
|
{
|
|
return cls.numlocalservers;
|
|
}
|
|
|
|
extern serverInfo_t *SysLink_GetServer( int index );
|
|
|
|
const char *Syslink_GetServerMap( const int index )
|
|
{
|
|
serverInfo_t *s = SysLink_GetServer( index );
|
|
|
|
int mapIndex = mapNameToIndex( s->mapName );
|
|
if( mapIndex == MAP_ARRAY_SIZE )
|
|
return "";
|
|
|
|
return mapIndexToLongName( mapIndex );
|
|
}
|
|
|
|
const char *Syslink_GetServerClients( const int index )
|
|
{
|
|
serverInfo_t *s = SysLink_GetServer( index );
|
|
|
|
return va( "%d/%d", s->clients, s->maxClients );
|
|
}
|
|
|
|
const char *Syslink_GetServerGametype( const int index )
|
|
{
|
|
serverInfo_t *s = SysLink_GetServer( index );
|
|
|
|
return gameTypeIndexToName( s->gameType );
|
|
}
|
|
|
|
int Syslink_GetServerSaberOnly( const int index )
|
|
{
|
|
serverInfo_t *s = SysLink_GetServer( index );
|
|
|
|
if( s->saberOnly )
|
|
return RE_RegisterShaderNoMip( "gfx/mp/saber_only" );
|
|
else
|
|
return -1;
|
|
}
|
|
|
|
int Syslink_GetServerDisableForce( const int index )
|
|
{
|
|
serverInfo_t *s = SysLink_GetServer( index );
|
|
|
|
if( s->forceDisable )
|
|
return RE_RegisterShaderNoMip( "gfx/mp/noforce_bw" );
|
|
else
|
|
return -1;
|
|
}
|
|
|
|
//
|
|
// Big pile of functions that the UI uses to pull optimatch results for display
|
|
//
|
|
int XBL_MM_GetNumServers( void )
|
|
{
|
|
return query.Results.Size();
|
|
}
|
|
|
|
const char *XBL_MM_GetServerName( const int index )
|
|
{
|
|
static char retVal[XATTRIB_SESSION_NAME_MAX_LEN+1];
|
|
|
|
wcharToChar( retVal, query.Results[index].SessionName );
|
|
return retVal;
|
|
}
|
|
|
|
const char *XBL_MM_GetServerMap( const int index )
|
|
{
|
|
return mapIndexToLongName( query.Results[index].CurrentMap );
|
|
}
|
|
|
|
const char *XBL_MM_GetServerClients( const int index )
|
|
{
|
|
int openSlots = query.Results[index].PublicOpen + query.Results[index].PrivateOpen;
|
|
int filledSlots = query.Results[index].PublicFilled + query.Results[index].PrivateFilled;
|
|
return va( "%d/%d", filledSlots, filledSlots + openSlots );
|
|
}
|
|
|
|
const char *XBL_MM_GetServerGametype( const int index )
|
|
{
|
|
return gameTypeIndexToName( query.Results[index].GameType );
|
|
}
|
|
|
|
int XBL_MM_GetServerSaberOnly( const int index )
|
|
{
|
|
if( query.Results[index].SaberOnly )
|
|
return RE_RegisterShaderNoMip( "gfx/mp/saber_only" );
|
|
else
|
|
return -1;
|
|
}
|
|
|
|
int XBL_MM_GetServerDisableForce( const int index )
|
|
{
|
|
if( query.Results[index].JediMastery == 0 )
|
|
return RE_RegisterShaderNoMip( "gfx/mp/noforce_bw" );
|
|
else
|
|
return -1;
|
|
}
|
|
|
|
int XBL_MM_GetServerPing( const int index )
|
|
{
|
|
int pingVal = getQoSValue( query.Results[index].pQosInfo );
|
|
|
|
switch( pingVal )
|
|
{
|
|
case 1: // Bad
|
|
return RE_RegisterShaderNoMip( "gfx/mp/dot_red" );
|
|
case 2: // Average
|
|
return RE_RegisterShaderNoMip( "gfx/mp/dot_yellow" );
|
|
case 3: // Good
|
|
return RE_RegisterShaderNoMip( "gfx/mp/dot_green" );
|
|
case 0: // Unknown
|
|
default:
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
void XBL_MM_SetChosenServerIndex( const int index )
|
|
{
|
|
// Just save this away:
|
|
optiMatchIndex = index;
|
|
|
|
// Also set the cvar that UI uses to draw the host's gamertag in the status line
|
|
Cvar_Set( "xbl_gamertag", XBL_MM_GetServerName( index ) );
|
|
}
|
|
|
|
// Used by the UI when the user picks a server to join, or backs out of the results screen
|
|
void XBL_MM_CancelProbing( void )
|
|
{
|
|
query.Cancel();
|
|
}
|