//----------------------------------------------------------------------------- // 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 "T3D/gameBase/hifi/hifiGameProcess.h" #include "platform/profiler.h" #include "core/frameAllocator.h" #include "core/stream/bitStream.h" #include "math/mathUtils.h" #include "T3D/gameBase/hifi/hifiMoveList.h" #include "T3D/gameBase/gameConnection.h" #include "T3D/gameFunctions.h" MODULE_BEGIN( ProcessList ) MODULE_INIT { HifiServerProcessList::init(); HifiClientProcessList::init(); } MODULE_SHUTDOWN { HifiServerProcessList::shutdown(); HifiClientProcessList::shutdown(); } MODULE_END; void HifiServerProcessList::init() { smServerProcessList = new HifiServerProcessList(); } void HifiServerProcessList::shutdown() { delete smServerProcessList; } void HifiClientProcessList::init() { smClientProcessList = new HifiClientProcessList(); } void HifiClientProcessList::shutdown() { delete smClientProcessList; } //---------------------------------------------------------------------------- F32 gMaxHiFiVelSq = 100 * 100; namespace { inline GameBase * GetGameBase(ProcessObject * obj) { return static_cast(obj); } // local work class struct GameBaseListNode { GameBaseListNode() { mPrev=this; mNext=this; mObject=NULL; } GameBaseListNode * mPrev; GameBaseListNode * mNext; GameBase * mObject; void linkBefore(GameBaseListNode * obj) { // Link this before obj mNext = obj; mPrev = obj->mPrev; obj->mPrev = this; mPrev->mNext = this; } }; // Structure used for synchronizing move lists on client/server struct MoveSync { enum { ActionCount = 4 }; S32 moveDiff; S32 moveDiffSteadyCount; S32 moveDiffSameSignCount; bool doAction() { return moveDiffSteadyCount>=ActionCount || moveDiffSameSignCount>=4*ActionCount; } void reset() { moveDiff=0; moveDiffSteadyCount=0; moveDiffSameSignCount=0; } void update(S32 diff); } moveSync; void MoveSync::update(S32 diff) { if (diff && diff==moveDiff) { moveDiffSteadyCount++; moveDiffSameSignCount++; } else if (diff*moveDiff>0) { moveDiffSteadyCount = 0; moveDiffSameSignCount++; } else reset(); moveDiff = diff; } } // namespace //-------------------------------------------------------------------------- // HifiClientProcessList //-------------------------------------------------------------------------- HifiClientProcessList::HifiClientProcessList() { mSkipAdvanceObjectsMs = 0; mForceHifiReset = false; mCatchup = 0; } bool HifiClientProcessList::advanceTime( SimTime timeDelta ) { PROFILE_SCOPE( AdvanceClientTime ); if ( mSkipAdvanceObjectsMs && timeDelta > mSkipAdvanceObjectsMs ) { timeDelta -= mSkipAdvanceObjectsMs; advanceTime( mSkipAdvanceObjectsMs ); AssertFatal( !mSkipAdvanceObjectsMs, "mSkipAdvanceObjectsMs must always be positive." ); } if ( doBacklogged( timeDelta ) ) return false; // remember interpolation value because we might need to set it back F32 oldLastDelta = mLastDelta; bool ret = Parent::advanceTime( timeDelta ); if ( !mSkipAdvanceObjectsMs ) { AssertFatal( mLastDelta >= 0.0f && mLastDelta <= 1.0f, "mLastDelta must always be zero to one." ); for ( ProcessObject *pobj = mHead.mProcessLink.next; pobj != &mHead; pobj = pobj->mProcessLink.next ) { if ( pobj->isTicking() ) pobj->interpolateTick( mLastDelta ); } // Inform objects of total elapsed delta so they can advance // client side animations. F32 dt = F32( timeDelta ) / 1000; for ( ProcessObject *pobj = mHead.mProcessLink.next; pobj != &mHead; pobj = pobj->mProcessLink.next) { pobj->advanceTime( dt ); } } else { mSkipAdvanceObjectsMs -= timeDelta; mLastDelta = oldLastDelta; } return ret; } void HifiClientProcessList::onAdvanceObjects() { GameConnection* connection = GameConnection::getConnectionToServer(); if(connection) { // process any demo blocks that are NOT moves, and exactly one move // we advance time in the demo stream by a move inserted on // each tick. So before doing the tick processing we advance // the demo stream until a move is ready if(connection->isPlayingBack()) { U32 blockType; do { blockType = connection->getNextBlockType(); bool res = connection->processNextBlock(); // if there are no more blocks, exit out of this function, // as no more client time needs to process right now - we'll // get it all on the next advanceClientTime() if(!res) return; } while(blockType != GameConnection::BlockTypeMove); } if (!mSkipAdvanceObjectsMs) { connection->mMoveList->collectMove(); advanceObjects(); } connection->mMoveList->onAdvanceObjects(); } } void HifiClientProcessList::onTickObject(ProcessObject * pobj) { // Each object is advanced a single tick // If it's controlled by a client, tick using a move. Move *movePtr; U32 numMoves; GameConnection *con = pobj->getControllingClient(); SimObjectPtr obj = getGameBase( pobj ); if ( obj && con && con->getControlObject() == obj && con->mMoveList->getMoves( &movePtr, &numMoves) ) { #ifdef TORQUE_DEBUG_NET_MOVES U32 sum = Move::ChecksumMask & obj->getPacketDataChecksum( obj->getControllingClient() ); #endif obj->processTick( movePtr ); if ( bool(obj) && obj->getControllingClient() ) { U32 newsum = Move::ChecksumMask & obj->getPacketDataChecksum( obj->getControllingClient() ); // set checksum if not set or check against stored value if set movePtr->checksum = newsum; #ifdef TORQUE_DEBUG_NET_MOVES Con::printf( "move checksum: %i, (start %i), (move %f %f %f)", movePtr->checksum,sum,movePtr->yaw,movePtr->y,movePtr->z ); #endif } con->mMoveList->clearMoves( 1 ); } else if ( pobj->isTicking() ) pobj->processTick( 0 ); if ( obj && ( obj->getTypeMask() & GameBaseHiFiObjectType ) ) { GameConnection * serverConnection = GameConnection::getConnectionToServer(); TickCacheEntry * tce = obj->getTickCache().addCacheEntry(); BitStream bs( tce->packetData, TickCacheEntry::MaxPacketSize ); obj->writePacketData( serverConnection, &bs ); Point3F vel = obj->getVelocity(); F32 velSq = mDot( vel, vel ); gMaxHiFiVelSq = getMax( gMaxHiFiVelSq, velSq ); } } void HifiClientProcessList::advanceObjects() { #ifdef TORQUE_DEBUG_NET_MOVES Con::printf("Advance client time..."); #endif // client re-computes this each time objects are advanced gMaxHiFiVelSq = 0; Parent::advanceObjects(); // We need to consume a move on the connections whether // there is a control object to consume the move or not, // otherwise client and server can get out of sync move-wise // during startup. If there is a control object, we cleared // a move above. Handle case where no control object here. // Note that we might consume an extra move here and there when // we had a control object in above loop but lost it during tick. // That is no big deal so we don't bother trying to carefully // track it. GameConnection * client = GameConnection::getConnectionToServer(); if (client && client->getControlObject() == NULL) client->mMoveList->clearMoves(1); #ifdef TORQUE_DEBUG_NET_MOVES Con::printf("---------"); #endif } void HifiClientProcessList::ageTickCache(S32 numToAge, S32 len) { for (ProcessObject * pobj = mHead.mProcessLink.next; pobj != &mHead; pobj = pobj->mProcessLink.next) { GameBase *obj = getGameBase(pobj); if ( obj && obj->getTypeMask() & GameBaseHiFiObjectType ) obj->getTickCache().ageCache(numToAge,len); } } void HifiClientProcessList::updateMoveSync(S32 moveDiff) { moveSync.update(moveDiff); if (moveSync.doAction() && moveDiff<0) { skipAdvanceObjects(TickMs * -moveDiff); moveSync.reset(); } } void HifiClientProcessList::clientCatchup(GameConnection * connection) { #ifdef TORQUE_DEBUG_NET_MOVES Con::printf("client catching up... (%i)%s", mCatchup, mForceHifiReset ? " reset" : ""); #endif if (connection->getControlObject() && connection->getControlObject()->isGhostUpdated()) // if control object is reset, make sure moves are reset too connection->mMoveList->resetCatchup(); const F32 maxVel = mSqrt(gMaxHiFiVelSq) * 1.25f; F32 dt = F32(mCatchup+1) * TickSec; Point3F bigDelta(maxVel*dt,maxVel*dt,maxVel*dt); // walk through all process objects looking for ones which were updated // -- during first pass merely collect neighbors which need to be reset and updated in unison ProcessObject * pobj; if (mCatchup && !mForceHifiReset) { for (pobj = mHead.mProcessLink.next; pobj != &mHead; pobj = pobj->mProcessLink.next) { GameBase *obj = getGameBase( pobj ); static SimpleQueryList nearby; nearby.mList.clear(); // check for nearby objects which need to be reset and then caught up // note the funky loop logic -- first time through obj is us, then // we start iterating through nearby list (to look for objects nearby // the nearby objects), which is why index starts at -1 // [objects nearby the nearby objects also get added to the nearby list] for (S32 i=-1; obj; obj = ++iisGhostUpdated() && (obj->getTypeMask() & GameBaseHiFiObjectType) && !obj->isNetNearbyAdded()) { Point3F start = obj->getWorldSphere().center; Point3F end = start + 1.1f * dt * obj->getVelocity(); F32 rad = 1.5f * obj->getWorldSphere().radius; // find nearby items not updated but are hi fi, mark them as updated (and restore old loc) // check to see if added items have neighbors that need updating Box3F box; Point3F rads(rad,rad,rad); box.minExtents = box.maxExtents = start; box.minExtents -= bigDelta + rads; box.maxExtents += bigDelta + rads; // CodeReview - this is left in for MBU, but also so we can deal with the issue later. // add marble blast hack so hifi networking can see hidden objects // (since hidden is under control of hifi networking) // gForceNotHidden = true; S32 j = nearby.mList.size(); gClientContainer.findObjects(box, GameBaseHiFiObjectType, SimpleQueryList::insertionCallback, &nearby); // CodeReview - this is left in for MBU, but also so we can deal with the issue later. // disable above hack // gForceNotHidden = false; // drop anyone not heading toward us or already checked for (; jisHifiPassive() && obj2->isHifiPassive(); if (!obj2->isGhostUpdated() && !passive) { // compare swept spheres of obj and obj2 // if collide, reset obj2, setGhostUpdated(true), and continue Point3F end2 = obj2->getWorldSphere().center; Point3F start2 = end2 - 1.1f * dt * obj2->getVelocity(); F32 rad2 = 1.5f * obj->getWorldSphere().radius; if (MathUtils::capsuleCapsuleOverlap(start,end,rad,start2,end2,rad2)) { // better add obj2 obj2->getTickCache().beginCacheList(); TickCacheEntry * tce = obj2->getTickCache().incCacheList(); BitStream bs(tce->packetData,TickCacheEntry::MaxPacketSize); obj2->readPacketData(connection,&bs); obj2->setGhostUpdated(true); // continue so we later add the neighbors too continue; } } // didn't pass above test...so don't add it or nearby objects nearby.mList[j] = nearby.mList.last(); nearby.mList.decrement(); j--; } obj->setNetNearbyAdded(true); } } } } // save water mark -- for game base list FrameAllocatorMarker mark; // build ordered list of client objects which need to be caught up GameBaseListNode list; for (pobj = mHead.mProcessLink.next; pobj != &mHead; pobj = pobj->mProcessLink.next) { GameBase *obj = getGameBase( pobj ); //GameBase *obj = dynamic_cast( pobj ); //GameBase *obj = (GameBase*)pobj; // Not a GameBase object so nothing to do. if ( !obj ) continue; if (obj->isGhostUpdated() && (obj->getTypeMask() & GameBaseHiFiObjectType)) { // construct process object and add it to the list // hold pointer to our object in mAfterObject GameBaseListNode * po = (GameBaseListNode*)FrameAllocator::alloc(sizeof(GameBaseListNode)); po->mObject = obj; po->linkBefore(&list); // begin iterating through tick list (skip first tick since that is the state we've been reset to) obj->getTickCache().beginCacheList(); obj->getTickCache().incCacheList(); } else if (mForceHifiReset && (obj->getTypeMask() & GameBaseHiFiObjectType)) { // add all hifi objects obj->getTickCache().beginCacheList(); TickCacheEntry * tce = obj->getTickCache().incCacheList(); BitStream bs(tce->packetData,TickCacheEntry::MaxPacketSize); obj->readPacketData(connection,&bs); obj->setGhostUpdated(true); // construct process object and add it to the list // hold pointer to our object in mAfterObject GameBaseListNode * po = (GameBaseListNode*)FrameAllocator::alloc(sizeof(GameBaseListNode)); po->mObject = obj; po->linkBefore(&list); } else if (obj == connection->getControlObject() && obj->isGhostUpdated()) { // construct process object and add it to the list // hold pointer to our object in mAfterObject // .. but this is not a hi fi object, so don't mess with tick cache GameBaseListNode * po = (GameBaseListNode*)FrameAllocator::alloc(sizeof(GameBaseListNode)); po->mObject = obj; po->linkBefore(&list); } else if (obj->isGhostUpdated()) { // not hifi but we were updated, so perform net smooth now obj->computeNetSmooth(mLastDelta); } // clear out work flags obj->setNetNearbyAdded(false); obj->setGhostUpdated(false); } // run through all the moves in the move list so we can play them with our control object Move* movePtr; U32 numMoves; connection->mMoveList->resetClientMoves(); connection->mMoveList->getMoves(&movePtr, &numMoves); AssertFatal(mCatchup<=numMoves,"doh"); // tick catchup time for (U32 m=0; mmNext) { // note that we get object from after object not getGameBase function // this is because we are an on the fly linked list which uses mAfterObject // rather than the linked list embedded in GameBase (clean this up?) GameBase * obj = walk->mObject; // it's possible for a non-hifi object to get in here, but // only if it is a control object...make sure we don't do any // of the tick cache stuff if we are not hifi. bool hifi = obj->getTypeMask() & GameBaseHiFiObjectType; TickCacheEntry * tce = hifi ? obj->getTickCache().incCacheList() : NULL; // tick object if (obj==connection->getControlObject()) { obj->processTick(movePtr); movePtr->checksum = obj->getPacketDataChecksum(connection); movePtr++; } else { AssertFatal(tce && hifi,"Should not get in here unless a hi fi object!!!"); obj->processTick(tce->move); } if (hifi) { BitStream bs(tce->packetData,TickCacheEntry::MaxPacketSize); obj->writePacketData(connection,&bs); } } if (connection->getControlObject() == NULL) movePtr++; } connection->mMoveList->clearMoves(mCatchup); // Handle network error smoothing here...but only for control object GameBase * control = connection->getControlObject(); if (control && !control->isNewGhost()) { control->computeNetSmooth(mLastDelta); control->setNewGhost(false); } if (moveSync.doAction() && moveSync.moveDiff>0) { S32 moveDiff = moveSync.moveDiff; #ifdef TORQUE_DEBUG_NET_MOVES Con::printf("client timewarping to catchup %i moves",moveDiff); #endif while (moveDiff--) advanceObjects(); moveSync.reset(); } #ifdef TORQUE_DEBUG_NET_MOVES Con::printf("---------"); #endif // all caught up mCatchup = 0; } //-------------------------------------------------------------------------- // HifiServerProcessList //-------------------------------------------------------------------------- void HifiServerProcessList::onTickObject(ProcessObject * pobj) { // Each object is advanced a single tick // If it's controlled by a client, tick using a move. Move *movePtr; U32 numMoves; GameConnection *con = pobj->getControllingClient(); SimObjectPtr obj = getGameBase( pobj ); if ( obj && con && con->getControlObject() == obj && con->mMoveList->getMoves( &movePtr, &numMoves ) ) { #ifdef TORQUE_DEBUG_NET_MOVES U32 sum = Move::ChecksumMask & obj->getPacketDataChecksum( obj->getControllingClient() ); #endif obj->processTick(movePtr); if ( bool(obj) && obj->getControllingClient() ) { U32 newsum = Move::ChecksumMask & obj->getPacketDataChecksum( obj->getControllingClient() ); // check move checksum if ( movePtr->checksum != newsum ) { #ifdef TORQUE_DEBUG_NET_MOVES if ( !obj->mIsAiControlled ) Con::printf( "move %i checksum disagree: %i != %i, (start %i), (move %f %f %f)", movePtr->id, movePtr->checksum, newsum, sum, movePtr->yaw, movePtr->y, movePtr->z ); #endif movePtr->checksum = Move::ChecksumMismatch; } else { #ifdef TORQUE_DEBUG_NET_MOVES Con::printf( "move %i checksum agree: %i == %i, (start %i), (move %f %f %f)", movePtr->id, movePtr->checksum, newsum, sum, movePtr->yaw, movePtr->y, movePtr->z ); #endif } // Adding this seems to fix constant corrections, but is it // really a sound fix? con->mMoveList->clearMoves( 1 ); } } else if ( pobj->isTicking() ) pobj->processTick( 0 ); }