ClientRepository.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. """ClientRepository module: contains the ClientRepository class"""
  2. from PandaModules import *
  3. from MsgTypes import *
  4. import Task
  5. import DirectNotifyGlobal
  6. import ClientDistClass
  7. import CRCache
  8. import ConnectionRepository
  9. import PythonUtil
  10. import ParentMgr
  11. import time
  12. class ClientRepository(ConnectionRepository.ConnectionRepository):
  13. notify = DirectNotifyGlobal.directNotify.newCategory("ClientRepository")
  14. def __init__(self, dcFileName):
  15. ConnectionRepository.ConnectionRepository.__init__(self, base.config)
  16. self.number2cdc={}
  17. self.name2cdc={}
  18. self.doId2do={}
  19. self.doId2cdc={}
  20. self.parseDcFile(dcFileName)
  21. self.cache=CRCache.CRCache()
  22. self.serverDelta = 0
  23. self.bootedIndex = None
  24. self.bootedText = None
  25. # create a parentMgr to handle distributed reparents
  26. # this used to be 'token2nodePath'
  27. self.parentMgr = ParentMgr.ParentMgr()
  28. def setServerDelta(self, delta):
  29. """
  30. Indicates the approximate difference in seconds between the
  31. client's clock and the server's clock, in universal time (not
  32. including timezone shifts). This is mainly useful for
  33. reporting synchronization information to the logs; don't
  34. depend on it for any precise timing requirements.
  35. Also see Notify.setServerDelta(), which also accounts for a
  36. timezone shift.
  37. """
  38. self.serverDelta = delta
  39. def getServerDelta(self):
  40. return self.serverDelta
  41. def getServerTimeOfDay(self):
  42. """
  43. Returns the current time of day (seconds elapsed since the
  44. 1972 epoch) according to the server's clock. This is in GMT,
  45. and hence is irrespective of timezones.
  46. The value is computed based on the client's clock and the
  47. known delta from the server's clock, which is not terribly
  48. precisely measured and may drift slightly after startup, but
  49. it should be accurate plus or minus a couple of seconds.
  50. """
  51. return time.time() + self.cr.getServerDelta()
  52. def parseDcFile(self, dcFileName):
  53. self.dcFile = DCFile()
  54. readResult = self.dcFile.read(dcFileName)
  55. if not readResult:
  56. self.notify.error("Could not read dcfile: %s" % dcFileName.cStr())
  57. self.hashVal = self.dcFile.getHash()
  58. return self.parseDcClasses(self.dcFile)
  59. def parseDcClasses(self, dcFile):
  60. numClasses = dcFile.getNumClasses()
  61. for i in range(0, numClasses):
  62. # Create a clientDistClass from the dcClass
  63. dcClass = dcFile.getClass(i)
  64. clientDistClass = ClientDistClass.ClientDistClass(dcClass)
  65. # List the cdc in the number and name dictionaries
  66. self.number2cdc[dcClass.getNumber()]=clientDistClass
  67. self.name2cdc[dcClass.getName()]=clientDistClass
  68. return None
  69. def handleGenerateWithRequired(self, di):
  70. # Get the class Id
  71. classId = di.getArg(STUint16);
  72. # Get the DO Id
  73. doId = di.getArg(STUint32)
  74. # Look up the cdc
  75. cdc = self.number2cdc[classId]
  76. # Create a new distributed object, and put it in the dictionary
  77. distObj = self.generateWithRequiredFields(cdc, doId, di)
  78. return None
  79. def handleGenerateWithRequiredOther(self, di):
  80. # Get the class Id
  81. classId = di.getArg(STUint16);
  82. # Get the DO Id
  83. doId = di.getArg(STUint32)
  84. # Look up the cdc
  85. cdc = self.number2cdc[classId]
  86. # Create a new distributed object, and put it in the dictionary
  87. distObj = self.generateWithRequiredOtherFields(cdc, doId, di)
  88. return None
  89. def handleQuietZoneGenerateWithRequired(self, di):
  90. # Special handler for quiet zone generates -- we need to filter
  91. # Get the class Id
  92. classId = di.getArg(STUint16);
  93. # Get the DO Id
  94. doId = di.getArg(STUint32)
  95. # Look up the cdc
  96. cdc = self.number2cdc[classId]
  97. # If the class is a neverDisable class (which implies uberzone) we
  98. # should go ahead and generate it even though we are in the quiet zone
  99. if cdc.constructor.neverDisable:
  100. # Create a new distributed object, and put it in the dictionary
  101. distObj = self.generateWithRequiredFields(cdc, doId, di)
  102. return None
  103. def handleQuietZoneGenerateWithRequiredOther(self, di):
  104. # Special handler for quiet zone generates -- we need to filter
  105. # Get the class Id
  106. classId = di.getArg(STUint16);
  107. # Get the DO Id
  108. doId = di.getArg(STUint32)
  109. # Look up the cdc
  110. cdc = self.number2cdc[classId]
  111. # If the class is a neverDisable class (which implies uberzone) we
  112. # should go ahead and generate it even though we are in the quiet zone
  113. if cdc.constructor.neverDisable:
  114. # Create a new distributed object, and put it in the dictionary
  115. distObj = self.generateWithRequiredOtherFields(cdc, doId, di)
  116. return None
  117. def generateWithRequiredFields(self, cdc, doId, di):
  118. # Is it in our dictionary?
  119. if self.doId2do.has_key(doId):
  120. # If so, just update it.
  121. distObj = self.doId2do[doId]
  122. distObj.generate()
  123. distObj.updateRequiredFields(cdc, di)
  124. # updateRequiredFields calls announceGenerate
  125. # Is it in the cache? If so, pull it out, put it in the dictionaries,
  126. # and update it.
  127. elif self.cache.contains(doId):
  128. # If so, pull it out of the cache...
  129. distObj = self.cache.retrieve(doId)
  130. # put it in both dictionaries...
  131. self.doId2do[doId] = distObj
  132. self.doId2cdc[doId] = cdc
  133. # and update it.
  134. distObj.generate()
  135. distObj.updateRequiredFields(cdc, di)
  136. # updateRequiredFields calls announceGenerate
  137. # If it is not in the dictionary or the cache, then...
  138. else:
  139. # Construct a new one
  140. distObj = cdc.constructor(self)
  141. # Assign it an Id
  142. distObj.doId = doId
  143. # Put the new do in both dictionaries
  144. self.doId2do[doId] = distObj
  145. self.doId2cdc[doId] = cdc
  146. # Update the required fields
  147. distObj.generateInit() # Only called when constructed
  148. distObj.generate()
  149. distObj.updateRequiredFields(cdc, di)
  150. # updateRequiredFields calls announceGenerate
  151. return distObj
  152. def generateWithRequiredOtherFields(self, cdc, doId, di):
  153. # Is it in our dictionary?
  154. if self.doId2do.has_key(doId):
  155. # If so, just update it.
  156. distObj = self.doId2do[doId]
  157. distObj.generate()
  158. distObj.updateRequiredOtherFields(cdc, di)
  159. # updateRequiredOtherFields calls announceGenerate
  160. # Is it in the cache? If so, pull it out, put it in the dictionaries,
  161. # and update it.
  162. elif self.cache.contains(doId):
  163. # If so, pull it out of the cache...
  164. distObj = self.cache.retrieve(doId)
  165. # put it in both dictionaries...
  166. self.doId2do[doId] = distObj
  167. self.doId2cdc[doId] = cdc
  168. # and update it.
  169. distObj.generate()
  170. distObj.updateRequiredOtherFields(cdc, di)
  171. # updateRequiredOtherFields calls announceGenerate
  172. # If it is not in the dictionary or the cache, then...
  173. else:
  174. # Construct a new one
  175. if cdc.constructor == None:
  176. self.notify.error("Could not create an undefined %s object." % (cdc.name))
  177. distObj = cdc.constructor(self)
  178. # Assign it an Id
  179. distObj.doId = doId
  180. # Put the new do in both dictionaries
  181. self.doId2do[doId] = distObj
  182. self.doId2cdc[doId] = cdc
  183. # Update the required fields
  184. distObj.generateInit() # Only called when constructed
  185. distObj.generate()
  186. distObj.updateRequiredOtherFields(cdc, di)
  187. # updateRequiredOtherFields calls announceGenerate
  188. return distObj
  189. def handleDisable(self, di):
  190. # Get the DO Id
  191. doId = di.getArg(STUint32)
  192. # disable it.
  193. self.disableDoId(doId)
  194. return None
  195. def disableDoId(self, doId):
  196. # Make sure the object exists
  197. if self.doId2do.has_key(doId):
  198. # Look up the object
  199. distObj = self.doId2do[doId]
  200. # remove the object from both dictionaries
  201. del(self.doId2do[doId])
  202. del(self.doId2cdc[doId])
  203. assert(len(self.doId2do) == len(self.doId2cdc))
  204. # Only cache the object if it is a "cacheable" type
  205. # object; this way we don't clutter up the caches with
  206. # trivial objects that don't benefit from caching.
  207. if distObj.getCacheable():
  208. self.cache.cache(distObj)
  209. else:
  210. distObj.deleteOrDelay()
  211. else:
  212. ClientRepository.notify.warning("Disable failed. DistObj " +
  213. str(doId) +
  214. " is not in dictionary")
  215. return None
  216. def handleDelete(self, di):
  217. # Get the DO Id
  218. doId = di.getArg(STUint32)
  219. self.deleteObject(doId)
  220. def deleteObject(self, doId):
  221. """deleteObject(self, doId)
  222. Removes the object from the client's view of the world. This
  223. should normally not be called except in the case of error
  224. recovery, since the server will normally be responsible for
  225. deleting and disabling objects as they go out of scope.
  226. After this is called, future updates by server on this object
  227. will be ignored (with a warning message). The object will
  228. become valid again the next time the server sends a generate
  229. message for this doId.
  230. This is not a distributed message and does not delete the
  231. object on the server or on any other client.
  232. """
  233. # If it is in the dictionaries, remove it.
  234. if self.doId2do.has_key(doId):
  235. obj = self.doId2do[doId]
  236. # Remove it from the dictionaries
  237. del(self.doId2do[doId])
  238. del(self.doId2cdc[doId])
  239. # Sanity check the dictionaries
  240. assert(len(self.doId2do) == len(self.doId2cdc))
  241. # Disable, announce, and delete the object itself...
  242. # unless delayDelete is on...
  243. obj.deleteOrDelay()
  244. # If it is in the cache, remove it.
  245. elif self.cache.contains(doId):
  246. self.cache.delete(doId)
  247. # Otherwise, ignore it
  248. else:
  249. ClientRepository.notify.warning(
  250. "Asked to delete non-existent DistObj " + str(doId))
  251. return None
  252. def handleUpdateField(self, di):
  253. # Get the DO Id
  254. doId = di.getArg(STUint32)
  255. #print("Updating " + str(doId))
  256. if self.rsDoReport:
  257. self.rsUpdateObjs[doId] = self.rsUpdateObjs.get(doId, 0) + 1
  258. # Find the DO
  259. do = self.doId2do.get(doId)
  260. cdc = self.doId2cdc.get(doId)
  261. if (do != None and cdc != None):
  262. # Let the cdc finish the job
  263. cdc.updateField(do, di)
  264. else:
  265. ClientRepository.notify.warning(
  266. "Asked to update non-existent DistObj " + str(doId))
  267. return None
  268. def handleGoGetLost(self, di):
  269. # The server told us it's about to drop the connection on us.
  270. # Get ready!
  271. if (di.getRemainingSize() > 0):
  272. self.bootedIndex = di.getUint16()
  273. self.bootedText = di.getString()
  274. ClientRepository.notify.warning(
  275. "Server is booting us out (%d): %s" % (self.bootedIndex, self.bootedText))
  276. else:
  277. self.bootedIndex = None
  278. self.bootedText = None
  279. ClientRepository.notify.warning(
  280. "Server is booting us out with no explanation.")
  281. def handleServerHeartbeat(self, di):
  282. # Got a heartbeat message from the server.
  283. if base.config.GetBool('server-heartbeat-info', 1):
  284. ClientRepository.notify.info("Server heartbeat.")
  285. def handleUnexpectedMsgType(self, msgType, di):
  286. if msgType == CLIENT_GO_GET_LOST:
  287. self.handleGoGetLost(di)
  288. elif msgType == CLIENT_HEARTBEAT:
  289. self.handleServerHeartbeat(di)
  290. else:
  291. currentLoginState = self.loginFSM.getCurrentState()
  292. if currentLoginState:
  293. currentLoginStateName = currentLoginState.getName()
  294. else:
  295. currentLoginStateName = "None"
  296. currentGameState = self.gameFSM.getCurrentState()
  297. if currentGameState:
  298. currentGameStateName = currentGameState.getName()
  299. else:
  300. currentGameStateName = "None"
  301. ClientRepository.notify.warning(
  302. "Ignoring unexpected message type: " +
  303. str(msgType) +
  304. " login state: " +
  305. currentLoginStateName +
  306. " game state: " +
  307. currentGameStateName)
  308. return None
  309. def sendSetShardMsg(self, shardId):
  310. datagram = Datagram()
  311. # Add message type
  312. datagram.addUint16(CLIENT_SET_SHARD)
  313. # Add shard id
  314. datagram.addUint32(shardId)
  315. # send the message
  316. self.send(datagram)
  317. return None
  318. def sendSetZoneMsg(self, zoneId, visibleZoneList=None):
  319. datagram = Datagram()
  320. # Add message type
  321. datagram.addUint16(CLIENT_SET_ZONE)
  322. # Add zone id
  323. datagram.addUint32(zoneId)
  324. # if we have an explicit list of visible zones, add them
  325. if visibleZoneList is not None:
  326. for zone in visibleZoneList:
  327. datagram.addUint32(zone)
  328. # send the message
  329. self.send(datagram)
  330. def sendUpdate(self, do, fieldName, args, sendToId = None):
  331. # Get the DO id
  332. doId = do.doId
  333. # Get the cdc
  334. cdc = self.doId2cdc.get(doId, None)
  335. if cdc:
  336. # Let the cdc finish the job
  337. cdc.sendUpdate(self, do, fieldName, args, sendToId)
  338. def replaceMethod(self, oldMethod, newFunction):
  339. foundIt = 0
  340. import new
  341. # Iterate over the ClientDistClasses
  342. for cdc in self.number2cdc.values():
  343. # Iterate over the ClientDistUpdates
  344. for cdu in cdc.allCDU:
  345. method = cdu.func
  346. # See if this is a match
  347. if (method and (method.im_func == oldMethod)):
  348. # Create a new unbound method out of this new function
  349. newMethod = new.instancemethod(newFunction,
  350. method.im_self,
  351. method.im_class)
  352. # Set the new method on the cdu
  353. cdu.func = newMethod
  354. foundIt = 1
  355. return foundIt
  356. def getAllOfType(self, type):
  357. # Returns a list of all DistributedObjects in the repository
  358. # of a particular type.
  359. result = []
  360. for obj in self.doId2do.values():
  361. if isinstance(obj, type):
  362. result.append(obj)
  363. return result