hifiGameProcess.cpp 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. //-----------------------------------------------------------------------------
  2. // Copyright (c) 2012 GarageGames, LLC
  3. //
  4. // Permission is hereby granted, free of charge, to any person obtaining a copy
  5. // of this software and associated documentation files (the "Software"), to
  6. // deal in the Software without restriction, including without limitation the
  7. // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
  8. // sell copies of the Software, and to permit persons to whom the Software is
  9. // furnished to do so, subject to the following conditions:
  10. //
  11. // The above copyright notice and this permission notice shall be included in
  12. // all copies or substantial portions of the Software.
  13. //
  14. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  19. // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
  20. // IN THE SOFTWARE.
  21. //-----------------------------------------------------------------------------
  22. #include "platform/platform.h"
  23. #include "T3D/gameBase/hifi/hifiGameProcess.h"
  24. #include "platform/profiler.h"
  25. #include "core/frameAllocator.h"
  26. #include "core/stream/bitStream.h"
  27. #include "math/mathUtils.h"
  28. #include "T3D/gameBase/hifi/hifiMoveList.h"
  29. #include "T3D/gameBase/gameConnection.h"
  30. #include "T3D/gameFunctions.h"
  31. MODULE_BEGIN( ProcessList )
  32. MODULE_INIT
  33. {
  34. HifiServerProcessList::init();
  35. HifiClientProcessList::init();
  36. }
  37. MODULE_SHUTDOWN
  38. {
  39. HifiServerProcessList::shutdown();
  40. HifiClientProcessList::shutdown();
  41. }
  42. MODULE_END;
  43. void HifiServerProcessList::init()
  44. {
  45. smServerProcessList = new HifiServerProcessList();
  46. }
  47. void HifiServerProcessList::shutdown()
  48. {
  49. delete smServerProcessList;
  50. }
  51. void HifiClientProcessList::init()
  52. {
  53. smClientProcessList = new HifiClientProcessList();
  54. }
  55. void HifiClientProcessList::shutdown()
  56. {
  57. delete smClientProcessList;
  58. }
  59. //----------------------------------------------------------------------------
  60. F32 gMaxHiFiVelSq = 100 * 100;
  61. namespace
  62. {
  63. inline GameBase * GetGameBase(ProcessObject * obj)
  64. {
  65. return static_cast<GameBase*>(obj);
  66. }
  67. // local work class
  68. struct GameBaseListNode
  69. {
  70. GameBaseListNode()
  71. {
  72. mPrev=this;
  73. mNext=this;
  74. mObject=NULL;
  75. }
  76. GameBaseListNode * mPrev;
  77. GameBaseListNode * mNext;
  78. GameBase * mObject;
  79. void linkBefore(GameBaseListNode * obj)
  80. {
  81. // Link this before obj
  82. mNext = obj;
  83. mPrev = obj->mPrev;
  84. obj->mPrev = this;
  85. mPrev->mNext = this;
  86. }
  87. };
  88. // Structure used for synchronizing move lists on client/server
  89. struct MoveSync
  90. {
  91. enum { ActionCount = 4 };
  92. S32 moveDiff;
  93. S32 moveDiffSteadyCount;
  94. S32 moveDiffSameSignCount;
  95. bool doAction() { return moveDiffSteadyCount>=ActionCount || moveDiffSameSignCount>=4*ActionCount; }
  96. void reset() { moveDiff=0; moveDiffSteadyCount=0; moveDiffSameSignCount=0; }
  97. void update(S32 diff);
  98. } moveSync;
  99. void MoveSync::update(S32 diff)
  100. {
  101. if (diff && diff==moveDiff)
  102. {
  103. moveDiffSteadyCount++;
  104. moveDiffSameSignCount++;
  105. }
  106. else if (diff*moveDiff>0)
  107. {
  108. moveDiffSteadyCount = 0;
  109. moveDiffSameSignCount++;
  110. }
  111. else
  112. reset();
  113. moveDiff = diff;
  114. }
  115. } // namespace
  116. //--------------------------------------------------------------------------
  117. // HifiClientProcessList
  118. //--------------------------------------------------------------------------
  119. HifiClientProcessList::HifiClientProcessList()
  120. {
  121. mSkipAdvanceObjectsMs = 0;
  122. mForceHifiReset = false;
  123. mCatchup = 0;
  124. }
  125. bool HifiClientProcessList::advanceTime( SimTime timeDelta )
  126. {
  127. PROFILE_SCOPE( AdvanceClientTime );
  128. if ( mSkipAdvanceObjectsMs && timeDelta > mSkipAdvanceObjectsMs )
  129. {
  130. timeDelta -= mSkipAdvanceObjectsMs;
  131. advanceTime( mSkipAdvanceObjectsMs );
  132. AssertFatal( !mSkipAdvanceObjectsMs, "mSkipAdvanceObjectsMs must always be positive." );
  133. }
  134. if ( doBacklogged( timeDelta ) )
  135. return false;
  136. // remember interpolation value because we might need to set it back
  137. F32 oldLastDelta = mLastDelta;
  138. bool ret = Parent::advanceTime( timeDelta );
  139. if ( !mSkipAdvanceObjectsMs )
  140. {
  141. AssertFatal( mLastDelta >= 0.0f && mLastDelta <= 1.0f, "mLastDelta must always be zero to one." );
  142. for ( ProcessObject *pobj = mHead.mProcessLink.next; pobj != &mHead; pobj = pobj->mProcessLink.next )
  143. {
  144. if ( pobj->isTicking() )
  145. pobj->interpolateTick( mLastDelta );
  146. }
  147. // Inform objects of total elapsed delta so they can advance
  148. // client side animations.
  149. F32 dt = F32( timeDelta ) / 1000;
  150. for ( ProcessObject *pobj = mHead.mProcessLink.next; pobj != &mHead; pobj = pobj->mProcessLink.next)
  151. {
  152. pobj->advanceTime( dt );
  153. }
  154. }
  155. else
  156. {
  157. mSkipAdvanceObjectsMs -= timeDelta;
  158. mLastDelta = oldLastDelta;
  159. }
  160. return ret;
  161. }
  162. void HifiClientProcessList::onAdvanceObjects()
  163. {
  164. GameConnection* connection = GameConnection::getConnectionToServer();
  165. if(connection)
  166. {
  167. // process any demo blocks that are NOT moves, and exactly one move
  168. // we advance time in the demo stream by a move inserted on
  169. // each tick. So before doing the tick processing we advance
  170. // the demo stream until a move is ready
  171. if(connection->isPlayingBack())
  172. {
  173. U32 blockType;
  174. do
  175. {
  176. blockType = connection->getNextBlockType();
  177. bool res = connection->processNextBlock();
  178. // if there are no more blocks, exit out of this function,
  179. // as no more client time needs to process right now - we'll
  180. // get it all on the next advanceClientTime()
  181. if(!res)
  182. return;
  183. }
  184. while(blockType != GameConnection::BlockTypeMove);
  185. }
  186. if (!mSkipAdvanceObjectsMs)
  187. {
  188. connection->mMoveList->collectMove();
  189. advanceObjects();
  190. }
  191. connection->mMoveList->onAdvanceObjects();
  192. }
  193. }
  194. void HifiClientProcessList::onTickObject(ProcessObject * pobj)
  195. {
  196. // Each object is advanced a single tick
  197. // If it's controlled by a client, tick using a move.
  198. Move *movePtr;
  199. U32 numMoves;
  200. GameConnection *con = pobj->getControllingClient();
  201. SimObjectPtr<GameBase> obj = getGameBase( pobj );
  202. if ( obj && con && con->getControlObject() == obj && con->mMoveList->getMoves( &movePtr, &numMoves) )
  203. {
  204. #ifdef TORQUE_DEBUG_NET_MOVES
  205. U32 sum = Move::ChecksumMask & obj->getPacketDataChecksum( obj->getControllingClient() );
  206. #endif
  207. obj->processTick( movePtr );
  208. if ( bool(obj) && obj->getControllingClient() )
  209. {
  210. U32 newsum = Move::ChecksumMask & obj->getPacketDataChecksum( obj->getControllingClient() );
  211. // set checksum if not set or check against stored value if set
  212. movePtr->checksum = newsum;
  213. #ifdef TORQUE_DEBUG_NET_MOVES
  214. Con::printf( "move checksum: %i, (start %i), (move %f %f %f)",
  215. movePtr->checksum,sum,movePtr->yaw,movePtr->y,movePtr->z );
  216. #endif
  217. }
  218. con->mMoveList->clearMoves( 1 );
  219. }
  220. else if ( pobj->isTicking() )
  221. pobj->processTick( 0 );
  222. if ( obj && ( obj->getTypeMask() & GameBaseHiFiObjectType ) )
  223. {
  224. GameConnection * serverConnection = GameConnection::getConnectionToServer();
  225. TickCacheEntry * tce = obj->getTickCache().addCacheEntry();
  226. BitStream bs( tce->packetData, TickCacheEntry::MaxPacketSize );
  227. obj->writePacketData( serverConnection, &bs );
  228. Point3F vel = obj->getVelocity();
  229. F32 velSq = mDot( vel, vel );
  230. gMaxHiFiVelSq = getMax( gMaxHiFiVelSq, velSq );
  231. }
  232. }
  233. void HifiClientProcessList::advanceObjects()
  234. {
  235. #ifdef TORQUE_DEBUG_NET_MOVES
  236. Con::printf("Advance client time...");
  237. #endif
  238. // client re-computes this each time objects are advanced
  239. gMaxHiFiVelSq = 0;
  240. Parent::advanceObjects();
  241. // We need to consume a move on the connections whether
  242. // there is a control object to consume the move or not,
  243. // otherwise client and server can get out of sync move-wise
  244. // during startup. If there is a control object, we cleared
  245. // a move above. Handle case where no control object here.
  246. // Note that we might consume an extra move here and there when
  247. // we had a control object in above loop but lost it during tick.
  248. // That is no big deal so we don't bother trying to carefully
  249. // track it.
  250. GameConnection * client = GameConnection::getConnectionToServer();
  251. if (client && client->getControlObject() == NULL)
  252. client->mMoveList->clearMoves(1);
  253. #ifdef TORQUE_DEBUG_NET_MOVES
  254. Con::printf("---------");
  255. #endif
  256. }
  257. void HifiClientProcessList::ageTickCache(S32 numToAge, S32 len)
  258. {
  259. for (ProcessObject * pobj = mHead.mProcessLink.next; pobj != &mHead; pobj = pobj->mProcessLink.next)
  260. {
  261. GameBase *obj = getGameBase(pobj);
  262. if ( obj && obj->getTypeMask() & GameBaseHiFiObjectType )
  263. obj->getTickCache().ageCache(numToAge,len);
  264. }
  265. }
  266. void HifiClientProcessList::updateMoveSync(S32 moveDiff)
  267. {
  268. moveSync.update(moveDiff);
  269. if (moveSync.doAction() && moveDiff<0)
  270. {
  271. skipAdvanceObjects(TickMs * -moveDiff);
  272. moveSync.reset();
  273. }
  274. }
  275. void HifiClientProcessList::clientCatchup(GameConnection * connection)
  276. {
  277. #ifdef TORQUE_DEBUG_NET_MOVES
  278. Con::printf("client catching up... (%i)%s", mCatchup, mForceHifiReset ? " reset" : "");
  279. #endif
  280. if (connection->getControlObject() && connection->getControlObject()->isGhostUpdated())
  281. // if control object is reset, make sure moves are reset too
  282. connection->mMoveList->resetCatchup();
  283. const F32 maxVel = mSqrt(gMaxHiFiVelSq) * 1.25f;
  284. F32 dt = F32(mCatchup+1) * TickSec;
  285. Point3F bigDelta(maxVel*dt,maxVel*dt,maxVel*dt);
  286. // walk through all process objects looking for ones which were updated
  287. // -- during first pass merely collect neighbors which need to be reset and updated in unison
  288. ProcessObject * pobj;
  289. if (mCatchup && !mForceHifiReset)
  290. {
  291. for (pobj = mHead.mProcessLink.next; pobj != &mHead; pobj = pobj->mProcessLink.next)
  292. {
  293. GameBase *obj = getGameBase( pobj );
  294. static SimpleQueryList nearby;
  295. nearby.mList.clear();
  296. // check for nearby objects which need to be reset and then caught up
  297. // note the funky loop logic -- first time through obj is us, then
  298. // we start iterating through nearby list (to look for objects nearby
  299. // the nearby objects), which is why index starts at -1
  300. // [objects nearby the nearby objects also get added to the nearby list]
  301. for (S32 i=-1; obj; obj = ++i<nearby.mList.size() ? (GameBase*)nearby.mList[i] : NULL)
  302. {
  303. if (obj->isGhostUpdated() && (obj->getTypeMask() & GameBaseHiFiObjectType) && !obj->isNetNearbyAdded())
  304. {
  305. Point3F start = obj->getWorldSphere().center;
  306. Point3F end = start + 1.1f * dt * obj->getVelocity();
  307. F32 rad = 1.5f * obj->getWorldSphere().radius;
  308. // find nearby items not updated but are hi fi, mark them as updated (and restore old loc)
  309. // check to see if added items have neighbors that need updating
  310. Box3F box;
  311. Point3F rads(rad,rad,rad);
  312. box.minExtents = box.maxExtents = start;
  313. box.minExtents -= bigDelta + rads;
  314. box.maxExtents += bigDelta + rads;
  315. // CodeReview - this is left in for MBU, but also so we can deal with the issue later.
  316. // add marble blast hack so hifi networking can see hidden objects
  317. // (since hidden is under control of hifi networking)
  318. // gForceNotHidden = true;
  319. S32 j = nearby.mList.size();
  320. gClientContainer.findObjects(box, GameBaseHiFiObjectType, SimpleQueryList::insertionCallback, &nearby);
  321. // CodeReview - this is left in for MBU, but also so we can deal with the issue later.
  322. // disable above hack
  323. // gForceNotHidden = false;
  324. // drop anyone not heading toward us or already checked
  325. for (; j<nearby.mList.size(); j++)
  326. {
  327. GameBase * obj2 = (GameBase*)nearby.mList[j];
  328. // if both passive, these guys don't interact with each other
  329. bool passive = obj->isHifiPassive() && obj2->isHifiPassive();
  330. if (!obj2->isGhostUpdated() && !passive)
  331. {
  332. // compare swept spheres of obj and obj2
  333. // if collide, reset obj2, setGhostUpdated(true), and continue
  334. Point3F end2 = obj2->getWorldSphere().center;
  335. Point3F start2 = end2 - 1.1f * dt * obj2->getVelocity();
  336. F32 rad2 = 1.5f * obj->getWorldSphere().radius;
  337. if (MathUtils::capsuleCapsuleOverlap(start,end,rad,start2,end2,rad2))
  338. {
  339. // better add obj2
  340. obj2->getTickCache().beginCacheList();
  341. TickCacheEntry * tce = obj2->getTickCache().incCacheList();
  342. BitStream bs(tce->packetData,TickCacheEntry::MaxPacketSize);
  343. obj2->readPacketData(connection,&bs);
  344. obj2->setGhostUpdated(true);
  345. // continue so we later add the neighbors too
  346. continue;
  347. }
  348. }
  349. // didn't pass above test...so don't add it or nearby objects
  350. nearby.mList[j] = nearby.mList.last();
  351. nearby.mList.decrement();
  352. j--;
  353. }
  354. obj->setNetNearbyAdded(true);
  355. }
  356. }
  357. }
  358. }
  359. // save water mark -- for game base list
  360. FrameAllocatorMarker mark;
  361. // build ordered list of client objects which need to be caught up
  362. GameBaseListNode list;
  363. for (pobj = mHead.mProcessLink.next; pobj != &mHead; pobj = pobj->mProcessLink.next)
  364. {
  365. GameBase *obj = getGameBase( pobj );
  366. //GameBase *obj = dynamic_cast<GameBase*>( pobj );
  367. //GameBase *obj = (GameBase*)pobj;
  368. // Not a GameBase object so nothing to do.
  369. if ( !obj )
  370. continue;
  371. if (obj->isGhostUpdated() && (obj->getTypeMask() & GameBaseHiFiObjectType))
  372. {
  373. // construct process object and add it to the list
  374. // hold pointer to our object in mAfterObject
  375. GameBaseListNode * po = (GameBaseListNode*)FrameAllocator::alloc(sizeof(GameBaseListNode));
  376. po->mObject = obj;
  377. po->linkBefore(&list);
  378. // begin iterating through tick list (skip first tick since that is the state we've been reset to)
  379. obj->getTickCache().beginCacheList();
  380. obj->getTickCache().incCacheList();
  381. }
  382. else if (mForceHifiReset && (obj->getTypeMask() & GameBaseHiFiObjectType))
  383. {
  384. // add all hifi objects
  385. obj->getTickCache().beginCacheList();
  386. TickCacheEntry * tce = obj->getTickCache().incCacheList();
  387. BitStream bs(tce->packetData,TickCacheEntry::MaxPacketSize);
  388. obj->readPacketData(connection,&bs);
  389. obj->setGhostUpdated(true);
  390. // construct process object and add it to the list
  391. // hold pointer to our object in mAfterObject
  392. GameBaseListNode * po = (GameBaseListNode*)FrameAllocator::alloc(sizeof(GameBaseListNode));
  393. po->mObject = obj;
  394. po->linkBefore(&list);
  395. }
  396. else if (obj == connection->getControlObject() && obj->isGhostUpdated())
  397. {
  398. // construct process object and add it to the list
  399. // hold pointer to our object in mAfterObject
  400. // .. but this is not a hi fi object, so don't mess with tick cache
  401. GameBaseListNode * po = (GameBaseListNode*)FrameAllocator::alloc(sizeof(GameBaseListNode));
  402. po->mObject = obj;
  403. po->linkBefore(&list);
  404. }
  405. else if (obj->isGhostUpdated())
  406. {
  407. // not hifi but we were updated, so perform net smooth now
  408. obj->computeNetSmooth(mLastDelta);
  409. }
  410. // clear out work flags
  411. obj->setNetNearbyAdded(false);
  412. obj->setGhostUpdated(false);
  413. }
  414. // run through all the moves in the move list so we can play them with our control object
  415. Move* movePtr;
  416. U32 numMoves;
  417. connection->mMoveList->resetClientMoves();
  418. connection->mMoveList->getMoves(&movePtr, &numMoves);
  419. AssertFatal(mCatchup<=numMoves,"doh");
  420. // tick catchup time
  421. for (U32 m=0; m<mCatchup; m++)
  422. {
  423. for (GameBaseListNode * walk = list.mNext; walk != &list; walk = walk->mNext)
  424. {
  425. // note that we get object from after object not getGameBase function
  426. // this is because we are an on the fly linked list which uses mAfterObject
  427. // rather than the linked list embedded in GameBase (clean this up?)
  428. GameBase * obj = walk->mObject;
  429. // it's possible for a non-hifi object to get in here, but
  430. // only if it is a control object...make sure we don't do any
  431. // of the tick cache stuff if we are not hifi.
  432. bool hifi = obj->getTypeMask() & GameBaseHiFiObjectType;
  433. TickCacheEntry * tce = hifi ? obj->getTickCache().incCacheList() : NULL;
  434. // tick object
  435. if (obj==connection->getControlObject())
  436. {
  437. obj->processTick(movePtr);
  438. movePtr->checksum = obj->getPacketDataChecksum(connection);
  439. movePtr++;
  440. }
  441. else
  442. {
  443. AssertFatal(tce && hifi,"Should not get in here unless a hi fi object!!!");
  444. obj->processTick(tce->move);
  445. }
  446. if (hifi)
  447. {
  448. BitStream bs(tce->packetData,TickCacheEntry::MaxPacketSize);
  449. obj->writePacketData(connection,&bs);
  450. }
  451. }
  452. if (connection->getControlObject() == NULL)
  453. movePtr++;
  454. }
  455. connection->mMoveList->clearMoves(mCatchup);
  456. // Handle network error smoothing here...but only for control object
  457. GameBase * control = connection->getControlObject();
  458. if (control && !control->isNewGhost())
  459. {
  460. control->computeNetSmooth(mLastDelta);
  461. control->setNewGhost(false);
  462. }
  463. if (moveSync.doAction() && moveSync.moveDiff>0)
  464. {
  465. S32 moveDiff = moveSync.moveDiff;
  466. #ifdef TORQUE_DEBUG_NET_MOVES
  467. Con::printf("client timewarping to catchup %i moves",moveDiff);
  468. #endif
  469. while (moveDiff--)
  470. advanceObjects();
  471. moveSync.reset();
  472. }
  473. #ifdef TORQUE_DEBUG_NET_MOVES
  474. Con::printf("---------");
  475. #endif
  476. // all caught up
  477. mCatchup = 0;
  478. }
  479. //--------------------------------------------------------------------------
  480. // HifiServerProcessList
  481. //--------------------------------------------------------------------------
  482. void HifiServerProcessList::onTickObject(ProcessObject * pobj)
  483. {
  484. // Each object is advanced a single tick
  485. // If it's controlled by a client, tick using a move.
  486. Move *movePtr;
  487. U32 numMoves;
  488. GameConnection *con = pobj->getControllingClient();
  489. SimObjectPtr<GameBase> obj = getGameBase( pobj );
  490. if ( obj && con && con->getControlObject() == obj && con->mMoveList->getMoves( &movePtr, &numMoves ) )
  491. {
  492. #ifdef TORQUE_DEBUG_NET_MOVES
  493. U32 sum = Move::ChecksumMask & obj->getPacketDataChecksum( obj->getControllingClient() );
  494. #endif
  495. obj->processTick(movePtr);
  496. if ( bool(obj) && obj->getControllingClient() )
  497. {
  498. U32 newsum = Move::ChecksumMask & obj->getPacketDataChecksum( obj->getControllingClient() );
  499. // check move checksum
  500. if ( movePtr->checksum != newsum )
  501. {
  502. #ifdef TORQUE_DEBUG_NET_MOVES
  503. if ( !obj->mIsAiControlled )
  504. Con::printf( "move %i checksum disagree: %i != %i, (start %i), (move %f %f %f)",
  505. movePtr->id, movePtr->checksum, newsum, sum, movePtr->yaw, movePtr->y, movePtr->z );
  506. #endif
  507. movePtr->checksum = Move::ChecksumMismatch;
  508. }
  509. else
  510. {
  511. #ifdef TORQUE_DEBUG_NET_MOVES
  512. Con::printf( "move %i checksum agree: %i == %i, (start %i), (move %f %f %f)",
  513. movePtr->id, movePtr->checksum, newsum, sum, movePtr->yaw, movePtr->y, movePtr->z );
  514. #endif
  515. }
  516. // Adding this seems to fix constant corrections, but is it
  517. // really a sound fix?
  518. con->mMoveList->clearMoves( 1 );
  519. }
  520. }
  521. else if ( pobj->isTicking() )
  522. pobj->processTick( 0 );
  523. }