DistributedObjectAI.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. """DistributedObjectAI module: contains the DistributedObjectAI class"""
  2. from direct.directnotify.DirectNotifyGlobal import *
  3. from direct.showbase import PythonUtil
  4. from direct.showbase import DirectObject
  5. from pandac.PandaModules import *
  6. from PyDatagram import PyDatagram
  7. from PyDatagramIterator import PyDatagramIterator
  8. class DistributedObjectAI(DirectObject.DirectObject):
  9. """Distributed Object class:"""
  10. notify = directNotify.newCategory("DistributedObjectAI")
  11. QuietZone = 1
  12. def __init__(self, air):
  13. try:
  14. self.DistributedObjectAI_initialized
  15. except:
  16. self.DistributedObjectAI_initialized = 1
  17. self.accountName=''
  18. # Record the repository
  19. self.air = air
  20. # Record our distributed class
  21. className = self.__class__.__name__
  22. self.dclass = self.air.dclassesByName[className]
  23. # init doId pre-allocated flag
  24. self.__preallocDoId = 0
  25. # used to track zone changes across the quiet zone
  26. # NOTE: the quiet zone is defined in OTP, but we need it
  27. # here.
  28. self.lastNonQuietZone = None
  29. self._DOAI_requestedDelete = False
  30. # These are used to implement beginBarrier().
  31. self.__nextBarrierContext = 0
  32. self.__barriers = {}
  33. # Uncomment if you want to debug DO leaks
  34. #def __del__(self):
  35. # """
  36. # For debugging purposes, this just prints out what got deleted
  37. # """
  38. # print ("Destructing: " + self.__class__.__name__)
  39. def getDeleteEvent(self):
  40. # this is sent just before we get deleted
  41. if hasattr(self, 'doId'):
  42. return 'distObjDelete-%s' % self.doId
  43. return None
  44. def sendDeleteEvent(self):
  45. # this is called just before we get deleted
  46. delEvent = self.getDeleteEvent()
  47. if delEvent:
  48. messenger.send(delEvent)
  49. def delete(self):
  50. """
  51. Inheritors should redefine this to take appropriate action on delete
  52. Note that this may be called multiple times if a class inherits
  53. from DistributedObjectAI more than once.
  54. """
  55. # prevent this code from executing multiple times
  56. if self.air is not None:
  57. # self.doId may not exist. The __dict__ syntax works around that.
  58. assert(self.notify.debug('delete(): %s' % (self.__dict__.get("doId"))))
  59. if not self._DOAI_requestedDelete:
  60. # this logs every delete that was not requested by us.
  61. # TODO: this currently prints warnings for deletes of objects
  62. # that we did not create. We need to add a 'locally created'
  63. # flag to every object to filter these out.
  64. """
  65. DistributedObjectAI.notify.warning(
  66. 'delete() called but requestDelete never called for %s: %s'
  67. % (self.__dict__.get('doId'), self.__class__.__name__))
  68. """
  69. """
  70. # print a stack trace so we can detect whether this is the
  71. # result of a network msg.
  72. # this is slow.
  73. from direct.showbase.PythonUtil import StackTrace
  74. DistributedObjectAI.notify.warning(
  75. 'stack trace: %s' % StackTrace())
  76. """
  77. self._DOAI_requestedDelete = False
  78. # Clean up all the pending barriers.
  79. for barrier in self.__barriers.values():
  80. barrier.cleanup()
  81. self.__barriers = {}
  82. if not hasattr(self, "doNotDeallocateChannel"):
  83. if self.air:
  84. self.air.deallocateChannel(self.doId)
  85. self.air = None
  86. if hasattr(self, 'parentId'):
  87. del self.parentId
  88. del self.zoneId
  89. def isDeleted(self):
  90. """
  91. Returns true if the object has been deleted,
  92. or if it is brand new and hasn't yet been generated.
  93. """
  94. return (self.air == None)
  95. def isGenerated(self):
  96. """
  97. Returns true if the object has been generated
  98. """
  99. return hasattr(self, 'zoneId')
  100. def getDoId(self):
  101. """
  102. Return the distributed object id
  103. """
  104. return self.doId
  105. def preAllocateDoId(self):
  106. """
  107. objects that need to have a doId before they are generated
  108. can call this to pre-allocate a doId for the object
  109. """
  110. assert not self.__preallocDoId
  111. self.doId = self.air.allocateChannel()
  112. self.__preallocDoId = 1
  113. def updateRequiredFields(self, dclass, di):
  114. dclass.receiveUpdateBroadcastRequired(self, di)
  115. def updateAllRequiredFields(self, dclass, di):
  116. dclass.receiveUpdateAllRequired(self, di)
  117. def updateRequiredOtherFields(self, dclass, di):
  118. dclass.receiveUpdateBroadcastRequired(self, di)
  119. dclass.receiveUpdateOther(self, di)
  120. def updateAllRequiredOtherFields(self, dclass, di):
  121. dclass.receiveUpdateAllRequired(self, di)
  122. dclass.receiveUpdateOther(self, di)
  123. def sendSetZone(self, zoneId):
  124. self.air.sendSetZone(self, zoneId)
  125. def getZoneChangeEvent(self):
  126. # this event is generated whenever this object changes zones.
  127. # arguments are newZoneId, oldZoneId
  128. # includes the quiet zone.
  129. return 'DOChangeZone-%s' % self.doId
  130. def getLogicalZoneChangeEvent(self):
  131. # this event is generated whenever this object changes to a
  132. # non-quiet-zone zone.
  133. # arguments are newZoneId, oldZoneId
  134. # does not include the quiet zone.
  135. return 'DOLogicalChangeZone-%s' % self.doId
  136. def handleZoneChange(self, newParentId, newZoneId, oldParentId, oldZoneId):
  137. assert oldParentId == self.parentId
  138. ##assert oldZoneId == self.zoneId
  139. self.parentId = newParentId
  140. self.zoneId = newZoneId
  141. self.air.changeDOZoneInTables(self, newZoneId, oldZoneId)
  142. messenger.send(self.getZoneChangeEvent(), [newZoneId, oldZoneId])
  143. # if we are not going into the quiet zone, send a 'logical' zone change
  144. # message
  145. if newZoneId != DistributedObjectAI.QuietZone:
  146. lastLogicalZone = oldZoneId
  147. if oldZoneId == DistributedObjectAI.QuietZone:
  148. lastLogicalZone = self.lastNonQuietZone
  149. self.handleLogicalZoneChange(newZoneId, lastLogicalZone)
  150. self.lastNonQuietZone = newZoneId
  151. def handleLogicalZoneChange(self, newZoneId, oldZoneId):
  152. """this function gets called as if we never go through the
  153. quiet zone. Note that it is called once you reach the newZone,
  154. and not at the time that you leave the oldZone."""
  155. messenger.send(self.getLogicalZoneChangeEvent(),
  156. [newZoneId, oldZoneId])
  157. def getRender(self):
  158. # note that this will return a different node if we change zones
  159. return self.air.getRender(self.zoneId)
  160. def getParentMgr(self):
  161. return self.air.getParentMgr(self.zoneId)
  162. def getCollTrav(self):
  163. return self.air.getCollTrav(self.zoneId)
  164. def sendUpdate(self, fieldName, args = []):
  165. assert self.notify.debugStateCall(self)
  166. if self.air:
  167. self.air.sendUpdate(self, fieldName, args)
  168. def sendUpdateToAvatarId(self, avId, fieldName, args):
  169. assert self.notify.debugStateCall(self)
  170. channelId = avId + 1
  171. self.sendUpdateToChannel(channelId, fieldName, args)
  172. def sendUpdateToChannel(self, channelId, fieldName, args):
  173. assert self.notify.debugStateCall(self)
  174. if self.air:
  175. self.air.sendUpdateToChannel(self, channelId, fieldName, args)
  176. def generateWithRequired(self, zoneId, optionalFields=[]):
  177. assert self.notify.debugStateCall(self)
  178. # have we already allocated a doId?
  179. if self.__preallocDoId:
  180. self.__preallocDoId = 0
  181. return self.generateWithRequiredAndId(
  182. self.doId, zoneId, optionalFields)
  183. # The repository is the one that really does the work
  184. self.air.generateWithRequired(self, zoneId, optionalFields)
  185. if wantOtpServer:
  186. #HACK:
  187. parentId = simbase.air.districtId
  188. self.parentId = parentId
  189. self.zoneId = zoneId
  190. self.generate()
  191. # this is a special generate used for estates, or anything else that
  192. # needs to have a hard coded doId as assigned by the server
  193. def generateWithRequiredAndId(self, doId, zoneId, optionalFields=[]):
  194. assert self.notify.debugStateCall(self)
  195. # have we already allocated a doId?
  196. if self.__preallocDoId:
  197. assert doId == self.__preallocDoId
  198. self.__preallocDoId = 0
  199. # The repository is the one that really does the work
  200. self.air.generateWithRequiredAndId(self, doId, zoneId, optionalFields)
  201. if wantOtpServer:
  202. #HACK:
  203. parentId = simbase.air.districtId
  204. self.parentId = parentId
  205. self.zoneId = zoneId
  206. self.generate()
  207. if wantOtpServer:
  208. def generateOtpObject(self, parentId, zoneId, optionalFields=[], doId=None):
  209. assert self.notify.debugStateCall(self)
  210. # have we already allocated a doId?
  211. if self.__preallocDoId:
  212. assert doId is None or doId == self.__preallocDoId
  213. doId=self.__preallocDoId
  214. self.__preallocDoId = 0
  215. # The repository is the one that really does the work
  216. self.air.sendGenerateOtpObject(
  217. self, parentId, zoneId, optionalFields, doId=doId)
  218. self.parentId = parentId
  219. self.zoneId = zoneId
  220. self.generate()
  221. def generate(self):
  222. """
  223. Inheritors should put functions that require self.zoneId or
  224. other networked info in this function.
  225. """
  226. assert self.notify.debugStateCall(self)
  227. if wantOtpServer:
  228. def generateInit(self):
  229. """
  230. First generate (not from cache).
  231. """
  232. assert self.notify.debugStateCall(self)
  233. def sendGenerateWithRequired(self, repository, parentId, zoneId, optionalFields=[]):
  234. assert self.notify.debugStateCall(self)
  235. if not wantOtpServer:
  236. parentId = 0
  237. # Make the dclass do the hard work
  238. dg = self.dclass.aiFormatGenerate(
  239. self, self.doId, parentId, zoneId,
  240. repository.serverId,
  241. repository.ourChannel,
  242. optionalFields)
  243. repository.send(dg)
  244. def initFromServerResponse(self, valDict):
  245. assert self.notify.debugStateCall(self)
  246. # This is a special method used for estates, etc., which get
  247. # their fields set from the database indirectly by way of the
  248. # AI. The input parameter is a dictionary of field names to
  249. # datagrams that describes the initial field values from the
  250. # database.
  251. dclass = self.dclass
  252. for key, value in valDict.items():
  253. # Update the field
  254. dclass.directUpdate(self, key, value)
  255. def requestDelete(self):
  256. assert self.notify.debugStateCall(self)
  257. if not self.air:
  258. doId = "none"
  259. if hasattr(self, "doId"):
  260. doId = self.doId
  261. self.notify.warning("Tried to delete a %s (doId %s) that is already deleted" % (self.__class__, doId))
  262. return
  263. self.air.requestDelete(self)
  264. self._DOAI_requestedDelete = True
  265. def taskName(self, taskString):
  266. return (taskString + "-" + str(self.getDoId()))
  267. def uniqueName(self, idString):
  268. return (idString + "-" + str(self.getDoId()))
  269. def validate(self, avId, bool, msg):
  270. if not bool:
  271. self.air.writeServerEvent('suspicious', avId, msg)
  272. self.notify.warning('validate error: avId: %s -- %s' % (avId, msg))
  273. return bool
  274. def beginBarrier(self, name, avIds, timeout, callback):
  275. # Begins waiting for a set of avatars. When all avatars in
  276. # the list have reported back in or the callback has expired,
  277. # calls the indicated callback with the list of toons that
  278. # made it through. There may be multiple barriers waiting
  279. # simultaneously on different lists of avatars, although they
  280. # should have different names.
  281. from toontown.ai import ToonBarrier
  282. context = self.__nextBarrierContext
  283. # We assume the context number is passed as a uint16.
  284. self.__nextBarrierContext = (self.__nextBarrierContext + 1) & 0xffff
  285. assert(self.notify.debug('beginBarrier(%s, %s, %s, %s)' % (context, name, avIds, timeout)))
  286. if avIds:
  287. barrier = ToonBarrier.ToonBarrier(
  288. self.uniqueName(name), avIds, timeout,
  289. doneFunc = PythonUtil.Functor(self.__barrierCallback, context, callback))
  290. self.__barriers[context] = barrier
  291. # Send the context number to each involved client.
  292. self.sendUpdate("setBarrierData", [self.__getBarrierData()])
  293. else:
  294. # No avatars; just call the callback immediately.
  295. callback(avIds)
  296. return context
  297. def __getBarrierData(self):
  298. # Returns the barrier data formatted as a blob for sending to
  299. # the clients. This lists all of the current outstanding
  300. # barriers and the avIds waiting for them.
  301. dg = PyDatagram()
  302. for context, barrier in self.__barriers.items():
  303. toons = barrier.pendingToons
  304. if toons:
  305. dg.addUint16(context)
  306. dg.addUint16(len(toons))
  307. for avId in toons:
  308. dg.addUint32(avId)
  309. return dg.getMessage()
  310. def ignoreBarrier(self, context):
  311. # Aborts a previously-set barrier. The context is the return
  312. # value from the previous call to beginBarrier().
  313. barrier = self.__barriers.get(context)
  314. if barrier:
  315. barrier.cleanup()
  316. del self.__barriers[context]
  317. def setBarrierReady(self, context):
  318. # Generated by the clients to check in after a beginBarrier()
  319. # call.
  320. avId = self.air.msgSender
  321. assert(self.notify.debug('setBarrierReady(%s, %s)' % (context, avId)))
  322. barrier = self.__barriers.get(context)
  323. if barrier == None:
  324. # This may be None if a client was slow and missed an
  325. # earlier timeout. Too bad.
  326. return
  327. barrier.clear(avId)
  328. def __barrierCallback(self, context, callback, avIds):
  329. assert(self.notify.debug('barrierCallback(%s, %s)' % (context, avIds)))
  330. # The callback that is generated when a barrier is completed.
  331. barrier = self.__barriers.get(context)
  332. if barrier:
  333. barrier.cleanup()
  334. del self.__barriers[context]
  335. callback(avIds)
  336. else:
  337. self.notify.warning("Unexpected completion from barrier %s" % (context))