/*
** Command & Conquer Generals Zero Hour(tm)
** Copyright 2025 Electronic Arts Inc.
**
** This program 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 3 of the License, or
** (at your option) any later version.
**
** This program 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 this program. If not, see .
*/
////////////////////////////////////////////////////////////////////////////////
// //
// (c) 2001-2003 Electronic Arts Inc. //
// //
////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////
// FILE: LANAPICallbacks.cpp
// Author: Chris Huybregts, October 2001
// Description: LAN API Callbacks
///////////////////////////////////////////////////////////////////////////////////////
#include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine
#include "strtok_r.h"
#include "Common/GameEngine.h"
#include "Common/GlobalData.h"
#include "Common/MessageStream.h"
#include "Common/MultiplayerSettings.h"
#include "Common/PlayerTemplate.h"
#include "Common/QuotedPrintable.h"
#include "Common/RandomValue.h"
#include "Common/UserPreferences.h"
#include "GameClient/GameText.h"
#include "GameClient/LanguageFilter.h"
#include "GameClient/MapUtil.h"
#include "GameClient/MessageBox.h"
#include "GameLogic/GameLogic.h"
#include "GameNetwork/FileTransfer.h"
#include "GameNetwork/LANAPICallbacks.h"
#include "GameNetwork/NetworkUtil.h"
LANAPI *TheLAN = NULL;
extern Bool LANbuttonPushed;
#ifdef _INTERNAL
// for occasional debugging...
//#pragma optimize("", off)
//#pragma MESSAGE("************************************** WARNING, optimization disabled for debugging purposes")
#endif
//Colors used for the chat dialogs
const Color playerColor = GameMakeColor(255,255,255,255);
const Color gameColor = GameMakeColor(255,255,255,255);
const Color gameInProgressColor = GameMakeColor(128,128,128,255);
const Color chatNormalColor = GameMakeColor(50,215,230,255);
const Color chatActionColor = GameMakeColor(255,0,255,255);
const Color chatLocalNormalColor = GameMakeColor(255,128,0,255);
const Color chatLocalActionColor = GameMakeColor(128,255,255,255);
const Color chatSystemColor = GameMakeColor(255,255,255,255);
const Color acceptTrueColor = GameMakeColor(0,255,0,255);
const Color acceptFalseColor = GameMakeColor(255,0,0,255);
UnicodeString LANAPIInterface::getErrorStringFromReturnType( ReturnType ret )
{
switch (ret)
{
case RET_OK:
return TheGameText->fetch("LAN:OK");
case RET_TIMEOUT:
return TheGameText->fetch("LAN:ErrorTimeout");
case RET_GAME_FULL:
return TheGameText->fetch("LAN:ErrorGameFull");
case RET_DUPLICATE_NAME:
return TheGameText->fetch("LAN:ErrorDuplicateName");
case RET_CRC_MISMATCH:
return TheGameText->fetch("LAN:ErrorCRCMismatch");
case RET_GAME_STARTED:
return TheGameText->fetch("LAN:ErrorGameStarted");
case RET_GAME_EXISTS:
return TheGameText->fetch("LAN:ErrorGameExists");
case RET_GAME_GONE:
return TheGameText->fetch("LAN:ErrorGameGone");
case RET_BUSY:
return TheGameText->fetch("LAN:ErrorBusy");
case RET_SERIAL_DUPE:
return TheGameText->fetch("WOL:ChatErrorSerialDup");
default:
return TheGameText->fetch("LAN:ErrorUnknown");
}
}
// On functions are (generally) the result of network traffic
void LANAPI::OnAccept( UnsignedInt playerIP, Bool status )
{
if( AmIHost() )
{
for (Int i = 0; i < MAX_SLOTS; i++)
{
if (m_currentGame->getIP(i) == playerIP)
{
if(status)
m_currentGame->getLANSlot(i)->setAccept();
else
m_currentGame->getLANSlot(i)->unAccept();
break;
}// if
}// for
if (i != MAX_SLOTS )
{
RequestGameOptions( GenerateGameOptionsString(), false );
lanUpdateSlotList();
}
}//if
else
{
//i'm not the host but if the accept came from the host...
if( m_currentGame->getIP(0) == playerIP )
{
UnicodeString text;
text = TheGameText->fetch("GUI:HostWantsToStart");
OnChat(UnicodeString(L"SYSTEM"), m_localIP, text, LANCHAT_SYSTEM);
}
}
}// void LANAPI::OnAccept( UnicodeString player, Bool status )
void LANAPI::OnHasMap( UnsignedInt playerIP, Bool status )
{
if( AmIHost() )
{
for (Int i = 0; i < MAX_SLOTS; i++)
{
if (m_currentGame->getIP(i) == playerIP)
{
m_currentGame->getLANSlot(i)->setMapAvailability( status );
break;
}// if
}// for
if (i != MAX_SLOTS )
{
UnicodeString mapDisplayName;
const MapMetaData *mapData = TheMapCache->findMap( m_currentGame->getMap() );
Bool willTransfer = TRUE;
if (mapData)
{
mapDisplayName.format(L"%ls", mapData->m_displayName.str());
if (mapData->m_isOfficial)
willTransfer = FALSE;
}
else
{
mapDisplayName.format(L"%hs", m_currentGame->getMap().str());
willTransfer = WouldMapTransfer(m_currentGame->getMap());
}
if (!status)
{
UnicodeString text;
if (willTransfer)
text.format(TheGameText->fetch("GUI:PlayerNoMapWillTransfer"), m_currentGame->getLANSlot(i)->getName().str(), mapDisplayName.str());
else
text.format(TheGameText->fetch("GUI:PlayerNoMap"), m_currentGame->getLANSlot(i)->getName().str(), mapDisplayName.str());
OnChat(UnicodeString(L"SYSTEM"), m_localIP, text, LANCHAT_SYSTEM);
}
lanUpdateSlotList();
}
}//if
}// void LANAPI::OnHasMap( UnicodeString player, Bool status )
void LANAPI::OnGameStartTimer( Int seconds )
{
UnicodeString text;
if (seconds == 1)
text.format(TheGameText->fetch("LAN:GameStartTimerSingular"), seconds);
else
text.format(TheGameText->fetch("LAN:GameStartTimerPlural"), seconds);
OnChat(UnicodeString(L"SYSTEM"), m_localIP, text, LANCHAT_SYSTEM);
}
void LANAPI::OnGameStart( void )
{
//DEBUG_LOG(("Map is '%s', preview is '%s'\n", m_currentGame->getMap().str(), GetPreviewFromMap(m_currentGame->getMap()).str()));
//DEBUG_LOG(("Map is '%s', INI is '%s'\n", m_currentGame->getMap().str(), GetINIFromMap(m_currentGame->getMap()).str()));
if (m_currentGame)
{
LANPreferences pref;
AsciiString option;
option.format("%d", m_currentGame->getLANSlot( m_currentGame->getLocalSlotNum() )->getPlayerTemplate());
pref["PlayerTemplate"] = option;
option.format("%d", m_currentGame->getLANSlot( m_currentGame->getLocalSlotNum() )->getColor());
pref["Color"] = option;
if (m_currentGame->amIHost())
{
pref["Map"] = AsciiStringToQuotedPrintable(m_currentGame->getMap());
pref.setSuperweaponRestricted( m_currentGame->getSuperweaponRestriction() > 0 );
pref.setStartingCash( m_currentGame->getStartingCash() );
}
pref.write();
m_isInLANMenu = FALSE;
//m_currentGame->startGame(0);
// Set up the game network
DEBUG_ASSERTCRASH(TheNetwork == NULL, ("For some reason TheNetwork isn't NULL at the start of this game. Better look into that."));
if (TheNetwork != NULL) {
delete TheNetwork;
TheNetwork = NULL;
}
// Time to initialize TheNetwork for this game.
TheNetwork = NetworkInterface::createNetwork();
TheNetwork->init();
TheNetwork->setLocalAddress(m_localIP, 8088);
TheNetwork->initTransport();
TheNetwork->parseUserList(m_currentGame);
if (TheGameLogic->isInGame())
TheGameLogic->clearGameData();
Bool filesOk = DoAnyMapTransfers(m_currentGame);
// see if we really have the map. if not, back out.
TheMapCache->updateCache();
if (!filesOk || TheMapCache->findMap(m_currentGame->getMap()) == NULL)
{
DEBUG_LOG(("After transfer, we didn't really have the map. Bailing...\n"));
OnPlayerLeave(m_name);
removeGame(m_currentGame);
m_currentGame = NULL;
m_inLobby = TRUE;
if (TheNetwork != NULL) {
delete TheNetwork;
TheNetwork = NULL;
}
OnChat(UnicodeString::TheEmptyString, 0, TheGameText->fetch("GUI:CouldNotTransferMap"), LANCHAT_SYSTEM);
return;
}
m_currentGame->startGame(0);
// shutdown the top, but do not pop it off the stack
//TheShell->hideShell();
// setup the Global Data with the Map and Seed
TheWritableGlobalData->m_pendingFile = m_currentGame->getMap();
// send a message to the logic for a new game
GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_NEW_GAME );
msg->appendIntegerArgument(GAME_LAN);
TheWritableGlobalData->m_useFpsLimit = false;
// Set the random seed
InitGameLogicRandom( m_currentGame->getSeed() );
DEBUG_LOG(("InitGameLogicRandom( %d )\n", m_currentGame->getSeed()));
}
}
void LANAPI::OnGameOptions( UnsignedInt playerIP, Int playerSlot, AsciiString options )
{
if (!m_currentGame)
return;
if (m_currentGame->getIP(playerSlot) != playerIP)
return; // He's not in our game?!?
if (m_currentGame->isGameInProgress())
return; // we don't want to process any game options while in game.
if (playerSlot == 0 && !m_currentGame->amIHost())
{
m_currentGame->setLastHeard(timeGetTime());
AsciiString oldOptions = GameInfoToAsciiString(m_currentGame); // save these off for if we get booted
if(ParseGameOptionsString(m_currentGame,options))
{
lanUpdateSlotList();
updateGameOptions();
}
Bool booted = true;
for(Int player = 1; player< MAX_SLOTS; player++)
{
if(m_currentGame->getIP(player) == m_localIP)
{
booted = false;
break;
}
}
if(booted)
{
// restore the options with us in so we can save prefs
ParseGameOptionsString(m_currentGame, oldOptions);
OnPlayerLeave(m_name);
}
}
else
{
// Check for user/host updates
{
AsciiString key;
AsciiString munkee = options;
munkee.nextToken(&key, "=");
//DEBUG_LOG(("GameOpt request: key=%s, val=%s from player %d\n", key.str(), munkee.str(), playerSlot));
LANGameSlot *slot = m_currentGame->getLANSlot(playerSlot);
if (!slot)
return;
if (key == "User")
{
slot->setLogin(munkee.str()+1);
return;
}
else if (key == "Host")
{
slot->setHost(munkee.str()+1);
return;
}
}
// Parse player requests (side, color, etc)
if( AmIHost() && m_localIP != playerIP)
{
if (options.compare("HELLO") == 0)
{
m_currentGame->setPlayerLastHeard(playerSlot, timeGetTime());
}
else
{
m_currentGame->setPlayerLastHeard(playerSlot, timeGetTime());
Bool change = false;
Bool shouldUnaccept = false;
AsciiString key;
options.nextToken(&key, "=");
Int val = atoi(options.str()+1);
DEBUG_LOG(("GameOpt request: key=%s, val=%s from player %d\n", key.str(), options.str(), playerSlot));
LANGameSlot *slot = m_currentGame->getLANSlot(playerSlot);
if (!slot)
return;
if (key == "Color")
{
if (val >= -1 && val < TheMultiplayerSettings->getNumColors() && val != slot->getColor() && slot->getPlayerTemplate() != PLAYERTEMPLATE_OBSERVER)
{
Bool colorAvailable = TRUE;
if(val != -1 )
{
for(Int i=0; i getLANSlot(i);
if(val == checkSlot->getColor() && slot != checkSlot)
{
colorAvailable = FALSE;
break;
}
}
}
if(colorAvailable)
slot->setColor(val);
change = true;
}
else
{
DEBUG_LOG(("Rejecting invalid color %d\n", val));
}
}
else if (key == "PlayerTemplate")
{
if (val >= PLAYERTEMPLATE_MIN && val < ThePlayerTemplateStore->getPlayerTemplateCount() && val != slot->getPlayerTemplate())
{
slot->setPlayerTemplate(val);
if (val == PLAYERTEMPLATE_OBSERVER)
{
slot->setColor(-1);
slot->setStartPos(-1);
slot->setTeamNumber(-1);
}
change = true;
shouldUnaccept = true;
}
else
{
DEBUG_LOG(("Rejecting invalid PlayerTemplate %d\n", val));
}
}
else if (key == "StartPos" && slot->getPlayerTemplate() != PLAYERTEMPLATE_OBSERVER)
{
if (val >= -1 && val < MAX_SLOTS && val != slot->getStartPos())
{
Bool startPosAvailable = TRUE;
if(val != -1)
for(Int i=0; i getLANSlot(i);
if(val == checkSlot->getStartPos() && slot != checkSlot)
{
startPosAvailable = FALSE;
break;
}
}
if(startPosAvailable)
slot->setStartPos(val);
change = true;
shouldUnaccept = true;
}
else
{
DEBUG_LOG(("Rejecting invalid startPos %d\n", val));
}
}
else if (key == "Team")
{
if (val >= -1 && val < MAX_SLOTS/2 && val != slot->getTeamNumber() && slot->getPlayerTemplate() != PLAYERTEMPLATE_OBSERVER)
{
slot->setTeamNumber(val);
change = true;
shouldUnaccept = true;
}
else
{
DEBUG_LOG(("Rejecting invalid team %d\n", val));
}
}
else if (key == "NAT")
{
if ((val >= FirewallHelperClass::FIREWALL_TYPE_SIMPLE) &&
(val <= FirewallHelperClass::FIREWALL_TYPE_DESTINATION_PORT_DELTA))
{
slot->setNATBehavior((FirewallHelperClass::FirewallBehaviorType)val);
DEBUG_LOG(("NAT behavior set to %d for player %d\n", val, playerSlot));
change = true;
}
else
{
DEBUG_LOG(("Rejecting invalid NAT behavior %d\n", (Int)val));
}
}
if (change)
{
if (shouldUnaccept)
m_currentGame->resetAccepted();
RequestGameOptions(GenerateGameOptionsString(), true);
lanUpdateSlotList();
DEBUG_LOG(("Slot value is color=%d, PlayerTemplate=%d, startPos=%d, team=%d\n",
slot->getColor(), slot->getPlayerTemplate(), slot->getStartPos(), slot->getTeamNumber()));
DEBUG_LOG(("Slot list updated to %s\n", GenerateGameOptionsString().str()));
}
}
}
}
}
/*
void LANAPI::OnSlotList( ReturnType ret, LANGameInfo *theGame )
{
if (!theGame || theGame != m_currentGame)
return;
Bool foundMe = false;
for (int player = 0; player < MAX_SLOTS; ++player)
{
if (m_currentGame->getIP(player) == m_localIP)
{
foundMe = true;
break;
}
}
if (!foundMe)
{
// I've been kicked - back to the lobby for me!
// We're counting on the fact that OnPlayerLeave winds up calling reset on TheLAN.
OnPlayerLeave(m_name);
return;
}
lanUpdateSlotList();
}
*/
void LANAPI::OnPlayerJoin( Int slot, UnicodeString playerName )
{
if (m_currentGame && m_currentGame->getIP(0) == m_localIP)
{
// Someone New Joined.. lets reset the accepts
m_currentGame->resetAccepted();
// Send out the game options
RequestGameOptions(GenerateGameOptionsString(), true);
}
lanUpdateSlotList();
}
void LANAPI::OnGameJoin( ReturnType ret, LANGameInfo *theGame )
{
if (ret == RET_OK)
{
LANbuttonPushed = true;
TheShell->push( AsciiString("Menus/LanGameOptionsMenu.wnd") );
//lanUpdateSlotList();
LANPreferences pref;
AsciiString options;
options.format("PlayerTemplate=%d", pref.getPreferredFaction());
RequestGameOptions(options, true);
options.format("Color=%d", pref.getPreferredColor());
RequestGameOptions(options, true);
options.format("User=%s", m_userName.str());
RequestGameOptions( options, true );
options.format("Host=%s", m_hostName.str());
RequestGameOptions( options, true );
options.format("NAT=%d", FirewallHelperClass::FIREWALL_TYPE_SIMPLE); // BGC: This is a LAN game, so there is no firewall.
RequestGameOptions( options, true );
}
else if (ret != RET_BUSY)
{
/// @todo: re-enable lobby controls? Error msgs?
UnicodeString title, body;
title = TheGameText->fetch("LAN:JoinFailed");
body = getErrorStringFromReturnType(ret);
MessageBoxOk(title, body, NULL);
}
}
void LANAPI::OnHostLeave( void )
{
DEBUG_ASSERTCRASH(!m_inLobby && m_currentGame, ("Game info is gone!"));
if (m_inLobby || !m_currentGame)
return;
LANbuttonPushed = true;
DEBUG_LOG(("Host left - popping to lobby\n"));
TheShell->pop();
}
void LANAPI::OnPlayerLeave( UnicodeString player )
{
DEBUG_ASSERTCRASH(!m_inLobby && m_currentGame, ("Game info is gone!"));
if (m_inLobby || !m_currentGame || m_currentGame->isGameInProgress())
return;
if (m_name.compare(player) == 0)
{
// We're leaving. Save options and Pop the shell up a screen.
//DEBUG_ASSERTCRASH(false, ("Slot is %d\n", m_currentGame->getLocalSlotNum()));
if (m_currentGame && m_currentGame->isInGame() && m_currentGame->getLocalSlotNum() >= 0)
{
LANPreferences pref;
AsciiString option;
option.format("%d", m_currentGame->getLANSlot( m_currentGame->getLocalSlotNum() )->getPlayerTemplate());
pref["PlayerTemplate"] = option;
option.format("%d", m_currentGame->getLANSlot( m_currentGame->getLocalSlotNum() )->getColor());
pref["Color"] = option;
if (m_currentGame->amIHost())
pref["Map"] = AsciiStringToQuotedPrintable(m_currentGame->getMap());
pref.write();
}
LANbuttonPushed = true;
DEBUG_LOG(("OnPlayerLeave says we're leaving! pop away!\n"));
TheShell->pop();
}
else
{
if (m_currentGame && m_currentGame->getIP(0) == m_localIP)
{
// Force a new slotlist send
m_lastResendTime = 0;
lanUpdateSlotList();
RequestGameOptions( GenerateGameOptionsString(), true );
}
}
}
void LANAPI::OnGameList( LANGameInfo *gameList )
{
if (m_inLobby)
{
LANDisplayGameList(listboxGames, gameList);
}
}//void LANAPI::OnGameList( LANGameInfo *gameList )
void LANAPI::OnGameCreate( ReturnType ret )
{
if (ret == RET_OK)
{
LANbuttonPushed = true;
TheShell->push( AsciiString("Menus/LanGameOptionsMenu.wnd") );
RequestLobbyLeave( false );
//RequestGameAnnounce( ); // can't do this here, since we don't have a map set
}
else
{
if(m_inLobby)
{
switch( ret )
{
case RET_GAME_EXISTS:
GadgetListBoxAddEntryText(listboxChatWindow, TheGameText->fetch("LAN:ErrorGameExists"), chatSystemColor, -1, -1);
break;
case RET_BUSY:
GadgetListBoxAddEntryText(listboxChatWindow, TheGameText->fetch("LAN:ErrorBusy"), chatSystemColor, -1, -1);
break;
default:
GadgetListBoxAddEntryText(listboxChatWindow, TheGameText->fetch("LAN:ErrorUnknown"), chatSystemColor, -1, -1);
}
}
}
}//void OnGameCreate( ReturnType ret )
void LANAPI::OnPlayerList( LANPlayer *playerList )
{
if (m_inLobby)
{
UnsignedInt selectedIP = 0;
Int selectedIndex = -1;
Int indexToSelect = -1;
GadgetListBoxGetSelected(listboxPlayers, &selectedIndex);
if (selectedIndex != -1 )
selectedIP = (UnsignedInt) GadgetListBoxGetItemData(listboxPlayers, selectedIndex, 0);
GadgetListBoxReset(listboxPlayers);
LANPlayer *player = m_lobbyPlayers;
while (player)
{
Int addedIndex = GadgetListBoxAddEntryText(listboxPlayers, player->getName(), playerColor, -1, -1);
GadgetListBoxSetItemData(listboxPlayers, (void *)player->getIP(),addedIndex, 0 );
if (selectedIP == player->getIP())
indexToSelect = addedIndex;
player = player->getNext();
}
if (indexToSelect >= 0)
GadgetListBoxSetSelected(listboxPlayers, indexToSelect);
}
}
void LANAPI::OnNameChange( UnsignedInt IP, UnicodeString newName )
{
OnPlayerList(m_lobbyPlayers);
}
void LANAPI::OnInActive(UnsignedInt IP) {
}
void LANAPI::OnChat( UnicodeString player, UnsignedInt ip, UnicodeString message, ChatType format )
{
GameWindow *chatWindow = NULL;
if (m_inLobby)
{
chatWindow = listboxChatWindow;
}
else if( m_currentGame && m_currentGame->isGameInProgress() && TheShell->isShellActive())
{
chatWindow = listboxChatWindowScoreScreen;
}
else if( m_currentGame && !m_currentGame->isGameInProgress())
{
chatWindow = listboxChatWindowLanGame;
}
if (chatWindow == NULL)
return;
Int index = -1;
UnicodeString unicodeChat;
switch (format)
{
case LANAPIInterface::LANCHAT_SYSTEM:
unicodeChat = L"";
unicodeChat.concat(message);
unicodeChat.concat(L"");
index =GadgetListBoxAddEntryText(chatWindow, unicodeChat, chatSystemColor, -1, -1);
break;
case LANAPIInterface::LANCHAT_EMOTE:
unicodeChat = player;
unicodeChat.concat(L' ');
unicodeChat.concat(message);
if (ip == m_localIP)
index =GadgetListBoxAddEntryText(chatWindow, unicodeChat, chatLocalActionColor, -1, -1);
else
index =GadgetListBoxAddEntryText(chatWindow, unicodeChat, chatActionColor, -1, -1);
break;
case LANAPIInterface::LANCHAT_NORMAL:
default:
{
// Do the language filtering.
TheLanguageFilter->filterLine(message);
Color chatColor = GameMakeColor(255, 255, 255, 255);
if (m_currentGame)
{
Int slotNum = m_currentGame->getSlotNum(player);
// it'll be -1 if its invalid.
if (slotNum >= 0) {
GameSlot *gs = m_currentGame->getSlot(slotNum);
if (gs) {
Int colorIndex = gs->getColor();
MultiplayerColorDefinition *def = TheMultiplayerSettings->getColor(colorIndex);
if (def)
chatColor = def->getColor();
}
}
}
unicodeChat = L"[";
unicodeChat.concat(player);
unicodeChat.concat(L"] ");
unicodeChat.concat(message);
if (ip == m_localIP)
index =GadgetListBoxAddEntryText(chatWindow, unicodeChat, chatColor, -1, -1);
else
index =GadgetListBoxAddEntryText(chatWindow, unicodeChat, chatColor, -1, -1);
break;
}
}
GadgetListBoxSetItemData(chatWindow, (void *)-1, index);
}