ClientRepositoryBase.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  1. from pandac.PandaModules import *
  2. from MsgTypes import *
  3. from direct.task import Task
  4. from direct.directnotify import DirectNotifyGlobal
  5. import CRCache
  6. from direct.distributed.ConnectionRepository import ConnectionRepository
  7. from direct.showbase import PythonUtil
  8. import ParentMgr
  9. import RelatedObjectMgr
  10. import time
  11. from ClockDelta import *
  12. from PyDatagram import PyDatagram
  13. from PyDatagramIterator import PyDatagramIterator
  14. class ClientRepositoryBase(ConnectionRepository):
  15. """
  16. This maintains a client-side connection with a Panda server.
  17. This base class exists to collect the common code between
  18. ClientRepository, which is the CMU-provided, open-source version
  19. of the client repository code, and OTPClientRepository, which is
  20. the VR Studio's implementation of the same.
  21. """
  22. notify = DirectNotifyGlobal.directNotify.newCategory("ClientRepositoryBase")
  23. def __init__(self, dcFileNames = None):
  24. self.dcSuffix=""
  25. ConnectionRepository.__init__(self, ConnectionRepository.CM_HTTP, base.config, hasOwnerView=True)
  26. if hasattr(self, 'setVerbose'):
  27. if self.config.GetBool('verbose-clientrepository'):
  28. self.setVerbose(1)
  29. self.context=100000
  30. self.setClientDatagram(1)
  31. self.deferredGenerates = []
  32. self.deferredDoIds = {}
  33. self.lastGenerate = 0
  34. self.setDeferInterval(base.config.GetDouble('deferred-generate-interval', 0.2))
  35. self.noDefer = False # Set this True to temporarily disable deferring.
  36. self.recorder = base.recorder
  37. self.readDCFile(dcFileNames)
  38. self.cache=CRCache.CRCache()
  39. self.cacheOwner=CRCache.CRCache()
  40. self.serverDelta = 0
  41. self.bootedIndex = None
  42. self.bootedText = None
  43. # create a parentMgr to handle distributed reparents
  44. # this used to be 'token2nodePath'
  45. self.parentMgr = ParentMgr.ParentMgr()
  46. # The RelatedObjectMgr helps distributed objects find each
  47. # other.
  48. self.relatedObjectMgr = RelatedObjectMgr.RelatedObjectMgr(self)
  49. # Keep track of how recently we last sent a heartbeat message.
  50. # We want to keep these coming at heartbeatInterval seconds.
  51. self.heartbeatInterval = base.config.GetDouble('heartbeat-interval', 10)
  52. self.heartbeatStarted = 0
  53. self.lastHeartbeat = 0
  54. def setDeferInterval(self, deferInterval):
  55. """Specifies the minimum amount of time, in seconds, that must
  56. elapse before generating any two DistributedObjects whose
  57. class type is marked "deferrable". Set this to 0 to indicate
  58. no deferring will occur."""
  59. # Temporary condition for old Pandas.
  60. if hasattr(self, 'setHandleCUpdates'):
  61. self.deferInterval = deferInterval
  62. self.setHandleCUpdates(self.deferInterval != 0)
  63. else:
  64. self.deferInterval = 0
  65. if self.deferredGenerates:
  66. taskMgr.remove('deferredGenerate')
  67. taskMgr.doMethodLater(self.deferInterval, self.__doDeferredGenerate, 'deferredGenerate')
  68. ## def queryObjectAll(self, doID, context=0):
  69. ## """
  70. ## Get a one-time snapshot look at the object.
  71. ## """
  72. ## assert self.notify.debugStateCall(self)
  73. ## # Create a message
  74. ## datagram = PyDatagram()
  75. ## datagram.addServerHeader(
  76. ## doID, localAvatar.getDoId(), 2020)
  77. ## # A context that can be used to index the response if needed
  78. ## datagram.addUint32(context)
  79. ## self.send(datagram)
  80. ## # Make sure the message gets there.
  81. ## self.flush()
  82. # Define uniqueName
  83. def uniqueName(self, desc):
  84. return desc
  85. def getTables(self, ownerView):
  86. if ownerView:
  87. return self.doId2ownerView, self.cacheOwner
  88. else:
  89. return self.doId2do, self.cache
  90. def _getMsgName(self, msgId):
  91. # we might get a list of message names, use the first one
  92. return makeList(MsgId2Names.get(msgId, 'UNKNOWN MESSAGE: %s' % msgId))[0]
  93. def sendDisconnect(self):
  94. if self.isConnected():
  95. # Tell the game server that we're going:
  96. datagram = PyDatagram()
  97. # Add message type
  98. datagram.addUint16(CLIENT_DISCONNECT)
  99. # Send the message
  100. self.send(datagram)
  101. self.notify.info("Sent disconnect message to server")
  102. self.disconnect()
  103. self.stopHeartbeat()
  104. def allocateContext(self):
  105. self.context+=1
  106. return self.context
  107. def setServerDelta(self, delta):
  108. """
  109. Indicates the approximate difference in seconds between the
  110. client's clock and the server's clock, in universal time (not
  111. including timezone shifts). This is mainly useful for
  112. reporting synchronization information to the logs; don't
  113. depend on it for any precise timing requirements.
  114. Also see Notify.setServerDelta(), which also accounts for a
  115. timezone shift.
  116. """
  117. self.serverDelta = delta
  118. def getServerDelta(self):
  119. return self.serverDelta
  120. def getServerTimeOfDay(self):
  121. """
  122. Returns the current time of day (seconds elapsed since the
  123. 1972 epoch) according to the server's clock. This is in GMT,
  124. and hence is irrespective of timezones.
  125. The value is computed based on the client's clock and the
  126. known delta from the server's clock, which is not terribly
  127. precisely measured and may drift slightly after startup, but
  128. it should be accurate plus or minus a couple of seconds.
  129. """
  130. return time.time() + self.serverDelta
  131. def handleGenerateWithRequired(self, di):
  132. parentId = di.getUint32()
  133. zoneId = di.getUint32()
  134. assert parentId == self.GameGlobalsId or parentId in self.doId2do
  135. # Get the class Id
  136. classId = di.getUint16()
  137. # Get the DO Id
  138. doId = di.getUint32()
  139. # Look up the dclass
  140. dclass = self.dclassesByNumber[classId]
  141. dclass.startGenerate()
  142. # Create a new distributed object, and put it in the dictionary
  143. distObj = self.generateWithRequiredFields(dclass, doId, di, parentId, zoneId)
  144. dclass.stopGenerate()
  145. def handleGenerateWithRequiredOther(self, di):
  146. parentId = di.getUint32()
  147. zoneId = di.getUint32()
  148. assert parentId == self.GameGlobalsId or parentId in self.doId2do
  149. # Get the class Id
  150. classId = di.getUint16()
  151. # Get the DO Id
  152. doId = di.getUint32()
  153. dclass = self.dclassesByNumber[classId]
  154. deferrable = getattr(dclass.getClassDef(), 'deferrable', False)
  155. if not self.deferInterval or self.noDefer:
  156. deferrable = False
  157. now = globalClock.getFrameTime()
  158. if self.deferredGenerates or deferrable:
  159. # This object is deferrable, or there are already deferred
  160. # objects in the queue (so all objects have to be held
  161. # up).
  162. if self.deferredGenerates or now - self.lastGenerate < self.deferInterval:
  163. # Queue it for later.
  164. assert(self.notify.debug("deferring generate for %s %s" % (dclass.getName(), doId)))
  165. self.deferredGenerates.append(doId)
  166. # Keep a copy of the datagram, and move the di to the copy
  167. dg = Datagram(di.getDatagram())
  168. di = DatagramIterator(dg, di.getCurrentIndex())
  169. self.deferredDoIds[doId] = ((parentId, zoneId, classId, doId, di), deferrable, dg, [])
  170. if len(self.deferredGenerates) == 1:
  171. # We just deferred the first object on the queue;
  172. # start the task to generate it.
  173. taskMgr.remove('deferredGenerate')
  174. taskMgr.doMethodLater(self.deferInterval, self.__doDeferredGenerate, 'deferredGenerate')
  175. else:
  176. # We haven't generated any deferrable objects in a
  177. # while, so it's safe to go ahead and generate this
  178. # one immediately.
  179. self.lastGenerate = now
  180. self.__doGenerate(parentId, zoneId, classId, doId, di)
  181. else:
  182. self.__doGenerate(parentId, zoneId, classId, doId, di)
  183. def __doGenerate(self, parentId, zoneId, classId, doId, di):
  184. # Look up the dclass
  185. dclass = self.dclassesByNumber[classId]
  186. assert(self.notify.debug("performing generate for %s %s" % (dclass.getName(), doId)))
  187. dclass.startGenerate()
  188. # Create a new distributed object, and put it in the dictionary
  189. distObj = self.generateWithRequiredOtherFields(dclass, doId, di, parentId, zoneId)
  190. dclass.stopGenerate()
  191. def flushGenerates(self):
  192. """ Forces all pending generates to be performed immediately. """
  193. while self.deferredGenerates:
  194. doId = self.deferredGenerates[0]
  195. del self.deferredGenerates[0]
  196. if doId in self.deferredDoIds:
  197. args, deferrable, dg, updates = self.deferredDoIds[doId]
  198. del self.deferredDoIds[doId]
  199. self.__doGenerate(*args)
  200. for dg, di in updates:
  201. self.__doUpdate(doId, di)
  202. taskMgr.remove('deferredGenerate')
  203. def __doDeferredGenerate(self, task):
  204. """ This is the task that generates an object on the deferred
  205. queue. """
  206. now = globalClock.getFrameTime()
  207. if now - self.lastGenerate < self.deferInterval:
  208. # Come back later.
  209. return Task.again
  210. while self.deferredGenerates:
  211. # Generate the next deferred object.
  212. doId = self.deferredGenerates[0]
  213. del self.deferredGenerates[0]
  214. if doId in self.deferredDoIds:
  215. args, deferrable, dg, updates = self.deferredDoIds[doId]
  216. del self.deferredDoIds[doId]
  217. self.__doGenerate(*args)
  218. for dg, di in updates:
  219. self.__doUpdate(doId, di)
  220. if deferrable:
  221. # If this was an actual deferrable object, wait
  222. # for the next pass to generate any more.
  223. self.lastGenerate = now
  224. return Task.again
  225. # All objects are generaetd.
  226. return Task.done
  227. def handleGenerateWithRequiredOtherOwner(self, di):
  228. # Get the class Id
  229. classId = di.getUint16()
  230. # Get the DO Id
  231. doId = di.getUint32()
  232. # parentId and zoneId are not relevant here
  233. parentId = di.getUint32()
  234. zoneId = di.getUint32()
  235. # Look up the dclass
  236. dclass = self.dclassesByNumber[classId]
  237. dclass.startGenerate()
  238. # Create a new distributed object, and put it in the dictionary
  239. distObj = self.generateWithRequiredOtherFieldsOwner(dclass, doId, di)
  240. dclass.stopGenerate()
  241. def handleQuietZoneGenerateWithRequired(self, di):
  242. # Special handler for quiet zone generates -- we need to filter
  243. parentId = di.getUint32()
  244. zoneId = di.getUint32()
  245. assert parentId in self.doId2do
  246. # Get the class Id
  247. classId = di.getUint16()
  248. # Get the DO Id
  249. doId = di.getUint32()
  250. # Look up the dclass
  251. dclass = self.dclassesByNumber[classId]
  252. dclass.startGenerate()
  253. distObj = self.generateWithRequiredFields(dclass, doId, di, parentId, zoneId)
  254. dclass.stopGenerate()
  255. def handleQuietZoneGenerateWithRequiredOther(self, di):
  256. # Special handler for quiet zone generates -- we need to filter
  257. parentId = di.getUint32()
  258. zoneId = di.getUint32()
  259. assert parentId in self.doId2do
  260. # Get the class Id
  261. classId = di.getUint16()
  262. # Get the DO Id
  263. doId = di.getUint32()
  264. # Look up the dclass
  265. dclass = self.dclassesByNumber[classId]
  266. dclass.startGenerate()
  267. distObj = self.generateWithRequiredOtherFields(dclass, doId, di, parentId, zoneId)
  268. dclass.stopGenerate()
  269. def generateWithRequiredFields(self, dclass, doId, di, parentId, zoneId):
  270. if self.doId2do.has_key(doId):
  271. # ...it is in our dictionary.
  272. # Just update it.
  273. distObj = self.doId2do[doId]
  274. assert distObj.dclass == dclass
  275. distObj.generate()
  276. distObj.setLocation(parentId, zoneId)
  277. distObj.updateRequiredFields(dclass, di)
  278. # updateRequiredFields calls announceGenerate
  279. elif self.cache.contains(doId):
  280. # ...it is in the cache.
  281. # Pull it out of the cache:
  282. distObj = self.cache.retrieve(doId)
  283. assert distObj.dclass == dclass
  284. # put it in the dictionary:
  285. self.doId2do[doId] = distObj
  286. # and update it.
  287. distObj.generate()
  288. # make sure we don't have a stale location
  289. distObj.parentId = None
  290. distObj.zoneId = None
  291. distObj.setLocation(parentId, zoneId)
  292. distObj.updateRequiredFields(dclass, di)
  293. # updateRequiredFields calls announceGenerate
  294. else:
  295. # ...it is not in the dictionary or the cache.
  296. # Construct a new one
  297. classDef = dclass.getClassDef()
  298. if classDef == None:
  299. self.notify.error("Could not create an undefined %s object." % (dclass.getName()))
  300. distObj = classDef(self)
  301. distObj.dclass = dclass
  302. # Assign it an Id
  303. distObj.doId = doId
  304. # Put the new do in the dictionary
  305. self.doId2do[doId] = distObj
  306. # Update the required fields
  307. distObj.generateInit() # Only called when constructed
  308. distObj.generate()
  309. distObj.setLocation(parentId, zoneId)
  310. distObj.updateRequiredFields(dclass, di)
  311. # updateRequiredFields calls announceGenerate
  312. print "New DO:%s, dclass:%s"%(doId, dclass.getName())
  313. return distObj
  314. def generateWithRequiredOtherFields(self, dclass, doId, di,
  315. parentId = None, zoneId = None):
  316. if self.doId2do.has_key(doId):
  317. # ...it is in our dictionary.
  318. # Just update it.
  319. distObj = self.doId2do[doId]
  320. assert distObj.dclass == dclass
  321. distObj.generate()
  322. distObj.setLocation(parentId, zoneId)
  323. distObj.updateRequiredOtherFields(dclass, di)
  324. # updateRequiredOtherFields calls announceGenerate
  325. elif self.cache.contains(doId):
  326. # ...it is in the cache.
  327. # Pull it out of the cache:
  328. distObj = self.cache.retrieve(doId)
  329. assert distObj.dclass == dclass
  330. # put it in the dictionary:
  331. self.doId2do[doId] = distObj
  332. # and update it.
  333. distObj.generate()
  334. # make sure we don't have a stale location
  335. distObj.parentId = None
  336. distObj.zoneId = None
  337. distObj.setLocation(parentId, zoneId)
  338. distObj.updateRequiredOtherFields(dclass, di)
  339. # updateRequiredOtherFields calls announceGenerate
  340. else:
  341. # ...it is not in the dictionary or the cache.
  342. # Construct a new one
  343. classDef = dclass.getClassDef()
  344. if classDef == None:
  345. self.notify.error("Could not create an undefined %s object." % (dclass.getName()))
  346. distObj = classDef(self)
  347. distObj.dclass = dclass
  348. # Assign it an Id
  349. distObj.doId = doId
  350. # Put the new do in the dictionary
  351. self.doId2do[doId] = distObj
  352. # Update the required fields
  353. distObj.generateInit() # Only called when constructed
  354. distObj.generate()
  355. distObj.setLocation(parentId, zoneId)
  356. distObj.updateRequiredOtherFields(dclass, di)
  357. # updateRequiredOtherFields calls announceGenerate
  358. return distObj
  359. def generateWithRequiredOtherFieldsOwner(self, dclass, doId, di):
  360. if self.doId2ownerView.has_key(doId):
  361. # ...it is in our dictionary.
  362. # Just update it.
  363. distObj = self.doId2ownerView[doId]
  364. assert distObj.dclass == dclass
  365. distObj.generate()
  366. distObj.updateRequiredOtherFields(dclass, di)
  367. # updateRequiredOtherFields calls announceGenerate
  368. elif self.cacheOwner.contains(doId):
  369. # ...it is in the cache.
  370. # Pull it out of the cache:
  371. distObj = self.cacheOwner.retrieve(doId)
  372. assert distObj.dclass == dclass
  373. # put it in the dictionary:
  374. self.doId2ownerView[doId] = distObj
  375. # and update it.
  376. distObj.generate()
  377. distObj.updateRequiredOtherFields(dclass, di)
  378. # updateRequiredOtherFields calls announceGenerate
  379. else:
  380. # ...it is not in the dictionary or the cache.
  381. # Construct a new one
  382. classDef = dclass.getOwnerClassDef()
  383. if classDef == None:
  384. self.notify.error("Could not create an undefined %s object. Have you created an owner view?" % (dclass.getName()))
  385. distObj = classDef(self)
  386. distObj.dclass = dclass
  387. # Assign it an Id
  388. distObj.doId = doId
  389. # Put the new do in the dictionary
  390. self.doId2ownerView[doId] = distObj
  391. # Update the required fields
  392. distObj.generateInit() # Only called when constructed
  393. distObj.generate()
  394. distObj.updateRequiredOtherFields(dclass, di)
  395. # updateRequiredOtherFields calls announceGenerate
  396. return distObj
  397. def handleDisable(self, di, ownerView=False):
  398. # Get the DO Id
  399. doId = di.getUint32()
  400. # disable it.
  401. self.disableDoId(doId, ownerView)
  402. def disableDoId(self, doId, ownerView=False):
  403. table, cache = self.getTables(ownerView)
  404. # Make sure the object exists
  405. if table.has_key(doId):
  406. # Look up the object
  407. distObj = table[doId]
  408. # remove the object from the dictionary
  409. del table[doId]
  410. # Only cache the object if it is a "cacheable" type
  411. # object; this way we don't clutter up the caches with
  412. # trivial objects that don't benefit from caching.
  413. if distObj.getCacheable():
  414. cache.cache(distObj)
  415. else:
  416. distObj.deleteOrDelay()
  417. elif self.deferredDoIds.has_key(doId):
  418. # The object had been deferred. Great; we don't even have
  419. # to generate it now.
  420. del self.deferredDoIds[doId]
  421. i = self.deferredGenerates.index(doId)
  422. del self.deferredGenerates[i]
  423. if len(self.deferredGenerates) == 0:
  424. taskMgr.remove('deferredGenerate')
  425. else:
  426. self._logFailedDisable(doId, ownerView)
  427. def _logFailedDisable(self, doId, ownerView):
  428. self.notify.warning(
  429. "Disable failed. DistObj "
  430. + str(doId) +
  431. " is not in dictionary, ownerView=%s" % ownerView)
  432. def handleDelete(self, di):
  433. # overridden by ToontownClientRepository
  434. assert 0
  435. def handleUpdateField(self, di):
  436. """
  437. This method is called when a CLIENT_OBJECT_UPDATE_FIELD
  438. message is received; it decodes the update, unpacks the
  439. arguments, and calls the corresponding method on the indicated
  440. DistributedObject.
  441. In fact, this method is exactly duplicated by the C++ method
  442. cConnectionRepository::handle_update_field(), which was
  443. written to optimize the message loop by handling all of the
  444. CLIENT_OBJECT_UPDATE_FIELD messages in C++. That means that
  445. nowadays, this Python method will probably never be called,
  446. since UPDATE_FIELD messages will not even be passed to the
  447. Python message handlers. But this method remains for
  448. documentation purposes, and also as a "just in case" handler
  449. in case we ever do come across a situation in the future in
  450. which python might handle the UPDATE_FIELD message.
  451. """
  452. # Get the DO Id
  453. doId = di.getUint32()
  454. if doId in self.deferredDoIds:
  455. # This object hasn't really been generated yet. Sit on
  456. # the update.
  457. args, deferrable, dg0, updates = self.deferredDoIds[doId]
  458. # Keep a copy of the datagram, and move the di to the copy
  459. dg = Datagram(di.getDatagram())
  460. di = DatagramIterator(dg, di.getCurrentIndex())
  461. updates.append((dg, di))
  462. else:
  463. # This object has been fully generated. It's OK to update.
  464. self.__doUpdate(doId, di)
  465. def __doUpdate(self, doId, di):
  466. # Find the DO
  467. do = self.doId2do.get(doId)
  468. if do is not None:
  469. # Let the dclass finish the job
  470. do.dclass.receiveUpdate(do, di)
  471. else:
  472. self.notify.warning(
  473. "Asked to update non-existent DistObj " + str(doId))
  474. def handleGoGetLost(self, di):
  475. # The server told us it's about to drop the connection on us.
  476. # Get ready!
  477. if (di.getRemainingSize() > 0):
  478. self.bootedIndex = di.getUint16()
  479. self.bootedText = di.getString()
  480. self.notify.warning(
  481. "Server is booting us out (%d): %s" % (self.bootedIndex, self.bootedText))
  482. else:
  483. self.bootedIndex = None
  484. self.bootedText = None
  485. self.notify.warning(
  486. "Server is booting us out with no explanation.")
  487. def handleServerHeartbeat(self, di):
  488. # Got a heartbeat message from the server.
  489. if base.config.GetBool('server-heartbeat-info', 1):
  490. self.notify.info("Server heartbeat.")
  491. def handleSystemMessage(self, di):
  492. # Got a system message from the server.
  493. message = di.getString()
  494. self.notify.info('Message from server: %s' % (message))
  495. return message
  496. def getObjectsOfClass(self, objClass):
  497. """ returns dict of doId:object, containing all objects
  498. that inherit from 'class'. returned dict is safely mutable. """
  499. doDict = {}
  500. for doId, do in self.doId2do.items():
  501. if isinstance(do, objClass):
  502. doDict[doId] = do
  503. return doDict
  504. def getObjectsOfExactClass(self, objClass):
  505. """ returns dict of doId:object, containing all objects that
  506. are exactly of type 'class' (neglecting inheritance). returned
  507. dict is safely mutable. """
  508. doDict = {}
  509. for doId, do in self.doId2do.items():
  510. if do.__class__ == objClass:
  511. doDict[doId] = do
  512. return doDict
  513. def sendSetLocation(self, doId, parentId, zoneId):
  514. datagram = PyDatagram()
  515. datagram.addUint16(CLIENT_OBJECT_LOCATION)
  516. datagram.addUint32(doId)
  517. datagram.addUint32(parentId)
  518. datagram.addUint32(zoneId)
  519. self.send(datagram)
  520. def sendHeartbeat(self):
  521. datagram = PyDatagram()
  522. # Add message type
  523. datagram.addUint16(CLIENT_HEARTBEAT)
  524. # Send it!
  525. self.send(datagram)
  526. self.lastHeartbeat = globalClock.getRealTime()
  527. # This is important enough to consider flushing immediately
  528. # (particularly if we haven't run readerPollTask recently).
  529. self.considerFlush()
  530. def considerHeartbeat(self):
  531. """Send a heartbeat message if we haven't sent one recently."""
  532. if not self.heartbeatStarted:
  533. self.notify.debug("Heartbeats not started; not sending.")
  534. return
  535. elapsed = globalClock.getRealTime() - self.lastHeartbeat
  536. if elapsed < 0 or elapsed > self.heartbeatInterval:
  537. # It's time to send the heartbeat again (or maybe someone
  538. # reset the clock back).
  539. self.notify.info("Sending heartbeat mid-frame.")
  540. self.startHeartbeat()
  541. def stopHeartbeat(self):
  542. taskMgr.remove("heartBeat")
  543. self.heartbeatStarted = 0
  544. def startHeartbeat(self):
  545. self.stopHeartbeat()
  546. self.heartbeatStarted = 1
  547. self.sendHeartbeat()
  548. self.waitForNextHeartBeat()
  549. def sendHeartbeatTask(self, task):
  550. self.sendHeartbeat()
  551. self.waitForNextHeartBeat()
  552. return Task.done
  553. def waitForNextHeartBeat(self):
  554. taskMgr.doMethodLater(self.heartbeatInterval, self.sendHeartbeatTask,
  555. "heartBeat")
  556. def replaceMethod(self, oldMethod, newFunction):
  557. return 0
  558. def getWorld(self, doId):
  559. # Get the world node for this object
  560. obj = self.doId2do[doId]
  561. worldNP = obj.getParent()
  562. while 1:
  563. nextNP = worldNP.getParent()
  564. if nextNP == render:
  565. break
  566. elif worldNP.isEmpty():
  567. return None
  568. return worldNP
  569. def isLive(self):
  570. if base.config.GetBool('force-live', 0):
  571. return True
  572. return not (__dev__ or launcher.isTestServer())
  573. def isLocalId(self, id):
  574. # By default, no ID's are local. See also
  575. # ClientRepository.isLocalId().
  576. return 0