ClientRepository.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784
  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. from direct.distributed.ConnectionRepository 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):
  16. """
  17. This maintains a client-side connection with a Panda server.
  18. It currently supports several different versions of the server:
  19. within the VR Studio, we are currently in transition from the
  20. Toontown server to the OTP server; people outside the VR studio
  21. will use the Panda LAN server provided by CMU.
  22. """
  23. notify = DirectNotifyGlobal.directNotify.newCategory("ClientRepository")
  24. def __init__(self, dcFileNames = None):
  25. self.dcSuffix=""
  26. ConnectionRepository.__init__(self, base.config, hasOwnerView=True)
  27. self.context=100000
  28. self.setClientDatagram(1)
  29. self.recorder = base.recorder
  30. self.readDCFile(dcFileNames)
  31. self.cache=CRCache.CRCache()
  32. self.cacheOwner=CRCache.CRCache()
  33. self.serverDelta = 0
  34. self.bootedIndex = None
  35. self.bootedText = None
  36. if 0: # unused:
  37. self.worldScale = render.attachNewNode("worldScale") # for grid zones.
  38. self.worldScale.setScale(base.config.GetFloat('world-scale', 100))
  39. self.priorWorldPos = None
  40. # create a parentMgr to handle distributed reparents
  41. # this used to be 'token2nodePath'
  42. self.parentMgr = ParentMgr.ParentMgr()
  43. # The RelatedObjectMgr helps distributed objects find each
  44. # other.
  45. self.relatedObjectMgr = RelatedObjectMgr.RelatedObjectMgr(self)
  46. # Keep track of how recently we last sent a heartbeat message.
  47. # We want to keep these coming at heartbeatInterval seconds.
  48. self.heartbeatInterval = base.config.GetDouble('heartbeat-interval', 10)
  49. self.heartbeatStarted = 0
  50. self.lastHeartbeat = 0
  51. # By default, the ClientRepository is set up to respond to
  52. # datagrams from the CMU Panda LAN server. You can
  53. # reassign this member to change the response behavior
  54. # according to game context.
  55. self.handler = self.publicServerDatagramHandler
  56. # The DOID allocator. The CMU LAN server may choose to
  57. # send us a block of DOIDs. If it chooses to do so, then we
  58. # may create objects, using those DOIDs. These structures are
  59. # only used in conjunction with the CMU LAN server.
  60. self.DOIDbase = 0
  61. self.DOIDnext = 0
  62. self.DOIDlast = 0
  63. ## def queryObjectAll(self, doID, context=0):
  64. ## """
  65. ## Get a one-time snapshot look at the object.
  66. ## """
  67. ## assert self.notify.debugStateCall(self)
  68. ## # Create a message
  69. ## datagram = PyDatagram()
  70. ## datagram.addServerHeader(
  71. ## doID, localAvatar.getDoId(), 2020)
  72. ## # A context that can be used to index the response if needed
  73. ## datagram.addUint32(context)
  74. ## self.send(datagram)
  75. ## # Make sure the message gets there.
  76. ## self.flush()
  77. # Define uniqueName
  78. def uniqueName(self, desc):
  79. return desc
  80. def getTables(self, ownerView):
  81. if ownerView:
  82. return self.doId2ownerView, self.cacheOwner
  83. else:
  84. return self.doId2do, self.cache
  85. def abruptCleanup(self):
  86. """
  87. Call this method to clean up any pending hooks or tasks on
  88. distributed objects, but leave the ClientRepository in a sane
  89. state for creating more distributed objects.
  90. """
  91. self.relatedObjectMgr.abortAllRequests()
  92. def sendDisconnect(self):
  93. if self.isConnected():
  94. # Tell the game server that we're going:
  95. datagram = PyDatagram()
  96. # Add message type
  97. datagram.addUint16(CLIENT_DISCONNECT)
  98. # Send the message
  99. self.send(datagram)
  100. self.notify.info("Sent disconnect message to server")
  101. self.disconnect()
  102. self.stopHeartbeat()
  103. if 0: # Code that became obsolete before it was used:
  104. def setWorldOffset(self, xOffset=0, yOffset=0):
  105. self.worldXOffset=xOffset
  106. self.worldYOffset=yOffset
  107. def getWorldPos(self, nodePath):
  108. pos = nodePath.getPos(self.worldScale)
  109. return (int(round(pos.getX())), int(round(pos.getY())))
  110. def sendWorldPos(self, x, y):
  111. # The server will need to know the world
  112. # offset of our current render node path
  113. # and adjust the x, y accordingly. At one
  114. # point I considered adding the world offset
  115. # here, but that would just use extra bits.
  116. onScreenDebug.add("worldPos", "%-4d, %-4d"%(x, y))
  117. return #*#
  118. datagram = PyDatagram()
  119. # Add message type
  120. datagram.addUint16(CLIENT_SET_WORLD_POS)
  121. # Add x
  122. datagram.addInt16(x)
  123. # Add y
  124. datagram.addSint16(y)
  125. # send the message
  126. self.send(datagram)
  127. def checkWorldPos(self, nodePath):
  128. worldPos = self.getWorldPos(nodePath)
  129. if self.priorWorldPos != worldPos:
  130. self.priorWorldPos = worldPos
  131. self.sendWorldPos(worldPos[0], worldPos[1])
  132. def allocateContext(self):
  133. self.context+=1
  134. return self.context
  135. def setServerDelta(self, delta):
  136. """
  137. Indicates the approximate difference in seconds between the
  138. client's clock and the server's clock, in universal time (not
  139. including timezone shifts). This is mainly useful for
  140. reporting synchronization information to the logs; don't
  141. depend on it for any precise timing requirements.
  142. Also see Notify.setServerDelta(), which also accounts for a
  143. timezone shift.
  144. """
  145. self.serverDelta = delta
  146. def getServerDelta(self):
  147. return self.serverDelta
  148. def getServerTimeOfDay(self):
  149. """
  150. Returns the current time of day (seconds elapsed since the
  151. 1972 epoch) according to the server's clock. This is in GMT,
  152. and hence is irrespective of timezones.
  153. The value is computed based on the client's clock and the
  154. known delta from the server's clock, which is not terribly
  155. precisely measured and may drift slightly after startup, but
  156. it should be accurate plus or minus a couple of seconds.
  157. """
  158. return time.time() + self.serverDelta
  159. def handleGenerateWithRequired(self, di):
  160. parentId = di.getUint32()
  161. zoneId = di.getUint32()
  162. assert parentId == self.GameGlobalsId or parentId in self.doId2do
  163. # Get the class Id
  164. classId = di.getUint16()
  165. # Get the DO Id
  166. doId = di.getUint32()
  167. # Look up the dclass
  168. dclass = self.dclassesByNumber[classId]
  169. dclass.startGenerate()
  170. # Create a new distributed object, and put it in the dictionary
  171. distObj = self.generateWithRequiredFields(dclass, doId, di, parentId, zoneId)
  172. dclass.stopGenerate()
  173. def handleGenerateWithRequiredOther(self, di):
  174. parentId = di.getUint32()
  175. zoneId = di.getUint32()
  176. assert parentId == self.GameGlobalsId or parentId in self.doId2do
  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. dclass.startGenerate()
  184. # Create a new distributed object, and put it in the dictionary
  185. distObj = self.generateWithRequiredOtherFields(dclass, doId, di, parentId, zoneId)
  186. dclass.stopGenerate()
  187. def handleGenerateWithRequiredOtherOwner(self, di):
  188. # Get the class Id
  189. classId = di.getUint16()
  190. # Get the DO Id
  191. doId = di.getUint32()
  192. # parentId and zoneId are not relevant here
  193. parentId = di.getUint32()
  194. zoneId = di.getUint32()
  195. # Look up the dclass
  196. dclass = self.dclassesByNumber[classId]
  197. dclass.startGenerate()
  198. # Create a new distributed object, and put it in the dictionary
  199. distObj = self.generateWithRequiredOtherFieldsOwner(dclass, doId, di)
  200. dclass.stopGenerate()
  201. def handleQuietZoneGenerateWithRequired(self, di):
  202. # Special handler for quiet zone generates -- we need to filter
  203. parentId = di.getUint32()
  204. zoneId = di.getUint32()
  205. assert parentId in self.doId2do
  206. # Get the class Id
  207. classId = di.getUint16()
  208. # Get the DO Id
  209. doId = di.getUint32()
  210. # Look up the dclass
  211. dclass = self.dclassesByNumber[classId]
  212. dclass.startGenerate()
  213. distObj = self.generateWithRequiredFields(dclass, doId, di, parentId, zoneId)
  214. dclass.stopGenerate()
  215. def handleQuietZoneGenerateWithRequiredOther(self, di):
  216. # Special handler for quiet zone generates -- we need to filter
  217. parentId = di.getUint32()
  218. zoneId = di.getUint32()
  219. assert parentId in self.doId2do
  220. # Get the class Id
  221. classId = di.getUint16()
  222. # Get the DO Id
  223. doId = di.getUint32()
  224. # Look up the dclass
  225. dclass = self.dclassesByNumber[classId]
  226. dclass.startGenerate()
  227. distObj = self.generateWithRequiredOtherFields(dclass, doId, di, parentId, zoneId)
  228. dclass.stopGenerate()
  229. def generateWithRequiredFields(self, dclass, doId, di, parentId, zoneId):
  230. if self.doId2do.has_key(doId):
  231. # ...it is in our dictionary.
  232. # Just update it.
  233. distObj = self.doId2do[doId]
  234. assert(distObj.dclass == dclass)
  235. distObj.generate()
  236. distObj.setLocation(parentId, zoneId)
  237. distObj.updateRequiredFields(dclass, di)
  238. # updateRequiredFields calls announceGenerate
  239. elif self.cache.contains(doId):
  240. # ...it is in the cache.
  241. # Pull it out of the cache:
  242. distObj = self.cache.retrieve(doId)
  243. assert(distObj.dclass == dclass)
  244. # put it in the dictionary:
  245. self.doId2do[doId] = distObj
  246. # and update it.
  247. distObj.generate()
  248. distObj.setLocation(parentId, zoneId)
  249. distObj.updateRequiredFields(dclass, di)
  250. # updateRequiredFields calls announceGenerate
  251. else:
  252. # ...it is not in the dictionary or the cache.
  253. # Construct a new one
  254. classDef = dclass.getClassDef()
  255. if classDef == None:
  256. self.notify.error("Could not create an undefined %s object." % (dclass.getName()))
  257. distObj = classDef(self)
  258. distObj.dclass = dclass
  259. # Assign it an Id
  260. distObj.doId = doId
  261. # Put the new do in the dictionary
  262. self.doId2do[doId] = distObj
  263. # Update the required fields
  264. distObj.generateInit() # Only called when constructed
  265. distObj.generate()
  266. distObj.setLocation(parentId, zoneId)
  267. distObj.updateRequiredFields(dclass, di)
  268. # updateRequiredFields calls announceGenerate
  269. print "New DO:%s, dclass:%s"%(doId, dclass.getName())
  270. return distObj
  271. ## def generateGlobalObject(self, doId, dcname):
  272. ## # Look up the dclass
  273. ## dclass = self.dclassesByName[dcname]
  274. ## # Create a new distributed object, and put it in the dictionary
  275. ## #distObj = self.generateWithRequiredFields(dclass, doId, di)
  276. ## # Construct a new one
  277. ## classDef = dclass.getClassDef()
  278. ## if classDef == None:
  279. ## self.notify.error("Could not create an undefined %s object."%(
  280. ## dclass.getName()))
  281. ## distObj = classDef(self)
  282. ## distObj.dclass = dclass
  283. ## # Assign it an Id
  284. ## distObj.doId = doId
  285. ## # Put the new do in the dictionary
  286. ## self.doId2do[doId] = distObj
  287. ## # Update the required fields
  288. ## distObj.generateInit() # Only called when constructed
  289. ## distObj.generate()
  290. ## # TODO: ROGER: where should we get parentId and zoneId?
  291. ## parentId = None
  292. ## zoneId = None
  293. ## distObj.setLocation(parentId, zoneId)
  294. ## # updateRequiredFields calls announceGenerate
  295. ## return distObj
  296. def generateWithRequiredOtherFields(self, dclass, doId, di,
  297. parentId = None, zoneId = None):
  298. if self.doId2do.has_key(doId):
  299. # ...it is in our dictionary.
  300. # Just update it.
  301. distObj = self.doId2do[doId]
  302. assert(distObj.dclass == dclass)
  303. distObj.generate()
  304. distObj.setLocation(parentId, zoneId)
  305. distObj.updateRequiredOtherFields(dclass, di)
  306. # updateRequiredOtherFields calls announceGenerate
  307. elif self.cache.contains(doId):
  308. # ...it is in the cache.
  309. # Pull it out of the cache:
  310. distObj = self.cache.retrieve(doId)
  311. assert(distObj.dclass == dclass)
  312. # put it in the dictionary:
  313. self.doId2do[doId] = distObj
  314. # and update it.
  315. distObj.generate()
  316. distObj.setLocation(parentId, zoneId)
  317. distObj.updateRequiredOtherFields(dclass, di)
  318. # updateRequiredOtherFields calls announceGenerate
  319. else:
  320. # ...it is not in the dictionary or the cache.
  321. # Construct a new one
  322. classDef = dclass.getClassDef()
  323. if classDef == None:
  324. self.notify.error("Could not create an undefined %s object." % (dclass.getName()))
  325. distObj = classDef(self)
  326. distObj.dclass = dclass
  327. # Assign it an Id
  328. distObj.doId = doId
  329. # Put the new do in the dictionary
  330. self.doId2do[doId] = distObj
  331. # Update the required fields
  332. distObj.generateInit() # Only called when constructed
  333. distObj.generate()
  334. distObj.setLocation(parentId, zoneId)
  335. distObj.updateRequiredOtherFields(dclass, di)
  336. # updateRequiredOtherFields calls announceGenerate
  337. return distObj
  338. def generateWithRequiredOtherFieldsOwner(self, dclass, doId, di):
  339. if self.doId2ownerView.has_key(doId):
  340. # ...it is in our dictionary.
  341. # Just update it.
  342. distObj = self.doId2ownerView[doId]
  343. assert(distObj.dclass == dclass)
  344. distObj.generate()
  345. distObj.updateRequiredOtherFields(dclass, di)
  346. # updateRequiredOtherFields calls announceGenerate
  347. elif self.cacheOwner.contains(doId):
  348. # ...it is in the cache.
  349. # Pull it out of the cache:
  350. distObj = self.cacheOwner.retrieve(doId)
  351. assert(distObj.dclass == dclass)
  352. # put it in the dictionary:
  353. self.doId2ownerView[doId] = distObj
  354. # and update it.
  355. distObj.generate()
  356. distObj.updateRequiredOtherFields(dclass, di)
  357. # updateRequiredOtherFields calls announceGenerate
  358. else:
  359. # ...it is not in the dictionary or the cache.
  360. # Construct a new one
  361. classDef = dclass.getOwnerClassDef()
  362. if classDef == None:
  363. self.notify.error("Could not create an undefined %s object. Have you created an owner view?" % (dclass.getName()))
  364. distObj = classDef(self)
  365. distObj.dclass = dclass
  366. # Assign it an Id
  367. distObj.doId = doId
  368. # Put the new do in the dictionary
  369. self.doId2ownerView[doId] = distObj
  370. # Update the required fields
  371. distObj.generateInit() # Only called when constructed
  372. distObj.generate()
  373. distObj.updateRequiredOtherFields(dclass, di)
  374. # updateRequiredOtherFields calls announceGenerate
  375. return distObj
  376. def handleDisable(self, di, ownerView=False):
  377. # Get the DO Id
  378. doId = di.getUint32()
  379. # disable it.
  380. self.disableDoId(doId, ownerView)
  381. def disableDoId(self, doId, ownerView=False):
  382. table, cache = self.getTables(ownerView)
  383. # Make sure the object exists
  384. if table.has_key(doId):
  385. # Look up the object
  386. distObj = table[doId]
  387. # remove the object from the dictionary
  388. del table[doId]
  389. # Only cache the object if it is a "cacheable" type
  390. # object; this way we don't clutter up the caches with
  391. # trivial objects that don't benefit from caching.
  392. if distObj.getCacheable():
  393. cache.cache(distObj)
  394. else:
  395. distObj.deleteOrDelay()
  396. else:
  397. ClientRepository.notify.warning(
  398. "Disable failed. DistObj "
  399. + str(doId) +
  400. " is not in dictionary, ownerView=%s" % ownerView)
  401. def handleDelete(self, di):
  402. # overridden by ToontownClientRepository
  403. assert 0
  404. def handleUpdateField(self, di):
  405. """
  406. This method is called when a CLIENT_OBJECT_UPDATE_FIELD
  407. message is received; it decodes the update, unpacks the
  408. arguments, and calls the corresponding method on the indicated
  409. DistributedObject.
  410. In fact, this method is exactly duplicated by the C++ method
  411. cConnectionRepository::handle_update_field(), which was
  412. written to optimize the message loop by handling all of the
  413. CLIENT_OBJECT_UPDATE_FIELD messages in C++. That means that
  414. nowadays, this Python method will probably never be called,
  415. since UPDATE_FIELD messages will not even be passed to the
  416. Python message handlers. But this method remains for
  417. documentation purposes, and also as a "just in case" handler
  418. in case we ever do come across a situation in the future in
  419. which python might handle the UPDATE_FIELD message.
  420. """
  421. # Get the DO Id
  422. doId = di.getUint32()
  423. #print("Updating " + str(doId))
  424. # Find the DO
  425. do = self.doId2do.get(doId)
  426. if do is not None:
  427. # Let the dclass finish the job
  428. do.dclass.receiveUpdate(do, di)
  429. else:
  430. ClientRepository.notify.warning(
  431. "Asked to update non-existent DistObj " + str(doId))
  432. def handleGoGetLost(self, di):
  433. # The server told us it's about to drop the connection on us.
  434. # Get ready!
  435. if (di.getRemainingSize() > 0):
  436. self.bootedIndex = di.getUint16()
  437. self.bootedText = di.getString()
  438. ClientRepository.notify.warning(
  439. "Server is booting us out (%d): %s" % (self.bootedIndex, self.bootedText))
  440. else:
  441. self.bootedIndex = None
  442. self.bootedText = None
  443. ClientRepository.notify.warning(
  444. "Server is booting us out with no explanation.")
  445. def handleServerHeartbeat(self, di):
  446. # Got a heartbeat message from the server.
  447. if base.config.GetBool('server-heartbeat-info', 1):
  448. ClientRepository.notify.info("Server heartbeat.")
  449. def handleSystemMessage(self, di):
  450. # Got a system message from the server.
  451. message = di.getString()
  452. self.notify.info('Message from server: %s' % (message))
  453. return message
  454. def handleSetDOIDrange(self, di):
  455. # This method is only used in conjunction with the CMU LAN
  456. # server.
  457. self.DOIDbase = di.getUint32()
  458. self.DOIDlast = self.DOIDbase + di.getUint32()
  459. self.DOIDnext = self.DOIDbase
  460. def handleRequestGenerates(self, di):
  461. # When new clients join the zone of an object, they need to hear
  462. # about it, so we send out all of our information about objects in
  463. # that particular zone.
  464. # This method is only used in conjunction with the CMU LAN
  465. # server.
  466. assert self.DOIDnext < self.DOIDlast
  467. zone = di.getUint32()
  468. for obj in self.doId2do.values():
  469. if obj.zone == zone:
  470. id = obj.doId
  471. if (self.isLocalId(id)):
  472. self.send(obj.dclass.clientFormatGenerate(obj, id, zone, []))
  473. def handleMessageType(self, msgType, di):
  474. if msgType == CLIENT_GO_GET_LOST:
  475. self.handleGoGetLost(di)
  476. elif msgType == CLIENT_HEARTBEAT:
  477. self.handleServerHeartbeat(di)
  478. elif msgType == CLIENT_SYSTEM_MESSAGE:
  479. self.handleSystemMessage(di)
  480. elif msgType == CLIENT_CREATE_OBJECT_REQUIRED:
  481. self.handleGenerateWithRequired(di)
  482. elif msgType == CLIENT_CREATE_OBJECT_REQUIRED_OTHER:
  483. self.handleGenerateWithRequiredOther(di)
  484. elif msgType == CLIENT_CREATE_OBJECT_REQUIRED_OTHER_OWNER:
  485. self.handleGenerateWithRequiredOtherOwner(di)
  486. elif msgType == CLIENT_OBJECT_UPDATE_FIELD:
  487. self.handleUpdateField(di)
  488. elif msgType == CLIENT_OBJECT_DISABLE:
  489. self.handleDisable(di)
  490. elif msgType == CLIENT_OBJECT_DISABLE_OWNER:
  491. self.handleDisable(di, ownerView=True)
  492. elif msgType == CLIENT_OBJECT_DELETE_RESP:
  493. self.handleDelete(di)
  494. elif msgType == CLIENT_DONE_INTEREST_RESP:
  495. self.handleInterestDoneMessage(di)
  496. #Roger wants to remove this elif msgType == CLIENT_QUERY_ONE_FIELD_RESP:
  497. #Roger wants to remove this self.handleQueryOneFieldResp(di)
  498. elif msgType == CLIENT_OBJECT_LOCATION:
  499. self.handleObjectLocation(di)
  500. else:
  501. currentLoginState = self.loginFSM.getCurrentState()
  502. if currentLoginState:
  503. currentLoginStateName = currentLoginState.getName()
  504. else:
  505. currentLoginStateName = "None"
  506. currentGameState = self.gameFSM.getCurrentState()
  507. if currentGameState:
  508. currentGameStateName = currentGameState.getName()
  509. else:
  510. currentGameStateName = "None"
  511. ClientRepository.notify.warning(
  512. "Ignoring unexpected message type: " +
  513. str(msgType) +
  514. " login state: " +
  515. currentLoginStateName +
  516. " game state: " +
  517. currentGameStateName)
  518. def createWithRequired(self, className, zoneId = 0, optionalFields=None):
  519. # This method is only used in conjunction with the CMU LAN
  520. # server.
  521. if self.DOIDnext >= self.DOIDlast:
  522. self.notify.error(
  523. "Cannot allocate a distributed object ID: all IDs used up.")
  524. return None
  525. id = self.DOIDnext
  526. self.DOIDnext = self.DOIDnext + 1
  527. dclass = self.dclassesByName[className]
  528. classDef = dclass.getClassDef()
  529. if classDef == None:
  530. self.notify.error("Could not create an undefined %s object." % (
  531. dclass.getName()))
  532. obj = classDef(self)
  533. obj.dclass = dclass
  534. obj.zone = zoneId
  535. obj.doId = id
  536. self.doId2do[id] = obj
  537. obj.generateInit()
  538. obj.generate()
  539. obj.announceGenerate()
  540. datagram = dclass.clientFormatGenerate(obj, id, zoneId, optionalFields)
  541. self.send(datagram)
  542. return obj
  543. def sendDisableMsg(self, doId):
  544. # This method is only used in conjunction with the CMU LAN
  545. # server.
  546. datagram = PyDatagram()
  547. datagram.addUint16(CLIENT_OBJECT_DISABLE)
  548. datagram.addUint32(doId)
  549. self.send(datagram)
  550. def sendDeleteMsg(self, doId):
  551. # This method is only used in conjunction with the CMU LAN
  552. # server.
  553. datagram = PyDatagram()
  554. datagram.addUint16(CLIENT_OBJECT_DELETE)
  555. datagram.addUint32(doId)
  556. self.send(datagram)
  557. def sendRemoveZoneMsg(self, zoneId, visibleZoneList=None):
  558. # This method is only used in conjunction with the CMU LAN
  559. # server.
  560. datagram = PyDatagram()
  561. datagram.addUint16(CLIENT_REMOVE_ZONE)
  562. datagram.addUint32(zoneId)
  563. # if we have an explicit list of visible zones, add them
  564. if visibleZoneList is not None:
  565. vzl = list(visibleZoneList)
  566. vzl.sort()
  567. assert PythonUtil.uniqueElements(vzl)
  568. for zone in vzl:
  569. datagram.addUint32(zone)
  570. # send the message
  571. self.send(datagram)
  572. def getObjectsOfClass(self, objClass):
  573. """ returns dict of doId:object, containing all objects
  574. that inherit from 'class'. returned dict is safely mutable. """
  575. doDict = {}
  576. for doId, do in self.doId2do.items():
  577. if isinstance(do, objClass):
  578. doDict[doId] = do
  579. return doDict
  580. def getObjectsOfExactClass(self, objClass):
  581. """ returns dict of doId:object, containing all objects that
  582. are exactly of type 'class' (neglecting inheritance). returned
  583. dict is safely mutable. """
  584. doDict = {}
  585. for doId, do in self.doId2do.items():
  586. if do.__class__ == objClass:
  587. doDict[doId] = do
  588. return doDict
  589. def sendSetLocation(self,doId,parentId,zoneId):
  590. datagram = PyDatagram()
  591. datagram.addUint16(CLIENT_OBJECT_LOCATION)
  592. datagram.addUint32(doId)
  593. datagram.addUint32(parentId)
  594. datagram.addUint32(zoneId)
  595. self.send(datagram)
  596. def handleDatagram(self, di):
  597. if self.notify.getDebug():
  598. print "ClientRepository received datagram:"
  599. di.getDatagram().dumpHex(ostream)
  600. msgType = self.getMsgType()
  601. if self.handler == None:
  602. self.handleMessageType(msgType, di)
  603. else:
  604. self.handler(msgType, di)
  605. # If we're processing a lot of datagrams within one frame, we
  606. # may forget to send heartbeats. Keep them coming!
  607. self.considerHeartbeat()
  608. def publicServerDatagramHandler(self, msgType, di):
  609. # These are the sort of messages we may expect from the public
  610. # Panda server.
  611. if msgType == CLIENT_SET_DOID_RANGE:
  612. self.handleSetDOIDrange(di)
  613. elif msgType == CLIENT_CREATE_OBJECT_REQUIRED_RESP:
  614. self.handleGenerateWithRequired(di)
  615. elif msgType == CLIENT_CREATE_OBJECT_REQUIRED_OTHER_RESP:
  616. self.handleGenerateWithRequiredOther(di)
  617. elif msgType == CLIENT_OBJECT_UPDATE_FIELD_RESP:
  618. self.handleUpdateField(di)
  619. elif msgType == CLIENT_OBJECT_DELETE_RESP:
  620. self.handleDelete(di)
  621. elif msgType == CLIENT_OBJECT_DISABLE_RESP:
  622. self.handleDisable(di)
  623. elif msgType == CLIENT_REQUEST_GENERATES:
  624. self.handleRequestGenerates(di)
  625. else:
  626. self.handleMessageType(msgType, di)
  627. def sendHeartbeat(self):
  628. datagram = PyDatagram()
  629. # Add message type
  630. datagram.addUint16(CLIENT_HEARTBEAT)
  631. # Send it!
  632. self.send(datagram)
  633. self.lastHeartbeat = globalClock.getRealTime()
  634. # This is important enough to consider flushing immediately
  635. # (particularly if we haven't run readerPollTask recently).
  636. self.considerFlush()
  637. def considerHeartbeat(self):
  638. """Send a heartbeat message if we haven't sent one recently."""
  639. if not self.heartbeatStarted:
  640. self.notify.debug("Heartbeats not started; not sending.")
  641. return
  642. elapsed = globalClock.getRealTime() - self.lastHeartbeat
  643. if elapsed < 0 or elapsed > self.heartbeatInterval:
  644. # It's time to send the heartbeat again (or maybe someone
  645. # reset the clock back).
  646. self.notify.info("Sending heartbeat mid-frame.")
  647. self.startHeartbeat()
  648. def stopHeartbeat(self):
  649. taskMgr.remove("heartBeat")
  650. self.heartbeatStarted = 0
  651. def startHeartbeat(self):
  652. self.stopHeartbeat()
  653. self.heartbeatStarted = 1
  654. self.sendHeartbeat()
  655. self.waitForNextHeartBeat()
  656. def sendHeartbeatTask(self, task):
  657. self.sendHeartbeat()
  658. self.waitForNextHeartBeat()
  659. return Task.done
  660. def waitForNextHeartBeat(self):
  661. taskMgr.doMethodLater(self.heartbeatInterval, self.sendHeartbeatTask,
  662. "heartBeat")
  663. def sendUpdateZone(self, obj, zoneId):
  664. # This method is only used in conjunction with the CMU LAN
  665. # server.
  666. id = obj.doId
  667. assert(self.isLocalId(id))
  668. self.sendDeleteMsg(id, 1)
  669. obj.zone = zoneId
  670. self.send(obj.dclass.clientFormatGenerate(obj, id, zoneId, []))
  671. def replaceMethod(self, oldMethod, newFunction):
  672. return 0
  673. def isLocalId(self,id):
  674. return ((id >= self.DOIDbase) and (id < self.DOIDlast))
  675. def haveCreateAuthority(self):
  676. return (self.DOIDlast > self.DOIDnext)
  677. def getWorld(self, doId):
  678. # Get the world node for this object
  679. obj = self.doId2do[doId]
  680. worldNP = obj.getParent()
  681. while 1:
  682. nextNP = worldNP.getParent()
  683. if nextNP == render:
  684. break
  685. elif worldNP.isEmpty():
  686. return None
  687. return worldNP