"""Level.py: contains the Level class""" import DirectNotifyGlobal import string import LevelConstants from PythonUtil import lineInfo, uniqueElements import types """ 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 constant (there are at least three places where the entire editing interface must be duplicated). 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 Level: """Level: representation of a game level, keeps track of all of the entities and their interrelations, and creates and destroys entities""" notify = DirectNotifyGlobal.directNotify.newCategory('Level') def __init__(self): self.levelSpec = None self.initialized = 0 def initializeLevel(self, levelId, levelSpec, scenarioIndex): """ subclass should call this as soon as it has located its spec data """ self.levelId = levelId self.levelSpec = levelSpec self.scenarioIndex = scenarioIndex self.levelSpec.setScenario(self.scenarioIndex) if __debug__: self.levelSpec.setLevel(self) # create some handy tables # entranceId to entrance entity self.entranceId2entity = {} # dict of entId -> list of callbacks to be called upon creation self.entId2createCallbacks = {} # this list contains the entIds of entities that we have actually # created, in order of creation self.createdEntIds = [] # get an entity creator object self.entityCreator = self.createEntityCreator() # entity type -> list of entIds self.entType2ids = self.levelSpec.getEntType2ids( self.levelSpec.getAllEntIds()) # create empty list for any entity types that are not represented # in the spec for entType in self.entityCreator.getEntityTypes(): self.entType2ids.setdefault(entType, []) # create all the entities # TODO: maybe we should leave this to a subclass or the level user self.createAllEntities(priorityTypes=['levelMgr','zone','propSpinner']) # check on the singleton entities # we make our own references to them rather than expect them to # create the references so that the editor can create dummy # do-nothing entities # there should be one and only one levelMgr assert len(self.entType2ids['levelMgr']) == 1 assert self.entType2ids['levelMgr'][0] == LevelConstants.LevelMgrEntId self.levelMgrEntity = self.getEntity(LevelConstants.LevelMgrEntId) if __debug__: # there should be one and only one editMgr assert len(self.entType2ids['editMgr']) == 1 assert self.entType2ids['editMgr'][0] == \ LevelConstants.EditMgrEntId self.editMgrEntity = self.getEntity(LevelConstants.EditMgrEntId) # there should be one and only one UberZone assert LevelConstants.UberZoneEntId in self.entType2ids['zone'] self.uberZoneEntity = self.getEntity(LevelConstants.UberZoneEntId) self.initialized = 1 def isInitialized(self): return self.initialized def destroyLevel(self): self.destroyAllEntities() self.initialized = 0 del self.createdEntIds if hasattr(self, 'entities'): del self.entities if hasattr(self, 'levelSpec'): del self.levelSpec def createEntityCreator(self): Level.notify.error( 'concrete Level class must override %s' % lineInfo()[2]) 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 = {} # get list of all entity types we need to create entTypes = self.entityCreator.getEntityTypes() self.onLevelPreCreate() # 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) self.onLevelPostCreate() def destroyAllEntities(self): assert uniqueElements(self.createdEntIds) # destroy the entities in reverse order while len(self.createdEntIds) > 0: entId = self.createdEntIds.pop() entity = self.getEntity(entId) if entity is not None: Level.notify.debug('destroying %s %s' % ( self.getEntityType(entId), entId)) entity.destroy() assert not entId in self.entities else: Level.notify.error('trying to destroy entity %s, but ' 'it is already gone' % entId) def createAllEntitiesOfType(self, entType): """creates all entities of a given type""" assert entType in self.entityCreator.getEntityTypes() self.onEntityTypePreCreate(entType) for entId in self.entType2ids[entType]: self.createEntity(entId) self.onEntityTypePostCreate(entType) def createEntity(self, entId): assert not entId in self.createdEntIds spec = self.levelSpec.getEntitySpec(entId) Level.notify.debug('creating %s %s' % (spec['type'], entId)) entity = self.entityCreator.createEntity(entId) # NOTE: the entity is not considered to really be created until # it has all of its initial spec data; see 'initializeEntity' # below. if entity is not 'nothing': assert uniqueElements(self.createdEntIds) assert entId not in self.createdEntIds self.createdEntIds.append(entId) # call the create handler # we used to do this in initializeEntity, but that did not # allow for additional initialization to be performed in # derived entity __init__ funcs before their presence was announced # Note that now DistributedEntity's are responsible for calling # this for themselves self.onEntityCreate(entId) return entity def initializeEntity(self, entity): """populate an entity with its spec data. This is not done in createEntity in order to allow other pieces of code to create entities; this is called directly by Entity. """ entId = entity.entId spec = self.levelSpec.getEntitySpec(entId) # on initialization, set items directly on entity for key,value in spec.items(): if key in ('type', 'name', 'comment',): continue entity.setAttribInit(key, value) # entity is initialized, add it to the list of entities # if this assert fails, check distributed entities to make sure # they're calling down to Entity.destroy if __debug__: if entId in self.entities: self.notify.warning( 'entity %s already in entity table... ' 'make sure distributedEntity is calling down to ' 'Entity.destroy!') self.entities[entId] = entity def getEntity(self, entId): return self.entities.get(entId) def getEntityType(self, entId): return self.levelSpec.getEntityType(entId) def getEntityZoneEntId(self, entId): """return entId of zone that contains the entity""" return self.levelSpec.getEntityZoneEntId(entId) def getEntityZoneNum(self, entId): """return the model zoneNum of zone that contains the entity""" return self.levelSpec.getEntityZoneNum(entId) def getEntityZoneId(self, entId): """return network zoneId of zone that contains the entity""" # this is called during entity creation on the AI; we have to # handle this carefully, since the information required to # produce a zoneId is not available until the level's zone # entities have been instantiated. zoneEntId = self.getEntityZoneEntId(entId) # fundamental entities (levelMgr) are responsible for creating # tables like 'zoneEntId2zoneId'; if those tables haven't been # created yet, just return None if not hasattr(self, 'zoneNum2zoneId'): return None # this might return None if all of our zone entities haven't # been created yet. this could be a problem if zone entities # are ever distributed. it also means that no distributed entities # should be created before the zone entities. return self.zoneEntId2zoneId.get(zoneEntId) 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] # these events are thrown as the level initializes itself # LEVEL def getLevelPreCreateEvent(self): """This is the event that is thrown immediately before the level creates its entities.""" return 'levelPreCreate-%s' % (self.levelId) def getLevelPostCreateEvent(self): """This is the event that is thrown immediately after the level creates its entities.""" return 'levelPostCreate-%s' % (self.levelId) # ENTITY TYPE def getEntityTypePreCreateEvent(self, entType): """This is the event that is thrown immediately before the level creates the entities of the given type.""" return 'entityTypePreCreate-%s-%s' % (self.levelId, entType) def getEntityTypePostCreateEvent(self, entType): """This is the event that is thrown immediately after the level creates the entities of the given type.""" return 'entityTypePostCreate-%s-%s' % (self.levelId, entType) # ENTITY def getEntityCreateEvent(self, entId): """This is the event that is thrown immediately after a particular entity is initialized""" return 'entityCreate-%s-%s' % (self.levelId, entId) 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) # these handlers are called as the level initializes itself # LEVEL def onLevelPreCreate(self): """Level is about to create its entities""" messenger.send(self.getLevelPreCreateEvent()) def onLevelPostCreate(self): """Level is done creating its entities""" messenger.send(self.getLevelPostCreateEvent()) # ENTITY TYPE def onEntityTypePreCreate(self, entType): """Level is about to create these entities""" messenger.send(self.getEntityTypePreCreateEvent(entType)) def onEntityTypePostCreate(self, entType): """Level has just created these entities""" messenger.send(self.getEntityTypePostCreateEvent(entType)) # ENTITY def onEntityCreate(self, entId): """Level has just created this entity""" # send the entity-create event messenger.send(self.getEntityCreateEvent(entId)) # send the entity-of-type create event messenger.send( self.getEntityOfTypeCreateEvent(self.getEntityType(entId)), [entId]) # call any callbacks if entId in self.entId2createCallbacks: for callback in self.entId2createCallbacks[entId]: callback() del self.entId2createCallbacks[entId] # Use to set a callback to be called when entity is created. # If entity already exists, callback will be called immediately. def setEntityCreateCallback(self, entId, callback): ent = self.getEntity(entId) if ent is not None: callback() return self.entId2createCallbacks.setdefault(entId, []) self.entId2createCallbacks[entId].append(callback) # these are events and handlers that are invoked as entities are destroyed def getEntityDestroyEvent(self, entId): """This is the event that is thrown immediately before an entity is destroyed""" return 'entityDestroy-%s-%s' % (self.levelId, entId) def onEntityDestroy(self, entId): """Level is about to destroy this entity""" assert entId in self.entities # send the entity-destroy event messenger.send(self.getEntityDestroyEvent(entId)) del self.entities[entId] # if we created this entity, remove its entId from the # createdEntIds list if entId in self.createdEntIds: # this should only happen if someone deleted an entity # with an editor self.createdEntIds.remove(entId) if __debug__: # the level generates these events when the spec changes def getAttribChangeEventName(self): return 'attribChange-%s' % self.levelId def getInsertEntityEventName(self): return 'insertEntity-%s' % self.levelId def getRemoveEntityEventName(self): return 'removeEntity-%s' % self.levelId # these handlers are called directly by our levelSpec def handleAttribChange(self, entId, attrib, value, username=None): entity = self.getEntity(entId) # the entity might be AI- or client-only if entity is not None: entity.handleAttribChange(attrib, value) messenger.send(self.getAttribChangeEventName(), [entId, attrib, value, username]) def setEntityCreatorUsername(self, entId, editUsername): # this is called just before an entity is inserted, with the # entId of the new entity and the username of the editor # that requested its creation. pass def handleEntityInsert(self, entId): # update our local type->entId table self.entType2ids[self.getEntityType(entId)].append(entId) self.createEntity(entId) messenger.send(self.getInsertEntityEventName(), [entId]) def handleEntityRemove(self, entId): messenger.send(self.getRemoveEntityEventName(), [entId]) # if we didn't create it, don't destroy it (probably a distributed # entity on the client; wait for AI to destroy it) if entId in self.createdEntIds: entity = self.getEntity(entId) entity.destroy() # update our local type->entId table self.entType2ids[self.getEntityType(entId)].remove(entId) def handleVisChange(self): """the zone visibility lists have changed""" pass