ClientRepository.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701
  1. """ClientRepository module: contains the ClientRepository class"""
  2. from pandac.PandaModules import *
  3. from MsgTypes import *
  4. from direct.task import Task
  5. from direct.directnotify import DirectNotifyGlobal
  6. import CRCache
  7. import ConnectionRepository
  8. from direct.showbase import PythonUtil
  9. import ParentMgr
  10. import RelatedObjectMgr
  11. import time
  12. from ClockDelta import *
  13. from PyDatagram import PyDatagram
  14. from PyDatagramIterator import PyDatagramIterator
  15. class ClientRepository(ConnectionRepository.ConnectionRepository):
  16. notify = DirectNotifyGlobal.directNotify.newCategory("ClientRepository")
  17. def __init__(self):
  18. ConnectionRepository.ConnectionRepository.__init__(self, base.config)
  19. self.setClientDatagram(1)
  20. self.recorder = base.recorder
  21. self.doId2do={}
  22. self.readDCFile()
  23. self.cache=CRCache.CRCache()
  24. self.serverDelta = 0
  25. self.bootedIndex = None
  26. self.bootedText = None
  27. self.worldScale = render.attachNewNode("worldScale") # for grid zones.
  28. self.worldScale.setScale(base.config.GetFloat('world-scale', 100))
  29. self.priorWorldPos = None
  30. # create a parentMgr to handle distributed reparents
  31. # this used to be 'token2nodePath'
  32. self.parentMgr = ParentMgr.ParentMgr()
  33. # The RelatedObjectMgr helps distributed objects find each
  34. # other.
  35. self.relatedObjectMgr = RelatedObjectMgr.RelatedObjectMgr(self)
  36. # Keep track of how recently we last sent a heartbeat message.
  37. # We want to keep these coming at heartbeatInterval seconds.
  38. self.heartbeatInterval = base.config.GetDouble('heartbeat-interval', 10)
  39. self.heartbeatStarted = 0
  40. self.lastHeartbeat = 0
  41. if wantOtpServer:
  42. ##
  43. ## Top level Interest Manager
  44. ##
  45. self.__interest_id_assign = 1
  46. self.__interesthash = {}
  47. def abruptCleanup(self):
  48. """
  49. Call this method to clean up any pending hooks or tasks on
  50. distributed objects, but leave the ClientRepository in a sane
  51. state for creating more distributed objects.
  52. """
  53. self.relatedObjectMgr.abortAllRequests()
  54. def sendDisconnect(self):
  55. if self.isConnected():
  56. # Tell the game server that we're going:
  57. datagram = PyDatagram()
  58. # Add message type
  59. datagram.addUint16(CLIENT_DISCONNECT)
  60. # Send the message
  61. self.send(datagram)
  62. self.notify.info("Sent disconnect message to server")
  63. self.disconnect()
  64. self.stopHeartbeat()
  65. def setWorldOffset(self, xOffset=0, yOffset=0):
  66. self.worldXOffset=xOffset
  67. self.worldYOffset=yOffset
  68. def getWorldPos(self, nodePath):
  69. pos = nodePath.getPos(self.worldScale)
  70. return (int(round(pos.getX())), int(round(pos.getY())))
  71. def sendWorldPos(self, x, y):
  72. # The server will need to know the world
  73. # offset of our current render node path
  74. # and adjust the x, y accordingly. At one
  75. # point I considered adding the world offset
  76. # here, but that would just use extra bits.
  77. onScreenDebug.add("worldPos", "%-4d, %-4d"%(x, y))
  78. return #*#
  79. datagram = PyDatagram()
  80. # Add message type
  81. datagram.addUint16(CLIENT_SET_WORLD_POS)
  82. # Add x
  83. datagram.addInt16(x)
  84. # Add y
  85. datagram.addSint16(y)
  86. # send the message
  87. self.send(datagram)
  88. def checkWorldPos(self, nodePath):
  89. worldPos = self.getWorldPos(nodePath)
  90. if self.priorWorldPos != worldPos:
  91. self.priorWorldPos = worldPos
  92. self.sendWorldPos(worldPos[0], worldPos[1])
  93. def setServerDelta(self, delta):
  94. """
  95. Indicates the approximate difference in seconds between the
  96. client's clock and the server's clock, in universal time (not
  97. including timezone shifts). This is mainly useful for
  98. reporting synchronization information to the logs; don't
  99. depend on it for any precise timing requirements.
  100. Also see Notify.setServerDelta(), which also accounts for a
  101. timezone shift.
  102. """
  103. self.serverDelta = delta
  104. def getServerDelta(self):
  105. return self.serverDelta
  106. def getServerTimeOfDay(self):
  107. """
  108. Returns the current time of day (seconds elapsed since the
  109. 1972 epoch) according to the server's clock. This is in GMT,
  110. and hence is irrespective of timezones.
  111. The value is computed based on the client's clock and the
  112. known delta from the server's clock, which is not terribly
  113. precisely measured and may drift slightly after startup, but
  114. it should be accurate plus or minus a couple of seconds.
  115. """
  116. return time.time() + self.serverDelta
  117. def handleObjectLocation(self, di):
  118. # CLEINT_OBJECT_LOCATION
  119. ThedoId = di.getUint32()
  120. TheParent = di.getUint32()
  121. TheZone = di.getUint32()
  122. print "Object Location->Id=%s Parent=%s Zone=%s"%(ThedoId,TheParent, TheZone)
  123. def handleGenerateWithRequired(self, di):
  124. if wantOtpServer:
  125. TheParent = di.getUint32()
  126. TheZone = di.getUint32()
  127. # Get the class Id
  128. classId = di.getUint16();
  129. # Get the DO Id
  130. doId = di.getUint32()
  131. # Look up the dclass
  132. dclass = self.dclassesByNumber[classId]
  133. dclass.startGenerate()
  134. # Create a new distributed object, and put it in the dictionary
  135. distObj = self.generateWithRequiredFields(dclass, doId, di)
  136. dclass.stopGenerate()
  137. def handleGenerateWithRequiredOther(self, di):
  138. if wantOtpServer:
  139. TheParent = di.getUint32()
  140. TheZone = di.getUint32()
  141. # Get the class Id
  142. classId = di.getUint16();
  143. # Get the DO Id
  144. doId = di.getUint32()
  145. # Look up the dclass
  146. dclass = self.dclassesByNumber[classId]
  147. dclass.startGenerate()
  148. # Create a new distributed object, and put it in the dictionary
  149. distObj = self.generateWithRequiredOtherFields(dclass, doId, di)
  150. dclass.stopGenerate()
  151. def handleQuietZoneGenerateWithRequired(self, di):
  152. # Special handler for quiet zone generates -- we need to filter
  153. if wantOtpServer:
  154. TheParent = di.getUint32()
  155. TheZone = di.getUint32()
  156. # Get the class Id
  157. classId = di.getUint16();
  158. # Get the DO Id
  159. doId = di.getUint32()
  160. # Look up the dclass
  161. dclass = self.dclassesByNumber[classId]
  162. dclass.startGenerate()
  163. # If the class is a neverDisable class (which implies uberzone) we
  164. # should go ahead and generate it even though we are in the quiet zone
  165. if not wantOtpServer:
  166. if dclass.getClassDef().neverDisable:
  167. # Create a new distributed object, and put it in the dictionary
  168. distObj = self.generateWithRequiredFields(dclass, doId, di)
  169. else:
  170. distObj = self.generateWithRequiredFields(dclass, doId, di)
  171. dclass.stopGenerate()
  172. def handleQuietZoneGenerateWithRequiredOther(self, di):
  173. # Special handler for quiet zone generates -- we need to filter
  174. if wantOtpServer:
  175. TheParent = di.getUint32()
  176. TheZone = di.getUint32()
  177. # Get the class Id
  178. classId = di.getUint16();
  179. # Get the DO Id
  180. doId = di.getUint32()
  181. # Look up the dclass
  182. dclass = self.dclassesByNumber[classId]
  183. # If the class is a neverDisable class (which implies uberzone) we
  184. # should go ahead and generate it even though we are in the quiet zone
  185. dclass.startGenerate()
  186. if not wantOtpServer:
  187. if dclass.getClassDef().neverDisable:
  188. # Create a new distributed object, and put it in the dictionary
  189. distObj = self.generateWithRequiredOtherFields(dclass, doId, di)
  190. else:
  191. distObj = self.generateWithRequiredOtherFields(dclass, doId, di)
  192. dclass.stopGenerate()
  193. def generateWithRequiredFields(self, dclass, doId, di):
  194. if self.doId2do.has_key(doId):
  195. # ...it is in our dictionary.
  196. # Just update it.
  197. distObj = self.doId2do[doId]
  198. assert(distObj.dclass == dclass)
  199. distObj.generate()
  200. distObj.updateRequiredFields(dclass, di)
  201. # updateRequiredFields calls announceGenerate
  202. elif self.cache.contains(doId):
  203. # ...it is in the cache.
  204. # Pull it out of the cache:
  205. distObj = self.cache.retrieve(doId)
  206. assert(distObj.dclass == dclass)
  207. # put it in the dictionary:
  208. self.doId2do[doId] = distObj
  209. # and update it.
  210. distObj.generate()
  211. distObj.updateRequiredFields(dclass, di)
  212. # updateRequiredFields calls announceGenerate
  213. else:
  214. # ...it is not in the dictionary or the cache.
  215. # Construct a new one
  216. classDef = dclass.getClassDef()
  217. if classDef == None:
  218. self.notify.error("Could not create an undefined %s object." % (dclass.getName()))
  219. distObj = classDef(self)
  220. distObj.dclass = dclass
  221. # Assign it an Id
  222. distObj.doId = doId
  223. # Put the new do in the dictionary
  224. self.doId2do[doId] = distObj
  225. # Update the required fields
  226. distObj.generateInit() # Only called when constructed
  227. distObj.generate()
  228. distObj.updateRequiredFields(dclass, di)
  229. # updateRequiredFields calls announceGenerate
  230. if wantOtpServer:
  231. print "New DO:%s, dclass:%s"%(doId, dclass.getName())
  232. return distObj
  233. def generateGlobalObject(self , doId, dcname):
  234. # Look up the dclass
  235. dclass = self.dclassesByName[dcname]
  236. # Create a new distributed object, and put it in the dictionary
  237. #distObj = self.generateWithRequiredFields(dclass, doId, di)
  238. # Construct a new one
  239. classDef = dclass.getClassDef()
  240. if classDef == None:
  241. self.notify.error("Could not create an undefined %s object." % (dclass.getName()))
  242. distObj = classDef(self)
  243. distObj.dclass = dclass
  244. # Assign it an Id
  245. distObj.doId = doId
  246. # Put the new do in the dictionary
  247. self.doId2do[doId] = distObj
  248. # Update the required fields
  249. distObj.generateInit() # Only called when constructed
  250. distObj.generate()
  251. # updateRequiredFields calls announceGenerate
  252. return distObj
  253. def generateWithRequiredOtherFields(self, dclass, doId, di):
  254. if self.doId2do.has_key(doId):
  255. # ...it is in our dictionary.
  256. # Just update it.
  257. distObj = self.doId2do[doId]
  258. assert(distObj.dclass == dclass)
  259. distObj.generate()
  260. distObj.updateRequiredOtherFields(dclass, di)
  261. # updateRequiredOtherFields calls announceGenerate
  262. elif self.cache.contains(doId):
  263. # ...it is in the cache.
  264. # Pull it out of the cache:
  265. distObj = self.cache.retrieve(doId)
  266. assert(distObj.dclass == dclass)
  267. # put it in the dictionary:
  268. self.doId2do[doId] = distObj
  269. # and update it.
  270. distObj.generate()
  271. distObj.updateRequiredOtherFields(dclass, di)
  272. # updateRequiredOtherFields calls announceGenerate
  273. else:
  274. # ...it is not in the dictionary or the cache.
  275. # Construct a new one
  276. classDef = dclass.getClassDef()
  277. if classDef == None:
  278. self.notify.error("Could not create an undefined %s object." % (dclass.getName()))
  279. distObj = classDef(self)
  280. distObj.dclass = dclass
  281. # Assign it an Id
  282. distObj.doId = doId
  283. # Put the new do in the dictionary
  284. self.doId2do[doId] = distObj
  285. # Update the required fields
  286. distObj.generateInit() # Only called when constructed
  287. distObj.generate()
  288. distObj.updateRequiredOtherFields(dclass, di)
  289. # updateRequiredOtherFields calls announceGenerate
  290. return distObj
  291. def handleDisable(self, di):
  292. # Get the DO Id
  293. doId = di.getUint32()
  294. # disable it.
  295. self.disableDoId(doId)
  296. def disableDoId(self, doId):
  297. # Make sure the object exists
  298. if self.doId2do.has_key(doId):
  299. # Look up the object
  300. distObj = self.doId2do[doId]
  301. # remove the object from the dictionary
  302. del(self.doId2do[doId])
  303. # Only cache the object if it is a "cacheable" type
  304. # object; this way we don't clutter up the caches with
  305. # trivial objects that don't benefit from caching.
  306. if distObj.getCacheable():
  307. self.cache.cache(distObj)
  308. else:
  309. distObj.deleteOrDelay()
  310. else:
  311. ClientRepository.notify.warning(
  312. "Disable failed. DistObj "
  313. + str(doId) +
  314. " is not in dictionary")
  315. def handleDelete(self, di):
  316. # Get the DO Id
  317. doId = di.getUint32()
  318. self.deleteObject(doId)
  319. def deleteObject(self, doId):
  320. """
  321. Removes the object from the client's view of the world. This
  322. should normally not be called except in the case of error
  323. recovery, since the server will normally be responsible for
  324. deleting and disabling objects as they go out of scope.
  325. After this is called, future updates by server on this object
  326. will be ignored (with a warning message). The object will
  327. become valid again the next time the server sends a generate
  328. message for this doId.
  329. This is not a distributed message and does not delete the
  330. object on the server or on any other client.
  331. """
  332. if self.doId2do.has_key(doId):
  333. # If it is in the dictionary, remove it.
  334. obj = self.doId2do[doId]
  335. # Remove it from the dictionary
  336. del(self.doId2do[doId])
  337. # Disable, announce, and delete the object itself...
  338. # unless delayDelete is on...
  339. obj.deleteOrDelay()
  340. elif self.cache.contains(doId):
  341. # If it is in the cache, remove it.
  342. self.cache.delete(doId)
  343. else:
  344. # Otherwise, ignore it
  345. ClientRepository.notify.warning(
  346. "Asked to delete non-existent DistObj " + str(doId))
  347. def handleUpdateField(self, di):
  348. """
  349. This method is called when a CLIENT_OBJECT_UPDATE_FIELD
  350. message is received; it decodes the update, unpacks the
  351. arguments, and calls the corresponding method on the indicated
  352. DistributedObject.
  353. In fact, this method is exactly duplicated by the C++ method
  354. cConnectionRepository::handle_update_field(), which was
  355. written to optimize the message loop by handling all of the
  356. CLIENT_OBJECT_UPDATE_FIELD messages in C++. That means that
  357. nowadays, this Python method will probably never be called,
  358. since UPDATE_FIELD messages will not even be passed to the
  359. Python message handlers. But this method remains for
  360. documentation purposes, and also as a "just in case" handler
  361. in case we ever do come across a situation in the future in
  362. which python might handle the UPDATE_FIELD message.
  363. """
  364. # Get the DO Id
  365. doId = di.getUint32()
  366. #print("Updating " + str(doId))
  367. # Find the DO
  368. do = self.doId2do.get(doId)
  369. if (do != None):
  370. # Let the dclass finish the job
  371. do.dclass.receiveUpdate(do, di)
  372. else:
  373. ClientRepository.notify.warning(
  374. "Asked to update non-existent DistObj " + str(doId))
  375. def handleGoGetLost(self, di):
  376. # The server told us it's about to drop the connection on us.
  377. # Get ready!
  378. if (di.getRemainingSize() > 0):
  379. self.bootedIndex = di.getUint16()
  380. self.bootedText = di.getString()
  381. ClientRepository.notify.warning(
  382. "Server is booting us out (%d): %s" % (self.bootedIndex, self.bootedText))
  383. else:
  384. self.bootedIndex = None
  385. self.bootedText = None
  386. ClientRepository.notify.warning(
  387. "Server is booting us out with no explanation.")
  388. def handleServerHeartbeat(self, di):
  389. # Got a heartbeat message from the server.
  390. if base.config.GetBool('server-heartbeat-info', 1):
  391. ClientRepository.notify.info("Server heartbeat.")
  392. def handleSystemMessage(self, di):
  393. # Got a system message from the server.
  394. message = di.getString()
  395. self.notify.info('Message from server: %s' % (message))
  396. return message
  397. def handleUnexpectedMsgType(self, msgType, di):
  398. if msgType == CLIENT_CREATE_OBJECT_REQUIRED:
  399. self.handleGenerateWithRequired(di)
  400. elif msgType == CLIENT_CREATE_OBJECT_REQUIRED_OTHER:
  401. self.handleGenerateWithRequiredOther(di)
  402. elif msgType == CLIENT_OBJECT_UPDATE_FIELD:
  403. self.handleUpdateField(di)
  404. elif msgType == CLIENT_OBJECT_DISABLE_RESP:
  405. self.handleDisable(di)
  406. elif msgType == CLIENT_OBJECT_DELETE_RESP:
  407. self.handleDelete(di)
  408. elif msgType == CLIENT_GO_GET_LOST:
  409. self.handleGoGetLost(di)
  410. elif msgType == CLIENT_HEARTBEAT:
  411. self.handleServerHeartbeat(di)
  412. elif msgType == CLIENT_SYSTEM_MESSAGE:
  413. self.handleSystemMessage(di)
  414. elif wantOtpServer and msgType == CLIENT_CREATE_OBJECT_REQUIRED:
  415. self.handleGenerateWithRequired(di)
  416. elif wantOtpServer and msgType == CLIENT_CREATE_OBJECT_REQUIRED_OTHER:
  417. self.handleGenerateWithRequiredOther(di)
  418. elif wantOtpServer and msgType == CLIENT_DONE_SET_ZONE_RESP:
  419. self.handleSetZoneDone()
  420. elif wantOtpServer and msgType == CLEINT_OBJECT_LOCATION:
  421. self.handleObjectLocation(di)
  422. else:
  423. currentLoginState = self.loginFSM.getCurrentState()
  424. if currentLoginState:
  425. currentLoginStateName = currentLoginState.getName()
  426. else:
  427. currentLoginStateName = "None"
  428. currentGameState = self.gameFSM.getCurrentState()
  429. if currentGameState:
  430. currentGameStateName = currentGameState.getName()
  431. else:
  432. currentGameStateName = "None"
  433. ClientRepository.notify.warning(
  434. "Ignoring unexpected message type: " +
  435. str(msgType) +
  436. " login state: " +
  437. currentLoginStateName +
  438. " game state: " +
  439. currentGameStateName)
  440. def sendSetShardMsg(self, shardId):
  441. datagram = PyDatagram()
  442. # Add message type
  443. datagram.addUint16(CLIENT_SET_SHARD)
  444. # Add shard id
  445. datagram.addUint32(shardId)
  446. # send the message
  447. self.send(datagram)
  448. def sendSetZoneMsg(self, zoneId, visibleZoneList=None):
  449. datagram = PyDatagram()
  450. # Add message type
  451. datagram.addUint16(CLIENT_SET_ZONE)
  452. # Add zone id
  453. datagram.addUint32(zoneId)
  454. # if we have an explicit list of visible zones, add them
  455. if visibleZoneList is not None:
  456. vzl = list(visibleZoneList)
  457. vzl.sort()
  458. assert PythonUtil.uniqueElements(vzl)
  459. for zone in vzl:
  460. datagram.addUint32(zone)
  461. # send the message
  462. self.send(datagram)
  463. def handleDatagram(self, di):
  464. if self.notify.getDebug():
  465. print "ClientRepository received datagram:"
  466. di.getDatagram().dumpHex(ostream)
  467. msgType = self.getMsgType()
  468. # watch for setZoneDones
  469. if msgType == CLIENT_DONE_SET_ZONE_RESP:
  470. self.handleSetZoneDone()
  471. if self.handler == None:
  472. self.handleUnexpectedMsgType(msgType, di)
  473. else:
  474. self.handler(msgType, di)
  475. # If we're processing a lot of datagrams within one frame, we
  476. # may forget to send heartbeats. Keep them coming!
  477. self.considerHeartbeat()
  478. if wantOtpServer:
  479. ##
  480. ##
  481. ## interest managment
  482. ##
  483. ##
  484. def InterestAdd(self, parentId, zoneId, Description):
  485. """
  486. Part of the new otp-server code.
  487. """
  488. self.__interest_id_assign += 1
  489. self.__interesthash[self.__interest_id_assign] = Description
  490. contextId = self.__interest_id_assign;
  491. self.__sendAddInterest(contextId, parentId, zoneId)
  492. self.DumpInterests()
  493. return contextId;
  494. def InterestRemove(self, contextId):
  495. """
  496. Part of the new otp-server code.
  497. """
  498. answer = 0;
  499. if self.__interesthash.has_key(contextId):
  500. self.__sendRemoveInterest(contextId)
  501. del self.__interesthash[contextId]
  502. answer = 1
  503. self.DumpInterests()
  504. return answer
  505. def InterestAlter(self, contextId, parentId, zoneId, Description):
  506. """
  507. Part of the new otp-server code.
  508. Removes old and adds new..
  509. """
  510. answer = 0
  511. if self.__interesthash.has_key(contextId):
  512. self.__interesthash[contextId] = Description
  513. self.__sendAlterInterest(contextId, parentId, zoneId)
  514. answer = 1
  515. self.DumpInterests()
  516. return answer
  517. def DumpInterests(self):
  518. """
  519. Part of the new otp-server code.
  520. """
  521. print "*********************** Interest Sets **************"
  522. for i in self.__interesthash.keys():
  523. print "Interest ID:%s, Description=%s"%(i,self.__interesthash[i])
  524. print "****************************************************"
  525. def __sendAddInterest(self, contextId, parentId, zoneId):
  526. """
  527. Part of the new otp-server code.
  528. contextId is a client-side created number that refers to
  529. a set of interests. The same contextId number doesn't
  530. necessarily have any relationship to the same contextId
  531. on another client.
  532. """
  533. datagram = PyDatagram()
  534. # Add message type
  535. datagram.addUint16(CLIENT_ADD_INTEREST)
  536. datagram.addUint16(contextId)
  537. datagram.addUint32(parentId)
  538. datagram.addUint32(zoneId)
  539. self.send(datagram)
  540. def __sendAlterInterest(self, contextId, parentId, zoneId):
  541. """
  542. Part of the new otp-server code.
  543. contextId is a client-side created number that refers to
  544. a set of interests. The same contextId number doesn't
  545. necessarily have any relationship to the same contextId
  546. on another client.
  547. """
  548. datagram = PyDatagram()
  549. # Add message type
  550. datagram.addUint16(CLIENT_ALTER_INTEREST)
  551. datagram.addUint16(contextId)
  552. datagram.addUint32(parentId)
  553. datagram.addUint32(zoneId)
  554. self.send(datagram)
  555. def __sendRemoveInterest(self, contextId):
  556. """
  557. Part of the new otp-server code.
  558. contextId is a client-side created number that refers to
  559. a set of interests. The same contextId number doesn't
  560. necessarily have any relationship to the same contextId
  561. on another client.
  562. """
  563. datagram = PyDatagram()
  564. # Add message type
  565. datagram.addUint16(CLIENT_REMOVE_INTEREST)
  566. datagram.addUint16(contextId)
  567. self.send(datagram)
  568. def sendHeartbeat(self):
  569. datagram = PyDatagram()
  570. # Add message type
  571. datagram.addUint16(CLIENT_HEARTBEAT)
  572. # Send it!
  573. self.send(datagram)
  574. self.lastHeartbeat = globalClock.getRealTime()
  575. # This is important enough to consider flushing immediately
  576. # (particularly if we haven't run readerPollTask recently).
  577. self.considerFlush()
  578. def considerHeartbeat(self):
  579. """Send a heartbeat message if we haven't sent one recently."""
  580. if not self.heartbeatStarted:
  581. self.notify.debug("Heartbeats not started; not sending.")
  582. return
  583. elapsed = globalClock.getRealTime() - self.lastHeartbeat
  584. if elapsed < 0 or elapsed > self.heartbeatInterval:
  585. # It's time to send the heartbeat again (or maybe someone
  586. # reset the clock back).
  587. self.notify.info("Sending heartbeat mid-frame.")
  588. self.startHeartbeat()
  589. def stopHeartbeat(self):
  590. taskMgr.remove("heartBeat")
  591. self.heartbeatStarted = 0
  592. def startHeartbeat(self):
  593. self.stopHeartbeat()
  594. self.heartbeatStarted = 1
  595. self.sendHeartbeat()
  596. self.waitForNextHeartBeat()
  597. def sendHeartbeatTask(self, task):
  598. self.sendHeartbeat()
  599. self.waitForNextHeartBeat()
  600. return Task.done
  601. def waitForNextHeartBeat(self):
  602. taskMgr.doMethodLater(self.heartbeatInterval, self.sendHeartbeatTask,
  603. "heartBeat")
  604. def sendUpdate(self, do, fieldName, args, sendToId = None):
  605. dg = do.dclass.clientFormatUpdate(fieldName, sendToId or do.doId, args)
  606. self.send(dg)
  607. def replaceMethod(self, oldMethod, newFunction):
  608. return 0
  609. def getAllOfType(self, type):
  610. # Returns a list of all DistributedObjects in the repository
  611. # of a particular type.
  612. result = []
  613. for obj in self.doId2do.values():
  614. if isinstance(obj, type):
  615. result.append(obj)
  616. return result