From 6e1f2931406abdd56b02148d098b488e1818a9ef Mon Sep 17 00:00:00 2001 From: Darren Ranalli Date: Fri, 19 Sep 2003 01:02:10 +0000 Subject: [PATCH] zones are now entities, and lots of other tweaks --- direct/src/level/BasicEntities.py | 63 +++-- direct/src/level/DistributedEntity.py | 2 - direct/src/level/DistributedEntityAI.py | 7 +- .../level/DistributedInteractiveEntityAI.py | 8 +- direct/src/level/DistributedLevel.py | 243 +++++++++--------- direct/src/level/DistributedLevelAI.py | 147 +++++------ direct/src/level/Entity.py | 7 +- direct/src/level/EntityCreator.py | 32 +-- direct/src/level/EntityCreatorAI.py | 73 +++--- direct/src/level/EntityCreatorBase.py | 33 +++ direct/src/level/LevelBase.py | 195 +++++++++++--- direct/src/level/LevelMgr.py | 1 - direct/src/level/LevelMgrAI.py | 1 - direct/src/level/LogicGateAI.py | 1 - direct/src/level/ZoneEntity.py | 14 + direct/src/level/ZoneEntityAI.py | 7 + 16 files changed, 501 insertions(+), 333 deletions(-) create mode 100755 direct/src/level/EntityCreatorBase.py create mode 100755 direct/src/level/ZoneEntity.py create mode 100755 direct/src/level/ZoneEntityAI.py diff --git a/direct/src/level/BasicEntities.py b/direct/src/level/BasicEntities.py index fd90119391..4a9d1da87f 100755 --- a/direct/src/level/BasicEntities.py +++ b/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 diff --git a/direct/src/level/DistributedEntity.py b/direct/src/level/DistributedEntity.py index 332bea04bf..747548fba4 100755 --- a/direct/src/level/DistributedEntity.py +++ b/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) diff --git a/direct/src/level/DistributedEntityAI.py b/direct/src/level/DistributedEntityAI.py index c9fc105ce9..d7f4e9d0d6 100755 --- a/direct/src/level/DistributedEntityAI.py +++ b/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') diff --git a/direct/src/level/DistributedInteractiveEntityAI.py b/direct/src/level/DistributedInteractiveEntityAI.py index 0ef449ccda..b0c764c799 100644 --- a/direct/src/level/DistributedInteractiveEntityAI.py +++ b/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, diff --git a/direct/src/level/DistributedLevel.py b/direct/src/level/DistributedLevel.py index 2f699b3e1d..2958654f24 100755 --- a/direct/src/level/DistributedLevel.py +++ b/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]) + def getZoneNode(self, zoneNum): + return self.zoneNum2node[zoneNum] + + 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: - parentId = parent - assert(entity.entId != parentId) + # parent hasn't been created yet; schedule the reparent + self.notify.debug( + 'entity %s requesting reparent to %s, not yet created' % + (entity, parentId)) - 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) - 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] = [] - # 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) - # 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) + 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) diff --git a/direct/src/level/DistributedLevelAI.py b/direct/src/level/DistributedLevelAI.py index 8cac14cdcf..54991979d3 100755 --- a/direct/src/level/DistributedLevelAI.py +++ b/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 diff --git a/direct/src/level/Entity.py b/direct/src/level/Entity.py index b6154e699e..e5d8d080e9 100755 --- a/direct/src/level/Entity.py +++ b/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 diff --git a/direct/src/level/EntityCreator.py b/direct/src/level/EntityCreator.py index fe4b39e192..5a897145b5 100755 --- a/direct/src/level/EntityCreator.py +++ b/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) diff --git a/direct/src/level/EntityCreatorAI.py b/direct/src/level/EntityCreatorAI.py index 9071689687..923e304373 100755 --- a/direct/src/level/EntityCreatorAI.py +++ b/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) diff --git a/direct/src/level/EntityCreatorBase.py b/direct/src/level/EntityCreatorBase.py new file mode 100755 index 0000000000..26d343df34 --- /dev/null +++ b/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) diff --git a/direct/src/level/LevelBase.py b/direct/src/level/LevelBase.py index 0492b8360c..2260a4b5c0 100755 --- a/direct/src/level/LevelBase.py +++ b/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() + 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 makeEntityCreator(self): + def destroyLevel(self): + for entity in self.createdEntities: + entity.destroy() + del self.createdEntities + del self.entities + del self.entId2spec + del self.spec + + def createEntityCreator(self): self.notify.error( 'concrete Level class must override %s' % lineInfo()[2]) - def destroyLevel(self): - del self.entities - del self.entId2Spec - del self.spec + 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'] + 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] + 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] diff --git a/direct/src/level/LevelMgr.py b/direct/src/level/LevelMgr.py index 1fcf1e4763..6959ade787 100755 --- a/direct/src/level/LevelMgr.py +++ b/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): diff --git a/direct/src/level/LevelMgrAI.py b/direct/src/level/LevelMgrAI.py index ba32ba4742..00bdd16dda 100755 --- a/direct/src/level/LevelMgrAI.py +++ b/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() diff --git a/direct/src/level/LogicGateAI.py b/direct/src/level/LogicGateAI.py index 978057c404..761e7d68f3 100755 --- a/direct/src/level/LogicGateAI.py +++ b/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) diff --git a/direct/src/level/ZoneEntity.py b/direct/src/level/ZoneEntity.py new file mode 100755 index 0000000000..fb68d4e884 --- /dev/null +++ b/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 diff --git a/direct/src/level/ZoneEntityAI.py b/direct/src/level/ZoneEntityAI.py new file mode 100755 index 0000000000..6b20882757 --- /dev/null +++ b/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)