//-----------------------------------------------------------------------------------
//
// Torque Network Library - TNLTest example program
// Copyright (C) 2004 GarageGames.com, Inc.
// For more information see http://www.opentnl.org
//
// 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 2 of the License, or
// (at your option) any later version.
//
// For use in products that are not compatible with the terms of the GNU
// General Public License, alternative licensing options are available
// from GarageGames.com.
//
// 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, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
//------------------------------------------------------------------------------------
#include <math.h>
#include "testGame.h"
#include "../tnl/tnlBitStream.h"
#include "../tnl/tnlNetConnection.h"
#include "../tnl/tnlRandom.h"
#include "../tnl/tnlSymmetricCipher.h"
#include "../tnl/tnlAsymmetricKey.h"
namespace TNLTest {
TestGame *clientGame = NULL;
TestGame *serverGame = NULL;
//---------------------------------------------------------------------------------
TNL_IMPLEMENT_NETOBJECT(Player);
// Player constructor
Player::Player(Player::PlayerType pt)
{
// assign a random starting position for the player.
startPos.x = TNL::Random::readF();
startPos.y = TNL::Random::readF();
endPos.x = startPos.x;
endPos.y = startPos.y;
renderPos = startPos;
// preset the movement parameter for the player to the end of
// the path
t = 1.0;
tDelta = 0;
myPlayerType = pt;
mNetFlags.set(Ghostable);
game = NULL;
}
Player::~Player()
{
if(!game)
return;
// remove the player from the list of players in the game.
for( TNL::S32 i = 0; i < game->players.size(); i++)
{
if(game->players[i] == this)
{
game->players.erase_fast(i);
return;
}
}
}
void Player::addToGame(TestGame *theGame)
{
// add the player to the list of players in the game.
theGame->players.push_back(this);
game = theGame;
if(myPlayerType == PlayerTypeMyClient)
{
// set the client player for the game for drawing the
// scoping radius circle.
game->clientPlayer = this;
}
}
bool Player::onGhostAdd(TNL::GhostConnection *theConnection)
{
addToGame(((TestNetInterface *) theConnection->getInterface())->game);
return true;
}
void Player::performScopeQuery(TNL::GhostConnection *connection)
{
// find all the objects that are "in scope" - for the purposes
// of this test program, all buildings are considered to be in
// scope always, as well as all "players" in a circle of radius
// 0.25 around the scope object, or a radius squared of 0.0625
for(TNL::S32 i = 0; i < game->buildings.size(); i++)
connection->objectInScope(game->buildings[i]);
for(TNL::S32 i = 0; i < game->players.size(); i++)
{
Position playerP = game->players[i]->renderPos;
TNL::F32 dx = playerP.x - renderPos.x;
TNL::F32 dy = playerP.y - renderPos.y;
TNL::F32 distSquared = dx * dx + dy * dy;
if(distSquared < 0.0625)
connection->objectInScope(game->players[i]);
}
}
TNL::U32 Player::packUpdate(TNL::GhostConnection *connection, TNL::U32 updateMask, TNL::BitStream *stream)
{
// if this is the initial update, write out some static
// information about this player. Since we never call
// setMaskBits(InitialMask), we're guaranteed that this state
// will only be updated on the first update for this object.
if(stream->writeFlag(updateMask & InitialMask))
{
// write one bit if it's not an AI
if(stream->writeFlag(myPlayerType != PlayerTypeAI))
{
// write a flag to the client if this is the player that
// that client controls.
stream->writeFlag(connection->getScopeObject() == this);
}
}
// see if we need to write out the position data
// we must write a bit to let the unpackUpdate know whether to
// read the position data out of the stream.
if(stream->writeFlag(updateMask & PositionMask))
{
// since the position data is all 0 to 1, we can use
// the bit stream's writeFloat method to write it out.
// 12 bits should be sufficient precision
stream->writeFloat(startPos.x, 12);
stream->writeFloat(startPos.y, 12);
stream->writeFloat(endPos.x, 12);
stream->writeFloat(endPos.y, 12);
stream->write(t); // fully precise t and tDelta
stream->write(tDelta);
}
// if I had other states on the player that changed independently
// of position, I could test for and update them here.
// ...
// the return value from packUpdate is the update mask for this
// object on this client _AFTER_ this update has been sent.
// Normally this will be zero, but it could be nonzero if this
// object has a state that depends on some other object that
// hasn't yet been ghosted.
return 0;
}
void Player::unpackUpdate(TNL::GhostConnection *connection, TNL::BitStream *stream)
{
// see if the initial packet data was written:
if(stream->readFlag())
{
// check to see if it's not an AI player.
if(stream->readFlag())
{
if(stream->readFlag())
myPlayerType = PlayerTypeMyClient;
else
myPlayerType = PlayerTypeClient;
}
else
myPlayerType = PlayerTypeAIDummy;
}
// see if the player's position has been updated:
if(stream->readFlag())
{
startPos.x = stream->readFloat(12);
startPos.y = stream->readFloat(12);
endPos.x = stream->readFloat(12);
endPos.y = stream->readFloat(12);
stream->read(&t);
stream->read(&tDelta);
// update the render position
update(0);
}
}
void Player::serverSetPosition(Position inStartPos, Position inEndPos, TNL::F32 inT, TNL::F32 inTDelta)
{
// update the instance variables of the object
startPos = inStartPos;
endPos = inEndPos;
t = inT;
tDelta = inTDelta;
// notify the network system that the position state of this object has changed:
setMaskBits(PositionMask);
// call a quick RPC to all the connections that have this object in scope
rpcPlayerDidMove(inEndPos.x, inEndPos.y);
}
void Player::update(TNL::F32 timeDelta)
{
t += tDelta * timeDelta;
if(t >= 1.0)
{
t = 1.0;
tDelta = 0;
renderPos = endPos;
// if this is an AI player on the server,
if(myPlayerType == PlayerTypeAI)
{
startPos = renderPos;
t = 0;
endPos.x = TNL::Random::readF();
endPos.y = TNL::Random::readF();
tDelta = 0.2f + TNL::Random::readF() * 0.1f;
setMaskBits(PositionMask); // notify the network system that the network state has been updated
}
}
renderPos.x = startPos.x + (endPos.x - startPos.x) * t;
renderPos.y = startPos.y + (endPos.y - startPos.y) * t;
}
void Player::onGhostAvailable(TNL::GhostConnection *theConnection)
{
// this function is called every time a ghost of this object is known
// to be available on a given client.
// we'll use this to demonstrate targeting a NetObject RPC
// to a specific connection. Normally a RPC method marked as
// RPCToGhost will be broadcast to ALL ghosts of the object currently in
// scope.
// first we construct an event that will represent the RPC call...
// the RPC macros create a RPCNAME_construct function that returns
// a NetEvent that encapsulates the RPC.
TNL::NetEvent *theRPCEvent = TNL_RPC_CONSTRUCT_NETEVENT(this, rpcPlayerIsInScope, (renderPos.x, renderPos.y));
// then we can just post the event to whatever connections we want to have
// receive the message.
// In the case of NetObject RPCs, if the source object is not ghosted,
// the RPC will just be silently dropped.
theConnection->postNetEvent(theRPCEvent);
}
/*
class RPCEV_Player_rpcPlayerIsInScope :
public TNL::NetObjectRPCEvent
{
public: TNL::FunctorDecl<void (Player::*)(TNL::Float<6> x, TNL::Float<6> y)> mFunctorDecl;
RPCEV_Player_rpcPlayerIsInScope() :
mFunctorDecl(Player::rpcPlayerIsInScope_remote),
RPCEvent(TNL::RPCGuaranteedOrdered, TNL::RPCToGhost)
{ mFunctor = &mFunctorDecl; }
static TNL::NetClassRepInstance<RPCEV_Player_rpcPlayerIsInScope> dynClassRep;
virtual TNL::NetClassRep* getClassRep() const;
bool checkClassType(TNL::Object *theObject)
{ return dynamic_cast<Player *>(theObject) != __null; }
};
TNL::NetClassRep* RPCEV_Player_rpcPlayerIsInScope::getClassRep()
const { return &RPCEV_Player_rpcPlayerIsInScope::dynClassRep; }
TNL::NetClassRepInstance<RPCEV_Player_rpcPlayerIsInScope> RPCEV_Player_rpcPlayerIsInScope::dynClassRep("RPCEV_Player_rpcPlayerIsInScope",TNL::NetClassGroupGameMask, TNL::NetClassTypeEvent, 0);
void Player::rpcPlayerIsInScope (TNL::Float<6> x, TNL::Float<6> y)
{
RPCEV_Player_rpcPlayerIsInScope *theEvent = new RPCEV_Player_rpcPlayerIsInScope;
theEvent->mFunctorDecl.set (x, y) ;
postNetEvent(theEvent);
}
TNL::NetEvent * Player::rpcPlayerIsInScope_construct (TNL::Float<6> x, TNL::Float<6> y) { RPCEV_Player_rpcPlayerIsInScope *theEvent = new RPCEV_Player_rpcPlayerIsInScope; theEvent->mFunctorDecl.set (x, y) ; return theEvent; } void Player::rpcPlayerIsInScope_test (TNL::Float<6> x, TNL::Float<6> y) { RPCEV_Player_rpcPlayerIsInScope *theEvent = new RPCEV_Player_rpcPlayerIsInScope; theEvent->mFunctorDecl.set (x, y) ; TNL::PacketStream ps; theEvent->pack(this, &ps); ps.setBytePosition(0); theEvent->unpack(this, &ps); theEvent->process(this); } void Player::rpcPlayerIsInScope_remote (TNL::Float<6> x, TNL::Float<6> y)
{
TNL::F32 fx = x, fy = y;
TNL::logprintf("A player is now in scope at %g, %g", fx, fy);
}
*/
TNL_IMPLEMENT_NETOBJECT_RPC(Player, rpcPlayerIsInScope,
(TNL::Float<6> x, TNL::Float<6> y), (x, y),
TNL::NetClassGroupGameMask, TNL::RPCGuaranteedOrdered, TNL::RPCToGhost, 0)
{
TNL::F32 fx = x, fy = y;
TNL::logprintf("A player is now in scope at %g, %g", fx, fy);
}
TNL_IMPLEMENT_NETOBJECT_RPC(Player, rpcPlayerWillMove,
(TNL::StringPtr testString), (testString),
TNL::NetClassGroupGameMask, TNL::RPCGuaranteedOrdered, TNL::RPCToGhostParent, 0)
{
TNL::logprintf("Expecting a player move from the connection: %s", testString);
}
TNL_IMPLEMENT_NETOBJECT_RPC(Player, rpcPlayerDidMove,
(TNL::Float<6> x, TNL::Float<6> y), (x, y),
TNL::NetClassGroupGameMask, TNL::RPCGuaranteedOrdered, TNL::RPCToGhost, 0)
{
TNL::F32 fx = x, fy = y;
TNL::logprintf("A player moved to %g, %g", fx, fy);
}
//---------------------------------------------------------------------------------
TNL_IMPLEMENT_NETOBJECT(Building);
Building::Building()
{
// place the "building" in a random position on the screen
upperLeft.x = TNL::Random::readF();
upperLeft.y = TNL::Random::readF();
lowerRight.x = upperLeft.x + TNL::Random::readF() * 0.1f + 0.025f;
lowerRight.y = upperLeft.y + TNL::Random::readF() * 0.1f + 0.025f;
game = NULL;
// always scope the buildings to the clients
mNetFlags.set(Ghostable);
}
Building::~Building()
{
if(!game)
return;
// remove the building from the list of buildings in the game.
for( TNL::S32 i = 0; i < game->buildings.size(); i++)
{
if(game->buildings[i] == this)
{
game->buildings.erase_fast(i);
return;
}
}
}
void Building::addToGame(TestGame *theGame)
{
// add it to the list of buildings in the game
theGame->buildings.push_back(this);
game = theGame;
}
bool Building::onGhostAdd(TNL::GhostConnection *theConnection)
{
addToGame(((TestNetInterface *) theConnection->getInterface())->game);
return true;
}
TNL::U32 Building::packUpdate(TNL::GhostConnection *connection, TNL::U32 updateMask, TNL::BitStream *stream)
{
if(stream->writeFlag(updateMask & InitialMask))
{
// we know all the positions are 0 to 1.
// since this object is scope always, we don't care quite as much
// how efficient the initial state updating is, since we're only
// ever going to send this data when the client first connects
// to this mission.
stream->write(upperLeft.x);
stream->write(upperLeft.y);
stream->write(lowerRight.x);
stream->write(lowerRight.y);
}
// for later - add a "color" field to the buildings that can change
// when AIs or players move over them.
return 0;
}
void Building::unpackUpdate(TNL::GhostConnection *connection, TNL::BitStream *stream)
{
if(stream->readFlag())
{
stream->read(&upperLeft.x);
stream->read(&upperLeft.y);
stream->read(&lowerRight.x);
stream->read(&lowerRight.y);
}
}
//---------------------------------------------------------------------------------
TNL_IMPLEMENT_NETCONNECTION(TestConnection, TNL::NetClassGroupGame, true);
TNL_IMPLEMENT_RPC(TestConnection, rpcGotPlayerPos,
(bool b1, bool b2, TNL::StringTableEntry string, TNL::Float<TestConnection::PlayerPosReplyBitSize> x, TNL::Float<TestConnection::PlayerPosReplyBitSize> y), (b1, b2, string, x, y),
TNL::NetClassGroupGameMask, TNL::RPCGuaranteedOrdered, TNL::RPCDirAny, 0)
{
TNL::F32 xv = x, yv = y;
TNL::logprintf("Server acknowledged position update - %d %d %s %g %g", b1, b2, string.getString(), xv, yv);
}
TNL_IMPLEMENT_RPC(TestConnection, rpcSetPlayerPos,
(TNL::F32 x, TNL::F32 y), (x, y),
TNL::NetClassGroupGameMask, TNL::RPCGuaranteedOrdered, TNL::RPCDirClientToServer, 0)
{
Position newPosition;
newPosition.x = x;
newPosition.y = y;
TNL::logprintf("%s - received new position (%g, %g) from client",
getNetAddressString(),
newPosition.x, newPosition.y);
myPlayer->serverSetPosition(myPlayer->renderPos, newPosition, 0, 0.2f);
// send an RPC back the other way!
TNL::StringTableEntry helloString("Hello World!!");
rpcGotPlayerPos(true, false, helloString, x, y);
};
TestConnection::TestConnection()
{
// every connection class instance must have a net class group set
// before it is used.
//setIsAdaptive(); // <-- Uncomment me if you want to use adaptive rate instead of fixed rate...
}
bool TestConnection::isDataToTransmit()
{
// we always want packets to be sent.
return true;
}
void TestConnection::onConnectTerminated(NetConnection::TerminationReason reason, const char *string)
{
((TestNetInterface *) getInterface())->pingingServers = true;
}
void TestConnection::onConnectionTerminated(NetConnection::TerminationReason reason, const char *edString)
{
logprintf("%s - %s connection terminated - reason %d.", getNetAddressString(), isConnectionToServer() ? "server" : "client", reason);
if(isConnectionToServer())
((TestNetInterface *) getInterface())->pingingServers = true;
else
delete (Player*)myPlayer;
}
void TestConnection::onConnectionEstablished()
{
// call the parent's onConnectionEstablished.
// by default this will set the initiator to be a connection
// to "server" and the non-initiator to be a connection to "client"
Parent::onConnectionEstablished();
// To see how this program performs with 50% packet loss,
// Try uncommenting the next line :)
//setSimulatedNetParams(0.5, 0);
if(isInitiator())
{
setGhostFrom(false);
setGhostTo(true);
TNL::logprintf("%s - connected to server.", getNetAddressString());
((TestNetInterface *) getInterface())->connectionToServer = this;
}
else
{
// on the server, we create a player object that will be the scope object
// for this client.
Player *player = new Player;
myPlayer = player;
myPlayer->addToGame(((TestNetInterface *) getInterface())->game);
setScopeObject(myPlayer);
setGhostFrom(true);
setGhostTo(false);
activateGhosting();
TNL::logprintf("%s - client connected.", getNetAddressString());
}
}
//---------------------------------------------------------------------------------
TestNetInterface::TestNetInterface(TestGame *theGame, bool server, const TNL::Address &bindAddress, const TNL::Address &pingAddr) : NetInterface(bindAddress)
{
game = theGame;
isServer = server;
pingingServers = !server;
lastPingTime = 0;
pingAddress = pingAddr;
}
void TestNetInterface::sendPing()
{
TNL::PacketStream writeStream;
// the ping packet right now just has one byte for packet type
writeStream.write(TNL::U8(GamePingRequest));
writeStream.sendto(mSocket, pingAddress);
TNL::logprintf("%s - sending ping.", pingAddress.toString());
}
void TestNetInterface::tick()
{
TNL::U32 currentTime = TNL::Platform::getRealMilliseconds();
if(pingingServers && (lastPingTime + PingDelayTime < currentTime))
{
lastPingTime = currentTime;
sendPing();
}
checkIncomingPackets();
processConnections();
}
// handleInfoPacket for the test game is very simple - if this instance
// is a client, it pings for servers until one is found to connect to.
// If this instance is a server, it responds to ping packets from clients.
// More complicated games could maintain server lists, request game information
// and more.
void TestNetInterface::handleInfoPacket(const TNL::Address &address, TNL::U8 packetType, TNL::BitStream *stream)
{
TNL::PacketStream writeStream;
if(packetType == GamePingRequest && isServer)
{
TNL::logprintf("%s - received ping.", address.toString());
// we're a server, and we got a ping packet from a client,
// so send back a GamePingResponse to let the client know it
// has found a server.
writeStream.write(TNL::U8(GamePingResponse));
writeStream.sendto(mSocket, address);
TNL::logprintf("%s - sending ping response.", address.toString());
}
else if(packetType == GamePingResponse && pingingServers)
{
// we were pinging servers and we got a response. Stop the server
// pinging, and try to connect to the server.
TNL::logprintf("%s - received ping response.", address.toString());
TestConnection *connection = new TestConnection;
connection->connect(this, address); // connect to the server through the game's network interface
TNL::logprintf("Connecting to server: %s", address.toString());
pingingServers = false;
}
}
//---------------------------------------------------------------------------------
//---------------------------------------------------------------------------------
TestGame::TestGame(bool server, const TNL::Address &interfaceBindAddr, const TNL::Address &pingAddress)
{
isServer = server;
myNetInterface = new TestNetInterface(this, isServer, interfaceBindAddr, pingAddress);
TNL::AsymmetricKey *theKey = new TNL::AsymmetricKey(32);
myNetInterface->setPrivateKey(theKey);
myNetInterface->setRequiresKeyExchange(true);
lastTime = TNL::Platform::getRealMilliseconds();
if(isServer)
{
// generate some buildings and AIs:
for(TNL::S32 i = 0; i < 50; i ++)
{
Building *building = new Building;
building->addToGame(this);
}
for(TNL::S32 i = 0; i < 15; i ++)
{
Player *aiPlayer = new Player(Player::PlayerTypeAI);
aiPlayer->addToGame(this);
}
serverPlayer = new Player(Player::PlayerTypeMyClient);
serverPlayer->addToGame(this);
}
TNL::logprintf("Created a %s...", (server ? "server" : "client"));
}
TestGame::~TestGame()
{
delete myNetInterface;
for(TNL::S32 i = 0; i < buildings.size(); i++)
delete buildings[i];
for(TNL::S32 i = 0; i < players.size(); i++)
delete players[i];
TNL::logprintf("Destroyed a %s...", (this->isServer ? "server" : "client"));
}
void TestGame::createLocalConnection(TestGame *serverGame)
{
myNetInterface->pingingServers = false;
TestConnection *theConnection = new TestConnection;
theConnection->connectLocal(myNetInterface, serverGame->myNetInterface);
}
void TestGame::tick()
{
TNL::U32 currentTime = TNL::Platform::getRealMilliseconds();
if(currentTime == lastTime)
return;
TNL::F32 timeDelta = (currentTime - lastTime) / 1000.0f;
for(TNL::S32 i = 0; i < players.size(); i++)
players[i]->update(timeDelta);
myNetInterface->tick();
lastTime = currentTime;
}
void TestGame::moveMyPlayerTo(Position newPosition)
{
if(isServer)
{
serverPlayer->serverSetPosition(serverPlayer->renderPos, newPosition, 0, 0.2f);
}
else if(!myNetInterface->connectionToServer.isNull())
{
TNL::logprintf("posting new position (%g, %g) to server", newPosition.x, newPosition.y);
if(!clientPlayer.isNull())
clientPlayer->rpcPlayerWillMove("Whee! Foo!");
myNetInterface->connectionToServer->rpcSetPlayerPos(newPosition.x, newPosition.y);
}
}
};
syntax highlighted by Code2HTML, v. 0.9.1