warzone2100/src/multiopt.c

918 lines
22 KiB
C

/*
This file is part of Warzone 2100.
Copyright (C) 1999-2004 Eidos Interactive
Copyright (C) 2005-2007 Warzone Resurrection Project
Warzone 2100 is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
Warzone 2100 is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Warzone 2100; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
/*
* MultiOpt.c
*
* Alex Lee,97/98, Pumpkin Studios
*
* Routines for setting the game options and starting the init process.
*/
#include "lib/framework/frame.h" // for everything
#include "map.h"
#include "game.h" // for loading maps
#include "message.h" // for clearing messages.
#include "main.h"
#include "display3d.h" // for changing the viewpoint
#include "power.h"
#include "lib/widget/widget.h"
#include "lib/gamelib/gtime.h"
#include "lib/netplay/netplay.h"
#include "hci.h"
#include "configuration.h" // lobby cfg.
#include "clparse.h"
#include "lib/ivis_common/piestate.h"
#include "component.h"
#include "console.h"
#include "multiplay.h"
#include "lib/sound/audio.h"
#include "multijoin.h"
#include "frontend.h"
#include "levels.h"
#include "multistat.h"
#include "multiint.h"
#include "multilimit.h"
#include "multigifts.h"
#include "aiexperience.h" //for beacon messages
#include "multiint.h"
#include "multirecv.h"
// ////////////////////////////////////////////////////////////////////////////
// External Variables
extern char buildTime[8];
// ////////////////////////////////////////////////////////////////////////////
// ////////////////////////////////////////////////////////////////////////////
// send complete game info set!
// dpid == 0 for no new players.
void sendOptions(uint32_t dest, uint32_t play)
{
int i, j;
NETbeginEncode(NET_OPTIONS, NET_ALL_PLAYERS);
// First send information about the game
NETuint8_t(&game.type);
NETstring(game.map, 128);
NETstring(game.version, 8);
NETuint8_t(&game.maxPlayers);
NETstring(game.name, 128);
NETbool(&game.fog);
NETuint32_t(&game.power);
NETuint8_t(&game.base);
NETuint8_t(&game.alliance);
for (i = 0; i < MAX_PLAYERS; i++)
{
NETuint8_t(&game.skDiff[i]);
}
// Now the dpid array
for (i = 0; i < MAX_PLAYERS; i++)
{
// We should probably re-define player2dpid as a uint8_t
NETint32_t(&player2dpid[i]);
}
// Send the list of who is still joining
for (i = 0; i < MAX_PLAYERS; i++)
{
NETbool(&ingame.JoiningInProgress[i]);
}
// Send dest and play
NETuint32_t(&dest);
NETuint32_t(&play);
// Send over the list of player colours
for (i = 0; i < MAX_PLAYERS; i++)
{
NETuint8_t(&PlayerColour[i]);
}
// Same goes for the alliances
for (i = 0; i < MAX_PLAYERS; i++)
{
for (j = 0; j < MAX_PLAYERS; j++)
{
NETuint8_t(&alliances[i][j]);
}
}
// Send their team (this is apparently not related to alliances
for (i = 0; i < MAX_PLAYERS; i++)
{
// This should also be a uint8_t
NETint32_t(&playerTeamGUI[i]);
}
// Send the number of structure limits to expect
NETuint32_t(&ingame.numStructureLimits);
// Send the structures changed
for (i = 0; i < ingame.numStructureLimits; i++)
{
NETuint8_t(&ingame.pStructureLimits[i].id);
NETuint8_t(&ingame.pStructureLimits[i].limit);
}
// Send ready status of all players
for (i = 0; i < MAX_PLAYERS; i++)
{
NETbool(&bPlayerReadyGUI[i]);
}
NETend();
}
// ////////////////////////////////////////////////////////////////////////////
/*!
* check the wdg files that are being used.
*/
static BOOL checkGameWdg(const char *nm)
{
LEVEL_DATASET *lev;
for (lev = psLevels; lev; lev = lev->psNext)
{
if (strcmp(lev->pName, nm) == 0)
{
return true;
}
}
return false;
}
// ////////////////////////////////////////////////////////////////////////////
// options for a game. (usually recvd in frontend)
void recvOptions()
{
int i, j;
UDWORD play;
UDWORD newPl;
NETbeginDecode(NET_OPTIONS);
// Get general information about the game
NETuint8_t(&game.type);
NETstring(game.map, 128);
NETstring(game.version, 8);
NETuint8_t(&game.maxPlayers);
NETstring(game.name, 128);
NETbool(&game.fog);
NETuint32_t(&game.power);
NETuint8_t(&game.base);
NETuint8_t(&game.alliance);
for (i = 0; i < MAX_PLAYERS; i++)
{
NETuint8_t(&game.skDiff[i]);
}
// Check the version
if (strncmp(game.version, buildTime, 8) != 0)
{
debug(LOG_ERROR, "Host is running a different version of Warzone2100.");
}
// Now the dpid array
for (i = 0; i < MAX_PLAYERS; i++)
{
NETint32_t(&player2dpid[i]);
}
// Send the list of who is still joining
for (i = 0; i < MAX_PLAYERS; i++)
{
NETbool(&ingame.JoiningInProgress[i]);
}
NETuint32_t(&newPl);
NETuint32_t(&play);
// Player colours
for (i = 0; i < MAX_PLAYERS; i++)
{
NETuint8_t(&PlayerColour[i]);
}
// Alliances
for (i = 0; i < MAX_PLAYERS; i++)
{
for (j = 0; j < MAX_PLAYERS; j++)
{
NETuint8_t(&alliances[i][j]);
}
}
// Teams
for (i = 0; i < MAX_PLAYERS; i++)
{
NETint32_t(&playerTeamGUI[i]);
}
// Free any structure limits we may have in-place
if (ingame.numStructureLimits)
{
ingame.numStructureLimits = 0;
free(ingame.pStructureLimits);
ingame.pStructureLimits = NULL;
}
// Get the number of structure limits to expect
NETuint32_t(&ingame.numStructureLimits);
// If there were any changes allocate memory for them
if (ingame.numStructureLimits)
{
ingame.pStructureLimits = malloc(ingame.numStructureLimits * sizeof(MULTISTRUCTLIMITS));
}
for (i = 0; i < ingame.numStructureLimits; i++)
{
NETuint8_t(&ingame.pStructureLimits[i].id);
NETuint8_t(&ingame.pStructureLimits[i].limit);
}
// Receive ready status of all players
for (i = 0; i < MAX_PLAYERS; i++)
{
NETbool(&bPlayerReadyGUI[i]);
}
NETend();
// Post process
if (newPl != 0)
{
// If we are the new player
if (newPl == NetPlay.dpidPlayer)
{
selectedPlayer = play; // Select player
NETplayerInfo(); // Get player info
powerCalculated = false; // Turn off any power requirements
}
// Someone else is joining.
else
{
setupNewPlayer(newPl, play);
}
}
// Do the skirmish slider settings if they are up
for (i = 0; i < MAX_PLAYERS; i++)
{
if (widgGetFromID(psWScreen, MULTIOP_SKSLIDE + i))
{
widgSetSliderPos(psWScreen, MULTIOP_SKSLIDE + i, game.skDiff[i]);
}
}
// See if we have the map or not
if (!checkGameWdg(game.map))
{
// Request the map from the host
NETbeginEncode(NET_REQUESTMAP, NET_ALL_PLAYERS);
NETuint32_t(&NetPlay.dpidPlayer);
NETend();
addConsoleMessage("MAP REQUESTED!",DEFAULT_JUSTIFY, SYSTEM_MESSAGE);
}
else
{
loadMapPreview();
}
}
// ////////////////////////////////////////////////////////////////////////////
// Host Campaign.
BOOL hostCampaign(char *sGame, char *sPlayer)
{
PLAYERSTATS playerStats;
UDWORD pl,numpl,i,j;
debug(LOG_WZ, "Hosting campaign: '%s', player: '%s'", sGame, sPlayer);
freeMessages();
// If we had a single player (i.e. campaign) game before this value will
// have been set to 0. So revert back to the default value.
if (game.maxPlayers == 0)
{
game.maxPlayers = 4;
}
NEThostGame(sGame, sPlayer, game.type, 0, 0, 0, game.maxPlayers); // temporary stuff
for (i = 0; i < MAX_PLAYERS; i++)
{
player2dpid[i] = 0;
setPlayerName(i, ""); //Clear custom names (use default ones instead)
}
pl = rand()%game.maxPlayers; //pick a player
player2dpid[pl] = NetPlay.dpidPlayer; // add ourselves to the array.
selectedPlayer = pl;
ingame.localJoiningInProgress = true;
ingame.JoiningInProgress[selectedPlayer] = true;
bMultiPlayer = true; // enable messages
loadMultiStats(sPlayer,&playerStats); // stats stuff
setMultiStats(NetPlay.dpidPlayer,playerStats,false);
setMultiStats(NetPlay.dpidPlayer,playerStats,true);
bPlayerReadyGUI[0] = false;
if(!NetPlay.bComms)
{
NETplayerInfo();
strcpy(NetPlay.players[0].name,sPlayer);
numpl = 1;
}
else
{
numpl = NETplayerInfo();
}
// may be more than one player already. check and resolve!
if(numpl >1)
{
for(j = 0;j<MAX_PLAYERS;j++)
{
if(NetPlay.players[j].dpid && (NetPlay.players[j].dpid != NetPlay.dpidPlayer))
{
for(i = 0;player2dpid[i] != 0;i++);
player2dpid[i] = NetPlay.players[j].dpid;
}
}
}
return true;
}
// ////////////////////////////////////////////////////////////////////////////
// Join Campaign
BOOL joinCampaign(UDWORD gameNumber, char *sPlayer)
{
PLAYERSTATS playerStats;
if(!ingame.localJoiningInProgress)
{
NETjoinGame(gameNumber, sPlayer); // join
ingame.localJoiningInProgress = true;
loadMultiStats(sPlayer,&playerStats);
setMultiStats(NetPlay.dpidPlayer,playerStats,false);
setMultiStats(NetPlay.dpidPlayer,playerStats,true);
return false;
}
bMultiPlayer = true;
return true;
}
#if 0 // unused - useful?
// ////////////////////////////////////////////////////////////////////////////
// Lobby launched. fires the correct routine when the game was lobby launched.
BOOL LobbyLaunched(void)
{
UDWORD i;
PLAYERSTATS pl = { 0, 0, 0, 0, 0, 0, 0, 0, 0 };
// set the player info as soon as possible to avoid screwy scores appearing elsewhere.
NETplayerInfo();
NETfindGame();
for (i = 0; i < MAX_PLAYERS && NetPlay.players[i].dpid != NetPlay.dpidPlayer; i++);
if(!loadMultiStats(NetPlay.players[i].name, &pl) )
{
return false; // cheating was detected, so fail.
}
setMultiStats(NetPlay.dpidPlayer, pl, false);
setMultiStats(NetPlay.dpidPlayer, pl, true);
// setup text boxes on multiplay screen.
strcpy((char*) sPlayer, NetPlay.players[i].name);
strcpy((char*) game.name, NetPlay.games[0].name);
return true;
}
#endif
// ////////////////////////////////////////////////////////////////////////////
// say goodbye to everyone else
BOOL sendLeavingMsg(void)
{
/*
* Send a leaving message, This resolves a problem with tcpip which
* occasionally doesn't automatically notice a player leaving
*/
NETbeginEncode(NET_LEAVING, NET_ALL_PLAYERS);
{
uint32_t player_id = player2dpid[selectedPlayer];
BOOL host = NetPlay.bHost;
NETuint32_t(&player_id);
NETbool(&host);
}
NETend();
return true;
}
// ////////////////////////////////////////////////////////////////////////////
// called in Init.c to shutdown the whole netgame gubbins.
BOOL multiShutdown(void)
{
// shut down netplay lib.
debug(LOG_MAIN, "shutting down networking");
NETshutdown();
debug(LOG_MAIN, "free game data (structure limits)");
if(ingame.numStructureLimits)
{
ingame.numStructureLimits = 0;
free(ingame.pStructureLimits);
ingame.pStructureLimits = NULL;
}
return true;
}
// ////////////////////////////////////////////////////////////////////////////
// copy tempates from one player to another.
BOOL addTemplate(UDWORD player, DROID_TEMPLATE *psNew)
{
DROID_TEMPLATE *psTempl = malloc(sizeof(DROID_TEMPLATE));
if (psTempl == NULL)
{
debug(LOG_ERROR, "addTemplate: Out of memory");
return false;
}
memcpy(psTempl, psNew, sizeof(DROID_TEMPLATE));
psTempl->pName = NULL;
if (psNew->pName)
{
psTempl->pName = strdup(psNew->pName);
}
psTempl->psNext = apsDroidTemplates[player];
apsDroidTemplates[player] = psTempl;
return true;
}
BOOL addTemplateSet(UDWORD from,UDWORD to)
{
DROID_TEMPLATE *psCurr;
if(from == to)
{
return true;
}
for(psCurr = apsDroidTemplates[from];psCurr;psCurr= psCurr->psNext)
{
addTemplate(to, psCurr);
}
return true;
}
BOOL copyTemplateSet(UDWORD from,UDWORD to)
{
DROID_TEMPLATE *psTempl;
if(from == to)
{
return true;
}
while(apsDroidTemplates[to]) // clear the old template out.
{
psTempl = apsDroidTemplates[to]->psNext;
free(apsDroidTemplates[to]);
apsDroidTemplates[to] = psTempl;
}
return addTemplateSet(from,to);
}
// ////////////////////////////////////////////////////////////////////////////
// setup templates
BOOL multiTemplateSetup(void)
{
UDWORD player, pcPlayer = 0;
switch (game.type)
{
case CAMPAIGN:
for(player=0;player<game.maxPlayers;player++)
{
copyTemplateSet(CAMPAIGNTEMPLATES,player);
}
break;
case SKIRMISH:
// create the pc player list in deathmatch set.
addTemplateSet(CAMPAIGNTEMPLATES,DEATHMATCHTEMPLATES);
addTemplateSet(6,DEATHMATCHTEMPLATES);
addTemplateSet(2,DEATHMATCHTEMPLATES);
//choose which way to do this.
if(isHumanPlayer(CAMPAIGNTEMPLATES))
{
//pc first
for(player=0;player<game.maxPlayers;player++)
{
if(!isHumanPlayer(player))
{
copyTemplateSet(DEATHMATCHTEMPLATES,player);
}
}
//now players.
for(player=0;player<game.maxPlayers;player++)
{
if(isHumanPlayer(player))
{
copyTemplateSet(CAMPAIGNTEMPLATES,player);
}
}
}
else
{
// ensure a copy of pc templates to a pc player.
if(isHumanPlayer(DEATHMATCHTEMPLATES))
{
for(player=0;player<MAX_PLAYERS && isHumanPlayer(player);player++);
if(!isHumanPlayer(player))
{
pcPlayer = player;
copyTemplateSet(DEATHMATCHTEMPLATES,pcPlayer);
}
}
else
{
pcPlayer = DEATHMATCHTEMPLATES;
}
//players first
for(player=0;player<game.maxPlayers;player++)
{
if(isHumanPlayer(player))
{
copyTemplateSet(CAMPAIGNTEMPLATES,player);
}
}
//now pc
for(player=0;player<game.maxPlayers;player++)
{
if(!isHumanPlayer(player))
{
copyTemplateSet(pcPlayer,player);
}
}
}
break;
default:
break;
}
return true;
}
// ////////////////////////////////////////////////////////////////////////////
// remove structures from map before campaign play.
static BOOL cleanMap(UDWORD player)
{
DROID *psD,*psD2;
STRUCTURE *psStruct;
BOOL firstFact,firstRes;
bMultiPlayer = false;
firstFact = true;
firstRes = true;
// reverse so we always remove the last object. re-reverse afterwards.
// reverseObjectList((BASE_OBJECT**)&apsStructLists[player]);
switch(game.base)
{
case CAMP_CLEAN: //clean map
while(apsStructLists[player]) //strip away structures.
{
removeStruct(apsStructLists[player], true);
}
psD = apsDroidLists[player]; // remove all but construction droids.
while(psD)
{
psD2=psD->psNext;
//if(psD->droidType != DROID_CONSTRUCT)
if (!(psD->droidType == DROID_CONSTRUCT ||
psD->droidType == DROID_CYBORG_CONSTRUCT))
{
killDroid(psD);
}
psD = psD2;
}
break;
case CAMP_BASE: //just structs, no walls
psStruct = apsStructLists[player];
while(psStruct)
{
if ( (psStruct->pStructureType->type == REF_WALL)
||(psStruct->pStructureType->type == REF_WALLCORNER)
||(psStruct->pStructureType->type == REF_DEFENSE)
||(psStruct->pStructureType->type == REF_BLASTDOOR)
||(psStruct->pStructureType->type == REF_CYBORG_FACTORY)
||(psStruct->pStructureType->type == REF_COMMAND_CONTROL)
)
{
removeStruct(psStruct, true);
psStruct= apsStructLists[player]; //restart,(list may have changed).
}
else if( (psStruct->pStructureType->type == REF_FACTORY)
||(psStruct->pStructureType->type == REF_RESEARCH)
||(psStruct->pStructureType->type == REF_POWER_GEN))
{
if(psStruct->pStructureType->type == REF_FACTORY )
{
if(firstFact == true)
{
firstFact = false;
removeStruct(psStruct, true);
psStruct= apsStructLists[player];
}
else // don't delete, just rejig!
{
if(((FACTORY*)psStruct->pFunctionality)->capacity != 0)
{
((FACTORY*)psStruct->pFunctionality)->capacity = 0;
((FACTORY*)psStruct->pFunctionality)->productionOutput = (UBYTE)((PRODUCTION_FUNCTION*)psStruct->pStructureType->asFuncList[0])->productionOutput;
psStruct->sDisplay.imd = psStruct->pStructureType->pIMD;
psStruct->body = (UWORD)(structureBody(psStruct));
}
psStruct = psStruct->psNext;
}
}
else if(psStruct->pStructureType->type == REF_RESEARCH)
{
if(firstRes == true)
{
firstRes = false;
removeStruct(psStruct, true);
psStruct= apsStructLists[player];
}
else
{
if(((RESEARCH_FACILITY*)psStruct->pFunctionality)->capacity != 0)
{ // downgrade research
((RESEARCH_FACILITY*)psStruct->pFunctionality)->capacity = 0;
((RESEARCH_FACILITY*)psStruct->pFunctionality)->researchPoints = ((RESEARCH_FUNCTION*)psStruct->pStructureType->asFuncList[0])->researchPoints;
psStruct->sDisplay.imd = psStruct->pStructureType->pIMD;
psStruct->body = (UWORD)(structureBody(psStruct));
}
psStruct=psStruct->psNext;
}
}
else if(psStruct->pStructureType->type == REF_POWER_GEN)
{
if(((POWER_GEN*)psStruct->pFunctionality)->capacity != 0)
{ // downgrade powergen.
((POWER_GEN*)psStruct->pFunctionality)->capacity = 0;
((POWER_GEN*)psStruct->pFunctionality)->power = ((POWER_GEN_FUNCTION*)psStruct->pStructureType->asFuncList[0])->powerOutput;
((POWER_GEN*)psStruct->pFunctionality)->multiplier += ((POWER_GEN_FUNCTION*)psStruct->pStructureType->asFuncList[0])->powerMultiplier;
psStruct->sDisplay.imd = psStruct->pStructureType->pIMD;
psStruct->body = (UWORD)(structureBody(psStruct));
}
psStruct=psStruct->psNext;
}
}
else
{
psStruct=psStruct->psNext;
}
}
break;
case CAMP_WALLS: //everything.
break;
default:
debug( LOG_ERROR, "Unknown Campaign Style" );
abort();
break;
}
// rerev list to get back to normal.
// reverseObjectList((BASE_OBJECT**)&apsStructLists[player]);
bMultiPlayer = true;
return true;
}
// ////////////////////////////////////////////////////////////////////////////
// setup a campaign game
static BOOL campInit(void)
{
UDWORD player;
UBYTE newPlayerArray[MAX_PLAYERS];
UDWORD i,j,lastAI;
SDWORD newPlayerTeam[MAX_PLAYERS] = {-1,-1,-1,-1,-1,-1,-1,-1};
// if this is from a savegame, stop here!
if((getSaveGameType() == GTYPE_SAVE_START)
|| (getSaveGameType() == GTYPE_SAVE_MIDMISSION) )
{
// these two lines are the biggest hack in the world.
// the reticule seems to get detached from 'reticuleup'
// this forces it back in sync...
intRemoveReticule();
intAddReticule();
return true;
}
//Convert skirmish GUI player ids to in-game ids
if(game.type == SKIRMISH)
{
lastAI = 0; //last used AI slot
memset(newPlayerArray,1,MAX_PLAYERS * sizeof(newPlayerArray[0])); //'1' for humans
for(i=0;i<MAX_PLAYERS;i++)
{
if(game.skDiff[i] < UBYTE_MAX ) //slot with enabled or disabled AI
{
//find first unused slot
for(j=lastAI;j<MAX_PLAYERS && isHumanPlayer(j);j++); //skip humans
ASSERT(j<MAX_PLAYERS,"campInit: couldn't find free slot while assigning AI %d , lastAI=%d", i, lastAI);
newPlayerArray[j] = game.skDiff[i]; //copy over
newPlayerTeam[j] = playerTeamGUI[i];
//remove player if it was disabled in menus
if(game.skDiff[i] == 0)
clearPlayer(j,true,false);
lastAI = j;
lastAI++;
}
else if(game.skDiff[i] == UBYTE_MAX) //human player
{
//find player net id
for(j=0;(j < MAX_PLAYERS) && (player2dpid[j] != NetPlay.players[i].dpid);j++);
ASSERT(j<MAX_PLAYERS,"campInit: couldn't find player id for GUI id %d", i);
newPlayerTeam[j] = playerTeamGUI[i];
}
}
memcpy(game.skDiff,newPlayerArray,MAX_PLAYERS * sizeof(newPlayerArray[0]));
memcpy(playerTeam,newPlayerTeam,MAX_PLAYERS * sizeof(newPlayerTeam[0]));
}
for(player = 0;player<game.maxPlayers;player++) // clean up only to the player limit for this map..
{
if( (!isHumanPlayer(player)) && game.type != SKIRMISH) // strip away unused players
{
clearPlayer(player,true,true);
}
cleanMap(player);
}
// optionally remove other computer players.
// HACK: if actual number of players is 8, then drop baba player to avoid
// exceeding player number (babas need a player, too!) - Per
if ((game.type == CAMPAIGN && game.maxPlayers > 7) || game.type == SKIRMISH)
{
for(player=game.maxPlayers;player<MAX_PLAYERS;player++)
{
clearPlayer(player,true,false);
}
}
// add free research gifts..
if(NetPlay.bHost)
{
addOilDrum( NetPlay.playercount*2 ); // add some free power.
}
playerResponding(); // say howdy!
return true;
}
// ////////////////////////////////////////////////////////////////////////////
// say hi to everyone else....
void playerResponding(void)
{
ingame.startTime = gameTime;
ingame.localJoiningInProgress = false; // No longer joining.
ingame.JoiningInProgress[selectedPlayer] = false;
// Home the camera to the player
cameraToHome(selectedPlayer, false);
// Tell the world we're here
NETbeginEncode(NET_PLAYERRESPONDING, NET_ALL_PLAYERS);
NETuint32_t(&selectedPlayer);
NETend();
}
// ////////////////////////////////////////////////////////////////////////////
//called when the game finally gets fired up.
BOOL multiGameInit(void)
{
UDWORD player;
for (player = 0; player < MAX_PLAYERS; player++)
{
openchannels[player] =true; //open comms to this player.
}
campInit();
InitializeAIExperience();
msgStackReset(); //for multiplayer msgs, reset message stack
return true;
}
////////////////////////////////
// at the end of every game.
BOOL multiGameShutdown(void)
{
PLAYERSTATS st;
sendLeavingMsg(); // say goodbye
updateMultiStatsGames(); // update games played.
st = getMultiStats(selectedPlayer, true); // save stats
saveMultiStats(getPlayerName(selectedPlayer), getPlayerName(selectedPlayer), &st);
// close game
NETclose();
if (ingame.numStructureLimits)
{
ingame.numStructureLimits = 0;
free(ingame.pStructureLimits);
ingame.pStructureLimits = NULL;
}
ingame.localJoiningInProgress = false; // Clean up
ingame.localOptionsReceived = false;
ingame.bHostSetup = false; // Dont attempt a host
NetPlay.bHost = false;
bMultiPlayer = false; // Back to single player mode
selectedPlayer = 0; // Back to use player 0 (single player friendly)
return true;
}