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 DistributedEntity
 import NodePath
 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):
 class privNodePathImpl(NodePath.NodePath):
     def __init__(self, name):
     def __init__(self, name):
         node = hidden.attachNewNode(name)
         node = hidden.attachNewNode(name)
@@ -15,33 +44,32 @@ class privNodePathImpl(NodePath.NodePath):
         of its attributes have been set"""
         of its attributes have been set"""
         self.callSetters('pos','x','y','z',
         self.callSetters('pos','x','y','z',
                          'hpr','h','p','r',
                          '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):
     def destroy(self):
         self.removeNode()
         self.removeNode()
 
 
+    def getNodePath(self):
+        return self
+
 class NodePathEntity(Entity.Entity, privNodePathImpl):
 class NodePathEntity(Entity.Entity, privNodePathImpl):
     """This is an entity that represents a NodePath on the client.
     """This is an entity that represents a NodePath on the client.
     It may be instantiated directly or used as a base class for other
     It may be instantiated directly or used as a base class for other
     entity types."""
     entity types."""
     def __init__(self, level, entId):
     def __init__(self, level, entId):
+        privNodePathImpl.__init__(self, '')
         Entity.Entity.__init__(self, level, entId)
         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)
         privNodePathImpl.initNodePathAttribs(self)
 
 
     def destroy(self):
     def destroy(self):
         Entity.Entity.destroy(self)
         Entity.Entity.destroy(self)
         privNodePathImpl.destroy(self)
         privNodePathImpl.destroy(self)
 
 
+    def getNodePath(self):
+        return self
+
 class DistributedNodePathEntity(DistributedEntity.DistributedEntity,
 class DistributedNodePathEntity(DistributedEntity.DistributedEntity,
                                 privNodePathImpl):
                                 privNodePathImpl):
     """This is a distributed version of NodePathEntity. It should not
     """This is a distributed version of NodePathEntity. It should not
@@ -50,11 +78,14 @@ class DistributedNodePathEntity(DistributedEntity.DistributedEntity,
     def __init__(self, cr):
     def __init__(self, cr):
         DistributedEntity.DistributedEntity.__init__(self, cr)
         DistributedEntity.DistributedEntity.__init__(self, cr)
         privNodePathImpl.__init__(self, 'DistributedNodePathEntity')
         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):
     def destroy(self):
         DistributedEntity.DistributedEntity.destroy(self)
         DistributedEntity.DistributedEntity.destroy(self)
         privNodePathImpl.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
         # ask our level obj for our spec data
         level = toonbase.tcr.doId2do[self.levelDoId]
         level = toonbase.tcr.doId2do[self.levelDoId]
         Entity.Entity.__init__(self, level, self.entId)
         Entity.Entity.__init__(self, level, self.entId)
-        # this sets all of our initial spec parameters on ourselves
-        self.initializeEntity()
 
 
         DistributedObject.DistributedObject.announceGenerate(self)
         DistributedObject.DistributedObject.announceGenerate(self)
 
 

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

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

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

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

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

@@ -13,7 +13,7 @@ class DistributedLevel(DistributedObject.DistributedObject,
     """DistributedLevel"""
     """DistributedLevel"""
     notify = DirectNotifyGlobal.directNotify.newCategory('DistributedLevel')
     notify = DirectNotifyGlobal.directNotify.newCategory('DistributedLevel')
 
 
-    WantVisibility = config.GetBool('level-visibility', 0)
+    WantVisibility = config.GetBool('level-visibility', 1)
     HideZones = config.GetBool('level-hidezones', 1)
     HideZones = config.GetBool('level-hidezones', 1)
 
 
     def __init__(self, cr):
     def __init__(self, cr):
@@ -37,7 +37,9 @@ class DistributedLevel(DistributedObject.DistributedObject,
         # all our entities at that time.
         # all our entities at that time.
         toonbase.tcr.timeManager.synchronize('DistributedLevel.generate')
         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):
     def setZoneIds(self, zoneIds):
         self.notify.debug('setZoneIds: %s' % zoneIds)
         self.notify.debug('setZoneIds: %s' % zoneIds)
         self.zoneIds = zoneIds
         self.zoneIds = zoneIds
@@ -48,22 +50,73 @@ class DistributedLevel(DistributedObject.DistributedObject,
 
 
     def setScenarioIndex(self, scenarioIndex):
     def setScenarioIndex(self, scenarioIndex):
         self.scenarioIndex = 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):
     def initializeLevel(self, spec):
         """subclass should call this as soon as it's located its spec data.
         """subclass should call this as soon as it's located its spec data.
         Must be called after obj has been generated."""
         Must be called after obj has been generated."""
         LevelBase.LevelBase.initializeLevel(self, self.doId,
         LevelBase.LevelBase.initializeLevel(self, self.doId,
                                             spec, self.scenarioIndex)
                                             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
         # 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)
         self.geom = loader.loadModel(self.modelFilename)
 
 
         def findNumberedNodes(baseString, model=self.geom, self=self):
         def findNumberedNodes(baseString, model=self.geom, self=self):
@@ -87,50 +140,13 @@ class DistributedLevel(DistributedObject.DistributedObject,
             return num2node
             return num2node
 
 
         # find the zones in the model and fix them up
         # find the zones in the model and fix them up
-        self.zoneNum2Node = findNumberedNodes('Zone')
+        self.zoneNum2node = findNumberedNodes('Zone')
         # add the UberZone to the table
         # 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.zoneNums.sort()
         self.notify.debug('zones: %s' % self.zoneNums)
         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
         # hack in another doorway
         dw = self.geom.attachNewNode('Doorway27')
         dw = self.geom.attachNewNode('Doorway27')
@@ -140,48 +156,16 @@ class DistributedLevel(DistributedObject.DistributedObject,
         # find the doorway nodes
         # find the doorway nodes
         self.doorwayNum2Node = findNumberedNodes('Doorway')
         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):
     def announceGenerate(self):
         self.notify.debug('announceGenerate')
         self.notify.debug('announceGenerate')
         DistributedObject.DistributedObject.announceGenerate(self)
         DistributedObject.DistributedObject.announceGenerate(self)
 
 
     def disable(self):
     def disable(self):
         self.notify.debug('disable')
         self.notify.debug('disable')
+        self.destroyLevel()
         DistributedObject.DistributedObject.disable(self)
         DistributedObject.DistributedObject.disable(self)
         self.ignoreAll()
         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):
     def delete(self):
         self.notify.debug('delete')
         self.notify.debug('delete')
         DistributedObject.DistributedObject.delete(self)
         DistributedObject.DistributedObject.delete(self)
@@ -190,67 +174,79 @@ class DistributedLevel(DistributedObject.DistributedObject,
         # returns node that doors should parent themselves to
         # returns node that doors should parent themselves to
         return self.doorwayNum2Node[doorwayNum]
         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):
     def showZone(self, zoneNum):
-        self.zoneNum2Node[zoneNum].show()
+        self.zoneNum2node[zoneNum].show()
 
 
     def hideZone(self, zoneNum):
     def hideZone(self, zoneNum):
-        self.zoneNum2Node[zoneNum].hide()
+        self.zoneNum2node[zoneNum].hide()
 
 
     def setTransparency(self, alpha, zone=None):
     def setTransparency(self, alpha, zone=None):
         self.geom.setTransparency(1)
         self.geom.setTransparency(1)
         if zone is None:
         if zone is None:
             node = self.geom
             node = self.geom
         else:
         else:
-            node = self.zoneNum2Node[zone]
+            node = self.zoneNum2node[zone]
         node.setAlphaScale(alpha)
         node.setAlphaScale(alpha)
 
 
     def initVisibility(self):
     def initVisibility(self):
         # start out with every zone visible, since none of the zones have
         # start out with every zone visible, since none of the zones have
         # been hidden
         # been hidden
         self.curVisibleZoneNums = list2dict(self.zoneNums)
         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
         # we have not entered any zone yet
         self.curZoneNum = None
         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 no viz, listen to all the zones
         if not DistributedLevel.WantVisibility:
         if not DistributedLevel.WantVisibility:
             self.setVisibility(self.zoneNums)
             self.setVisibility(self.zoneNums)
@@ -270,8 +266,9 @@ class DistributedLevel(DistributedObject.DistributedObject,
         
         
         if zoneNum == self.curZoneNum:
         if zoneNum == self.curZoneNum:
             return
             return
-        
-        zoneSpec = self.spec['zones'][zoneNum]
+
+        zoneEntId = self.zoneNum2entId[zoneNum]
+        zoneSpec = self.entId2spec[zoneEntId]
         # use dicts to efficiently ensure that there are no duplicates
         # use dicts to efficiently ensure that there are no duplicates
         visibleZoneNums = list2dict([zoneNum])
         visibleZoneNums = list2dict([zoneNum])
         visibleZoneNums.update(list2dict(zoneSpec['visibility']))
         visibleZoneNums.update(list2dict(zoneSpec['visibility']))
@@ -310,10 +307,10 @@ class DistributedLevel(DistributedObject.DistributedObject,
         # accepts list of visible zone numbers
         # accepts list of visible zone numbers
         # convert the zone numbers into their actual zoneIds
         # convert the zone numbers into their actual zoneIds
         # always include Toontown and factory uberZones
         # always include Toontown and factory uberZones
-        factoryUberZone = self.getZoneId(0)
+        factoryUberZone = self.getZoneId(zoneNum=0)
         visibleZoneIds = [ToontownGlobals.UberZone, factoryUberZone]
         visibleZoneIds = [ToontownGlobals.UberZone, factoryUberZone]
         for vz in vizList:
         for vz in vizList:
-            visibleZoneIds.append(self.getZoneId(vz))
+            visibleZoneIds.append(self.getZoneId(zoneNum=vz))
         assert(uniqueElements(visibleZoneIds))
         assert(uniqueElements(visibleZoneIds))
         self.notify.debug('new viz list: %s' % 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"""
     """DistributedLevelAI"""
     notify = DirectNotifyGlobal.directNotify.newCategory('DistributedLevelAI')
     notify = DirectNotifyGlobal.directNotify.newCategory('DistributedLevelAI')
 
 
-    def __init__(self, air):
+    def __init__(self, air, zoneId):
         DistributedObjectAI.DistributedObjectAI.__init__(self, air)
         DistributedObjectAI.DistributedObjectAI.__init__(self, air)
         LevelBase.LevelBase.__init__(self)
         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')
         self.notify.debug('generate')
         DistributedObjectAI.DistributedObjectAI.generate(self)
         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):
     def delete(self):
         self.notify.debug('delete')
         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
         # we do not allocate the uberZone for now, so don't deallocate it
         for zoneId in self.zoneIds[1:]:
         for zoneId in self.zoneIds[1:]:
             self.air.deallocateZone(zoneId)
             self.air.deallocateZone(zoneId)
@@ -108,6 +38,67 @@ class DistributedLevelAI(DistributedObjectAI.DistributedObjectAI,
 
 
         DistributedObjectAI.DistributedObjectAI.delete(self)
         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__:
     if __debug__:
         # level editors should call this func to tweak attributes of level
         # level editors should call this func to tweak attributes of level
         # entities
         # entities

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

@@ -25,14 +25,11 @@ class Entity:
         if attribs is not None:
         if attribs is not None:
             self.attribs.update(attribs)
             self.attribs.update(attribs)
 
 
+        self.level.initializeEntity(self)
+
     def __str__(self):
     def __str__(self):
         return 'ent%s(%s)' % (self.entId, self.level.getEntityType(self.entId))
         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):
     def destroy(self):
         del self.level
         del self.level
         
         

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

@@ -1,42 +1,30 @@
 """EntityCreator module: contains the EntityCreator class"""
 """EntityCreator module: contains the EntityCreator class"""
 
 
+import EntityCreatorBase
 import BasicEntities
 import BasicEntities
 import DirectNotifyGlobal
 import DirectNotifyGlobal
 import LevelMgr
 import LevelMgr
+import ZoneEntity
 
 
 # some useful constructor functions
 # some useful constructor functions
 # ctor functions must take (level, entId)
 # 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"""
     """For entities that don't need to be created by the client"""
     return None
     return None
 
 
-class EntityCreator:
+class EntityCreator(EntityCreatorBase.EntityCreatorBase):
     """This class is responsible for creating instances of Entities on the
     """This class is responsible for creating instances of Entities on the
     client. It can be subclassed to handle more Entity types."""
     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({
         self.privRegisterTypes({
             'levelMgr': LevelMgr.LevelMgr,
             'levelMgr': LevelMgr.LevelMgr,
             'logicGate': nothing,
             'logicGate': nothing,
             'nodepath': BasicEntities.NodePathEntity,
             '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"""
 """EntityCreatorAI module: contains the EntityCreatorAI class"""
 
 
-import DirectNotifyGlobal
+import EntityCreatorBase
 import LogicGateAI
 import LogicGateAI
 import LevelMgrAI
 import LevelMgrAI
+import ZoneEntityAI
+from PythonUtil import Functor
 
 
 # some useful constructor functions
 # 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"""
     """create a distributed entity and call generate"""
-    ent = AIclass(air, levelDoId, entId)
+    ent = AIclass(air, level, entId)
     ent.generateWithRequired(zoneId)
     ent.generateWithRequired(zoneId)
     return ent
     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."""
     """Create entity that doesn't have a server side representation."""
     return None
     return None
 
 
-class EntityCreatorAI:
+class EntityCreatorAI(EntityCreatorBase.EntityCreatorBase):
     """This class is responsible for creating instances of Entities on the AI.
     """This class is responsible for creating instances of Entities on the AI.
     It can be subclassed to handle more Entity types."""
     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({
         self.privRegisterTypes({
-            'levelMgr': LevelMgrAI.LevelMgrAI,
-            'logicGate': LogicGateAI.LogicGateAI,
+            'levelMgr': Functor(cLE, LevelMgrAI.LevelMgrAI),
+            'logicGate': Functor(cLE, LogicGateAI.LogicGateAI),
             'nodepath': nothing,
             '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
 import string
 from PythonUtil import lineInfo
 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:
 class LevelBase:
     """LevelBase: shared client and AI code
     """LevelBase: shared client and AI code
     representation of a game level, keeps track of all of the
     representation of a game level, keeps track of all of the
     Level Entities and their interrelations"""
     Level Entities and their interrelations"""
     notify = DirectNotifyGlobal.directNotify.newCategory('LevelBase')
     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):
     def initializeLevel(self, levelId, spec, scenarioIndex):
         """ subclass should call this as soon as it has located
         """ 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
         # create a complete set of global and scenario-specific entity specs
         globalEntities = self.spec['globalEntities']
         globalEntities = self.spec['globalEntities']
         scenarioEntities = self.spec['scenarios'][self.scenarioIndex][0]
         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
         # 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
         # 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
         # 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):
     def destroyLevel(self):
+        for entity in self.createdEntities:
+            entity.destroy()
+        del self.createdEntities
         del self.entities
         del self.entities
-        del self.entId2Spec
+        del self.entId2spec
         del self.spec
         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):
     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
         entId = entity.entId
-        spec = self.entId2Spec[entId]
+        spec = self.entId2spec[entId]
         # on initialization, set items directly on entity
         # on initialization, set items directly on entity
         for key,value in spec.items():
         for key,value in spec.items():
             if key in ('type', 'name', 'comment',):
             if key in ('type', 'name', 'comment',):
@@ -73,8 +177,21 @@ class LevelBase:
 
 
         # entity is initialized, add it to the list of entities
         # entity is initialized, add it to the list of entities
         self.entities[entity.entId] = entity
         self.entities[entity.entId] = entity
-        # send the create event
+        # send the entity-create event
         messenger.send(self.getEntityCreateEvent(entity.entId))
         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):
     def getEntityCreateEvent(self, entId):
         """This is the event that is thrown immediately after an entity
         """This is the event that is thrown immediately after an entity
@@ -85,9 +202,15 @@ class LevelBase:
         return self.entities[entId]
         return self.entities[entId]
 
 
     def getEntityType(self, 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"""
     """This class manages editable client-side level attributes"""
     def __init__(self, level, entId):
     def __init__(self, level, entId):
         Entity.Entity.__init__(self, level, entId)
         Entity.Entity.__init__(self, level, entId)
-        self.initializeEntity()
         self.callSetters('modelFilename')
         self.callSetters('modelFilename')
 
 
     def setModelFilename(self, 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"""
     """This class manages editable AI level attributes"""
     def __init__(self, level, entId):
     def __init__(self, level, entId):
         Entity.Entity.__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.input1 = None
         self.input2 = None
         self.input2 = None
         Entity.Entity.__init__(self, level, entId)
         Entity.Entity.__init__(self, level, entId)
-        self.initializeEntity()
         self.setLogicType(self.logicType)
         self.setLogicType(self.logicType)
         self.setInput_input1_bool(self.input_input1_bool)
         self.setInput_input1_bool(self.input_input1_bool)
         self.setInput_input2_bool(self.input_input2_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)