DistributedObject.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. """DistributedObject module: contains the DistributedObject class"""
  2. from panda3d.core import *
  3. from panda3d.direct import *
  4. from direct.directnotify.DirectNotifyGlobal import directNotify
  5. from direct.distributed.DistributedObjectBase import DistributedObjectBase
  6. #from PyDatagram import PyDatagram
  7. #from PyDatagramIterator import PyDatagramIterator
  8. # Values for DistributedObject.activeState
  9. ESNew = 1
  10. ESDeleted = 2
  11. ESDisabling = 3
  12. ESDisabled = 4 # values here and lower are considered "disabled"
  13. ESGenerating = 5 # values here and greater are considered "generated"
  14. ESGenerated = 6
  15. # update this table if the values above change
  16. ESNum2Str = {
  17. ESNew: 'ESNew',
  18. ESDeleted: 'ESDeleted',
  19. ESDisabling: 'ESDisabling',
  20. ESDisabled: 'ESDisabled',
  21. ESGenerating: 'ESGenerating',
  22. ESGenerated: 'ESGenerated',
  23. }
  24. class DistributedObject(DistributedObjectBase):
  25. """
  26. The Distributed Object class is the base class for all network based
  27. (i.e. distributed) objects. These will usually (always?) have a
  28. dclass entry in a *.dc file.
  29. """
  30. notify = directNotify.newCategory("DistributedObject")
  31. # A few objects will set neverDisable to 1... Examples are
  32. # localToon, and anything that lives in the UberZone. This
  33. # keeps them from being disabled when you change zones,
  34. # even to the quiet zone.
  35. neverDisable = 0
  36. def __init__(self, cr):
  37. assert self.notify.debugStateCall(self)
  38. try:
  39. self.DistributedObject_initialized
  40. except:
  41. self.DistributedObject_initialized = 1
  42. DistributedObjectBase.__init__(self, cr)
  43. # Most DistributedObjects are simple and require no real
  44. # effort to load. Some, particularly actors, may take
  45. # some significant time to load; these we can optimize by
  46. # caching them when they go away instead of necessarily
  47. # deleting them. The object should set cacheable to 1 if
  48. # it needs to be optimized in this way.
  49. self.setCacheable(0)
  50. # this is for Toontown only, see toontown.distributed.DelayDeletable
  51. self._token2delayDeleteName = {}
  52. self._delayDeleteForceAllow = False
  53. self._delayDeleted = 0
  54. # Keep track of our state as a distributed object. This
  55. # is only trustworthy if the inheriting class properly
  56. # calls up the chain for disable() and generate().
  57. self.activeState = ESNew
  58. # These are used by getCallbackContext() and doCallbackContext().
  59. self.__nextContext = 0
  60. self.__callbacks = {}
  61. # This is used by doneBarrier().
  62. self.__barrierContext = None
  63. if __debug__:
  64. def status(self, indent=0):
  65. """
  66. print out "doId(parentId, zoneId) className
  67. and conditionally show generated, disabled, neverDisable,
  68. or cachable"
  69. """
  70. spaces = ' ' * (indent + 2)
  71. try:
  72. print("%s%s:" % (' ' * indent, self.__class__.__name__))
  73. flags = []
  74. if self.activeState == ESGenerated:
  75. flags.append("generated")
  76. if self.activeState < ESGenerating:
  77. flags.append("disabled")
  78. if self.neverDisable:
  79. flags.append("neverDisable")
  80. if self.cacheable:
  81. flags.append("cacheable")
  82. flagStr = ""
  83. if len(flags):
  84. flagStr = " (%s)" % (" ".join(flags))
  85. print("%sfrom DistributedObject doId:%s, parent:%s, zone:%s%s" % (
  86. spaces, self.doId, self.parentId, self.zoneId, flagStr))
  87. except Exception as e:
  88. print("%serror printing status %s" % (spaces, e))
  89. def getAutoInterests(self):
  90. # returns the sub-zones under this object that are automatically
  91. # opened for us by the server.
  92. # have we already cached it?
  93. def _getAutoInterests(cls):
  94. # returns set of auto-interests for this class and all derived
  95. # have we already computed this class's autoInterests?
  96. if 'autoInterests' in cls.__dict__:
  97. autoInterests = cls.autoInterests
  98. else:
  99. autoInterests = set()
  100. # grab autoInterests from base classes
  101. for base in cls.__bases__:
  102. autoInterests.update(_getAutoInterests(base))
  103. # grab autoInterests from this class
  104. if cls.__name__ in self.cr.dclassesByName:
  105. dclass = self.cr.dclassesByName[cls.__name__]
  106. field = dclass.getFieldByName('AutoInterest')
  107. if field is not None:
  108. p = DCPacker()
  109. p.setUnpackData(field.getDefaultValue())
  110. len = p.rawUnpackUint16()/4
  111. for i in range(len):
  112. zone = int(p.rawUnpackUint32())
  113. autoInterests.add(zone)
  114. autoInterests.update(autoInterests)
  115. cls.autoInterests = autoInterests
  116. return set(autoInterests)
  117. autoInterests = _getAutoInterests(self.__class__)
  118. # if the server starts supporting multiple auto-interest per class, this check
  119. # should be removed
  120. if len(autoInterests) > 1:
  121. self.notify.error(
  122. 'only one auto-interest allowed per DC class, %s has %s autoInterests (%s)' %
  123. (self.dclass.getName(), len(autoInterests), list(autoInterests)))
  124. _getAutoInterests = None
  125. return list(autoInterests)
  126. def setNeverDisable(self, bool):
  127. assert bool == 1 or bool == 0
  128. self.neverDisable = bool
  129. def getNeverDisable(self):
  130. return self.neverDisable
  131. def _retrieveCachedData(self):
  132. # once we know our doId, grab any data that might be stored in the data cache
  133. # from the last time we were on the client
  134. if self.cr.doDataCache.hasCachedData(self.doId):
  135. self._cachedData = self.cr.doDataCache.popCachedData(self.doId)
  136. def setCachedData(self, name, data):
  137. assert type(name) == type('')
  138. # ownership of the data passes to the repository data cache
  139. self.cr.doDataCache.setCachedData(self.doId, name, data)
  140. def hasCachedData(self, name):
  141. assert type(name) == type('')
  142. if not hasattr(self, '_cachedData'):
  143. return False
  144. return name in self._cachedData
  145. def getCachedData(self, name):
  146. assert type(name) == type('')
  147. # ownership of the data passes to the caller of this method
  148. data = self._cachedData[name]
  149. del self._cachedData[name]
  150. return data
  151. def flushCachedData(self, name):
  152. assert type(name) == type('')
  153. # call this to throw out cached data from a previous instantiation
  154. self._cachedData[name].flush()
  155. def setCacheable(self, bool):
  156. assert bool == 1 or bool == 0
  157. self.cacheable = bool
  158. def getCacheable(self):
  159. return self.cacheable
  160. def deleteOrDelay(self):
  161. if len(self._token2delayDeleteName) > 0:
  162. if not self._delayDeleted:
  163. self._delayDeleted = 1
  164. # Object is delayDeleted. Clean up DistributedObject state,
  165. # remove from repository tables, so that we won't crash if
  166. # another instance of the same object gets generated while
  167. # this instance is still delayDeleted.
  168. messenger.send(self.getDelayDeleteEvent())
  169. if len(self._token2delayDeleteName) > 0:
  170. self.delayDelete()
  171. if len(self._token2delayDeleteName) > 0:
  172. self._deactivateDO()
  173. else:
  174. self.disableAnnounceAndDelete()
  175. def disableAnnounceAndDelete(self):
  176. self.disableAndAnnounce()
  177. self.delete()
  178. self._destroyDO()
  179. def getDelayDeleteCount(self):
  180. return len(self._token2delayDeleteName)
  181. def getDelayDeleteEvent(self):
  182. return self.uniqueName("delayDelete")
  183. def getDisableEvent(self):
  184. return self.uniqueName("disable")
  185. def disableAndAnnounce(self):
  186. """
  187. Inheritors should *not* redefine this function.
  188. """
  189. # We must send the disable announce message *before* we
  190. # actually disable the object. That way, the various cleanup
  191. # tasks can run first and take care of restoring the object to
  192. # a normal, nondisabled state; and *then* the disable function
  193. # can properly disable it (for instance, by parenting it to
  194. # hidden).
  195. if self.activeState != ESDisabled:
  196. self.activeState = ESDisabling
  197. messenger.send(self.getDisableEvent())
  198. self.disable()
  199. self.activeState = ESDisabled
  200. if not self._delayDeleted:
  201. # if the object is DelayDeleted, _deactivateDO has
  202. # already been called
  203. self._deactivateDO()
  204. def announceGenerate(self):
  205. """
  206. Sends a message to the world after the object has been
  207. generated and all of its required fields filled in.
  208. """
  209. assert self.notify.debug('announceGenerate(): %s' % (self.doId))
  210. def _deactivateDO(self):
  211. # after this is called, the object is no longer an active DistributedObject
  212. # and it may be placed in the cache
  213. if not self.cr:
  214. # we are going to crash, output the destroyDo stacktrace
  215. self.notify.warning('self.cr is none in _deactivateDO %d' % self.doId)
  216. if hasattr(self, 'destroyDoStackTrace'):
  217. print(self.destroyDoStackTrace)
  218. self.__callbacks = {}
  219. self.cr.closeAutoInterests(self)
  220. self.setLocation(0,0)
  221. self.cr.deleteObjectLocation(self, self.parentId, self.zoneId)
  222. def _destroyDO(self):
  223. # after this is called, the object is no longer a DistributedObject
  224. # but may still be used as a DelayDeleted object
  225. if __debug__:
  226. # StackTrace is omitted in packed versions
  227. from direct.showbase.PythonUtil import StackTrace
  228. self.destroyDoStackTrace = StackTrace()
  229. # check for leftover cached data that was not retrieved or flushed by this object
  230. # this will catch typos in the data name in calls to get/setCachedData
  231. if hasattr(self, '_cachedData'):
  232. for name, cachedData in self._cachedData.items():
  233. self.notify.warning('flushing unretrieved cached data: %s' % name)
  234. cachedData.flush()
  235. del self._cachedData
  236. self.cr = None
  237. self.dclass = None
  238. def disable(self):
  239. """
  240. Inheritors should redefine this to take appropriate action on disable
  241. """
  242. assert self.notify.debug('disable(): %s' % (self.doId))
  243. pass
  244. def isDisabled(self):
  245. """
  246. Returns true if the object has been disabled and/or deleted,
  247. or if it is brand new and hasn't yet been generated.
  248. """
  249. return (self.activeState < ESGenerating)
  250. def isGenerated(self):
  251. """
  252. Returns true if the object has been fully generated by now,
  253. and not yet disabled.
  254. """
  255. assert self.notify.debugStateCall(self)
  256. return (self.activeState == ESGenerated)
  257. def delete(self):
  258. """
  259. Inheritors should redefine this to take appropriate action on delete
  260. """
  261. assert self.notify.debug('delete(): %s' % (self.doId))
  262. try:
  263. self.DistributedObject_deleted
  264. except:
  265. self.DistributedObject_deleted = 1
  266. def generate(self):
  267. """
  268. Inheritors should redefine this to take appropriate action on generate
  269. """
  270. assert self.notify.debugStateCall(self)
  271. self.activeState = ESGenerating
  272. # this has already been set at this point
  273. #self.cr.storeObjectLocation(self, self.parentId, self.zoneId)
  274. # semi-hack: we seem to be calling generate() more than once for objects that multiply-inherit
  275. if not hasattr(self, '_autoInterestHandle'):
  276. self.cr.openAutoInterests(self)
  277. def generateInit(self):
  278. """
  279. This method is called when the DistributedObject is first introduced
  280. to the world... Not when it is pulled from the cache.
  281. """
  282. self.activeState = ESGenerating
  283. def getDoId(self):
  284. """
  285. Return the distributed object id
  286. """
  287. return self.doId
  288. #This message was moved out of announce generate
  289. #to avoid ordering issues.
  290. def postGenerateMessage(self):
  291. if self.activeState != ESGenerated:
  292. self.activeState = ESGenerated
  293. messenger.send(self.uniqueName("generate"), [self])
  294. def updateRequiredFields(self, dclass, di):
  295. dclass.receiveUpdateBroadcastRequired(self, di)
  296. self.announceGenerate()
  297. self.postGenerateMessage()
  298. def updateAllRequiredFields(self, dclass, di):
  299. dclass.receiveUpdateAllRequired(self, di)
  300. self.announceGenerate()
  301. self.postGenerateMessage()
  302. def updateRequiredOtherFields(self, dclass, di):
  303. # First, update the required fields
  304. dclass.receiveUpdateBroadcastRequired(self, di)
  305. # Announce generate after updating all the required fields,
  306. # but before we update the non-required fields.
  307. self.announceGenerate()
  308. self.postGenerateMessage()
  309. dclass.receiveUpdateOther(self, di)
  310. def sendUpdate(self, fieldName, args = [], sendToId = None):
  311. if self.cr:
  312. dg = self.dclass.clientFormatUpdate(
  313. fieldName, sendToId or self.doId, args)
  314. self.cr.send(dg)
  315. else:
  316. assert self.notify.error("sendUpdate failed, because self.cr is not set")
  317. def sendDisableMsg(self):
  318. self.cr.sendDisableMsg(self.doId)
  319. def sendDeleteMsg(self):
  320. self.cr.sendDeleteMsg(self.doId)
  321. def taskName(self, taskString):
  322. return ("%s-%s" % (taskString, self.doId))
  323. def uniqueName(self, idString):
  324. return ("%s-%s" % (idString, self.doId))
  325. def getCallbackContext(self, callback, extraArgs = []):
  326. # Some objects implement a back-and-forth handshake operation
  327. # with the AI via an arbitrary context number. This method
  328. # (coupled with doCallbackContext(), below) maps a Python
  329. # callback onto that context number so that client code may
  330. # easily call the method and wait for a callback, rather than
  331. # having to negotiate context numbers.
  332. # This method generates a new context number and stores the
  333. # callback so that it may later be called when the response is
  334. # returned.
  335. # This is intended to be called within derivations of
  336. # DistributedObject, not directly by other objects.
  337. context = self.__nextContext
  338. self.__callbacks[context] = (callback, extraArgs)
  339. # We assume the context number is passed as a uint16.
  340. self.__nextContext = (self.__nextContext + 1) & 0xffff
  341. return context
  342. def getCurrentContexts(self):
  343. # Returns a list of the currently outstanding contexts created
  344. # by getCallbackContext().
  345. return list(self.__callbacks.keys())
  346. def getCallback(self, context):
  347. # Returns the callback that was passed in to the previous
  348. # call to getCallbackContext.
  349. return self.__callbacks[context][0]
  350. def getCallbackArgs(self, context):
  351. # Returns the extraArgs that were passed in to the previous
  352. # call to getCallbackContext.
  353. return self.__callbacks[context][1]
  354. def doCallbackContext(self, context, args):
  355. # This is called after the AI has responded to the message
  356. # sent via getCallbackContext(), above. The context number is
  357. # looked up in the table and the associated callback is
  358. # issued.
  359. # This is intended to be called within derivations of
  360. # DistributedObject, not directly by other objects.
  361. tuple = self.__callbacks.get(context)
  362. if tuple:
  363. callback, extraArgs = tuple
  364. completeArgs = args + extraArgs
  365. if callback != None:
  366. callback(*completeArgs)
  367. del self.__callbacks[context]
  368. else:
  369. self.notify.warning("Got unexpected context from AI: %s" % (context))
  370. def setBarrierData(self, data):
  371. # This message is sent by the AI to tell us the barriers and
  372. # avIds for which the AI is currently waiting. The client
  373. # needs to look up its pending context in the table (and
  374. # ignore the other contexts). When the client is done
  375. # handling whatever it should handle in its current state, it
  376. # should call doneBarrier(), which will send the context
  377. # number back to the AI.
  378. for context, name, avIds in data:
  379. for avId in avIds:
  380. if self.cr.isLocalId(avId):
  381. # We found the local avatar's id; stop here.
  382. self.__barrierContext = (context, name)
  383. assert self.notify.debug('setBarrierData(%s, %s)' % (context, name))
  384. return
  385. # This barrier didn't involve this client; ignore it.
  386. assert self.notify.debug('setBarrierData(%s)' % (None))
  387. self.__barrierContext = None
  388. def getBarrierData(self):
  389. # Return a trivially-empty (context, name, avIds) value.
  390. return ((0, '', []),)
  391. def doneBarrier(self, name = None):
  392. # Tells the AI we have finished handling our task. If the
  393. # optional name parameter is specified, it must match the
  394. # barrier name specified on the AI, or the barrier is ignored.
  395. # This is used to ensure we are not clearing the wrong
  396. # barrier.
  397. # If this is None, it either means we have called
  398. # doneBarrier() twice, or we have not received a barrier
  399. # context from the AI. I think in either case it's ok to
  400. # silently ignore the error.
  401. if self.__barrierContext != None:
  402. context, aiName = self.__barrierContext
  403. if name == None or name == aiName:
  404. assert self.notify.debug('doneBarrier(%s, %s)' % (context, aiName))
  405. self.sendUpdate("setBarrierReady", [context])
  406. self.__barrierContext = None
  407. else:
  408. assert self.notify.debug('doneBarrier(%s) ignored; current barrier is %s' % (name, aiName))
  409. else:
  410. assert self.notify.debug('doneBarrier(%s) ignored; no active barrier.' % (name))
  411. def addInterest(self, zoneId, note="", event=None):
  412. return self.cr.addInterest(self.getDoId(), zoneId, note, event)
  413. def removeInterest(self, handle, event=None):
  414. return self.cr.removeInterest(handle, event)
  415. def b_setLocation(self, parentId, zoneId):
  416. self.d_setLocation(parentId, zoneId)
  417. self.setLocation(parentId, zoneId)
  418. def d_setLocation(self, parentId, zoneId):
  419. self.cr.sendSetLocation(self.doId, parentId, zoneId)
  420. def setLocation(self, parentId, zoneId):
  421. self.cr.storeObjectLocation(self, parentId, zoneId)
  422. def getLocation(self):
  423. try:
  424. if self.parentId == 0 and self.zoneId == 0:
  425. return None
  426. # This is a -1 stuffed into a uint32
  427. if self.parentId == 0xffffffff and self.zoneId == 0xffffffff:
  428. return None
  429. return (self.parentId, self.zoneId)
  430. except AttributeError:
  431. return None
  432. def getParentObj(self):
  433. if self.parentId is None:
  434. return None
  435. return self.cr.doId2do.get(self.parentId)
  436. def isLocal(self):
  437. # This returns true if the distributed object is "local,"
  438. # which means the client created it instead of the AI, and it
  439. # gets some other special handling. Normally, only the local
  440. # avatar class overrides this to return true.
  441. return self.cr and self.cr.isLocalId(self.doId)
  442. def isGridParent(self):
  443. # If this distributed object is a DistributedGrid return 1. 0 by default
  444. return 0
  445. def execCommand(self, string, mwMgrId, avId, zoneId):
  446. pass