mirror of
https://github.com/Sneed-Group/Poodletooth-iLand
synced 2024-12-26 05:02:31 -06:00
523 lines
20 KiB
Python
523 lines
20 KiB
Python
|
"""DistributedObject module: contains the DistributedObject class"""
|
||
|
|
||
|
from pandac.PandaModules import *
|
||
|
from direct.directnotify.DirectNotifyGlobal import directNotify
|
||
|
from direct.distributed.DistributedObjectBase import DistributedObjectBase
|
||
|
from direct.showbase.PythonUtil import StackTrace
|
||
|
#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',
|
||
|
}
|
||
|
|
||
|
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
|
||
|
|
||
|
def __init__(self, cr):
|
||
|
assert self.notify.debugStateCall(self)
|
||
|
try:
|
||
|
self.DistributedObject_initialized
|
||
|
except:
|
||
|
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__:
|
||
|
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__)
|
||
|
print "%sfrom DistributedObject doId:%s, parent:%s, zone:%s"%(
|
||
|
spaces,
|
||
|
self.doId, self.parentId, self.zoneId),
|
||
|
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")
|
||
|
if len(flags):
|
||
|
print "(%s)"%(" ".join(flags),),
|
||
|
print
|
||
|
except Exception, e: print "%serror printing status"%(spaces,), e
|
||
|
|
||
|
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())
|
||
|
len = p.rawUnpackUint16()/4
|
||
|
for i in xrange(len):
|
||
|
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)
|
||
|
|
||
|
def setNeverDisable(self, bool):
|
||
|
assert bool == 1 or bool == 0
|
||
|
self.neverDisable = bool
|
||
|
|
||
|
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)
|
||
|
|
||
|
def setCachedData(self, name, data):
|
||
|
assert type(name) == type('')
|
||
|
# ownership of the data passes to the repository data cache
|
||
|
self.cr.doDataCache.setCachedData(self.doId, name, data)
|
||
|
|
||
|
def hasCachedData(self, name):
|
||
|
assert type(name) == type('')
|
||
|
if not hasattr(self, '_cachedData'):
|
||
|
return False
|
||
|
return name in self._cachedData
|
||
|
|
||
|
def getCachedData(self, name):
|
||
|
assert type(name) == type('')
|
||
|
# ownership of the data passes to the caller of this method
|
||
|
data = self._cachedData[name]
|
||
|
del self._cachedData[name]
|
||
|
return data
|
||
|
|
||
|
def flushCachedData(self, name):
|
||
|
assert type(name) == type('')
|
||
|
# call this to throw out cached data from a previous instantiation
|
||
|
self._cachedData[name].flush()
|
||
|
|
||
|
def setCacheable(self, bool):
|
||
|
assert bool == 1 or bool == 0
|
||
|
self.cacheable = bool
|
||
|
|
||
|
def getCacheable(self):
|
||
|
return self.cacheable
|
||
|
|
||
|
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()
|
||
|
|
||
|
def disableAnnounceAndDelete(self):
|
||
|
self.disableAndAnnounce()
|
||
|
self.delete()
|
||
|
self._destroyDO()
|
||
|
|
||
|
def getDelayDeleteCount(self):
|
||
|
return len(self._token2delayDeleteName)
|
||
|
|
||
|
def getDelayDeleteEvent(self):
|
||
|
return self.uniqueName("delayDelete")
|
||
|
|
||
|
def getDisableEvent(self):
|
||
|
return self.uniqueName("disable")
|
||
|
|
||
|
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()
|
||
|
|
||
|
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
|
||
|
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.iteritems():
|
||
|
self.notify.warning('flushing unretrieved cached data: %s' % name)
|
||
|
cachedData.flush()
|
||
|
del self._cachedData
|
||
|
self.cr = None
|
||
|
self.dclass = None
|
||
|
|
||
|
def disable(self):
|
||
|
"""
|
||
|
Inheritors should redefine this to take appropriate action on disable
|
||
|
"""
|
||
|
assert self.notify.debug('disable(): %s' % (self.doId))
|
||
|
pass
|
||
|
|
||
|
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)
|
||
|
|
||
|
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)
|
||
|
|
||
|
def delete(self):
|
||
|
"""
|
||
|
Inheritors should redefine this to take appropriate action on delete
|
||
|
"""
|
||
|
assert self.notify.debug('delete(): %s' % (self.doId))
|
||
|
try:
|
||
|
self.DistributedObject_deleted
|
||
|
except:
|
||
|
self.DistributedObject_deleted = 1
|
||
|
|
||
|
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)
|
||
|
|
||
|
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
|
||
|
|
||
|
def getDoId(self):
|
||
|
"""
|
||
|
Return the distributed object id
|
||
|
"""
|
||
|
return self.doId
|
||
|
|
||
|
|
||
|
#This message was moved out of announce generate
|
||
|
#to avoid ordering issues.
|
||
|
def postGenerateMessage(self):
|
||
|
if self.activeState != ESGenerated:
|
||
|
self.activeState = ESGenerated
|
||
|
messenger.send(self.uniqueName("generate"), [self])
|
||
|
|
||
|
def updateRequiredFields(self, dclass, di):
|
||
|
dclass.receiveUpdateBroadcastRequired(self, di)
|
||
|
self.announceGenerate()
|
||
|
self.postGenerateMessage()
|
||
|
|
||
|
def updateAllRequiredFields(self, dclass, di):
|
||
|
dclass.receiveUpdateAllRequired(self, di)
|
||
|
self.announceGenerate()
|
||
|
self.postGenerateMessage()
|
||
|
|
||
|
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)
|
||
|
|
||
|
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")
|
||
|
|
||
|
def sendDisableMsg(self):
|
||
|
self.cr.sendDisableMsg(self.doId)
|
||
|
|
||
|
def sendDeleteMsg(self):
|
||
|
self.cr.sendDeleteMsg(self.doId)
|
||
|
|
||
|
def taskName(self, taskString):
|
||
|
return ("%s-%s" % (taskString, self.doId))
|
||
|
|
||
|
def uniqueName(self, idString):
|
||
|
return ("%s-%s" % (idString, self.doId))
|
||
|
|
||
|
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
|
||
|
|
||
|
def getCurrentContexts(self):
|
||
|
# Returns a list of the currently outstanding contexts created
|
||
|
# by getCallbackContext().
|
||
|
return self.__callbacks.keys()
|
||
|
|
||
|
def getCallback(self, context):
|
||
|
# Returns the callback that was passed in to the previous
|
||
|
# call to getCallbackContext.
|
||
|
return self.__callbacks[context][0]
|
||
|
|
||
|
def getCallbackArgs(self, context):
|
||
|
# Returns the extraArgs that were passed in to the previous
|
||
|
# call to getCallbackContext.
|
||
|
return self.__callbacks[context][1]
|
||
|
|
||
|
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 != None:
|
||
|
callback(*completeArgs)
|
||
|
del self.__callbacks[context]
|
||
|
else:
|
||
|
self.notify.warning("Got unexpected context from AI: %s" % (context))
|
||
|
|
||
|
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
|
||
|
|
||
|
def getBarrierData(self):
|
||
|
# Return a trivially-empty (context, name, avIds) value.
|
||
|
return ((0, '', []),)
|
||
|
|
||
|
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 != None:
|
||
|
context, aiName = self.__barrierContext
|
||
|
if name == 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))
|
||
|
|
||
|
def addInterest(self, zoneId, note="", event=None):
|
||
|
return self.cr.addInterest(self.getDoId(), zoneId, note, event)
|
||
|
|
||
|
def removeInterest(self, handle, event=None):
|
||
|
return self.cr.removeInterest(handle, event)
|
||
|
|
||
|
def b_setLocation(self, parentId, zoneId):
|
||
|
self.d_setLocation(parentId, zoneId)
|
||
|
self.setLocation(parentId, zoneId)
|
||
|
|
||
|
def d_setLocation(self, parentId, zoneId):
|
||
|
self.cr.sendSetLocation(self.doId, parentId, zoneId)
|
||
|
|
||
|
def setLocation(self, parentId, zoneId):
|
||
|
self.cr.storeObjectLocation(self, parentId, zoneId)
|
||
|
|
||
|
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
|
||
|
|
||
|
def getParentObj(self):
|
||
|
if self.parentId is None:
|
||
|
return None
|
||
|
return self.cr.doId2do.get(self.parentId)
|
||
|
|
||
|
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)
|
||
|
|
||
|
def isGridParent(self):
|
||
|
# If this distributed object is a DistributedGrid return 1. 0 by default
|
||
|
return 0
|
||
|
|
||
|
def execCommand(self, string, mwMgrId, avId, zoneId):
|
||
|
pass
|