ClientRepository.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. """ClientRepository module: contains the ClientRepository class"""
  2. from ClientRepositoryBase import ClientRepositoryBase
  3. from direct.directnotify import DirectNotifyGlobal
  4. from MsgTypesCMU import *
  5. from PyDatagram import PyDatagram
  6. from PyDatagramIterator import PyDatagramIterator
  7. from pandac.PandaModules import UniqueIdAllocator
  8. import types
  9. class ClientRepository(ClientRepositoryBase):
  10. """
  11. This is the open-source ClientRepository as provided by CMU. It
  12. communicates with the ServerRepository in this same directory.
  13. If you are looking for the VR Studio's implementation of the
  14. client repository, look to OTPClientRepository (elsewhere).
  15. """
  16. notify = DirectNotifyGlobal.directNotify.newCategory("ClientRepository")
  17. # This is required by DoCollectionManager, even though it's not
  18. # used by this implementation.
  19. GameGlobalsId = 0
  20. def __init__(self, dcFileNames = None, dcSuffix = ''):
  21. ClientRepositoryBase.__init__(self, dcFileNames = dcFileNames, dcSuffix = dcSuffix)
  22. self.setHandleDatagramsInternally(False)
  23. # The doId allocator. The CMU LAN server may choose to
  24. # send us a block of doIds. If it chooses to do so, then we
  25. # may create objects, using those doIds.
  26. self.doIdAllocator = None
  27. self.doIdBase = 0
  28. self.doIdLast = 0
  29. # The doIdBase of the client message currently being
  30. # processed.
  31. self.currentSenderId = None
  32. def handleSetDoIdrange(self, di):
  33. self.doIdBase = di.getUint32()
  34. self.doIdLast = self.doIdBase + di.getUint32()
  35. self.doIdAllocator = UniqueIdAllocator(self.doIdBase, self.doIdLast - 1)
  36. # Now that we've got a doId range, we can safely generate new
  37. # distributed objects.
  38. messenger.send('createReady')
  39. def handleRequestGenerates(self, di):
  40. # When new clients join the zone of an object, they need to hear
  41. # about it, so we send out all of our information about objects in
  42. # that particular zone.
  43. zone = di.getUint32()
  44. for obj in self.doId2do.values():
  45. if obj.zoneId == zone:
  46. if (self.isLocalId(obj.doId)):
  47. self.resendGenerate(obj)
  48. def resendGenerate(self, obj):
  49. """ Sends the generate message again for an already-generated
  50. object, presumably to inform any newly-arrived clients of this
  51. object's current state. """
  52. # get the list of "ram" fields that aren't
  53. # required. These are fields whose values should
  54. # persist even if they haven't been received
  55. # lately, so we have to re-broadcast these values
  56. # in case the new client hasn't heard their latest
  57. # values.
  58. extraFields = []
  59. for i in range(obj.dclass.getNumInheritedFields()):
  60. field = obj.dclass.getInheritedField(i)
  61. if field.hasKeyword('broadcast') and field.hasKeyword('ram') and not field.hasKeyword('required'):
  62. if field.asMolecularField():
  63. # It's a molecular field; this means
  64. # we have to pack the components.
  65. # Fortunately, we'll find those
  66. # separately through the iteration, so
  67. # we can ignore this field itself.
  68. continue
  69. extraFields.append(field.getName())
  70. datagram = self.formatGenerate(obj, extraFields)
  71. self.send(datagram)
  72. def handleGenerate(self, di):
  73. self.currentSenderId = di.getUint32()
  74. zoneId = di.getUint32()
  75. classId = di.getUint16()
  76. doId = di.getUint32()
  77. # Look up the dclass
  78. dclass = self.dclassesByNumber[classId]
  79. distObj = self.doId2do.get(doId)
  80. if distObj and distObj.dclass == dclass:
  81. # We've already got this object. Probably this is just a
  82. # repeat-generate, synthesized for the benefit of someone
  83. # else who just entered the zone. Accept the new updates,
  84. # but don't make a formal generate.
  85. assert(self.notify.debug("performing generate-update for %s %s" % (dclass.getName(), doId)))
  86. dclass.receiveUpdateBroadcastRequired(distObj, di)
  87. dclass.receiveUpdateOther(distObj, di)
  88. return
  89. assert(self.notify.debug("performing generate for %s %s" % (dclass.getName(), doId)))
  90. dclass.startGenerate()
  91. # Create a new distributed object, and put it in the dictionary
  92. distObj = self.generateWithRequiredOtherFields(dclass, doId, di, 0, zoneId)
  93. dclass.stopGenerate()
  94. def allocateDoId(self):
  95. """ Returns a newly-allocated doId. Call freeDoId() when the
  96. object has been deleted. """
  97. return self.doIdAllocator.allocate()
  98. def freeDoId(self, doId):
  99. """ Returns a doId back into the free pool for re-use. """
  100. assert self.isLocalId(doId)
  101. self.doIdAllocator.free(doId)
  102. def createDistributedObject(self, className = None, distObj = None,
  103. zoneId = 0, optionalFields = None):
  104. """ To create a DistributedObject, you must pass in either the
  105. name of the object's class, or an already-created instance of
  106. the class (or both). If you pass in just a class name (to the
  107. className parameter), then a default instance of the object
  108. will be created, with whatever parameters the default
  109. constructor supplies. Alternatively, if you wish to create
  110. some initial values different from the default, you can create
  111. the instance yourself and supply it to the distObj parameter,
  112. then that instance will be used instead. (It should be a
  113. newly-created object, not one that has already been manifested
  114. on the network or previously passed through
  115. createDistributedObject.) In either case, the new
  116. DistributedObject is returned from this method.
  117. This method will issue the appropriate network commands to
  118. make this object appear on all of the other clients.
  119. You should supply an initial zoneId in which to manifest the
  120. object. The fields marked "required" or "ram" will be
  121. broadcast to all of the other clients; if you wish to
  122. broadcast additional field values at this time as well, pass a
  123. list of field names in the optionalFields parameters.
  124. """
  125. if not className:
  126. if not distObj:
  127. self.notify.error("Must specify either a className or a distObj.")
  128. className = distObj.__class__.__name__
  129. doId = self.allocateDoId()
  130. dclass = self.dclassesByName.get(className)
  131. if not dclass:
  132. self.notify.error("Unknown distributed class: %s" % (distObj.__class__))
  133. classDef = dclass.getClassDef()
  134. if classDef == None:
  135. self.notify.error("Could not create an undefined %s object." % (
  136. dclass.getName()))
  137. if not distObj:
  138. distObj = classDef(self)
  139. if not isinstance(distObj, classDef):
  140. self.notify.error("Object %s is not an instance of %s" % (distObj.__class__.__name__, classDef.__name__))
  141. distObj.dclass = dclass
  142. distObj.doId = doId
  143. self.doId2do[doId] = distObj
  144. distObj.generateInit()
  145. distObj._retrieveCachedData()
  146. distObj.generate()
  147. distObj.setLocation(0, zoneId)
  148. distObj.announceGenerate()
  149. datagram = self.formatGenerate(distObj, optionalFields)
  150. self.send(datagram)
  151. return distObj
  152. def formatGenerate(self, distObj, extraFields):
  153. """ Returns a datagram formatted for sending the generate message for the indicated object. """
  154. return distObj.dclass.clientFormatGenerateCMU(distObj, distObj.doId, distObj.zoneId, extraFields)
  155. def sendDeleteMsg(self, doId):
  156. datagram = PyDatagram()
  157. datagram.addUint16(OBJECT_DELETE_CMU)
  158. datagram.addUint32(doId)
  159. self.send(datagram)
  160. def sendDisconnect(self):
  161. if self.isConnected():
  162. # Tell the game server that we're going:
  163. datagram = PyDatagram()
  164. # Add message type
  165. datagram.addUint16(CLIENT_DISCONNECT_CMU)
  166. # Send the message
  167. self.send(datagram)
  168. self.notify.info("Sent disconnect message to server")
  169. self.disconnect()
  170. self.stopHeartbeat()
  171. def setInterestZones(self, interestZoneIds):
  172. """ Changes the set of zones that this particular client is
  173. interested in hearing about. """
  174. datagram = PyDatagram()
  175. # Add message type
  176. datagram.addUint16(CLIENT_SET_INTEREST_CMU)
  177. for zoneId in interestZoneIds:
  178. datagram.addUint32(zoneId)
  179. # send the message
  180. self.send(datagram)
  181. def setObjectZone(self, distObj, zoneId):
  182. """ Moves the object into the indicated zone. """
  183. distObj.b_setLocation(0, zoneId)
  184. assert distObj.zoneId == zoneId
  185. # Tell all of the clients monitoring the new zone that we've
  186. # arrived.
  187. self.resendGenerate(distObj)
  188. def sendSetLocation(self, doId, parentId, zoneId):
  189. datagram = PyDatagram()
  190. datagram.addUint16(OBJECT_SET_ZONE_CMU)
  191. datagram.addUint32(doId)
  192. datagram.addUint32(zoneId)
  193. self.send(datagram)
  194. def sendHeartbeat(self):
  195. datagram = PyDatagram()
  196. # Add message type
  197. datagram.addUint16(CLIENT_HEARTBEAT_CMU)
  198. # Send it!
  199. self.send(datagram)
  200. self.lastHeartbeat = globalClock.getRealTime()
  201. # This is important enough to consider flushing immediately
  202. # (particularly if we haven't run readerPollTask recently).
  203. self.considerFlush()
  204. def isLocalId(self, doId):
  205. """ Returns true if this doId is one that we're the owner of,
  206. false otherwise. """
  207. return ((doId >= self.doIdBase) and (doId < self.doIdLast))
  208. def haveCreateAuthority(self):
  209. """ Returns true if this client has been assigned a range of
  210. doId's it may use to create objects, false otherwise. """
  211. return (self.doIdLast > self.doIdBase)
  212. def getAvatarIdFromSender(self):
  213. """ Returns the doIdBase of the client that originally sent
  214. the current update message. This is only defined when
  215. processing an update message or a generate message. """
  216. return self.currentSenderId
  217. def handleDatagram(self, di):
  218. if self.notify.getDebug():
  219. print "ClientRepository received datagram:"
  220. di.getDatagram().dumpHex(ostream)
  221. msgType = self.getMsgType()
  222. self.currentSenderId = None
  223. # These are the sort of messages we may expect from the public
  224. # Panda server.
  225. if msgType == SET_DOID_RANGE_CMU:
  226. self.handleSetDoIdrange(di)
  227. elif msgType == OBJECT_GENERATE_CMU:
  228. self.handleGenerate(di)
  229. elif msgType == OBJECT_UPDATE_FIELD_CMU:
  230. self.handleUpdateField(di)
  231. elif msgType == OBJECT_DISABLE_CMU:
  232. self.handleDisable(di)
  233. elif msgType == OBJECT_DELETE_CMU:
  234. self.handleDelete(di)
  235. elif msgType == REQUEST_GENERATES_CMU:
  236. self.handleRequestGenerates(di)
  237. else:
  238. self.handleMessageType(msgType, di)
  239. # If we're processing a lot of datagrams within one frame, we
  240. # may forget to send heartbeats. Keep them coming!
  241. self.considerHeartbeat()
  242. def handleMessageType(self, msgType, di):
  243. self.notify.error("unrecognized message type %s" % (msgType))
  244. def handleUpdateField(self, di):
  245. # The CMU update message starts with an additional field, not
  246. # present in the Disney update message: the doIdBase of the
  247. # original sender. Extract that and call up to the parent.
  248. self.currentSenderId = di.getUint32()
  249. ClientRepositoryBase.handleUpdateField(self, di)
  250. def handleDisable(self, di):
  251. # Receives a list of doIds.
  252. while di.getRemainingSize() > 0:
  253. doId = di.getUint32()
  254. # We should never get a disable message for our own object.
  255. assert not self.isLocalId(doId)
  256. self.disableDoId(doId)
  257. def handleDelete(self, di):
  258. # Receives a single doId.
  259. doId = di.getUint32()
  260. self.deleteObject(doId)
  261. def deleteObject(self, doId):
  262. """
  263. Removes the object from the client's view of the world. This
  264. should normally not be called directly except in the case of
  265. error recovery, since the server will normally be responsible
  266. for deleting and disabling objects as they go out of scope.
  267. After this is called, future updates by server on this object
  268. will be ignored (with a warning message). The object will
  269. become valid again the next time the server sends a generate
  270. message for this doId.
  271. This is not a distributed message and does not delete the
  272. object on the server or on any other client.
  273. """
  274. if self.doId2do.has_key(doId):
  275. # If it is in the dictionary, remove it.
  276. obj = self.doId2do[doId]
  277. # Remove it from the dictionary
  278. del self.doId2do[doId]
  279. # Disable, announce, and delete the object itself...
  280. # unless delayDelete is on...
  281. obj.deleteOrDelay()
  282. if self.isLocalId(doId):
  283. self.freeDoId(doId)
  284. elif self.cache.contains(doId):
  285. # If it is in the cache, remove it.
  286. self.cache.delete(doId)
  287. if self.isLocalId(doId):
  288. self.freeDoId(doId)
  289. else:
  290. # Otherwise, ignore it
  291. self.notify.warning(
  292. "Asked to delete non-existent DistObj " + str(doId))
  293. def sendUpdate(self, distObj, fieldName, args):
  294. """ Sends a normal update for a single field. """
  295. dg = distObj.dclass.clientFormatUpdate(
  296. fieldName, distObj.doId, args)
  297. self.send(dg)
  298. def sendUpdateToChannel(self, distObj, channelId, fieldName, args):
  299. """ Sends a targeted update of a single field to a particular
  300. client. The top 32 bits of channelId is ignored; the lower 32
  301. bits should be the client Id of the recipient (i.e. the
  302. client's doIdbase). The field update will be sent to the
  303. indicated client only. The field must be marked clsend or
  304. p2p, and may not be marked broadcast. """
  305. datagram = distObj.dclass.clientFormatUpdate(
  306. fieldName, distObj.doId, args)
  307. dgi = PyDatagramIterator(datagram)
  308. # Reformat the packed datagram to change the message type and
  309. # add the target id.
  310. dgi.getUint16()
  311. dg = PyDatagram()
  312. dg.addUint16(CLIENT_OBJECT_UPDATE_FIELD_TARGETED_CMU)
  313. dg.addUint32(channelId & 0xffffffff)
  314. dg.appendData(dgi.getRemainingBytes())
  315. self.send(dg)