ClientRepositoryBase.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  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.recorder = base.recorder
  32. self.readDCFile(dcFileNames)
  33. self.cache=CRCache.CRCache()
  34. self.cacheOwner=CRCache.CRCache()
  35. self.serverDelta = 0
  36. self.bootedIndex = None
  37. self.bootedText = None
  38. # create a parentMgr to handle distributed reparents
  39. # this used to be 'token2nodePath'
  40. self.parentMgr = ParentMgr.ParentMgr()
  41. # The RelatedObjectMgr helps distributed objects find each
  42. # other.
  43. self.relatedObjectMgr = RelatedObjectMgr.RelatedObjectMgr(self)
  44. # Keep track of how recently we last sent a heartbeat message.
  45. # We want to keep these coming at heartbeatInterval seconds.
  46. self.heartbeatInterval = base.config.GetDouble('heartbeat-interval', 10)
  47. self.heartbeatStarted = 0
  48. self.lastHeartbeat = 0
  49. ## def queryObjectAll(self, doID, context=0):
  50. ## """
  51. ## Get a one-time snapshot look at the object.
  52. ## """
  53. ## assert self.notify.debugStateCall(self)
  54. ## # Create a message
  55. ## datagram = PyDatagram()
  56. ## datagram.addServerHeader(
  57. ## doID, localAvatar.getDoId(), 2020)
  58. ## # A context that can be used to index the response if needed
  59. ## datagram.addUint32(context)
  60. ## self.send(datagram)
  61. ## # Make sure the message gets there.
  62. ## self.flush()
  63. # Define uniqueName
  64. def uniqueName(self, desc):
  65. return desc
  66. def getTables(self, ownerView):
  67. if ownerView:
  68. return self.doId2ownerView, self.cacheOwner
  69. else:
  70. return self.doId2do, self.cache
  71. def _getMsgName(self, msgId):
  72. return makeList(MsgId2Names.get(msgId, 'UNKNOWN MESSAGE: %s' % msgId))[0]
  73. def sendDisconnect(self):
  74. if self.isConnected():
  75. # Tell the game server that we're going:
  76. datagram = PyDatagram()
  77. # Add message type
  78. datagram.addUint16(CLIENT_DISCONNECT)
  79. # Send the message
  80. self.send(datagram)
  81. self.notify.info("Sent disconnect message to server")
  82. self.disconnect()
  83. self.stopHeartbeat()
  84. def allocateContext(self):
  85. self.context+=1
  86. return self.context
  87. def setServerDelta(self, delta):
  88. """
  89. Indicates the approximate difference in seconds between the
  90. client's clock and the server's clock, in universal time (not
  91. including timezone shifts). This is mainly useful for
  92. reporting synchronization information to the logs; don't
  93. depend on it for any precise timing requirements.
  94. Also see Notify.setServerDelta(), which also accounts for a
  95. timezone shift.
  96. """
  97. self.serverDelta = delta
  98. def getServerDelta(self):
  99. return self.serverDelta
  100. def getServerTimeOfDay(self):
  101. """
  102. Returns the current time of day (seconds elapsed since the
  103. 1972 epoch) according to the server's clock. This is in GMT,
  104. and hence is irrespective of timezones.
  105. The value is computed based on the client's clock and the
  106. known delta from the server's clock, which is not terribly
  107. precisely measured and may drift slightly after startup, but
  108. it should be accurate plus or minus a couple of seconds.
  109. """
  110. return time.time() + self.serverDelta
  111. def handleGenerateWithRequired(self, di):
  112. parentId = di.getUint32()
  113. zoneId = di.getUint32()
  114. assert parentId == self.GameGlobalsId or parentId in self.doId2do
  115. # Get the class Id
  116. classId = di.getUint16()
  117. # Get the DO Id
  118. doId = di.getUint32()
  119. # Look up the dclass
  120. dclass = self.dclassesByNumber[classId]
  121. dclass.startGenerate()
  122. # Create a new distributed object, and put it in the dictionary
  123. distObj = self.generateWithRequiredFields(dclass, doId, di, parentId, zoneId)
  124. dclass.stopGenerate()
  125. def handleGenerateWithRequiredOther(self, di):
  126. parentId = di.getUint32()
  127. zoneId = di.getUint32()
  128. assert parentId == self.GameGlobalsId or parentId in self.doId2do
  129. # Get the class Id
  130. classId = di.getUint16()
  131. # Get the DO Id
  132. doId = di.getUint32()
  133. # Look up the dclass
  134. dclass = self.dclassesByNumber[classId]
  135. dclass.startGenerate()
  136. # Create a new distributed object, and put it in the dictionary
  137. distObj = self.generateWithRequiredOtherFields(dclass, doId, di, parentId, zoneId)
  138. dclass.stopGenerate()
  139. def handleGenerateWithRequiredOtherOwner(self, di):
  140. # Get the class Id
  141. classId = di.getUint16()
  142. # Get the DO Id
  143. doId = di.getUint32()
  144. # parentId and zoneId are not relevant here
  145. parentId = di.getUint32()
  146. zoneId = di.getUint32()
  147. # Look up the dclass
  148. dclass = self.dclassesByNumber[classId]
  149. dclass.startGenerate()
  150. # Create a new distributed object, and put it in the dictionary
  151. distObj = self.generateWithRequiredOtherFieldsOwner(dclass, doId, di)
  152. dclass.stopGenerate()
  153. def handleQuietZoneGenerateWithRequired(self, di):
  154. # Special handler for quiet zone generates -- we need to filter
  155. parentId = di.getUint32()
  156. zoneId = di.getUint32()
  157. assert parentId in self.doId2do
  158. # Get the class Id
  159. classId = di.getUint16()
  160. # Get the DO Id
  161. doId = di.getUint32()
  162. # Look up the dclass
  163. dclass = self.dclassesByNumber[classId]
  164. dclass.startGenerate()
  165. distObj = self.generateWithRequiredFields(dclass, doId, di, parentId, zoneId)
  166. dclass.stopGenerate()
  167. def handleQuietZoneGenerateWithRequiredOther(self, di):
  168. # Special handler for quiet zone generates -- we need to filter
  169. parentId = di.getUint32()
  170. zoneId = di.getUint32()
  171. assert parentId in self.doId2do
  172. # Get the class Id
  173. classId = di.getUint16()
  174. # Get the DO Id
  175. doId = di.getUint32()
  176. # Look up the dclass
  177. dclass = self.dclassesByNumber[classId]
  178. dclass.startGenerate()
  179. distObj = self.generateWithRequiredOtherFields(dclass, doId, di, parentId, zoneId)
  180. dclass.stopGenerate()
  181. def generateWithRequiredFields(self, dclass, doId, di, parentId, zoneId):
  182. if self.doId2do.has_key(doId):
  183. # ...it is in our dictionary.
  184. # Just update it.
  185. distObj = self.doId2do[doId]
  186. assert distObj.dclass == dclass
  187. distObj.generate()
  188. distObj.setLocation(parentId, zoneId)
  189. distObj.updateRequiredFields(dclass, di)
  190. # updateRequiredFields calls announceGenerate
  191. elif self.cache.contains(doId):
  192. # ...it is in the cache.
  193. # Pull it out of the cache:
  194. distObj = self.cache.retrieve(doId)
  195. assert distObj.dclass == dclass
  196. # put it in the dictionary:
  197. self.doId2do[doId] = distObj
  198. # and update it.
  199. distObj.generate()
  200. # make sure we don't have a stale location
  201. distObj.parentId = None
  202. distObj.zoneId = None
  203. distObj.setLocation(parentId, zoneId)
  204. distObj.updateRequiredFields(dclass, di)
  205. # updateRequiredFields calls announceGenerate
  206. else:
  207. # ...it is not in the dictionary or the cache.
  208. # Construct a new one
  209. classDef = dclass.getClassDef()
  210. if classDef == None:
  211. self.notify.error("Could not create an undefined %s object." % (dclass.getName()))
  212. distObj = classDef(self)
  213. distObj.dclass = dclass
  214. # Assign it an Id
  215. distObj.doId = doId
  216. # Put the new do in the dictionary
  217. self.doId2do[doId] = distObj
  218. # Update the required fields
  219. distObj.generateInit() # Only called when constructed
  220. distObj.generate()
  221. distObj.setLocation(parentId, zoneId)
  222. distObj.updateRequiredFields(dclass, di)
  223. # updateRequiredFields calls announceGenerate
  224. print "New DO:%s, dclass:%s"%(doId, dclass.getName())
  225. return distObj
  226. def generateWithRequiredOtherFields(self, dclass, doId, di,
  227. parentId = None, zoneId = None):
  228. if self.doId2do.has_key(doId):
  229. # ...it is in our dictionary.
  230. # Just update it.
  231. distObj = self.doId2do[doId]
  232. assert distObj.dclass == dclass
  233. distObj.generate()
  234. distObj.setLocation(parentId, zoneId)
  235. distObj.updateRequiredOtherFields(dclass, di)
  236. # updateRequiredOtherFields calls announceGenerate
  237. elif self.cache.contains(doId):
  238. # ...it is in the cache.
  239. # Pull it out of the cache:
  240. distObj = self.cache.retrieve(doId)
  241. assert distObj.dclass == dclass
  242. # put it in the dictionary:
  243. self.doId2do[doId] = distObj
  244. # and update it.
  245. distObj.generate()
  246. # make sure we don't have a stale location
  247. distObj.parentId = None
  248. distObj.zoneId = None
  249. distObj.setLocation(parentId, zoneId)
  250. distObj.updateRequiredOtherFields(dclass, di)
  251. # updateRequiredOtherFields calls announceGenerate
  252. else:
  253. # ...it is not in the dictionary or the cache.
  254. # Construct a new one
  255. classDef = dclass.getClassDef()
  256. if classDef == None:
  257. self.notify.error("Could not create an undefined %s object." % (dclass.getName()))
  258. distObj = classDef(self)
  259. distObj.dclass = dclass
  260. # Assign it an Id
  261. distObj.doId = doId
  262. # Put the new do in the dictionary
  263. self.doId2do[doId] = distObj
  264. # Update the required fields
  265. distObj.generateInit() # Only called when constructed
  266. distObj.generate()
  267. distObj.setLocation(parentId, zoneId)
  268. distObj.updateRequiredOtherFields(dclass, di)
  269. # updateRequiredOtherFields calls announceGenerate
  270. return distObj
  271. def generateWithRequiredOtherFieldsOwner(self, dclass, doId, di):
  272. if self.doId2ownerView.has_key(doId):
  273. # ...it is in our dictionary.
  274. # Just update it.
  275. distObj = self.doId2ownerView[doId]
  276. assert distObj.dclass == dclass
  277. distObj.generate()
  278. distObj.updateRequiredOtherFields(dclass, di)
  279. # updateRequiredOtherFields calls announceGenerate
  280. elif self.cacheOwner.contains(doId):
  281. # ...it is in the cache.
  282. # Pull it out of the cache:
  283. distObj = self.cacheOwner.retrieve(doId)
  284. assert distObj.dclass == dclass
  285. # put it in the dictionary:
  286. self.doId2ownerView[doId] = distObj
  287. # and update it.
  288. distObj.generate()
  289. distObj.updateRequiredOtherFields(dclass, di)
  290. # updateRequiredOtherFields calls announceGenerate
  291. else:
  292. # ...it is not in the dictionary or the cache.
  293. # Construct a new one
  294. classDef = dclass.getOwnerClassDef()
  295. if classDef == None:
  296. self.notify.error("Could not create an undefined %s object. Have you created an owner view?" % (dclass.getName()))
  297. distObj = classDef(self)
  298. distObj.dclass = dclass
  299. # Assign it an Id
  300. distObj.doId = doId
  301. # Put the new do in the dictionary
  302. self.doId2ownerView[doId] = distObj
  303. # Update the required fields
  304. distObj.generateInit() # Only called when constructed
  305. distObj.generate()
  306. distObj.updateRequiredOtherFields(dclass, di)
  307. # updateRequiredOtherFields calls announceGenerate
  308. return distObj
  309. def handleDisable(self, di, ownerView=False):
  310. # Get the DO Id
  311. doId = di.getUint32()
  312. # disable it.
  313. self.disableDoId(doId, ownerView)
  314. def disableDoId(self, doId, ownerView=False):
  315. table, cache = self.getTables(ownerView)
  316. # Make sure the object exists
  317. if table.has_key(doId):
  318. # Look up the object
  319. distObj = table[doId]
  320. # remove the object from the dictionary
  321. del table[doId]
  322. # Only cache the object if it is a "cacheable" type
  323. # object; this way we don't clutter up the caches with
  324. # trivial objects that don't benefit from caching.
  325. if distObj.getCacheable():
  326. cache.cache(distObj)
  327. else:
  328. distObj.deleteOrDelay()
  329. else:
  330. self._logFailedDisable(doId, ownerView)
  331. def _logFailedDisable(self, doId, ownerView):
  332. self.notify.warning(
  333. "Disable failed. DistObj "
  334. + str(doId) +
  335. " is not in dictionary, ownerView=%s" % ownerView)
  336. def handleDelete(self, di):
  337. # overridden by ToontownClientRepository
  338. assert 0
  339. def handleUpdateField(self, di):
  340. """
  341. This method is called when a CLIENT_OBJECT_UPDATE_FIELD
  342. message is received; it decodes the update, unpacks the
  343. arguments, and calls the corresponding method on the indicated
  344. DistributedObject.
  345. In fact, this method is exactly duplicated by the C++ method
  346. cConnectionRepository::handle_update_field(), which was
  347. written to optimize the message loop by handling all of the
  348. CLIENT_OBJECT_UPDATE_FIELD messages in C++. That means that
  349. nowadays, this Python method will probably never be called,
  350. since UPDATE_FIELD messages will not even be passed to the
  351. Python message handlers. But this method remains for
  352. documentation purposes, and also as a "just in case" handler
  353. in case we ever do come across a situation in the future in
  354. which python might handle the UPDATE_FIELD message.
  355. """
  356. # Get the DO Id
  357. doId = di.getUint32()
  358. #print("Updating " + str(doId))
  359. # Find the DO
  360. do = self.doId2do.get(doId)
  361. if do is not None:
  362. # Let the dclass finish the job
  363. do.dclass.receiveUpdate(do, di)
  364. else:
  365. self.notify.warning(
  366. "Asked to update non-existent DistObj " + str(doId))
  367. def handleGoGetLost(self, di):
  368. # The server told us it's about to drop the connection on us.
  369. # Get ready!
  370. if (di.getRemainingSize() > 0):
  371. self.bootedIndex = di.getUint16()
  372. self.bootedText = di.getString()
  373. self.notify.warning(
  374. "Server is booting us out (%d): %s" % (self.bootedIndex, self.bootedText))
  375. else:
  376. self.bootedIndex = None
  377. self.bootedText = None
  378. self.notify.warning(
  379. "Server is booting us out with no explanation.")
  380. def handleServerHeartbeat(self, di):
  381. # Got a heartbeat message from the server.
  382. if base.config.GetBool('server-heartbeat-info', 1):
  383. self.notify.info("Server heartbeat.")
  384. def handleSystemMessage(self, di):
  385. # Got a system message from the server.
  386. message = di.getString()
  387. self.notify.info('Message from server: %s' % (message))
  388. return message
  389. def getObjectsOfClass(self, objClass):
  390. """ returns dict of doId:object, containing all objects
  391. that inherit from 'class'. returned dict is safely mutable. """
  392. doDict = {}
  393. for doId, do in self.doId2do.items():
  394. if isinstance(do, objClass):
  395. doDict[doId] = do
  396. return doDict
  397. def getObjectsOfExactClass(self, objClass):
  398. """ returns dict of doId:object, containing all objects that
  399. are exactly of type 'class' (neglecting inheritance). returned
  400. dict is safely mutable. """
  401. doDict = {}
  402. for doId, do in self.doId2do.items():
  403. if do.__class__ == objClass:
  404. doDict[doId] = do
  405. return doDict
  406. def sendSetLocation(self, doId, parentId, zoneId):
  407. datagram = PyDatagram()
  408. datagram.addUint16(CLIENT_OBJECT_LOCATION)
  409. datagram.addUint32(doId)
  410. datagram.addUint32(parentId)
  411. datagram.addUint32(zoneId)
  412. self.send(datagram)
  413. def sendHeartbeat(self):
  414. datagram = PyDatagram()
  415. # Add message type
  416. datagram.addUint16(CLIENT_HEARTBEAT)
  417. # Send it!
  418. self.send(datagram)
  419. self.lastHeartbeat = globalClock.getRealTime()
  420. # This is important enough to consider flushing immediately
  421. # (particularly if we haven't run readerPollTask recently).
  422. self.considerFlush()
  423. def considerHeartbeat(self):
  424. """Send a heartbeat message if we haven't sent one recently."""
  425. if not self.heartbeatStarted:
  426. self.notify.debug("Heartbeats not started; not sending.")
  427. return
  428. elapsed = globalClock.getRealTime() - self.lastHeartbeat
  429. if elapsed < 0 or elapsed > self.heartbeatInterval:
  430. # It's time to send the heartbeat again (or maybe someone
  431. # reset the clock back).
  432. self.notify.info("Sending heartbeat mid-frame.")
  433. self.startHeartbeat()
  434. def stopHeartbeat(self):
  435. taskMgr.remove("heartBeat")
  436. self.heartbeatStarted = 0
  437. def startHeartbeat(self):
  438. self.stopHeartbeat()
  439. self.heartbeatStarted = 1
  440. self.sendHeartbeat()
  441. self.waitForNextHeartBeat()
  442. def sendHeartbeatTask(self, task):
  443. self.sendHeartbeat()
  444. self.waitForNextHeartBeat()
  445. return Task.done
  446. def waitForNextHeartBeat(self):
  447. taskMgr.doMethodLater(self.heartbeatInterval, self.sendHeartbeatTask,
  448. "heartBeat")
  449. def replaceMethod(self, oldMethod, newFunction):
  450. return 0
  451. def getWorld(self, doId):
  452. # Get the world node for this object
  453. obj = self.doId2do[doId]
  454. worldNP = obj.getParent()
  455. while 1:
  456. nextNP = worldNP.getParent()
  457. if nextNP == render:
  458. break
  459. elif worldNP.isEmpty():
  460. return None
  461. return worldNP
  462. def isLive(self):
  463. if base.config.GetBool('force-live', 0):
  464. return True
  465. return not (__dev__ or launcher.isTestServer())
  466. def isLocalId(self, id):
  467. # By default, no ID's are local. See also
  468. # ClientRepository.isLocalId().
  469. return 0