warzone2100/src/multijoin.cpp

478 lines
13 KiB
C++

/*
This file is part of Warzone 2100.
Copyright (C) 1999-2004 Eidos Interactive
Copyright (C) 2005-2013 Warzone 2100 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
*/
/*
* Multijoin.c
*
* Alex Lee, pumpkin studios, bath,
*
* Stuff to handle the comings and goings of players.
*/
#include <physfs.h>
#include "lib/framework/frame.h"
#include "lib/framework/strres.h"
#include "lib/framework/math_ext.h"
#include "lib/gamelib/gtime.h"
#include "lib/ivis_opengl/textdraw.h"
#include "lib/netplay/netplay.h"
#include "lib/sound/audio.h"
#include "lib/sound/audio_id.h"
#include "lib/script/script.h"
#include "multijoin.h"
#include "objmem.h"
#include "statsdef.h"
#include "droiddef.h"
#include "game.h"
#include "projectile.h"
#include "droid.h"
#include "map.h"
#include "power.h"
#include "game.h" // for loading maps
#include "message.h" // for clearing game messages
#include "order.h"
#include "console.h"
#include "orderdef.h" // for droid_order_data
#include "hci.h"
#include "component.h"
#include "research.h"
#include "wrappers.h"
#include "intimage.h"
#include "data.h"
#include "scripttabs.h"
#include "multimenu.h"
#include "multiplay.h"
#include "multirecv.h"
#include "multiint.h"
#include "multistat.h"
#include "multigifts.h"
#include "qtscript.h"
#include "scriptcb.h"
// ////////////////////////////////////////////////////////////////////////////
// Local Functions
static void resetMultiVisibility(UDWORD player);
// ////////////////////////////////////////////////////////////////////////////
// Wait For Players
bool intDisplayMultiJoiningStatus(UBYTE joinCount)
{
UDWORD x,y,w,h;
char sTmp[6];
w = RET_FORMWIDTH;
h = RET_FORMHEIGHT;
x = RET_X;
y = RET_Y;
// cameraToHome(selectedPlayer); // home the camera to the player.
RenderWindowFrame(FRAME_NORMAL, x, y ,w, h); // draw a wee blu box.
// display how far done..
iV_SetFont(font_regular);
iV_DrawText(_("Players Still Joining"),
x+(w/2)-(iV_GetTextWidth(_("Players Still Joining"))/2),
y+(h/2)-8 );
unsigned playerCount = 0; // Calculate what NetPlay.playercount should be, which is apparently only non-zero for the host.
for (unsigned player = 0; player < game.maxPlayers; ++player)
{
if (isHumanPlayer(player))
{
++playerCount;
}
}
if (!playerCount)
{
return true;
}
iV_SetFont(font_large);
sprintf(sTmp, "%d%%", PERCENT(playerCount - joinCount, playerCount));
iV_DrawText(sTmp ,x + (w / 2) - 10, y + (h / 2) + 10);
iV_SetFont(font_small);
int yStep = iV_GetTextLineSize();
int yPos = RET_Y - yStep*game.maxPlayers;
static const std::string statusStrings[3] = {"", "", ""};
for (unsigned player = 0; player < game.maxPlayers; ++player)
{
int status = -1;
if (isHumanPlayer(player))
{
status = ingame.JoiningInProgress[player]? 0 : 1; // Human player, still joining or joined.
}
else if (NetPlay.players[player].ai >= 0)
{
status = 2; // AI player (automatically joined).
}
if (status >= 0)
{
iV_DrawText((statusStrings[status] + getPlayerName(player)).c_str(), x + 5, yPos + yStep*NetPlay.players[player].position);
}
}
return true;
}
//////////////////////////////////////////////////////////////////////////////
/*
** when a remote player leaves an arena game do this!
**
** @param player -- the one we need to clear
** @param quietly -- true means without any visible effects
*/
void clearPlayer(UDWORD player,bool quietly)
{
UDWORD i;
STRUCTURE *psStruct,*psNext;
debug(LOG_NET, "R.I.P. %s (%u). quietly is %s", getPlayerName(player), player, quietly ? "true":"false");
ingame.JoiningInProgress[player] = false; // if they never joined, reset the flag
ingame.DataIntegrity[player] = false;
(void)setPlayerName(player,""); //clear custom player name (will use default instead)
for(i = 0;i<MAX_PLAYERS;i++) // remove alliances
{
alliances[player][i] = ALLIANCE_BROKEN;
alliances[i][player] = ALLIANCE_BROKEN;
}
debug(LOG_DEATH, "killing off all droids for player %d", player);
while(apsDroidLists[player]) // delete all droids
{
if(quietly) // don't show effects
{
killDroid(apsDroidLists[player]);
}
else // show effects
{
destroyDroid(apsDroidLists[player], gameTime);
}
}
debug(LOG_DEATH, "killing off all structures for player %d", player);
psStruct = apsStructLists[player];
while(psStruct) // delete all structs
{
psNext = psStruct->psNext;
// FIXME: look why destroyStruct() doesn't put back the feature like removeStruct() does
if(quietly || psStruct->pStructureType->type == REF_RESOURCE_EXTRACTOR) // don't show effects
{
removeStruct(psStruct, true);
}
else // show effects
{
destroyStruct(psStruct, gameTime);
}
psStruct = psNext;
}
return;
}
// Reset visibilty, so a new player can't see the old stuff!!
static void resetMultiVisibility(UDWORD player)
{
UDWORD owned;
DROID *pDroid;
STRUCTURE *pStruct;
for(owned = 0 ; owned <MAX_PLAYERS ;owned++) // for each player
{
if(owned != player) // done reset own stuff..
{
//droids
for(pDroid = apsDroidLists[owned];pDroid;pDroid=pDroid->psNext)
{
pDroid->visible[player] = false;
}
//structures
for(pStruct= apsStructLists[owned];pStruct;pStruct=pStruct->psNext)
{
pStruct->visible[player] = false;
}
}
}
return;
}
static void sendPlayerLeft(uint32_t playerIndex)
{
ASSERT(NetPlay.isHost, "Only host should call this.");
uint32_t forcedPlayerIndex = whosResponsible(playerIndex);
NETQUEUE (*netQueueType)(unsigned) = forcedPlayerIndex != selectedPlayer? NETgameQueueForced : NETgameQueue;
NETbeginEncode(netQueueType(forcedPlayerIndex), GAME_PLAYER_LEFT);
NETuint32_t(&playerIndex);
NETend();
}
void recvPlayerLeft(NETQUEUE queue)
{
uint32_t playerIndex = 0;
NETbeginDecode(queue, GAME_PLAYER_LEFT);
NETuint32_t(&playerIndex);
NETend();
if (whosResponsible(playerIndex) != queue.index)
{
return;
}
turnOffMultiMsg(true);
clearPlayer(playerIndex, false); // don't do it quietly
turnOffMultiMsg(false);
NetPlay.players[playerIndex].allocated = false;
char buf[256];
ssprintf(buf, _("%s has Left the Game"), getPlayerName(playerIndex));
addConsoleMessage(buf, DEFAULT_JUSTIFY, SYSTEM_MESSAGE);
NETsetPlayerConnectionStatus(CONNECTIONSTATUS_PLAYER_DROPPED, playerIndex);
debug(LOG_INFO, "** player %u has dropped, in-game!", playerIndex);
}
// ////////////////////////////////////////////////////////////////////////////
// A remote player has left the game
bool MultiPlayerLeave(UDWORD playerIndex)
{
if (playerIndex >= MAX_PLAYERS)
{
ASSERT(false, "Bad player number");
return false;
}
NETlogEntry("Player leaving game", SYNC_FLAG, playerIndex);
debug(LOG_NET,"** Player %u [%s], has left the game at game time %u.", playerIndex, getPlayerName(playerIndex), gameTime);
if (ingame.localJoiningInProgress)
{
clearPlayer(playerIndex, false);
}
else if (NetPlay.isHost) // If hosting, and game has started (not in pre-game lobby screen, that is).
{
sendPlayerLeft(playerIndex);
}
game.skDiff[playerIndex] = 0;
if (NetPlay.players[playerIndex].wzFile.isSending)
{
char buf[256];
ssprintf(buf, _("File transfer has been aborted for %d.") , playerIndex);
addConsoleMessage(buf, DEFAULT_JUSTIFY, SYSTEM_MESSAGE);
debug(LOG_INFO, "=== File has been aborted for %d ===", playerIndex);
NetPlay.players[playerIndex].wzFile.isSending = false;
NetPlay.players[playerIndex].needFile = false;
}
if (widgGetFromID(psWScreen, IDRET_FORM))
{
audio_QueueTrack(ID_CLAN_EXIT);
}
// fire script callback to reassign skirmish players.
CBPlayerLeft = playerIndex;
eventFireCallbackTrigger((TRIGGER_TYPE)CALL_PLAYERLEFT);
triggerEventPlayerLeft(playerIndex);
netPlayersUpdated = true;
return true;
}
// ////////////////////////////////////////////////////////////////////////////
// A Remote Player has joined the game.
bool MultiPlayerJoin(UDWORD playerIndex)
{
if(widgGetFromID(psWScreen,IDRET_FORM)) // if ingame.
{
audio_QueueTrack( ID_CLAN_ENTER );
}
if(widgGetFromID(psWScreen,MULTIOP_PLAYERS)) // if in multimenu.
{
if (!multiRequestUp && (bHosted || ingame.localJoiningInProgress))
{
addPlayerBox(true); // update the player box.
}
}
if(NetPlay.isHost) // host responsible for welcoming this player.
{
// if we've already received a request from this player don't reallocate.
if (ingame.JoiningInProgress[playerIndex])
{
return true;
}
ASSERT(NetPlay.playercount <= MAX_PLAYERS, "Too many players!");
// setup data for this player, then broadcast it to the other players.
setupNewPlayer(playerIndex); // setup all the guff for that player.
if (bHosted)
{
sendOptions();
}
// if skirmish and game full, then kick...
if (NetPlay.playercount > game.maxPlayers)
{
kickPlayer(playerIndex, "the game is already full.", ERROR_FULL);
}
// send everyone's stats to the new guy
{
int i;
for (i = 0; i < MAX_PLAYERS; i++)
{
if (NetPlay.players[i].allocated)
{
setMultiStats(i, getMultiStats(i), false);
}
}
}
}
return true;
}
bool sendDataCheck(void)
{
int i = 0;
NETbeginEncode(NETnetQueue(NET_HOST_ONLY), NET_DATA_CHECK); // only need to send to HOST
for(i = 0; i < DATA_MAXDATA; i++)
{
NETuint32_t(&DataHash[i]);
}
NETend();
debug(LOG_NET, "sent hash to host");
return true;
}
bool recvDataCheck(NETQUEUE queue)
{
int i = 0;
uint32_t player = queue.index;
uint32_t tempBuffer[DATA_MAXDATA] = {0};
if(!NetPlay.isHost) // only host should act
{
ASSERT(false, "Host only routine detected for client!");
return false;
}
NETbeginDecode(queue, NET_DATA_CHECK);
for(i = 0; i < DATA_MAXDATA; i++)
{
NETuint32_t(&tempBuffer[i]);
}
NETend();
if (player >= MAX_PLAYERS) // invalid player number.
{
debug(LOG_ERROR, "invalid player number (%u) detected.", player);
return false;
}
if (whosResponsible(player) != queue.index)
{
HandleBadParam("NET_DATA_CHECK given incorrect params.", player, queue.index);
return false;
}
debug(LOG_NET, "** Received NET_DATA_CHECK from player %u", player);
if (NetPlay.isHost)
{
if (memcmp(DataHash, tempBuffer, sizeof(DataHash)))
{
char msg[256] = {'\0'};
for (i=0; i<DATA_MAXDATA; i++)
{
if (DataHash[i] != tempBuffer[i]) break;
}
sprintf(msg, _("%s (%u) has an incompatible mod, and has been kicked."), getPlayerName(player), player);
sendTextMessage(msg, true);
addConsoleMessage(msg, LEFT_JUSTIFY, NOTIFY_MESSAGE);
kickPlayer(player, "your data doesn't match the host's!", ERROR_WRONGDATA);
debug(LOG_WARNING, "%s (%u) has an incompatible mod. ([%d] got %x, expected %x)", getPlayerName(player), player, i, tempBuffer[i], DataHash[i]);
debug(LOG_POPUP, "%s (%u), has an incompatible mod. ([%d] got %x, expected %x)", getPlayerName(player), player, i, tempBuffer[i], DataHash[i]);
return false;
}
else
{
debug(LOG_NET, "DataCheck message received and verified for player %s (slot=%u)", getPlayerName(player), player);
ingame.DataIntegrity[player] = true;
}
}
return true;
}
// ////////////////////////////////////////////////////////////////////////////
// Setup Stuff for a new player.
void setupNewPlayer(UDWORD player)
{
UDWORD i;
ingame.PingTimes[player] = 0; // Reset ping time
ingame.JoiningInProgress[player] = true; // Note that player is now joining
ingame.DataIntegrity[player] = false;
for (i = 0; i < MAX_PLAYERS; i++) // Set all alliances to broken
{
alliances[selectedPlayer][i] = ALLIANCE_BROKEN;
alliances[i][selectedPlayer] = ALLIANCE_BROKEN;
}
resetMultiVisibility(player); // set visibility flags.
setMultiStats(player, getMultiStats(player), true); // get the players score
if (selectedPlayer != player)
{
char buf[255];
ssprintf(buf, _("%s is joining the game"), getPlayerName(player));
addConsoleMessage(buf, DEFAULT_JUSTIFY, SYSTEM_MESSAGE);
}
}
// While not the perfect place for this, it has to do when a HOST joins (hosts) game
// unfortunatly, we don't get the message until after the setup is done.
void ShowMOTD(void)
{
// when HOST joins the game, show server MOTD message first
addConsoleMessage(_("Server message:"), DEFAULT_JUSTIFY, NOTIFY_MESSAGE);
addConsoleMessage(NetPlay.MOTD, DEFAULT_JUSTIFY, NOTIFY_MESSAGE);
}