Source code for direct.distributed.DistributedObject

"""DistributedObject module: contains the DistributedObject class"""

from panda3d.core import *
from panda3d.direct import *
from direct.showbase.MessengerGlobal import messenger
from direct.directnotify.DirectNotifyGlobal import directNotify
from direct.distributed.DistributedObjectBase import DistributedObjectBase
#from PyDatagram import PyDatagram
#from PyDatagramIterator import PyDatagramIterator

# Values for DistributedObject.activeState

ESNew          = 1
ESDeleted      = 2
ESDisabling    = 3
ESDisabled     = 4  # values here and lower are considered "disabled"
ESGenerating   = 5  # values here and greater are considered "generated"
ESGenerated    = 6

# update this table if the values above change
ESNum2Str = {
    ESNew: 'ESNew',
    ESDeleted: 'ESDeleted',
    ESDisabling: 'ESDisabling',
    ESDisabled: 'ESDisabled',
    ESGenerating: 'ESGenerating',
    ESGenerated: 'ESGenerated',
    }


[docs]class DistributedObject(DistributedObjectBase): """ The Distributed Object class is the base class for all network based (i.e. distributed) objects. These will usually (always?) have a dclass entry in a \\*.dc file. """ notify = directNotify.newCategory("DistributedObject") # A few objects will set neverDisable to 1... Examples are # localToon, and anything that lives in the UberZone. This # keeps them from being disabled when you change zones, # even to the quiet zone. neverDisable = 0
[docs] def __init__(self, cr): assert self.notify.debugStateCall(self) if not hasattr(self, 'DistributedObject_initialized'): self.DistributedObject_initialized = 1 DistributedObjectBase.__init__(self, cr) # Most DistributedObjects are simple and require no real # effort to load. Some, particularly actors, may take # some significant time to load; these we can optimize by # caching them when they go away instead of necessarily # deleting them. The object should set cacheable to 1 if # it needs to be optimized in this way. self.setCacheable(0) # this is for Toontown only, see toontown.distributed.DelayDeletable self._token2delayDeleteName = {} self._delayDeleteForceAllow = False self._delayDeleted = 0 # Keep track of our state as a distributed object. This # is only trustworthy if the inheriting class properly # calls up the chain for disable() and generate(). self.activeState = ESNew # These are used by getCallbackContext() and doCallbackContext(). self.__nextContext = 0 self.__callbacks = {} # This is used by doneBarrier(). self.__barrierContext = None
if __debug__:
[docs] def status(self, indent=0): """ print out "doId(parentId, zoneId) className and conditionally show generated, disabled, neverDisable, or cachable" """ spaces = ' ' * (indent + 2) try: print("%s%s:" % (' ' * indent, self.__class__.__name__)) flags = [] if self.activeState == ESGenerated: flags.append("generated") if self.activeState < ESGenerating: flags.append("disabled") if self.neverDisable: flags.append("neverDisable") if self.cacheable: flags.append("cacheable") flagStr = "" if len(flags) > 0: flagStr = " (%s)" % (" ".join(flags)) print("%sfrom DistributedObject doId:%s, parent:%s, zone:%s%s" % ( spaces, self.doId, self.parentId, self.zoneId, flagStr)) except Exception as e: print("%serror printing status %s" % (spaces, e))
[docs] def getAutoInterests(self): # returns the sub-zones under this object that are automatically # opened for us by the server. # have we already cached it? def _getAutoInterests(cls): # returns set of auto-interests for this class and all derived # have we already computed this class's autoInterests? if 'autoInterests' in cls.__dict__: autoInterests = cls.autoInterests else: autoInterests = set() # grab autoInterests from base classes for base in cls.__bases__: autoInterests.update(_getAutoInterests(base)) # grab autoInterests from this class if cls.__name__ in self.cr.dclassesByName: dclass = self.cr.dclassesByName[cls.__name__] field = dclass.getFieldByName('AutoInterest') if field is not None: p = DCPacker() p.setUnpackData(field.getDefaultValue()) length = p.rawUnpackUint16() // 4 for i in range(length): zone = int(p.rawUnpackUint32()) autoInterests.add(zone) autoInterests.update(autoInterests) cls.autoInterests = autoInterests return set(autoInterests) autoInterests = _getAutoInterests(self.__class__) # if the server starts supporting multiple auto-interest per class, this check # should be removed if len(autoInterests) > 1: self.notify.error( 'only one auto-interest allowed per DC class, %s has %s autoInterests (%s)' % (self.dclass.getName(), len(autoInterests), list(autoInterests))) _getAutoInterests = None return list(autoInterests)
[docs] def setNeverDisable(self, boolean): assert boolean == 1 or boolean == 0 self.neverDisable = boolean
[docs] def getNeverDisable(self): return self.neverDisable
def _retrieveCachedData(self): # once we know our doId, grab any data that might be stored in the data cache # from the last time we were on the client if self.cr.doDataCache.hasCachedData(self.doId): self._cachedData = self.cr.doDataCache.popCachedData(self.doId)
[docs] def setCachedData(self, name, data): assert isinstance(name, str) # ownership of the data passes to the repository data cache self.cr.doDataCache.setCachedData(self.doId, name, data)
[docs] def hasCachedData(self, name): assert isinstance(name, str) if not hasattr(self, '_cachedData'): return False return name in self._cachedData
[docs] def getCachedData(self, name): assert isinstance(name, str) # ownership of the data passes to the caller of this method data = self._cachedData[name] del self._cachedData[name] return data
[docs] def flushCachedData(self, name): assert isinstance(name, str) # call this to throw out cached data from a previous instantiation self._cachedData[name].flush()
[docs] def setCacheable(self, boolean): assert boolean == 1 or boolean == 0 self.cacheable = boolean
[docs] def getCacheable(self): return self.cacheable
[docs] def deleteOrDelay(self): if len(self._token2delayDeleteName) > 0: if not self._delayDeleted: self._delayDeleted = 1 # Object is delayDeleted. Clean up DistributedObject state, # remove from repository tables, so that we won't crash if # another instance of the same object gets generated while # this instance is still delayDeleted. messenger.send(self.getDelayDeleteEvent()) if len(self._token2delayDeleteName) > 0: self.delayDelete() if len(self._token2delayDeleteName) > 0: self._deactivateDO() else: self.disableAnnounceAndDelete()
[docs] def disableAnnounceAndDelete(self): self.disableAndAnnounce() self.delete() self._destroyDO()
[docs] def getDelayDeleteCount(self): return len(self._token2delayDeleteName)
[docs] def getDelayDeleteEvent(self): return self.uniqueName("delayDelete")
[docs] def getDisableEvent(self): return self.uniqueName("disable")
[docs] def disableAndAnnounce(self): """ Inheritors should *not* redefine this function. """ # We must send the disable announce message *before* we # actually disable the object. That way, the various cleanup # tasks can run first and take care of restoring the object to # a normal, nondisabled state; and *then* the disable function # can properly disable it (for instance, by parenting it to # hidden). if self.activeState != ESDisabled: self.activeState = ESDisabling messenger.send(self.getDisableEvent()) self.disable() self.activeState = ESDisabled if not self._delayDeleted: # if the object is DelayDeleted, _deactivateDO has # already been called self._deactivateDO()
[docs] def announceGenerate(self): """ Sends a message to the world after the object has been generated and all of its required fields filled in. """ assert self.notify.debug('announceGenerate(): %s' % (self.doId))
def _deactivateDO(self): # after this is called, the object is no longer an active DistributedObject # and it may be placed in the cache if not self.cr: # we are going to crash, output the destroyDo stacktrace self.notify.warning('self.cr is none in _deactivateDO %d' % self.doId) if hasattr(self, 'destroyDoStackTrace'): print(self.destroyDoStackTrace) self.__callbacks = {} self.cr.closeAutoInterests(self) self.setLocation(0,0) self.cr.deleteObjectLocation(self, self.parentId, self.zoneId) def _destroyDO(self): # after this is called, the object is no longer a DistributedObject # but may still be used as a DelayDeleted object if __debug__: # StackTrace is omitted in packed versions from direct.showbase.PythonUtil import StackTrace self.destroyDoStackTrace = StackTrace() # check for leftover cached data that was not retrieved or flushed by this object # this will catch typos in the data name in calls to get/setCachedData if hasattr(self, '_cachedData'): for name, cachedData in self._cachedData.items(): self.notify.warning('flushing unretrieved cached data: %s' % name) cachedData.flush() del self._cachedData self.cr = None self.dclass = None
[docs] def disable(self): """ Inheritors should redefine this to take appropriate action on disable """ assert self.notify.debug('disable(): %s' % (self.doId))
[docs] def isDisabled(self): """ Returns true if the object has been disabled and/or deleted, or if it is brand new and hasn't yet been generated. """ return self.activeState < ESGenerating
[docs] def isGenerated(self): """ Returns true if the object has been fully generated by now, and not yet disabled. """ assert self.notify.debugStateCall(self) return self.activeState == ESGenerated
[docs] def delete(self): """ Inheritors should redefine this to take appropriate action on delete """ assert self.notify.debug('delete(): %s' % (self.doId)) self.DistributedObject_deleted = 1
[docs] def generate(self): """ Inheritors should redefine this to take appropriate action on generate """ assert self.notify.debugStateCall(self) self.activeState = ESGenerating # this has already been set at this point #self.cr.storeObjectLocation(self, self.parentId, self.zoneId) # semi-hack: we seem to be calling generate() more than once for objects that multiply-inherit if not hasattr(self, '_autoInterestHandle'): self.cr.openAutoInterests(self)
[docs] def generateInit(self): """ This method is called when the DistributedObject is first introduced to the world... Not when it is pulled from the cache. """ self.activeState = ESGenerating
[docs] def getDoId(self): """ Return the distributed object id """ return self.doId
#This message was moved out of announce generate #to avoid ordering issues.
[docs] def postGenerateMessage(self): if self.activeState != ESGenerated: self.activeState = ESGenerated messenger.send(self.uniqueName("generate"), [self])
[docs] def updateRequiredFields(self, dclass, di): dclass.receiveUpdateBroadcastRequired(self, di) self.announceGenerate() self.postGenerateMessage()
[docs] def updateAllRequiredFields(self, dclass, di): dclass.receiveUpdateAllRequired(self, di) self.announceGenerate() self.postGenerateMessage()
[docs] def updateRequiredOtherFields(self, dclass, di): # First, update the required fields dclass.receiveUpdateBroadcastRequired(self, di) # Announce generate after updating all the required fields, # but before we update the non-required fields. self.announceGenerate() self.postGenerateMessage() dclass.receiveUpdateOther(self, di)
[docs] def sendUpdate(self, fieldName, args = [], sendToId = None): if self.cr: dg = self.dclass.clientFormatUpdate( fieldName, sendToId or self.doId, args) self.cr.send(dg) else: assert self.notify.error("sendUpdate failed, because self.cr is not set")
[docs] def sendDisableMsg(self): self.cr.sendDisableMsg(self.doId)
[docs] def sendDeleteMsg(self): self.cr.sendDeleteMsg(self.doId)
[docs] def taskName(self, taskString): return "%s-%s" % (taskString, self.doId)
[docs] def uniqueName(self, idString): return "%s-%s" % (idString, self.doId)
[docs] def getCallbackContext(self, callback, extraArgs = []): # Some objects implement a back-and-forth handshake operation # with the AI via an arbitrary context number. This method # (coupled with doCallbackContext(), below) maps a Python # callback onto that context number so that client code may # easily call the method and wait for a callback, rather than # having to negotiate context numbers. # This method generates a new context number and stores the # callback so that it may later be called when the response is # returned. # This is intended to be called within derivations of # DistributedObject, not directly by other objects. context = self.__nextContext self.__callbacks[context] = (callback, extraArgs) # We assume the context number is passed as a uint16. self.__nextContext = (self.__nextContext + 1) & 0xffff return context
[docs] def getCurrentContexts(self): # Returns a list of the currently outstanding contexts created # by getCallbackContext(). return list(self.__callbacks.keys())
[docs] def getCallback(self, context): # Returns the callback that was passed in to the previous # call to getCallbackContext. return self.__callbacks[context][0]
[docs] def getCallbackArgs(self, context): # Returns the extraArgs that were passed in to the previous # call to getCallbackContext. return self.__callbacks[context][1]
[docs] def doCallbackContext(self, context, args): # This is called after the AI has responded to the message # sent via getCallbackContext(), above. The context number is # looked up in the table and the associated callback is # issued. # This is intended to be called within derivations of # DistributedObject, not directly by other objects. tuple = self.__callbacks.get(context) if tuple: callback, extraArgs = tuple completeArgs = args + extraArgs if callback is not None: callback(*completeArgs) del self.__callbacks[context] else: self.notify.warning("Got unexpected context from AI: %s" % (context))
[docs] def setBarrierData(self, data): # This message is sent by the AI to tell us the barriers and # avIds for which the AI is currently waiting. The client # needs to look up its pending context in the table (and # ignore the other contexts). When the client is done # handling whatever it should handle in its current state, it # should call doneBarrier(), which will send the context # number back to the AI. for context, name, avIds in data: for avId in avIds: if self.cr.isLocalId(avId): # We found the local avatar's id; stop here. self.__barrierContext = (context, name) assert self.notify.debug('setBarrierData(%s, %s)' % (context, name)) return # This barrier didn't involve this client; ignore it. assert self.notify.debug('setBarrierData(%s)' % (None)) self.__barrierContext = None
[docs] def getBarrierData(self): # Return a trivially-empty (context, name, avIds) value. return ((0, '', []),)
[docs] def doneBarrier(self, name = None): # Tells the AI we have finished handling our task. If the # optional name parameter is specified, it must match the # barrier name specified on the AI, or the barrier is ignored. # This is used to ensure we are not clearing the wrong # barrier. # If this is None, it either means we have called # doneBarrier() twice, or we have not received a barrier # context from the AI. I think in either case it's ok to # silently ignore the error. if self.__barrierContext is not None: context, aiName = self.__barrierContext if name is None or name == aiName: assert self.notify.debug('doneBarrier(%s, %s)' % (context, aiName)) self.sendUpdate("setBarrierReady", [context]) self.__barrierContext = None else: assert self.notify.debug('doneBarrier(%s) ignored; current barrier is %s' % (name, aiName)) else: assert self.notify.debug('doneBarrier(%s) ignored; no active barrier.' % (name))
[docs] def addInterest(self, zoneId, note="", event=None): return self.cr.addInterest(self.getDoId(), zoneId, note, event)
[docs] def removeInterest(self, handle, event=None): return self.cr.removeInterest(handle, event)
[docs] def b_setLocation(self, parentId, zoneId): self.d_setLocation(parentId, zoneId) self.setLocation(parentId, zoneId)
[docs] def d_setLocation(self, parentId, zoneId): self.cr.sendSetLocation(self.doId, parentId, zoneId)
[docs] def setLocation(self, parentId, zoneId): self.cr.storeObjectLocation(self, parentId, zoneId)
[docs] def getLocation(self): try: if self.parentId == 0 and self.zoneId == 0: return None # This is a -1 stuffed into a uint32 if self.parentId == 0xffffffff and self.zoneId == 0xffffffff: return None return (self.parentId, self.zoneId) except AttributeError: return None
[docs] def getParentObj(self): if self.parentId is None: return None return self.cr.doId2do.get(self.parentId)
[docs] def isLocal(self): # This returns true if the distributed object is "local," # which means the client created it instead of the AI, and it # gets some other special handling. Normally, only the local # avatar class overrides this to return true. return self.cr and self.cr.isLocalId(self.doId)
[docs] def isGridParent(self): # If this distributed object is a DistributedGrid return 1. 0 by default return 0
[docs] def execCommand(self, string, mwMgrId, avId, zoneId): pass