"""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