Browse Source

zones are now entities, and lots of other tweaks

Darren Ranalli 22 years ago
parent
commit
6e1f293140

+ 47 - 16
direct/src/level/BasicEntities.py

@@ -4,7 +4,36 @@ import Entity
 import DistributedEntity
 import NodePath
 
-# this is an internal class, do not instantiate.
+# this is an abstract class, do not instantiate.
+class NodePathAttribs:
+    """Derive from this class to give an entity the behavior of a
+    NodePath, without necessarily deriving from NodePath. Derived class
+    must implement getNodePath()."""
+    def initNodePathAttribs(self, doReparent=1):
+        """Call this after the entity has been initialized"""
+        self.callSetters('pos','x','y','z',
+                         'hpr','h','p','r',
+                         'scale','sx','sy','sz')
+        if doReparent and hasattr(self, 'parent'):
+            self.level.requestReparent(self.getNodePath(), self.parent)
+
+    def reparentTo(self, *args): self.getNodePath().reparentTo(*args)
+
+    def setPos(self, *args): self.getNodePath().setPos(*args)
+    def setX(self, *args): self.getNodePath().setX(*args)
+    def setY(self, *args): self.getNodePath().setY(*args)
+    def setZ(self, *args): self.getNodePath().setZ(*args)
+
+    def setHpr(self, *args): self.getNodePath().setHpr(*args)
+    def setH(self, *args): self.getNodePath().setH(*args)
+    def setP(self, *args): self.getNodePath().setP(*args)
+    def setR(self, *args): self.getNodePath().setR(*args)
+
+    def setScale(self, *args): self.getNodePath().setScale(*args)
+    def setSx(self, *args): self.getNodePath().setSx(*args)
+    def setSy(self, *args): self.getNodePath().setSy(*args)
+    def setSz(self, *args): self.getNodePath().setSz(*args)
+    
 class privNodePathImpl(NodePath.NodePath):
     def __init__(self, name):
         node = hidden.attachNewNode(name)
@@ -15,33 +44,32 @@ class privNodePathImpl(NodePath.NodePath):
         of its attributes have been set"""
         self.callSetters('pos','x','y','z',
                          'hpr','h','p','r',
-                         'scale','sx','sx','sz')
-
-        if hasattr(self, 'parent'):
-            self.level.requestReparent(self, self.parent)
-        else:
-            self.level.requestReparent(self, 'zone')
+                         'scale','sx','sy','sz')
+        self.level.requestReparent(self, self.parent)
         
     def destroy(self):
         self.removeNode()
 
+    def getNodePath(self):
+        return self
+
 class NodePathEntity(Entity.Entity, privNodePathImpl):
     """This is an entity that represents a NodePath on the client.
     It may be instantiated directly or used as a base class for other
     entity types."""
     def __init__(self, level, entId):
+        privNodePathImpl.__init__(self, '')
         Entity.Entity.__init__(self, level, entId)
-        privNodePathImpl.__init__(self, str(self))
-        self.initializeEntity()
-
-    def initializeEntity(self):
-        Entity.Entity.initializeEntity(self)
+        self.setName(str(self))
         privNodePathImpl.initNodePathAttribs(self)
 
     def destroy(self):
         Entity.Entity.destroy(self)
         privNodePathImpl.destroy(self)
 
+    def getNodePath(self):
+        return self
+
 class DistributedNodePathEntity(DistributedEntity.DistributedEntity,
                                 privNodePathImpl):
     """This is a distributed version of NodePathEntity. It should not
@@ -50,11 +78,14 @@ class DistributedNodePathEntity(DistributedEntity.DistributedEntity,
     def __init__(self, cr):
         DistributedEntity.DistributedEntity.__init__(self, cr)
         privNodePathImpl.__init__(self, 'DistributedNodePathEntity')
-        
-    def initializeEntity(self):
-        DistributedEntity.DistributedEntity.initializeEntity(self)
-        privNodePathImpl.initNodePathAttribs(self)
 
+    def announceGenerate(self):
+        DistributedEntity.DistributedEntity.announceGenerate(self)
+        privNodePathImpl.initNodePathAttribs(self)
+        
     def destroy(self):
         DistributedEntity.DistributedEntity.destroy(self)
         privNodePathImpl.destroy(self)
+
+    def getNodePath(self):
+        return self

+ 0 - 2
direct/src/level/DistributedEntity.py

@@ -32,8 +32,6 @@ class DistributedEntity(DistributedObject.DistributedObject, Entity.Entity):
         # ask our level obj for our spec data
         level = toonbase.tcr.doId2do[self.levelDoId]
         Entity.Entity.__init__(self, level, self.entId)
-        # this sets all of our initial spec parameters on ourselves
-        self.initializeEntity()
 
         DistributedObject.DistributedObject.announceGenerate(self)
 

+ 2 - 5
direct/src/level/DistributedEntityAI.py

@@ -7,13 +7,10 @@ class DistributedEntityAI(DistributedObjectAI.DistributedObjectAI,
     notify = DirectNotifyGlobal.directNotify.newCategory(
         'DistributedEntityAI')
 
-    def __init__(self, air, levelDoId, entId):
+    def __init__(self, air, level, entId):
         DistributedObjectAI.DistributedObjectAI.__init__(self, air)
-        self.levelDoId = levelDoId
-        level = self.air.doId2do[self.levelDoId]
+        self.levelDoId = level.doId
         Entity.Entity.__init__(self, level, entId)
-        # get our spec data
-        self.initializeEntity()
 
     def generate(self):
         self.notify.debug('generate')

+ 4 - 4
direct/src/level/DistributedInteractiveEntityAI.py

@@ -24,13 +24,13 @@ class DistributedInteractiveEntityAI(DistributedEntityAI.DistributedEntityAI):
     if __debug__:
         notify = DirectNotifyGlobal.directNotify.newCategory('DistributedInteractiveEntityAI')
 
-    def __init__(self, air, levelDoId, entId):
+    def __init__(self, air, level, entId):
         """entId: a unique identifier for this prop."""
         DistributedEntityAI.DistributedEntityAI.__init__(self, air,
-                                                         levelDoId, entId)
+                                                         level, entId)
         assert(self.debugPrint(
-                "DistributedInteractiveEntityAI(air=%s, entId=%s)"
-                %("the air", entId)))
+                "DistributedInteractiveEntityAI(entId=%s)"
+                %(entId)))
         self.fsm = FSM.FSM('DistributedInteractiveEntityAI',
                            [State.State('off',
                                         self.enterOff,

+ 124 - 127
direct/src/level/DistributedLevel.py

@@ -13,7 +13,7 @@ class DistributedLevel(DistributedObject.DistributedObject,
     """DistributedLevel"""
     notify = DirectNotifyGlobal.directNotify.newCategory('DistributedLevel')
 
-    WantVisibility = config.GetBool('level-visibility', 0)
+    WantVisibility = config.GetBool('level-visibility', 1)
     HideZones = config.GetBool('level-hidezones', 1)
 
     def __init__(self, cr):
@@ -37,7 +37,9 @@ class DistributedLevel(DistributedObject.DistributedObject,
         # all our entities at that time.
         toonbase.tcr.timeManager.synchronize('DistributedLevel.generate')
 
-    # required fields
+    # required fields (these ought to be required fields, but
+    # the AI level obj doesn't know the data values until it has been
+    # generated.)
     def setZoneIds(self, zoneIds):
         self.notify.debug('setZoneIds: %s' % zoneIds)
         self.zoneIds = zoneIds
@@ -48,22 +50,73 @@ class DistributedLevel(DistributedObject.DistributedObject,
 
     def setScenarioIndex(self, scenarioIndex):
         self.scenarioIndex = scenarioIndex
+        # ugly hack: we treat these DC fields as if they were required,
+        # and use 'levelAnnounceGenerate()' in place of regular old
+        # announceGenerate(). Note that we have to call
+        # levelAnnounceGenerate() in the last 'faux-required' DC update
+        # handler. If you add another field, move this to the last one.
+        self.levelAnnounceGenerate()
+
+    def levelAnnounceGenerate(self):
+        pass
 
     def initializeLevel(self, spec):
         """subclass should call this as soon as it's located its spec data.
         Must be called after obj has been generated."""
         LevelBase.LevelBase.initializeLevel(self, self.doId,
                                             spec, self.scenarioIndex)
-        self.localEntities = {}
-
-        # get list of entity types we need to create
-        entTypes = self.entType2Ids.keys()
 
-        # create the levelMgr
-        self.levelMgr = self.createEntity(self.entType2Ids['levelMgr'][0])
-        entTypes.remove('levelMgr')
+        # there should not be any pending reparents left at this point
+        assert len(self.parent2ChildIds) == 0
+        # make sure the zoneNums from the model match the zoneNums from
+        # the zone entities
+        assert sameElements(self.zoneNums, self.zoneNum2entId.keys())
 
         # load stuff
+
+        # fix up the floor collisions for walkable zones
+        for zoneNum in self.zoneNums:
+            zoneNode = self.zoneNum2node[zoneNum]
+
+            # if this is a walkable zone, fix up the model
+            floorColls = zoneNode.findAllMatches('**/+CollisionNode').asList()
+            if len(floorColls) > 0:
+                # rename the floor collision nodes, and make sure no other
+                # nodes under the ZoneNode have that name
+                floorCollName = '%s' % zoneNum
+                others = zoneNode.findAllMatches(
+                    '**/%s' % floorCollName).asList()
+                for other in others:
+                    other.setName('%s_renamed' % floorCollName)
+                for floorColl in floorColls:
+                    floorColl.setName(floorCollName)
+
+                # listen for zone enter events from floor collisions
+                def handleZoneEnter(collisionEntry,
+                                    self=self, zoneNum=zoneNum):
+                    # eat the collisionEntry
+                    self.toonEnterZone(zoneNum)
+                self.accept('enter%s' % floorCollName, handleZoneEnter)
+
+        self.initVisibility()
+
+    def createEntityCreator(self):
+        """Create the object that will be used to create Entities.
+        Inheritors, override if desired."""
+        return EntityCreator.EntityCreator(level=self)
+
+    def setupEntityCreationHandlers(self):
+        LevelBase.LevelBase.setupEntityCreationHandlers(self)
+        # load up the model ASAP so that we can get zone info out of it
+        self.acceptOnce(self.getEntityTypeCreateEvent('levelMgr'),
+                        self.handleLevelMgrCreated)
+
+    def removeEntityCreationHandlers(self):
+        LevelBase.LevelBase.removeEntityCreationHandlers(self)
+
+    def handleLevelMgrCreated(self):
+        # as soon as the levelMgr has been created, load up the model
+        # and extract zone info
         self.geom = loader.loadModel(self.modelFilename)
 
         def findNumberedNodes(baseString, model=self.geom, self=self):
@@ -87,50 +140,13 @@ class DistributedLevel(DistributedObject.DistributedObject,
             return num2node
 
         # find the zones in the model and fix them up
-        self.zoneNum2Node = findNumberedNodes('Zone')
+        self.zoneNum2node = findNumberedNodes('Zone')
         # add the UberZone to the table
-        self.zoneNum2Node[0] = self.geom
+        self.zoneNum2node[0] = self.geom
 
-        self.zoneNums = self.zoneNum2Node.keys()
-        # take the UberZone out of the list
-        self.zoneNums.remove(0)
+        self.zoneNums = self.zoneNum2node.keys()
         self.zoneNums.sort()
         self.notify.debug('zones: %s' % self.zoneNums)
-        assert sameElements(self.zoneNums, self.spec['zones'].keys())
-
-        # fix up the floor collisions for walkable zones
-        for zoneNum in self.zoneNums:
-            zoneNode = self.zoneNum2Node[zoneNum]
-
-            # if this is a walkable zone, fix up the model
-            floorColls = zoneNode.findAllMatches('**/+CollisionNode').asList()
-            if len(floorColls) > 0:
-                # rename the floor collision nodes, and make sure no other
-                # nodes under the ZoneNode have that name
-                floorCollName = '%s' % zoneNum
-                others = zoneNode.findAllMatches(
-                    '**/%s' % floorCollName).asList()
-                for other in others:
-                    other.setName('%s_renamed' % floorCollName)
-                for floorColl in floorColls:
-                    floorColl.setName(floorCollName)
-
-                # listen for zone enter events from floor collisions
-                def handleZoneEnter(collisionEntry,
-                                    self=self, zoneNum=zoneNum):
-                    # eat the collisionEntry
-                    self.toonEnterZone(zoneNum)
-                self.accept('enter%s' % floorCollName, handleZoneEnter)
-
-        # listen for camera-ray/floor collision events
-        def handleCameraRayFloorCollision(collEntry, self=self):
-            name = collEntry.getIntoNode().getName()
-            try:
-                zoneNum = int(name)
-                self.camEnterZone(zoneNum)
-            except:
-                self.notify.warning('Invalid floor collision node: %s' % name)
-        self.accept('on-floor', handleCameraRayFloorCollision)
 
         # hack in another doorway
         dw = self.geom.attachNewNode('Doorway27')
@@ -140,48 +156,16 @@ class DistributedLevel(DistributedObject.DistributedObject,
         # find the doorway nodes
         self.doorwayNum2Node = findNumberedNodes('Doorway')
 
-        self.initVisibility()
-
-        # create the rest of the client-side Entities
-        # TODO: only create client-side Entities for the
-        # currently-visible zones?
-        for entType in entTypes:
-            for entId in self.entType2Ids[entType]:
-                self.createEntity(entId)
-
-        # there should not be any pending reparents left at this point
-        assert len(self.parent2ChildIds) == 0
-
-    def makeEntityCreator(self):
-        """Create the object that will be used to create Entities.
-        Inheritors, override if desired."""
-        return EntityCreator.EntityCreator()
-
-    def createEntity(self, entId):
-        assert not self.localEntities.has_key(entId)
-        spec = self.entId2Spec[entId]
-        self.notify.debug('creating %s %s' % (spec['type'], entId))
-        entity = self.entityCreator.createEntity(spec['type'], self, entId)
-        if entity is not None:
-            self.localEntities[entId] = entity
-        return entity
-
     def announceGenerate(self):
         self.notify.debug('announceGenerate')
         DistributedObject.DistributedObject.announceGenerate(self)
 
     def disable(self):
         self.notify.debug('disable')
+        self.destroyLevel()
         DistributedObject.DistributedObject.disable(self)
         self.ignoreAll()
 
-        # destroy all of the local entities
-        for entId, entity in self.localEntities.items():
-            entity.destroy()
-        del self.localEntities
-
-        self.destroyLevel()
-
     def delete(self):
         self.notify.debug('delete')
         DistributedObject.DistributedObject.delete(self)
@@ -190,67 +174,79 @@ class DistributedLevel(DistributedObject.DistributedObject,
         # returns node that doors should parent themselves to
         return self.doorwayNum2Node[doorwayNum]
 
-    def requestReparent(self, entity, parent):
-        if parent is 'zone':
-            entity.reparentTo(self.zoneNum2Node[entity.zone])
-        else:
-            parentId = parent
-            assert(entity.entId != parentId)
+    def getZoneNode(self, zoneNum):
+        return self.zoneNum2node[zoneNum]
 
-            if self.entities.has_key(parentId):
-                # parent has already been created
-                entity.reparentTo(self.entities[parentId])
-            else:
-                # parent hasn't been created yet; schedule the reparent
-                self.notify.debug(
-                    'entity %s requesting reparent to %s, not yet created' %
-                    (entity, parent))
-
-                entId = entity.entId
-                entity.reparentTo(hidden)
-
-                # if this parent doesn't already have another child pending,
-                # do some setup
-                if not self.parent2ChildIds.has_key(parentId):
-                    self.parent2ChildIds[parentId] = []
-
-                    # do the reparent once the parent is initialized
-                    def doReparent(parentId=parentId, self=self):
-                        assert self.parent2ChildIds.has_key(parentId)
-                        parent=self.getEntity(parentId)
-                        for entId in self.parent2ChildIds[parentId]:
-                            entity=self.getEntity(entId)
-                            self.notify.debug(
-                                'performing pending reparent of %s to %s' %
-                                (entity, parent))
-                            entity.reparentTo(parent)
-                        del self.parent2ChildIds[parentId]
-
-                    self.accept(self.getEntityCreateEvent(parentId), doReparent)
-
-                self.parent2ChildIds[parentId].append(entId)
+    def requestReparent(self, entity, parentId):
+        assert(entity.entId != parentId)
+        if self.entities.has_key(parentId):
+            # parent has already been created
+            entity.reparentTo(self.entities[parentId].getNodePath())
+        else:
+            # parent hasn't been created yet; schedule the reparent
+            self.notify.debug(
+                'entity %s requesting reparent to %s, not yet created' %
+                (entity, parentId))
+
+            entId = entity.entId
+            entity.reparentTo(hidden)
+
+            # if this parent doesn't already have another child pending,
+            # do some setup
+            if not self.parent2ChildIds.has_key(parentId):
+                self.parent2ChildIds[parentId] = []
+
+                # do the reparent once the parent is initialized
+                def doReparent(parentId=parentId, self=self):
+                    assert self.parent2ChildIds.has_key(parentId)
+                    parent=self.getEntity(parentId)
+                    for entId in self.parent2ChildIds[parentId]:
+                        entity=self.getEntity(entId)
+                        self.notify.debug(
+                            'performing pending reparent of %s to %s' %
+                            (entity, parent))
+                        entity.reparentTo(parent.getNodePath())
+                    del self.parent2ChildIds[parentId]
+                    
+                self.accept(self.getEntityCreateEvent(parentId), doReparent)
+
+            self.parent2ChildIds[parentId].append(entId)
 
     def showZone(self, zoneNum):
-        self.zoneNum2Node[zoneNum].show()
+        self.zoneNum2node[zoneNum].show()
 
     def hideZone(self, zoneNum):
-        self.zoneNum2Node[zoneNum].hide()
+        self.zoneNum2node[zoneNum].hide()
 
     def setTransparency(self, alpha, zone=None):
         self.geom.setTransparency(1)
         if zone is None:
             node = self.geom
         else:
-            node = self.zoneNum2Node[zone]
+            node = self.zoneNum2node[zone]
         node.setAlphaScale(alpha)
 
     def initVisibility(self):
         # start out with every zone visible, since none of the zones have
         # been hidden
         self.curVisibleZoneNums = list2dict(self.zoneNums)
+        # the UberZone is always visible, so it's not included in the
+        # viz lists
+        del self.curVisibleZoneNums[0]
         # we have not entered any zone yet
         self.curZoneNum = None
 
+        # listen for camera-ray/floor collision events
+        def handleCameraRayFloorCollision(collEntry, self=self):
+            name = collEntry.getIntoNode().getName()
+            try:
+                zoneNum = int(name)
+            except:
+                self.notify.warning('Invalid floor collision node: %s' % name)
+            else:
+                self.camEnterZone(zoneNum)
+        self.accept('on-floor', handleCameraRayFloorCollision)
+
         # if no viz, listen to all the zones
         if not DistributedLevel.WantVisibility:
             self.setVisibility(self.zoneNums)
@@ -270,8 +266,9 @@ class DistributedLevel(DistributedObject.DistributedObject,
         
         if zoneNum == self.curZoneNum:
             return
-        
-        zoneSpec = self.spec['zones'][zoneNum]
+
+        zoneEntId = self.zoneNum2entId[zoneNum]
+        zoneSpec = self.entId2spec[zoneEntId]
         # use dicts to efficiently ensure that there are no duplicates
         visibleZoneNums = list2dict([zoneNum])
         visibleZoneNums.update(list2dict(zoneSpec['visibility']))
@@ -310,10 +307,10 @@ class DistributedLevel(DistributedObject.DistributedObject,
         # accepts list of visible zone numbers
         # convert the zone numbers into their actual zoneIds
         # always include Toontown and factory uberZones
-        factoryUberZone = self.getZoneId(0)
+        factoryUberZone = self.getZoneId(zoneNum=0)
         visibleZoneIds = [ToontownGlobals.UberZone, factoryUberZone]
         for vz in vizList:
-            visibleZoneIds.append(self.getZoneId(vz))
+            visibleZoneIds.append(self.getZoneId(zoneNum=vz))
         assert(uniqueElements(visibleZoneIds))
         self.notify.debug('new viz list: %s' % visibleZoneIds)
 

+ 69 - 78
direct/src/level/DistributedLevelAI.py

@@ -12,94 +12,24 @@ class DistributedLevelAI(DistributedObjectAI.DistributedObjectAI,
     """DistributedLevelAI"""
     notify = DirectNotifyGlobal.directNotify.newCategory('DistributedLevelAI')
 
-    def __init__(self, air):
+    def __init__(self, air, zoneId):
         DistributedObjectAI.DistributedObjectAI.__init__(self, air)
         LevelBase.LevelBase.__init__(self)
+        self.uberZoneId = zoneId
 
-    def initializeLevel(self, spec, uberZoneId):
-        self.uberZoneId = uberZoneId
-
-        # We need a unique level ID to init the level system, and we need
-        # the level system to get the data for our required fields.
-        # Pre-allocate a doId.
-        self.preAllocateDoId()
-
-        # choose a scenario
-        wc = WeightedChoice.WeightedChoice(spec['scenarios'], 1)
-        scenario = wc.choose()
-        scenarioIndex = spec['scenarios'].index(scenario)
-
-        LevelBase.LevelBase.initializeLevel(self, self.doId,
-                                            spec, scenarioIndex)
-        self.aiEntities = {}
-        # get list of entity types we need to create
-        self.entTypes = self.entType2Ids.keys()
-
-        # create the levelMgr
-        self.levelMgr = self.createEntity(self.entType2Ids['levelMgr'][0])
-        self.entTypes.remove('levelMgr')
-
-        # allocate the rest of the zones; add one for the uber-zone
-        self.numZones = len(self.spec['zones']) + 1
-        self.zoneIds = [self.uberZoneId]
-        for i in range(1,self.numZones):
-            # there is error checking in air.allocateZone
-            self.zoneIds.append(self.air.allocateZone())
-
-        # record the level's start time so that we can sync the clients
-        self.startTime = globalClock.getRealTime()
-        self.startTimestamp = globalClockDelta.localToNetworkTime(
-            self.startTime, bits=32)
-
-    def makeEntityCreator(self):
-        """Create the object that will be used to create Entities.
-        Inheritors, override if desired."""
-        return EntityCreatorAI.EntityCreatorAI()
-
-    def createEntity(self, entId):
-        assert not self.aiEntities.has_key(entId)
-        spec = self.entId2Spec[entId]
-        self.notify.debug('creating %s %s' % (spec['type'], entId))
-        zone = spec.get('zone')
-        # we might attempt to create non-distributed entities before
-        # we've been generated
-        if zone is not None:
-            zone = self.getZoneId(zone)
-            entity = self.entityCreator.createEntity(
-                spec['type'], self, entId, self.air, zone)
-        else:
-            entity = self.entityCreator.createEntity(
-                spec['type'], self, entId)
-        if entity is not None:
-            self.aiEntities[entId] = entity
-        return entity
-
-    # required-field getters
-    def getZoneIds(self):
-        return self.zoneIds
-
-    def getStartTimestamp(self):
-        return self.startTimestamp
-
-    def getScenarioIndex(self):
-        return self.scenarioIndex
-
-    def generate(self):
+    def generate(self, spec):
         self.notify.debug('generate')
         DistributedObjectAI.DistributedObjectAI.generate(self)
 
-        # create the rest of the Entities
-        for entType in self.entTypes:
-            for entId in self.entType2Ids[entType]:
-                self.createEntity(entId)
+        self.initializeLevel(spec)
+
+        self.sendUpdate('setZoneIds', [self.zoneIds])
+        self.sendUpdate('setStartTimestamp', [self.startTimestamp])
+        self.sendUpdate('setScenarioIndex', [self.scenarioIndex])
 
     def delete(self):
         self.notify.debug('delete')
         
-        for entId in self.aiEntities.keys():
-            self.aiEntities[entId].destroy()
-        del self.aiEntities
-        
         # we do not allocate the uberZone for now, so don't deallocate it
         for zoneId in self.zoneIds[1:]:
             self.air.deallocateZone(zoneId)
@@ -108,6 +38,67 @@ class DistributedLevelAI(DistributedObjectAI.DistributedObjectAI,
 
         DistributedObjectAI.DistributedObjectAI.delete(self)
 
+    def initializeLevel(self, spec):
+        # record the level's start time so that we can sync the clients
+        self.startTime = globalClock.getRealTime()
+        self.startTimestamp = globalClockDelta.localToNetworkTime(
+            self.startTime, bits=32)
+
+        # choose a scenario
+        wc = WeightedChoice.WeightedChoice(spec['scenarios'], 1)
+        scenario = wc.choose()
+        scenarioIndex = spec['scenarios'].index(scenario)
+
+        # this will hold the network zoneIds that we allocate
+        self.zoneIds = [self.uberZoneId]
+
+        LevelBase.LevelBase.initializeLevel(self, self.doId,
+                                            spec, scenarioIndex)
+
+    def createEntityCreator(self):
+        """Create the object that will be used to create Entities.
+        Inheritors, override if desired."""
+        return EntityCreatorAI.EntityCreatorAI(self.air, level=self)
+
+    def setupEntityCreationHandlers(self):
+        LevelBase.LevelBase.setupEntityCreationHandlers(self)
+        # listen for the creation of each zone object
+        self.accept(self.getEntityOfTypeCreateEvent('zone'),
+                    self.zoneEntCreated)
+
+    def removeEntityCreationHandlers(self):
+        LevelBase.LevelBase.removeEntityCreationHandlers(self)
+        self.ignore(self.getEntityOfTypeCreateEvent('zone'))
+
+    def zoneEntCreated(self, entId):
+        if entId == LevelBase.LevelBase.UberZoneEntId:
+            return
+        # there is error checking in air.allocateZone
+        self.zoneIds.append(self.air.allocateZone())
+
+    def getEntityZoneId(self, entId):
+        """figure out what network zoneId an entity is in"""
+        # TODO: where should the zone info come from? It could come
+        # from the 'parent' scene-graph info... but what about intangible
+        # distributed objects? I guess if they don't inherit from
+        # NodePathEntity et al, they'll just have an unused 'parent'
+        # attribute hanging around, which isn't the end of the world...
+
+        # this func is called before the entity has been created; look
+        # into the spec data, since we can't get a handle on the object itself
+        spec = self.entId2spec[entId]
+        type = spec['type']
+        if type == 'zone':
+            if not hasattr(self, 'zoneNum2zoneId'):
+                # we haven't even created our zone entities yet;
+                # we have no idea yet which zoneNums map to which
+                # network zoneIds. just return None.
+                return None
+            return self.zoneNum2zoneId[spec['modelZoneNum']]
+        if not spec.has_key('parent'):
+            return None
+        return self.getEntityZoneId(spec['parent'])
+
     if __debug__:
         # level editors should call this func to tweak attributes of level
         # entities

+ 2 - 5
direct/src/level/Entity.py

@@ -25,14 +25,11 @@ class Entity:
         if attribs is not None:
             self.attribs.update(attribs)
 
+        self.level.initializeEntity(self)
+
     def __str__(self):
         return 'ent%s(%s)' % (self.entId, self.level.getEntityType(self.entId))
     
-    def initializeEntity(self):
-        """Call this once on initialization to set this entity's
-        spec data"""
-        self.level.initializeEntity(self)
-
     def destroy(self):
         del self.level
         

+ 10 - 22
direct/src/level/EntityCreator.py

@@ -1,42 +1,30 @@
 """EntityCreator module: contains the EntityCreator class"""
 
+import EntityCreatorBase
 import BasicEntities
 import DirectNotifyGlobal
 import LevelMgr
+import ZoneEntity
 
 # some useful constructor functions
 # ctor functions must take (level, entId)
-def nothing(level, entId):
+def nothing(*args):
     """For entities that don't need to be created by the client"""
     return None
 
-class EntityCreator:
+class EntityCreator(EntityCreatorBase.EntityCreatorBase):
     """This class is responsible for creating instances of Entities on the
     client. It can be subclassed to handle more Entity types."""
-    notify = DirectNotifyGlobal.directNotify.newCategory('EntityCreator')
     
-    def __init__(self):
-        self.entType2Ctor = {}
+    def __init__(self, level):
+        EntityCreatorBase.EntityCreatorBase.__init__(self, level)
+        self.level = level
         self.privRegisterTypes({
             'levelMgr': LevelMgr.LevelMgr,
             'logicGate': nothing,
             'nodepath': BasicEntities.NodePathEntity,
+            'zone': ZoneEntity.ZoneEntity,
             })
 
-    def privRegisterType(self, entType, ctor):
-        if self.entType2Ctor.has_key(entType):
-            EntityCreator.notify.warning(
-                'replacing %s ctor %s with %s' %
-                (entType, self.entType2Ctor[entType], ctor))
-        self.entType2Ctor[entType] = ctor
-
-    def privRegisterTypes(self, type2ctor):
-        for entType, ctor in type2ctor.items():
-            self.privRegisterType(entType, ctor)
-
-    def createEntity(self, entType, level, entId):
-        if not self.entType2Ctor.has_key(entType):
-            EntityCreator.notify.warning(
-                'createEntity(entType=%s): entType not found' % entType)
-            return None
-        return self.entType2Ctor[entType](level, entId)
+    def doCreateEntity(self, ctor, entId):
+        return ctor(self.level, entId)

+ 34 - 39
direct/src/level/EntityCreatorAI.py

@@ -1,59 +1,54 @@
 """EntityCreatorAI module: contains the EntityCreatorAI class"""
 
-import DirectNotifyGlobal
+import EntityCreatorBase
 import LogicGateAI
 import LevelMgrAI
+import ZoneEntityAI
+from PythonUtil import Functor
 
 # some useful constructor functions
-# ctor functions for distributed entities must take
-#  (air, level doId, entId, zoneId)
-# ctor functions for non-distributed entities must take
-#  (level, entId)
-def createDistributedEntity(AIclass, air, levelDoId, entId, zoneId):
+# ctor functions for entities must take
+#  (air, level, entId, zoneId)
+
+# this func creates distributed entities whose constructors take
+#  (air, level doId, entId)
+# and do not generate themselves
+def createDistributedEntity(AIclass, air, level, entId, zoneId):
     """create a distributed entity and call generate"""
-    ent = AIclass(air, levelDoId, entId)
+    ent = AIclass(air, level, entId)
     ent.generateWithRequired(zoneId)
     return ent
 
-def nothing(air, levelDoId, entId, zoneId):
+# this func creates local entities whose constructors take
+#  (level, entId)
+def createLocalEntity(AIclass, air, level, entId, zoneId):
+    """create a local entity"""
+    ent = AIclass(level, entId)
+
+# take any number of args to support local and distributed entities
+def nothing(*args):
     """Create entity that doesn't have a server side representation."""
     return None
 
-class EntityCreatorAI:
+class EntityCreatorAI(EntityCreatorBase.EntityCreatorBase):
     """This class is responsible for creating instances of Entities on the AI.
     It can be subclassed to handle more Entity types."""
-    notify = DirectNotifyGlobal.directNotify.newCategory('EntityCreatorAI')
 
-    def __init__(self):
-        self.entType2Ctor = {}
+    def __init__(self, air, level):
+        EntityCreatorBase.EntityCreatorBase.__init__(self, level)
+        self.air = air
+
+        # create short aliases for ctor funcs
+        cLE = createLocalEntity
+
         self.privRegisterTypes({
-            'levelMgr': LevelMgrAI.LevelMgrAI,
-            'logicGate': LogicGateAI.LogicGateAI,
+            'levelMgr': Functor(cLE, LevelMgrAI.LevelMgrAI),
+            'logicGate': Functor(cLE, LogicGateAI.LogicGateAI),
             'nodepath': nothing,
+            'zone': Functor(cLE, ZoneEntityAI.ZoneEntityAI),
             })
 
-    def privRegisterType(self, entType, ctor):
-        if self.entType2Ctor.has_key(entType):
-            EntityCreatorAI.notify.warning(
-                'replacing %s ctor %s with %s' %
-                (entType, self.entType2Ctor[entType], ctor))
-        self.entType2Ctor[entType] = ctor
-
-    def privRegisterTypes(self, type2ctor):
-        for entType, ctor in type2ctor.items():
-            self.privRegisterType(entType, ctor)
-
-    def createEntity(self, entType, level, entId, air=None, zoneId=None):
-        """zoneId=None indicates a non-distributed entity"""
-        if not self.entType2Ctor.has_key(entType):
-            EntityCreatorAI.notify.warning(
-                'createEntity(entType=%s, levelDoId=%s, '
-                'entId=%s, zoneId=%s) not found' %
-                (entType, level.doId, entId, zoneId))
-            return None
-
-        if zoneId is None:
-            return self.entType2Ctor[entType](level, entId)
-        else:
-            levelDoId = level.doId
-            return self.entType2Ctor[entType](air, levelDoId, entId, zoneId)
+    def doCreateEntity(self, ctor, entId):
+        zoneId = self.level.getEntityZoneId(entId)
+        self.notify.debug('creating entity %s in zone %s' % (entId, zoneId))
+        return ctor(self.air, self.level, entId, zoneId)

+ 33 - 0
direct/src/level/EntityCreatorBase.py

@@ -0,0 +1,33 @@
+"""EntityCreatorBase module: contains the EntityCreatorBase class"""
+
+import DirectNotifyGlobal
+
+class EntityCreatorBase:
+    """This class is responsible for creating instances of Entities on the
+    AI and on the client. It must be subclassed to specify what entity
+    types it can create, and to provide the creation implementation."""
+    notify = DirectNotifyGlobal.directNotify.newCategory('EntityCreator')
+
+    def __init__(self, level):
+        self.level = level
+        self.entType2Ctor = {}
+
+    def createEntity(self, entId):
+        entType = self.level.getEntityType(entId)
+        
+        if not self.entType2Ctor.has_key(entType):
+            self.notify.error('unknown entity type: %s (ent%s)' %
+                              (entType, entId))
+
+        # inheritor must define doCreateEntity
+        self.doCreateEntity(self.entType2Ctor[entType], entId)
+
+    def privRegisterType(self, entType, ctor):
+        if self.entType2Ctor.has_key(entType):
+            self.notify.warning('replacing %s ctor %s with %s' %
+                                (entType, self.entType2Ctor[entType], ctor))
+        self.entType2Ctor[entType] = ctor
+
+    def privRegisterTypes(self, type2ctor):
+        for entType, ctor in type2ctor.items():
+            self.privRegisterType(entType, ctor)

+ 160 - 37
direct/src/level/LevelBase.py

@@ -4,15 +4,38 @@ import DirectNotifyGlobal
 import string
 from PythonUtil import lineInfo
 
+"""
+Any data that can be edited by a level editor must be represented as
+an attribute of an entity owned by the level, in order to keep the
+level-editing interface simple and unchanging.
+
+To support this, we have entities such as 'levelMgr' and 'zoneEntity' that
+contain crucial level information, much of which is needed when setting
+up the level object, and is needed before other entity types can be
+effectively created. (If you try to create a distributed entity, but
+you don't yet have the information for the zone that it's in, because
+you haven't created the zone's ZoneEntity, you're hurting.)
+
+
+"""
+
+"""
+ZONE TERMINOLOGY
+zoneNum / modelZoneNum : the number that a modeler chooses for a zone
+zoneEntId : the entity ID of the ZoneEntity that represents a zone
+zoneId : the network ID of the zone
+"""
+
 class LevelBase:
     """LevelBase: shared client and AI code
     representation of a game level, keeps track of all of the
     Level Entities and their interrelations"""
     notify = DirectNotifyGlobal.directNotify.newCategory('LevelBase')
 
-    def __init__(self, levelId=None):
-        if levelId is not None:
-            self.setLevelId(levelId)
+    UberZoneEntId = 0
+
+    def __init__(self):
+        pass
 
     def initializeLevel(self, levelId, spec, scenarioIndex):
         """ subclass should call this as soon as it has located
@@ -24,47 +47,128 @@ class LevelBase:
         # create a complete set of global and scenario-specific entity specs
         globalEntities = self.spec['globalEntities']
         scenarioEntities = self.spec['scenarios'][self.scenarioIndex][0]
-        entId2Spec = {}
-        entId2Spec.update(globalEntities)
-        entId2Spec.update(scenarioEntities)
-        self.entId2Spec = entId2Spec
+        entId2spec = {}
+        entId2spec.update(globalEntities)
+        entId2spec.update(scenarioEntities)
+        self.entId2spec = entId2spec
 
         # create some handy tables
-        self.entType2Ids = {}
-        self.entZone2Ids = {}
-        self.nonZoneEntIds = []
-        for entId, spec in self.entId2Spec.items():
-            entType = spec['type']
-            self.entType2Ids.setdefault(entType, [])
-            self.entType2Ids[entType].append(entId)
 
-            entZone = spec.get('zone')
-            # note that entities with no Zone will be filed under 'None'
-            self.entZone2Ids.setdefault(entZone, [])
-            self.entZone2Ids[entZone].append(entId)
+        # entity type -> list of entIds
+        self.entType2ids = {}
+        for entId, spec in self.entId2spec.items():
+            entType = spec['type']
+            self.entType2ids.setdefault(entType, [])
+            self.entType2ids[entType].append(entId)
 
         # there should be one and only one levelMgr
-        assert len(self.entType2Ids['levelMgr']) == 1
-
-        # this will be filled in as the entities are created and report in
-        self.entities = {}
+        assert len(self.entType2ids['levelMgr']) == 1
 
         # get an entity creator object
-        self.entityCreator = self.makeEntityCreator()
-
-    def makeEntityCreator(self):
-        self.notify.error(
-            'concrete Level class must override %s' % lineInfo()[2])
+        self.entityCreator = self.createEntityCreator()
+        # set up handlers for entity creation
+        self.setupEntityCreationHandlers()
+        # create all the entities
+        self.createAllEntities(priorityTypes=['levelMgr','zone'])
+        # tear down the entity creation handlers
+        self.removeEntityCreationHandlers()
 
     def destroyLevel(self):
+        for entity in self.createdEntities:
+            entity.destroy()
+        del self.createdEntities
         del self.entities
-        del self.entId2Spec
+        del self.entId2spec
         del self.spec
 
+    def createEntityCreator(self):
+        self.notify.error(
+            'concrete Level class must override %s' % lineInfo()[2])
+
+    def setupEntityCreationHandlers(self):
+        # set up any handlers for entity creation events
+        # override if desired, but be sure to call down
+        self.acceptOnce(
+            self.getEntityTypeCreateEvent('zone'),
+            self.handleAllZonesCreated)
+
+    def removeEntityCreationHandlers(self):
+        pass
+
+    def handleAllZonesCreated(self):
+        """once all the zone entities have been created, and we've got a
+        list of zoneIds in self.zoneIds, init zone tables"""
+        # create a table mapping the model's zone numbers to the zone
+        # entIds; zone entities are tied to model zone nums in the level spec,
+        # this is just for convenient lookup
+        self.zoneNum2entId = {}
+        for entId in self.entType2ids['zone']:
+            zoneEnt = self.getEntity(entId)
+            self.zoneNum2entId[zoneEnt.modelZoneNum] = entId
+
+        # At this point, we need to have a 'self.zoneIds' table of network
+        # zoneIds, one for each zone including the UberZone. This is where
+        # we decide which zoneNum/Entity gets mapped to which zoneId.
+        # We sort the zoneNums, and then pair the sorted zoneNums up with the
+        # zoneIds in the order that they appear in the self.zoneIds table.
+        modelZoneNums = self.zoneNum2entId.keys()
+        modelZoneNums.sort()
+        # maps of zoneNum and zoneEntId to network zoneId
+        self.zoneNum2zoneId = {}
+        self.zoneEntId2zoneId = {}
+        for i in range(len(modelZoneNums)):
+            modelZoneNum = modelZoneNums[i]
+            zoneEntId = self.zoneNum2entId[modelZoneNum]
+            zoneId = self.zoneIds[i]
+            self.zoneNum2zoneId[modelZoneNum] = zoneId
+            self.zoneEntId2zoneId[zoneEntId] = zoneId
+
+    def createAllEntities(self, priorityTypes=[]):
+        """creates all entities in the spec. priorityTypes is an
+        optional ordered list of entity types to create first."""
+        # this will be filled in as the entities are created and report in
+        # this includes distributed objects on the client
+        self.entities = {}
+        # this list contains the entities that we have actually created
+        self.createdEntities = []
+
+        # get list of all entity types we need to create
+        entTypes = self.entType2ids.keys()
+
+        # first create the types in the priority list
+        for type in priorityTypes:
+            assert type in entTypes
+            self.createAllEntitiesOfType(type)
+            entTypes.remove(type)
+
+        # create the other entities in any old order
+        for type in entTypes:
+            self.createAllEntitiesOfType(type)
+
+    def createAllEntitiesOfType(self, entType):
+        """creates all entities of a given type"""
+        assert entType in self.entType2ids
+        for entId in self.entType2ids[entType]:
+            self.createEntity(entId)
+        # send the entity type-create event
+        messenger.send(self.getEntityTypeCreateEvent(entType))
+
+    def createEntity(self, entId):
+        assert not self.entities.has_key(entId)
+        spec = self.entId2spec[entId]
+        self.notify.debug('creating %s %s' % (spec['type'], entId))
+        entity = self.entityCreator.createEntity(entId)
+        if entity is not None:
+            self.createdEntities.append(entity)
+        return entity
+
     def initializeEntity(self, entity):
-        """populate an entity with its spec data"""
+        """populate an entity with its spec data. This is not done
+        in createEntity in order to allow other pieces of code create
+        entities; this is called directly by Entity.
+        """
         entId = entity.entId
-        spec = self.entId2Spec[entId]
+        spec = self.entId2spec[entId]
         # on initialization, set items directly on entity
         for key,value in spec.items():
             if key in ('type', 'name', 'comment',):
@@ -73,8 +177,21 @@ class LevelBase:
 
         # entity is initialized, add it to the list of entities
         self.entities[entity.entId] = entity
-        # send the create event
+        # send the entity-create event
         messenger.send(self.getEntityCreateEvent(entity.entId))
+        # send the entity-of-type create event
+        messenger.send(self.getEntityOfTypeCreateEvent(spec['type']),
+                                                       [entId])
+
+    def getEntityTypeCreateEvent(self, entType):
+        """This is the event that is thrown immediately after every
+        entity of a given type has been created"""
+        return 'entityTypeCreate-%s-%s' % (self.levelId, entType)
+
+    def getEntityOfTypeCreateEvent(self, entType):
+        """This event is thrown immediately after each instance of the
+        given entity type is created; handlers must accept an entId"""
+        return 'entityOfTypeCreate-%s-%s' % (self.levelId, entType)
 
     def getEntityCreateEvent(self, entId):
         """This is the event that is thrown immediately after an entity
@@ -85,9 +202,15 @@ class LevelBase:
         return self.entities[entId]
 
     def getEntityType(self, entId):
-        return self.entId2Spec[entId]['type']
-
-    def getZoneId(self, index):
-        # get the actual zoneId for this index
-        # TODO: perhaps the zones should be fixed up in the specs
-        return self.zoneIds[index]
+        return self.entId2spec[entId]['type']
+
+    def getZoneId(self, dummy=None, zoneNum=None, entId=None):
+        """look up network zoneId by zoneNum or entId"""
+        assert (zoneNum is not None) or (entId is not None)
+        assert not ((zoneNum is not None) and (entId is not None))
+        if zoneNum is not None:
+            assert zoneNum in self.zoneNum2zoneId
+            return self.zoneNum2zoneId[zoneNum]
+        else:
+            assert entId in self.zoneEntId2zoneId
+            return self.zoneEntId2zoneId[entId]

+ 0 - 1
direct/src/level/LevelMgr.py

@@ -6,7 +6,6 @@ class LevelMgr(Entity.Entity):
     """This class manages editable client-side level attributes"""
     def __init__(self, level, entId):
         Entity.Entity.__init__(self, level, entId)
-        self.initializeEntity()
         self.callSetters('modelFilename')
 
     def setModelFilename(self, modelFilename):

+ 0 - 1
direct/src/level/LevelMgrAI.py

@@ -6,4 +6,3 @@ class LevelMgrAI(Entity.Entity):
     """This class manages editable AI level attributes"""
     def __init__(self, level, entId):
         Entity.Entity.__init__(self, level, entId)
-        self.initializeEntity()

+ 0 - 1
direct/src/level/LogicGateAI.py

@@ -86,7 +86,6 @@ class LogicGateAI(Entity.Entity, PandaObject.PandaObject):
         self.input1 = None
         self.input2 = None
         Entity.Entity.__init__(self, level, entId)
-        self.initializeEntity()
         self.setLogicType(self.logicType)
         self.setInput_input1_bool(self.input_input1_bool)
         self.setInput_input2_bool(self.input_input2_bool)

+ 14 - 0
direct/src/level/ZoneEntity.py

@@ -0,0 +1,14 @@
+"""ZoneEntity module: contains the ZoneEntity class"""
+
+import Entity
+import BasicEntities
+
+class ZoneEntity(Entity.Entity, BasicEntities.NodePathAttribs):
+    def __init__(self, level, entId):
+        Entity.Entity.__init__(self, level, entId)
+
+        self.nodePath = self.level.getZoneNode(self.modelZoneNum)
+        self.initNodePathAttribs(doReparent=0)
+
+    def getNodePath(self):
+        return self.nodePath

+ 7 - 0
direct/src/level/ZoneEntityAI.py

@@ -0,0 +1,7 @@
+"""ZoneEntityAI module: contains the ZoneEntityAI class"""
+
+import Entity
+
+class ZoneEntityAI(Entity.Entity):
+    def __init__(self, level, entId):
+        Entity.Entity.__init__(self, level, entId)