//----------------------------------------------------------------------------- // Copyright (c) 2012 GarageGames, LLC // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. //----------------------------------------------------------------------------- #include "platform/platform.h" #include "core/dnet.h" #include "console/simBase.h" #include "sim/netConnection.h" #include "core/stream/bitStream.h" #include "core/stream/fileStream.h" #ifndef TORQUE_TGB_ONLY #include "scene/pathManager.h" #endif #include "console/consoleTypes.h" #include "sim/netInterface.h" #include "console/engineAPI.h" #include IMPLEMENT_SCOPE( NetAPI, Net,, "Networking functionality." ); IMPLEMENT_NONINSTANTIABLE_CLASS( NetEvent, "An event to be sent over the network." ) END_IMPLEMENT_CLASS; S32 gNetBitsSent = 0; extern S32 gNetBitsReceived; U32 gGhostUpdates = 0; enum NetConnectionConstants { PingTimeout = 4500, ///< milliseconds DefaultPingRetryCount = 15, }; SimObjectPtr NetConnection::mServerConnection; SimObjectPtr NetConnection::mLocalClientConnection; //---------------------------------------------------------------------- /// ConnectionMessageEvent /// /// This event is used inside by the connection and subclasses to message /// itself when sequencing events occur. Right now, the message event /// only uses 6 bits to transmit the message, so class ConnectionMessageEvent : public NetEvent { U32 sequence; U32 message; U32 ghostCount; public: typedef NetEvent Parent; ConnectionMessageEvent(U32 msg=0, U32 seq=0, U32 gc=0) { message = msg; sequence = seq; ghostCount = gc;} void pack(NetConnection *, BitStream *bstream) { bstream->write(sequence); bstream->writeInt(message, 3); bstream->writeInt(ghostCount, NetConnection::GhostIdBitSize + 1); } void write(NetConnection *, BitStream *bstream) { bstream->write(sequence); bstream->writeInt(message, 3); bstream->writeInt(ghostCount, NetConnection::GhostIdBitSize + 1); } void unpack(NetConnection *, BitStream *bstream) { bstream->read(&sequence); message = bstream->readInt(3); ghostCount = bstream->readInt(NetConnection::GhostIdBitSize + 1); } void process(NetConnection *ps) { ps->handleConnectionMessage(message, sequence, ghostCount); } DECLARE_CONOBJECT(ConnectionMessageEvent); }; IMPLEMENT_CO_NETEVENT_V1(ConnectionMessageEvent); ConsoleDocClass( ConnectionMessageEvent, "@brief This event is used inside by the connection and subclasses to message itself when sequencing events occur.\n\n" "Not intended for game development, for editors or internal use only.\n\n " "@internal"); void NetConnection::sendConnectionMessage(U32 message, U32 sequence, U32 ghostCount) { postNetEvent(new ConnectionMessageEvent(message, sequence, ghostCount)); } //-------------------------------------------------------------------- IMPLEMENT_CONOBJECT(NetConnection); ConsoleDocClass( NetConnection, "@brief Provides the basis for implementing a multiplayer game protocol.\n\n" "NetConnection combines a low-level notify protocol implemented in ConnectionProtocol with a SimGroup, " "and implements several distinct subsystems:\n\n" "- Event Manager This is responsible for transmitting NetEvents over the wire. " "It deals with ensuring that the various types of NetEvents are delivered appropriately, " "and with notifying the event of its delivery status.\n\n" "- Move Manager This is responsible for transferring a Move to the server 32 " "times a second (on the client) and applying it to the control object (on the server).\n\n" "- Ghost Manager This is responsible for doing scoping calculations (on the server " "side) and transmitting most-recent ghost information to the client.\n\n" "- File Transfer It is often the case that clients will lack important files when " "connecting to a server which is running a mod or new map. This subsystem allows the " "server to transfer such files to the client.\n\n" "- Networked String Table String data can easily soak up network bandwidth, so for " "efficiency, we implement a networked string table. We can then notify the connection " "of strings we will reference often, such as player names, and transmit only a tag, " "instead of the whole string.\n\n" "- Demo Recording A demo in Torque is a log of the network traffic between client " "and server; when a NetConnection records a demo, it simply logs this data to a file. When " "it plays a demo back, it replays the logged data.\n\n" "- Connection Database This is used to keep track of all the NetConnections; it can " "be iterated over (for instance, to send an event to all active connections), or queried " "by address.\n\n" "The NetConnection is a SimGroup. On the client side, it contains all the objects which have been " "ghosted to that client. On the server side, it is empty; it can be used (typically in script) to " "hold objects related to the connection. For instance, you might place an observation camera in the " "NetConnnection. In both cases, when the connection is destroyed, so are the contained objects.\n\n" "The NetConnection also has the concept of local connections. These are used when the client and " "server reside in the same process. A local connection is typically required to use the standard " "Torque world building tools. A local connection is also required when building a single player " "game.\n\n" "@see @ref Networking, @ref ghosting_scoping, @ref netconnection_simgroup, @ref local_connections, GameConnection, AIConnection, and AIClient.\n\n" "@ingroup Networking\n"); NetConnection* NetConnection::mConnectionList = NULL; NetConnection* NetConnection::mHashTable[NetConnection::HashTableSize] = { NULL, }; bool NetConnection::mFilesWereDownloaded = false; static inline U32 HashNetAddress(const NetAddress *addr) { return addr->getHash() % NetConnection::HashTableSize; } NetConnection *NetConnection::lookup(const NetAddress *addr) { U32 hashIndex = HashNetAddress(addr); for(NetConnection *walk = mHashTable[hashIndex]; walk; walk = walk->mNextTableHash) if(Net::compareAddresses(addr, walk->getNetAddress())) return walk; return NULL; } void NetConnection::netAddressTableInsert() { U32 hashIndex = HashNetAddress(&mNetAddress); mNextTableHash = mHashTable[hashIndex]; mHashTable[hashIndex] = this; } void NetConnection::netAddressTableRemove() { U32 hashIndex = HashNetAddress(&mNetAddress); NetConnection **walk = &mHashTable[hashIndex]; while(*walk) { if(*walk == this) { *walk = mNextTableHash; mNextTableHash = NULL; return; } walk = &((*walk)->mNextTableHash); } } void NetConnection::setNetAddress(const NetAddress *addr) { mNetAddress = *addr; } const NetAddress *NetConnection::getNetAddress() { return &mNetAddress; } void NetConnection::setSequence(U32 sequence) { mConnectSequence = sequence; } U32 NetConnection::getSequence() { return mConnectSequence; } static U32 gPacketRateToServer = 32; static U32 gPacketUpdateDelayToServer = 32; static U32 gPacketRateToClient = 10; static U32 gPacketSize = 508; void NetConnection::consoleInit() { Con::addVariable("$pref::Net::PacketRateToServer", TypeS32, &gPacketRateToServer, "@brief Sets how often packets are sent from the client to the server.\n\n" "It is possible to control how often packets may be sent to the server. This may be " "used to throttle the amount of bandwidth being used, but should be adjusted with " "caution.\n\n" "The actual formula used to calculate the delay between sending packets to the server is:\n" "@code\n" "Packet Update Delay To Server = 1024 / $pref::Net::PacketRateToServer" "@endcode\n" "with the result in ms. A minimum rate of 8 is enforced in the source code. The default " "value is 32.\n\n" "@note When using a local connection (@ref local_connections) be aware that this variable " "is always forced to 128.\n\n" "@ingroup Networking"); Con::addVariable("$pref::Net::PacketRateToClient", TypeS32, &gPacketRateToClient, "@brief Sets how often packets are sent from the server to a client.\n\n" "It is possible to control how often packets may be sent to the clients. This may be " "used to throttle the amount of bandwidth being used, but should be adjusted with " "caution.\n\n" "The actual formula used to calculate the delay between sending packets to a client is:\n" "@code\n" "Packet Update Delay To Client = 1024 / $pref::Net::PacketRateToClient" "@endcode\n" "with the result in ms. A minimum rate of 1 is enforced in the source code. The default " "value is 10.\n\n" "@note When using a local connection (@ref local_connections) be aware that this variable " "is always forced to 128.\n\n" "@ingroup Networking"); Con::addVariable("$pref::Net::PacketSize", TypeS32, &gPacketSize, "@brief Sets the maximum size in bytes an individual network packet may be.\n\n" "It is possible to control how large each individual network packet may be. Increasing " "its size from the default allows for more data to be sent on each network send. " "However, this value should be changed with caution as too large a value will cause " "packets to be split up by the networking platform or hardware, which is something " "Torque cannot handle.\n\n" "A minimum packet size of 100 bytes is enforced in the source code. There is no " "enforced maximum. The default value is 200 bytes.\n\n" "@note When using a local connection (@ref local_connections) be aware that this variable " "is always forced to 1024 bytes.\n\n" "@ingroup Networking"); Con::addVariable("$Stats::netBitsSent", TypeS32, &gNetBitsSent, "@brief The number of bytes sent during the last packet send operation.\n\n" "@note Even though this variable has 'Bits' in it, the value is indeed reported in bytes. This name " "is a legacy holdover and remains for compatibility reasons.\n" "@ingroup Networking"); Con::addVariable("$Stats::netBitsReceived", TypeS32, &gNetBitsReceived, "@brief The number of bytes received during the last packet process operation.\n\n" "@note Even though this variable has 'Bits' in it, the value is indeed reported in bytes. This name " "is a legacy holdover and remains for compatibility reasons.\n" "@ingroup Networking"); Con::addVariable("$Stats::netGhostUpdates", TypeS32, &gGhostUpdates, "@brief The total number of ghosts added, removed, and/or updated on the client " "during the last packet process operation.\n\n" "@ingroup Networking"); } void NetConnection::checkMaxRate() { // Enforce some minimum limits to the network settings. gPacketRateToServer = getMax( gPacketRateToServer, (U32)8 ); gPacketRateToClient = getMax( gPacketRateToClient, (U32)1 ); gPacketSize = getMax( gPacketSize, (U32)100 ); U32 packetRateToServer = gPacketRateToServer; U32 packetRateToClient = gPacketRateToClient; U32 packetSize = gPacketSize; if (isLocalConnection()) { packetRateToServer = 128; packetRateToClient = 128; // These changes introduced in T3D 1.1 Preview reduce the packet headroom which leads // to some spells and effects running out of room when dynamic variables are used // to send launch-time parameters to clients. packetSize = 512; } gPacketUpdateDelayToServer = 1024 / packetRateToServer; U32 toClientUpdateDelay = 1024 / packetRateToClient; if(mMaxRate.updateDelay != toClientUpdateDelay || mMaxRate.packetSize != packetSize) { mMaxRate.updateDelay = toClientUpdateDelay; mMaxRate.packetSize = packetSize; mMaxRate.changed = true; } } void NetConnection::setSendingEvents(bool sending) { AssertFatal(!mEstablished, "Error, cannot change event behavior after a connection has been established."); mSendingEvents = sending; } void NetConnection::setTranslatesStrings(bool xl) { AssertFatal(!mEstablished, "Error, cannot change event behavior after a connection has been established."); mTranslateStrings = xl; if(mTranslateStrings) mStringTable = new ConnectionStringTable(this); } void NetConnection::setNetClassGroup(U32 grp) { AssertFatal(!mEstablished, "Error, cannot change net class group after a connection has been established."); mNetClassGroup = grp; } NetConnection::NetConnection() : mNetAddress() { mTranslateStrings = false; mConnectSequence = 0; mStringTable = NULL; mSendingEvents = true; mNetClassGroup = NetClassGroupGame; AssertFatal(mNetClassGroup >= NetClassGroupGame && mNetClassGroup < NetClassGroupsCount, "Invalid net event class type."); mSimulatedPing = 0; mSimulatedPacketLoss = 0; #ifdef TORQUE_DEBUG_NET mLogging = false; #endif mEstablished = false; mLastUpdateTime = 0; mRoundTripTime = 0; mPacketLoss = 0; mNextTableHash = NULL; mSendDelayCredit = 0; mConnectionState = NotConnected; mCurrentDownloadingFile = NULL; mCurrentFileBuffer = NULL; mNextConnection = NULL; mPrevConnection = NULL; mNotifyQueueHead = NULL; mNotifyQueueTail = NULL; mCurRate.updateDelay = 102; mCurRate.packetSize = 200; mCurRate.changed = false; mMaxRate.updateDelay = 102; mMaxRate.packetSize = 200; mMaxRate.changed = false; checkMaxRate(); // event management data: mNotifyEventList = NULL; mSendEventQueueHead = NULL; mSendEventQueueTail = NULL; mUnorderedSendEventQueueHead = NULL; mUnorderedSendEventQueueTail = NULL; mWaitSeqEvents = NULL; mNextSendEventSeq = FirstValidSendEventSeq; mNextRecvEventSeq = FirstValidSendEventSeq; mLastAckedEventSeq = -1; // ghost management data: mScopeObject = NULL; mGhostingSequence = 0; mGhosting = false; mScoping = false; mGhostArray = NULL; mGhostRefs = NULL; mGhostLookupTable = NULL; mLocalGhosts = NULL; mGhostsActive = 0; mMissionPathsSent = false; mDemoWriteStream = NULL; mDemoReadStream = NULL; mPingSendCount = 0; mPingRetryCount = DefaultPingRetryCount; mLastPingSendTime = Platform::getVirtualMilliseconds(); mCurrentDownloadingFile = NULL; mCurrentFileBuffer = NULL; mCurrentFileBufferSize = 0; mCurrentFileBufferOffset = 0; mNumDownloadedFiles = 0; // Disable starting a new journal recording or playback from here on Journal::Disable(); // Ensure NetAddress is cleared dMemset(&mNetAddress, '\0', sizeof(NetAddress)); } NetConnection::~NetConnection() { AssertFatal(mNotifyQueueHead == NULL, "Uncleared notifies remain."); netAddressTableRemove(); dFree(mCurrentFileBuffer); if(mCurrentDownloadingFile) delete mCurrentDownloadingFile; delete[] mLocalGhosts; delete[] mGhostLookupTable; delete[] mGhostRefs; delete[] mGhostArray; delete mStringTable; if(mDemoWriteStream) delete mDemoWriteStream; if(mDemoReadStream) delete mDemoReadStream; } NetConnection::PacketNotify::PacketNotify() { rateChanged = false; maxRateChanged = false; sendTime = 0; eventList = 0; ghostList = 0; subList = NULL; nextPacket = NULL; } bool NetConnection::checkTimeout(U32 time) { if(!isNetworkConnection()) return false; if(time > mLastPingSendTime + PingTimeout) { if(mPingSendCount >= mPingRetryCount) return true; mLastPingSendTime = time; mPingSendCount++; sendPingPacket(); } return false; } void NetConnection::keepAlive() { mLastPingSendTime = Platform::getVirtualMilliseconds(); mPingSendCount = 0; } void NetConnection::handleConnectionEstablished() { } //-------------------------------------------------------------------------- #ifndef TORQUE_TGB_ONLY DefineEngineMethod( NetConnection, transmitPaths, void, (),, "@brief Sent by the server during phase 2 of the mission download to update motion spline paths.\n\n" "The server transmits all spline motion paths that are within the mission (Path) separate from " "other objects. This is due to the potentially large number of nodes within each path, which may " "saturate a packet sent to the client. By managing this step separately, Torque has finer control " "over how packets are organised vs. doing it during the ghosting stage.\n\n" "Internally a PathManager is used to track all paths defined within a mission on the server, and each " "one is transmitted using a PathManagerEvent. The client side collects these events and builds the " "given paths within its own PathManager. This is typically done during the standard mission start " "phase 2 when following Torque's example mission startup sequence.\n\n" "When a mission is ended, all paths need to be cleared from their respective path managers." "@tsexample\n" "function serverCmdMissionStartPhase2Ack(%client, %seq, %playerDB)\n" "{\n" " // Make sure to ignore calls from a previous mission load\n" " if (%seq != $missionSequence || !$MissionRunning)\n" " return;\n" " if (%client.currentPhase != 1.5)\n" " return;\n" " %client.currentPhase = 2;\n" "\n" " // Set the player datablock choice\n" " %client.playerDB = %playerDB;\n" "\n" " // Update mission paths (SimPath), this needs to get there before the objects.\n" " %client.transmitPaths();\n" "\n" " // Start ghosting objects to the client\n" " %client.activateGhosting();\n" "}\n" "@endtsexample\n" "@see clearPaths()\n" "@see Path\n") { gServerPathManager->transmitPaths(object); object->setMissionPathsSent(true); } DefineEngineMethod( NetConnection, clearPaths, void, (),, "@brief On the server, resets the connection to indicate that motion spline paths have not been transmitted.\n\n" "Typically when a mission has ended on the server, all connected clients are informed of this change " "and their connections are reset back to a starting state. This method resets a connection on the " "server to indicate that motion spline paths have not been transmitted.\n\n" "@tsexample\n" " // Inform the clients\n" " for (%clientIndex = 0; %clientIndex < ClientGroup.getCount(); %clientIndex++)\n" " {\n" " // clear ghosts and paths from all clients\n" " %cl = ClientGroup.getObject(%clientIndex);\n" " %cl.endMission();\n" " %cl.resetGhosting();\n" " %cl.clearPaths();\n" " }\n" "@endtsexample\n" "@see transmitPaths()\n" "@see Path\n") { object->setMissionPathsSent(false); } #endif DefineEngineMethod( NetConnection, getAddress, const char *, (),, "@brief Returns the far end network address for the connection.\n\n" "The address will be in one of the following forms:\n" "- IP:Broadcast:<port> for broadcast type addresses\n" "- IP:<address>:<port> for IP addresses\n" "- local when connected locally (server and client running in same process\n") { if(object->isLocalConnection()) return "local"; char *buffer = Con::getReturnBuffer(256); Net::addressToString(object->getNetAddress(), buffer); return buffer; } DefineEngineMethod( NetConnection, setSimulatedNetParams, void, (F32 packetLoss, S32 delay),, "@brief Simulate network issues on the connection for testing.\n\n" "@param packetLoss The fraction of packets that will be lost. Ranges from 0.0 (no loss) to 1.0 (complete loss)\n" "@param delay Delays packets being transmitted by simulating a particular ping. This is an absolute " "integer, measured in ms.\n") { object->setSimulatedNetParams(packetLoss, delay); } DefineEngineMethod( NetConnection, getPing, S32, (),, "@brief Returns the average round trip time (in ms) for the connection.\n\n" "The round trip time is recalculated every time a notify packet is received. Notify " "packets are used to information the connection that the far end successfully received " "the sent packet.\n") { return( S32( object->getRoundTripTime() ) ); } DefineEngineMethod( NetConnection, getPacketLoss, S32, (),, "@brief Returns the percentage of packets lost per tick.\n\n" "@note This method is not yet hooked up.\n") { return( S32( 100 * object->getPacketLoss() ) ); } DefineEngineMethod( NetConnection, checkMaxRate, void, (),, "@brief Ensures that all configured packet rates and sizes meet minimum requirements.\n\n" "This method is normally only called when a NetConnection class is first constructed. It need " "only be manually called if the global variables that set the packet rate or size have changed.\n\n" "@note If @$pref::Net::PacketRateToServer, @$pref::Net::PacketRateToClient or @$pref::Net::PacketSize " "have been changed since a NetConnection has been created, this method must be called on " "all connections for them to follow the new rates or size.\n") { object->checkMaxRate(); } #ifdef TORQUE_DEBUG_NET DefineEngineMethod( NetConnection, setLogging, void, (bool state),, "@brief Sets if debug statements should be written to the console log.\n\n" "@note Only valid if the executable has been compiled with TORQUE_DEBUG_NET.\n") { object->setLogging(state); } #endif //-------------------------------------------------------------------- void NetConnection::setEstablished() { AssertFatal(!mEstablished, "NetConnection::setEstablished - Error, this NetConnection has already been established."); mEstablished = true; mNextConnection = mConnectionList; if(mConnectionList) mConnectionList->mPrevConnection = this; mConnectionList = this; if(isNetworkConnection()) netAddressTableInsert(); } void NetConnection::onRemove() { // delete any ghosts that may exist for this connection, but aren't added while(mGhostAlwaysSaveList.size()) { delete mGhostAlwaysSaveList[0].ghost; mGhostAlwaysSaveList.pop_front(); } if(mNextConnection) mNextConnection->mPrevConnection = mPrevConnection; if(mPrevConnection) mPrevConnection->mNextConnection = mNextConnection; if(mConnectionList == this) mConnectionList = mNextConnection; while(mNotifyQueueHead) handleNotify(false); ghostOnRemove(); eventOnRemove(); Parent::onRemove(); } String NetConnection::mErrorBuffer; void NetConnection::setLastError(const char *fmt, ...) { va_list argptr; va_start(argptr, fmt); mErrorBuffer = String::ToString(fmt, argptr); va_end(argptr); #ifdef TORQUE_DEBUG_NET // setLastErrors assert in net_debug builds AssertFatal(false, mErrorBuffer.c_str()); #endif } //-------------------------------------------------------------------- void NetConnection::handleNotify(bool recvd) { // Con::printf("NET %d: NOTIFY - %d %s", getId(), gPacketId, recvd ? "RECVD" : "DROPPED"); PacketNotify *note = mNotifyQueueHead; AssertFatal(note != NULL, "Error: got a notify with a null notify head."); mNotifyQueueHead = mNotifyQueueHead->nextPacket; if(note->rateChanged && !recvd) mCurRate.changed = true; if(note->maxRateChanged && !recvd) mMaxRate.changed = true; if(recvd) { // Running average of roundTrip time U32 curTime = Platform::getVirtualMilliseconds(); mRoundTripTime = (mRoundTripTime + (curTime - note->sendTime)) * 0.5; packetReceived(note); } else packetDropped(note); delete note; } void NetConnection::processRawPacket(BitStream *bstream) { if(mDemoWriteStream) recordBlock(BlockTypePacket, bstream->getReadByteSize(), bstream->getBuffer()); ConnectionProtocol::processRawPacket(bstream); } void NetConnection::handlePacket(BitStream *bstream) { // Con::printf("NET %d: RECV - %d", getId(), mLastSeqRecvd); // clear out any errors mErrorBuffer = String(); if(bstream->readFlag()) { mCurRate.updateDelay = bstream->readInt(12); mCurRate.packetSize = bstream->readInt(12); } if(bstream->readFlag()) { U32 omaxDelay = bstream->readInt(12); S32 omaxSize = bstream->readInt(12); if(omaxDelay < mMaxRate.updateDelay) omaxDelay = mMaxRate.updateDelay; if(omaxSize > mMaxRate.packetSize) omaxSize = mMaxRate.packetSize; if(omaxDelay != mCurRate.updateDelay || omaxSize != mCurRate.packetSize) { mCurRate.updateDelay = omaxDelay; mCurRate.packetSize = omaxSize; mCurRate.changed = true; } } readPacket(bstream); if(mErrorBuffer.isNotEmpty()) connectionError(mErrorBuffer); } void NetConnection::connectionError(const char *errorString) { TORQUE_UNUSED(errorString); } //-------------------------------------------------------------------- NetConnection::PacketNotify *NetConnection::allocNotify() { return new PacketNotify; } /// Used when simulating lag. /// /// We post this SimEvent when we want to send a packet; it delays for a bit, then /// sends the actual packet. class NetDelayEvent : public SimEvent { U8 buffer[Net::MaxPacketDataSize]; BitStream stream; public: NetDelayEvent(BitStream *inStream) : stream(NULL, 0) { dMemcpy(buffer, inStream->getBuffer(), inStream->getPosition()); stream.setBuffer(buffer, inStream->getPosition()); stream.setPosition(inStream->getPosition()); } void process(SimObject *object) { ((NetConnection *) object)->sendPacket(&stream); } }; void NetConnection::checkPacketSend(bool force) { U32 curTime = Platform::getVirtualMilliseconds(); U32 delay = isConnectionToServer() ? gPacketUpdateDelayToServer : mCurRate.updateDelay; if(!force) { if(curTime < mLastUpdateTime + delay - mSendDelayCredit) return; mSendDelayCredit = curTime - (mLastUpdateTime + delay - mSendDelayCredit); if(mSendDelayCredit > 1000) mSendDelayCredit = 1000; if(mDemoWriteStream) recordBlock(BlockTypeSendPacket, 0, 0); } if(windowFull()) return; BitStream *stream = BitStream::getPacketStream(mCurRate.packetSize); buildSendPacketHeader(stream); mLastUpdateTime = curTime; PacketNotify *note = allocNotify(); if(!mNotifyQueueHead) mNotifyQueueHead = note; else mNotifyQueueTail->nextPacket = note; mNotifyQueueTail = note; note->nextPacket = NULL; note->sendTime = curTime; note->rateChanged = mCurRate.changed; note->maxRateChanged = mMaxRate.changed; if(stream->writeFlag(mCurRate.changed)) { stream->writeInt(mCurRate.updateDelay, 12); stream->writeInt(mCurRate.packetSize, 12); mCurRate.changed = false; } if(stream->writeFlag(mMaxRate.changed)) { stream->writeInt(mMaxRate.updateDelay, 12); stream->writeInt(mMaxRate.packetSize, 12); mMaxRate.changed = false; } #ifdef TORQUE_DEBUG_NET U32 start = stream->getCurPos(); #endif DEBUG_LOG(("PKLOG %d START", getId()) ); writePacket(stream, note); DEBUG_LOG(("PKLOG %d END - %d", getId(), stream->getCurPos() - start) ); if(mSimulatedPacketLoss && Platform::getRandom() < mSimulatedPacketLoss) { //Con::printf("NET %d: SENDDROP - %d", getId(), mLastSendSeq); return; } if(mSimulatedPing) { Sim::postEvent(getId(), new NetDelayEvent(stream), Sim::getCurrentTime() + mSimulatedPing); return; } sendPacket(stream); } Net::Error NetConnection::sendPacket(BitStream *stream) { //Con::printf("NET %d: SEND - %d", getId(), mLastSendSeq); // do nothing on send if this is a demo replay. if(mDemoReadStream) return Net::NoError; gNetBitsSent = stream->getPosition(); if(isLocalConnection()) { // short circuit connection to the other side. // handle the packet, then force a notify. stream->setBuffer(stream->getBuffer(), stream->getPosition(), stream->getPosition()); mRemoteConnection->processRawPacket(stream); return Net::NoError; } else { return Net::sendto(getNetAddress(), stream->getBuffer(), stream->getPosition()); } } //-------------------------------------------------------------------- //-------------------------------------------------------------------- // these are the virtual function defs for Connection - // if your subclass has additional data to read / write / notify, add it in these functions. void NetConnection::readPacket(BitStream *bstream) { eventReadPacket(bstream); ghostReadPacket(bstream); } void NetConnection::writePacket(BitStream *bstream, PacketNotify *note) { eventWritePacket(bstream, note); ghostWritePacket(bstream, note); } void NetConnection::packetReceived(PacketNotify *note) { eventPacketReceived(note); ghostPacketReceived(note); } void NetConnection::packetDropped(PacketNotify *note) { eventPacketDropped(note); ghostPacketDropped(note); } //-------------------------------------------------------------------- //-------------------------------------------------------------------- void NetConnection::writeDemoStartBlock(ResizeBitStream* stream) { ConnectionProtocol::writeDemoStartBlock(stream); stream->write(mRoundTripTime); stream->write(mPacketLoss); #ifndef TORQUE_TGB_ONLY // Write all the current paths to the stream... gClientPathManager->dumpState(stream); #endif stream->validate(); mStringTable->writeDemoStartBlock(stream); U32 start = 0; PacketNotify *note = mNotifyQueueHead; while(note) { start++; note = note->nextPacket; } stream->write(start); eventWriteStartBlock(stream); ghostWriteStartBlock(stream); } bool NetConnection::readDemoStartBlock(BitStream* stream) { ConnectionProtocol::readDemoStartBlock(stream); stream->read(&mRoundTripTime); stream->read(&mPacketLoss); #ifndef TORQUE_TGB_ONLY // Read gClientPathManager->readState(stream); #endif mStringTable->readDemoStartBlock(stream); U32 pos; stream->read(&pos); // notify count for(U32 i = 0; i < pos; i++) { PacketNotify *note = allocNotify(); note->nextPacket = NULL; if(!mNotifyQueueHead) mNotifyQueueHead = note; else mNotifyQueueTail->nextPacket = note; mNotifyQueueTail = note; } eventReadStartBlock(stream); ghostReadStartBlock(stream); return true; } bool NetConnection::startDemoRecord(const char *fileName) { FileStream *fs = new FileStream; if((fs = FileStream::createAndOpen( fileName, Torque::FS::File::Write )) == NULL) return false; mDemoWriteStream = fs; mDemoWriteStream->write(mProtocolVersion); ResizeBitStream bs; // then write out the start block writeDemoStartBlock(&bs); U32 size = bs.getPosition() + 1; mDemoWriteStream->write(size); mDemoWriteStream->write(size, bs.getBuffer()); return true; } bool NetConnection::replayDemoRecord(const char *fileName) { Stream *fs; if((fs = FileStream::createAndOpen( fileName, Torque::FS::File::Read )) == NULL) return false; mDemoReadStream = fs; mDemoReadStream->read(&mProtocolVersion); U32 size; mDemoReadStream->read(&size); U8 *block = new U8[size]; mDemoReadStream->read(size, block); BitStream bs(block, size); bool res = readDemoStartBlock(&bs); delete[] block; if(!res) return false; // prep for first block read // type/size stored in U16: [type:4][size:12] U16 typeSize; mDemoReadStream->read(&typeSize); mDemoNextBlockType = typeSize >> 12; mDemoNextBlockSize = typeSize & 0xFFF; if(mDemoReadStream->getStatus() != Stream::Ok) return false; return true; } void NetConnection::stopRecording() { if(mDemoWriteStream) { delete mDemoWriteStream; mDemoWriteStream = NULL; } } void NetConnection::recordBlock(U32 type, U32 size, void *data) { AssertFatal(type < MaxNumBlockTypes, "NetConnection::recordBlock: invalid type"); AssertFatal(size < MaxBlockSize, "NetConnection::recordBlock: invalid size"); if((type >= MaxNumBlockTypes) || (size >= MaxBlockSize)) return; if(mDemoWriteStream) { // store type/size in U16: [type:4][size:12] U16 typeSize = (type << 12) | size; mDemoWriteStream->write(typeSize); if(size) mDemoWriteStream->write(size, data); } } void NetConnection::handleRecordedBlock(U32 type, U32 size, void *data) { switch(type) { case BlockTypePacket: { BitStream bs(data, size); processRawPacket(&bs); break; } case BlockTypeSendPacket: checkPacketSend(true); break; } } void NetConnection::demoPlaybackComplete() { } void NetConnection::stopDemoPlayback() { demoPlaybackComplete(); deleteObject(); } bool NetConnection::processNextBlock() { U8 buffer[Net::MaxPacketDataSize]; // read in and handle if(mDemoReadStream->read(mDemoNextBlockSize, buffer)) handleRecordedBlock(mDemoNextBlockType, mDemoNextBlockSize, buffer); U16 typeSize; mDemoReadStream->read(&typeSize); mDemoNextBlockType = typeSize >> 12; mDemoNextBlockSize = typeSize & 0xFFF; if(mDemoReadStream->getStatus() != Stream::Ok) { stopDemoPlayback(); return false; } return true; } //-------------------------------------------------------------------- //-------------------------------------------------------------------- // some handy string functions for compressing strings over a connection: enum NetStringConstants { NullString = 0, CString, TagString, Integer }; void NetConnection::validateSendString(const char *str) { if(U8(*str) == StringTagPrefixByte) { NetStringHandle strHandle(dAtoi(str + 1)); checkString(strHandle); } } void NetConnection::packString(BitStream *stream, const char *str) { if(!*str) { stream->writeInt(NullString, 2); return; } if(U8(str[0]) == StringTagPrefixByte) { stream->writeInt(TagString, 2); stream->writeInt(dAtoi(str + 1), ConnectionStringTable::EntryBitSize); return; } if(str[0] == '-' || (str[0] >= '0' && str[0] <= '9')) { char buf[16]; S32 num = dAtoi(str); dSprintf(buf, sizeof(buf), "%d", num); if(!String::compare(buf, str)) { stream->writeInt(Integer, 2); if(stream->writeFlag(num < 0)) num = -num; if(stream->writeFlag(num < 128)) { stream->writeInt(num, 7); return; } if(stream->writeFlag(num < 32768)) { stream->writeInt(num, 15); return; } else { stream->writeInt(num, 31); return; } } } stream->writeInt(CString, 2); stream->writeString(str); } void NetConnection::unpackString(BitStream *stream, char readBuffer[1024]) { U32 code = stream->readInt(2); switch(code) { case NullString: readBuffer[0] = 0; return; case CString: { stream->readString(readBuffer); return; } case TagString: U32 tag; tag = stream->readInt(ConnectionStringTable::EntryBitSize); readBuffer[0] = StringTagPrefixByte; dSprintf(readBuffer+1, 1023, "%d", tag); return; case Integer: bool neg; neg = stream->readFlag(); S32 num; if(stream->readFlag()) num = stream->readInt(7); else if(stream->readFlag()) num = stream->readInt(15); else num = stream->readInt(31); if(neg) num = -num; dSprintf(readBuffer, 1024, "%d", num); } } void NetConnection::packNetStringHandleU(BitStream *stream, NetStringHandle &h) { if(stream->writeFlag(h.isValidString() )) { bool isReceived; U32 netIndex = checkString(h, &isReceived); if(stream->writeFlag(isReceived)) stream->writeInt(netIndex, ConnectionStringTable::EntryBitSize); else stream->writeString(h.getString()); } } NetStringHandle NetConnection::unpackNetStringHandleU(BitStream *stream) { NetStringHandle ret; if(stream->readFlag()) { if(stream->readFlag()) ret = mStringTable->lookupString(stream->readInt(ConnectionStringTable::EntryBitSize)); else { char buf[256]; stream->readString(buf); ret = NetStringHandle(buf); } } return ret; } //---------------------------------------------------------------------------- //---------------------------------------------------------------------------- //---------------------------------------------------------------------------- void NetConnection::setAddressDigest(U32 digest[4]) { mAddressDigest[0] = digest[0]; mAddressDigest[1] = digest[1]; mAddressDigest[2] = digest[2]; mAddressDigest[3] = digest[3]; } void NetConnection::getAddressDigest(U32 digest[4]) { digest[0] = mAddressDigest[0]; digest[1] = mAddressDigest[1]; digest[2] = mAddressDigest[2]; digest[3] = mAddressDigest[3]; } bool NetConnection::canRemoteCreate() { return false; } void NetConnection::onTimedOut() { } void NetConnection::connect(const NetAddress *address) { mNetAddress = *address; GNet->startConnection(this); } void NetConnection::onConnectTimedOut() { } void NetConnection::sendDisconnectPacket(const char *reason) { GNet->sendDisconnectPacket(this, reason); } void NetConnection::onDisconnect(const char *reason) { TORQUE_UNUSED(reason); } void NetConnection::onConnectionRejected(const char *reason) { } void NetConnection::onConnectionEstablished(bool isInitiator) { } void NetConnection::handleStartupError(const char *errorString) { } void NetConnection::writeConnectRequest(BitStream *stream) { stream->write(mNetClassGroup); stream->write(U32(AbstractClassRep::getClassCRC(mNetClassGroup))); } bool NetConnection::readConnectRequest(BitStream *stream, const char **errorString) { U32 classGroup, classCRC; stream->read(&classGroup); stream->read(&classCRC); if(classGroup == mNetClassGroup && classCRC == AbstractClassRep::getClassCRC(mNetClassGroup)) return true; *errorString = "CHR_INVALID"; return false; } void NetConnection::writeConnectAccept(BitStream *stream) { TORQUE_UNUSED(stream); } bool NetConnection::readConnectAccept(BitStream *stream, const char **errorString) { TORQUE_UNUSED(stream); TORQUE_UNUSED(errorString); return true; } DefineEngineMethod( NetConnection, resolveGhostID, S32, (S32 ghostID),, "@brief On the client, convert a ghost ID from this connection to a real SimObject ID.\n\n" "Torque's network ghosting system only exchanges ghost ID's between the server and client. Use " "this method on the client to discover an object's local SimObject ID when you only have a " "ghost ID.\n" "@param ghostID The ghost ID of the object as sent by the server.\n" "@returns The SimObject ID of the object, or 0 if it could not be resolved.\n\n" "@tsexample\n" "%object = ServerConnection.resolveGhostID( %ghostId );\n" "@endtsexample\n\n" "@see @ref ghosting_scoping for a description of the ghosting system.\n\n") { // Safety check if(ghostID < 0 || ghostID > NetConnection::MaxGhostCount) return 0; NetObject *foo = object->resolveGhost(ghostID); if(foo) return foo->getId(); else return 0; } DefineEngineMethod( NetConnection, resolveObjectFromGhostIndex, S32, (S32 ghostID),, "@brief On the server, convert a ghost ID from this connection to a real SimObject ID.\n\n" "Torque's network ghosting system only exchanges ghost ID's between the server and client. Use " "this method on the server to discover an object's local SimObject ID when you only have a " "ghost ID.\n" "@param ghostID The ghost ID of the object as sent by the server.\n" "@returns The SimObject ID of the object, or 0 if it could not be resolved.\n\n" "@tsexample\n" "%object = %client.resolveObjectFromGhostIndex( %ghostId );\n" "@endtsexample\n\n" "@see @ref ghosting_scoping for a description of the ghosting system.\n\n") { // Safety check if(ghostID < 0 || ghostID > NetConnection::MaxGhostCount) return 0; NetObject *foo = object->resolveObjectFromGhostIndex(ghostID); if(foo) return foo->getId(); else return 0; } DefineEngineMethod( NetConnection, getGhostID, S32, (S32 realID),, "@brief On server or client, convert a real id to the ghost id for this connection.\n\n" "Torque's network ghosting system only exchanges ghost ID's between the server and client. Use " "this method on the server or client to discover an object's ghost ID based on its real SimObject ID.\n" "@param realID The real SimObject ID of the object.\n" "@returns The ghost ID of the object for this connection, or -1 if it could not be resolved.\n\n" "@see @ref ghosting_scoping for a description of the ghosting system.\n\n") { NetObject * foo; if(Sim::findObject(realID, foo)) { return object->getGhostIndex(foo); } else { Con::errorf("NetConnection::serverToGhostID - could not find specified object"); return -1; } } DefineEngineMethod( NetConnection, connect, void, (const char* remoteAddress),, "@brief Connects to the remote address.\n\n" "Attempts to connect with another NetConnection on the given address. Typically once " "connected, a game's information is passed along from the server to the client, followed " "by the player entering the game world. The actual procedure is dependent on " "the NetConnection subclass that is used. i.e. GameConnection.\n" "@param remoteAddress The address to connect to in the form of IP:<address>:<port&rt; " "although the IP: portion is optional. The address portion may be in the form " "of w.x.y.z or as a host name, in which case a DNS lookup will be performed. You may also " "substitue the word broadcast for the address to broadcast the connect request over " "the local subnet.\n\n" "@see NetConnection::connectLocal() to connect to a server running within the same process " "as the client.\n" ) { NetAddress addr; if (Net::stringToAddress(remoteAddress, &addr) != Net::NoError) { Con::errorf("NetConnection::connect: invalid address - %s", remoteAddress); return; } object->connect(&addr); } DefineEngineMethod( NetConnection, connectLocal, const char*, (),, "@brief Connects with the server that is running within the same process as the client.\n\n" "@returns An error text message upon failure, or an empty string when successful.\n\n" "@see See @ref local_connections for a description of local connections and their use. See " "NetConnection::connect() to connect to a server running in another process (on the same machine or not).\n") { ConsoleObject *co = ConsoleObject::create(object->getClassName()); NetConnection *client = object; NetConnection *server = dynamic_cast(co); BitStream *stream = BitStream::getPacketStream(); if(!server || !server->canRemoteCreate()) { delete co; return "error"; } server->registerObject(); server->setIsLocalClientConnection(); server->setSequence(0); client->setSequence(0); client->setRemoteConnectionObject(server); server->setRemoteConnectionObject(client); //We need to reset the maxrate's here, because we // can't test if it is a local connection until RemoteConnectionObject // has been set server->checkMaxRate(); client->checkMaxRate(); stream->setPosition(0); client->writeConnectRequest(stream); stream->setPosition(0); const char* error; if( !server->readConnectRequest( stream, &error ) ) { client->onConnectionRejected( error ); server->deleteObject(); return "error"; } stream->setPosition(0); server->writeConnectAccept(stream); stream->setPosition(0); if( !client->readConnectAccept( stream, &error ) ) { client->handleStartupError( error ); server->deleteObject(); return "error"; } client->onConnectionEstablished(true); server->onConnectionEstablished(false); client->setEstablished(); server->setEstablished(); client->setConnectSequence(0); server->setConnectSequence(0); NetConnection::setLocalClientConnection(server); server->assignName("LocalClientConnection"); return ""; }