"""DistributedLevel.py: contains the DistributedLevel class""" from ClockDelta import * from PythonUtil import Functor, sameElements, list2dict, uniqueElements from IntervalGlobal import * import ToontownGlobals import DistributedObject import Level import LevelConstants import DirectNotifyGlobal import EntityCreator import OnscreenText import Task import LevelUtil import FactoryCameraViews class DistributedLevel(DistributedObject.DistributedObject, Level.Level): """DistributedLevel""" notify = DirectNotifyGlobal.directNotify.newCategory('DistributedLevel') WantVisibility = config.GetBool('level-visibility', 1) HideZones = config.GetBool('level-hidezones', 1) # TODO: move level-model stuff to LevelMgr or FactoryLevelMgr? FloorCollPrefix = 'zoneFloor' OuchTaskName = 'ouchTask' def __init__(self, cr): DistributedObject.DistributedObject.__init__(self, cr) Level.Level.__init__(self) self.lastToonZone = 0 self.titleColor = (1,1,1,1) self.titleText = OnscreenText.OnscreenText( "", fg = self.titleColor, shadow = (0,0,0,1), font = ToontownGlobals.getSuitFont(), pos = (0,-0.5), scale = 0.16, drawOrder = 0, mayChange = 1, ) self.smallTitleText = OnscreenText.OnscreenText( "", fg = self.titleColor, font = ToontownGlobals.getSuitFont(), pos = (0.65,0.9), scale = 0.08, drawOrder = 0, mayChange = 1, bg = (.5,.5,.5,.5), align = TextNode.ARight, ) self.zonesEnteredList = [] self.fColorZones = 0 def generate(self): DistributedLevel.notify.debug('generate') DistributedObject.DistributedObject.generate(self) # this dict stores entity reparents if the parent hasn't been # created yet self.parent2pendingChildren = {} # if the AI sends us a full spec, it will be put here self.curSpec = None # Most (if not all) of the timed entities of levels # run on looping intervals that are started once based on # the level's start time. # This sync request is *NOT* guaranteed to finish by the time # the entities get created. # We should listen for any and all time-sync events and re-sync # all our entities at that time. toonbase.tcr.timeManager.synchronize('DistributedLevel.generate') # add factory menu to SpeedChat toonbase.localToon.chatMgr.chatInputSpeedChat.addFactoryMenu() # add special camera views self.factoryViews = FactoryCameraViews.FactoryCameraViews(self) # the real required fields def setLevelZoneId(self, zoneId): # this is the zone that the level is in; we should listen to this # zone the entire time we're in here self.levelZone = zoneId def setPlayerIds(self, avIdList): self.avIdList = avIdList assert toonbase.localToon.doId in self.avIdList def setEntranceId(self, entranceId): self.entranceId = entranceId def getEntranceId(self): return self.entranceId # "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): DistributedLevel.notify.debug('setZoneIds: %s' % zoneIds) self.zoneIds = zoneIds def setStartTimestamp(self, timestamp): DistributedLevel.notify.debug('setStartTimestamp: %s' % timestamp) self.startTime = globalClockDelta.networkToLocalTime(timestamp,bits=32) def setScenarioIndex(self, scenarioIndex): self.scenarioIndex = scenarioIndex # ugly hack: we treat a few DC fields as if they were required, # and use 'levelAnnounceGenerate()' in place of regular old # announceGenerate(). Note that we have to call # gotAllRequired() in the last 'faux-required' DC update # handler. If you add another field, move this to the last one. self.privGotAllRequired() def privGotAllRequired(self): self.levelAnnounceGenerate() def levelAnnounceGenerate(self): pass def initializeLevel(self, levelSpec): """subclass should call this as soon as it's located its level spec. Must be called after obj has been generated.""" if __debug__: # if we're in debug, give the server the opportunity to send us # a full spec self.candidateSpec = levelSpec self.sendUpdate('requestCurrentLevelSpec', [hash(levelSpec), hash(levelSpec.entTypeReg)]) else: self.privGotSpec(levelSpec) if __debug__: def setSpecDeny(self, reason): DistributedLevel.notify.error(reason) def setSpecSenderDoId(self, doId): DistributedLevel.notify.debug('setSpecSenderDoId: %s' % doId) blobSender = toonbase.tcr.doId2do[doId] def setSpecBlob(specBlob, blobSender=blobSender, self=self): blobSender.sendAck() from LevelSpec import LevelSpec spec = eval(specBlob) if spec is None: spec = self.candidateSpec del self.candidateSpec self.privGotSpec(spec) if blobSender.isComplete(): setSpecBlob(blobSender.getBlob()) else: evtName = self.uniqueName('specDone') blobSender.setDoneEvent(evtName) self.acceptOnce(evtName, setSpecBlob) def privGotSpec(self, levelSpec): Level.Level.initializeLevel(self, self.doId, levelSpec, self.scenarioIndex) # all of the local entities have been created now. # TODO: have any of the distributed entities been created at this point? # there should not be any pending reparents left at this point # TODO: is it possible for a local entity to be parented to a # distributed entity? I think so! assert len(self.parent2pendingChildren) == 0 # make sure the zoneNums from the model match the zoneNums from # the zone entities modelZoneNums = self.zoneNums entityZoneNums = self.zoneNum2entId.keys() if not sameElements(modelZoneNums, entityZoneNums): DistributedLevel.notify.error( 'model zone nums (%s) do not match entity zone nums (%s)\n' 'use SpecUtil.updateSpec' % (modelZoneNums, entityZoneNums)) # load stuff self.initVisibility() self.placeLocalToon() def placeLocalToon(self): # the entrancePoint entities register themselves with us if self.entranceId not in self.entranceId2entity: self.notify.warning('unknown entranceId %s' % self.entranceId) toonbase.localToon.setPos(0,0,0) else: epEnt = self.entranceId2entity[self.entranceId] epEnt.placeToon(toonbase.localToon, self.avIdList.index(toonbase.localToon.doId), len(self.avIdList)) # kickstart the visibility firstZoneEnt = self.getEntity(epEnt.getZoneEntId()) self.enterZone(firstZoneEnt.getZoneNum()) def createEntityCreator(self): """Create the object that will be used to create Entities. Inheritors, override if desired.""" return EntityCreator.EntityCreator(level=self) def onEntityTypePostCreate(self, entType): """listen for certain entity types to be created""" Level.Level.onEntityTypePostCreate(self, entType) # NOTE: these handlers are private in order to avoid overriding # similar handlers in base classes if entType == 'levelMgr': self.__handleLevelMgrCreated() def __handleLevelMgrCreated(self): # as soon as the levelMgr has been created, load up the model # and extract zone info. We need to do this before any entities # get parented to the level! levelMgr = self.getEntity(LevelConstants.LevelMgrEntId) self.geom = levelMgr.geom # find the zones in the model and fix them up self.zoneNum2node = LevelUtil.getZoneNum2Node(self.geom) self.zoneNums = self.zoneNum2node.keys() self.zoneNums.sort() DistributedLevel.notify.debug('zones: %s' % self.zoneNums) # fix up the floor collisions for walkable zones *before* # any entities get put under the model for zoneNum,zoneNode in self.zoneNum2node.items(): # don't do this to the uberzone if zoneNum == LevelConstants.UberZoneNum: continue # if this is a walkable zone, fix up the model allColls = zoneNode.findAllMatches('**/+CollisionNode').asList() # which of them, if any, are floors? floorColls = [] for coll in allColls: bitmask = coll.node().getIntoCollideMask() if not (bitmask & ToontownGlobals.FloorBitmask).isZero(): floorColls.append(coll) if len(floorColls) > 0: # rename the floor collision nodes, and make sure no other # nodes under the ZoneNode have that name floorCollName = '%s%s' % (DistributedLevel.FloorCollPrefix, 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): self.toonEnterZone(zoneNum) floorNode = collisionEntry.getIntoNode() if floorNode.hasTag('ouch'): ouchLevel = int(floorNode.getTag('ouch')) self.startOuch(ouchLevel*2) self.accept('enter%s' % floorCollName, handleZoneEnter) # also listen for zone exit events for the sake of the # ouch system def handleZoneExit(collisionEntry, self=self, zoneNum=zoneNum): floorNode = collisionEntry.getIntoNode() if floorNode.hasTag('ouch'): self.stopOuch() self.accept('exit%s' % floorCollName, handleZoneExit) def announceGenerate(self): DistributedLevel.notify.debug('announceGenerate') DistributedObject.DistributedObject.announceGenerate(self) def disable(self): DistributedLevel.notify.debug('disable') # geom is owned by the levelMgr if hasattr(self, 'geom'): del self.geom self.destroyLevel() DistributedObject.DistributedObject.disable(self) self.ignoreAll() # NOTE: this should be moved to FactoryInterior if self.smallTitleText: self.smallTitleText.cleanup() self.smallTitleText = None if self.titleText: self.titleText.cleanup() self.titleText = None self.zonesEnteredList = [] # NOTE: this should be moved to ZoneEntity.disable toonbase.localToon.chatMgr.chatInputSpeedChat.removeFactoryMenu() def delete(self): DistributedLevel.notify.debug('delete') DistributedObject.DistributedObject.delete(self) # remove factory menu to SpeedChat toonbase.localToon.chatMgr.chatInputSpeedChat.removeFactoryMenu() # remove special camera views del self.factoryViews def getZoneNode(self, zoneNum): return self.zoneNum2node[zoneNum] def requestReparent(self, entity, parentId): if __debug__: # some things (like cogs) are not actually entities yet; # they don't have an entId. Big deal, let it go through. if hasattr(entity, 'entId'): assert(entity.entId != parentId) parent = self.getEntity(parentId) if parent is not None: # parent has already been created entity.reparentTo(parent.getNodePath()) else: # parent hasn't been created yet; schedule the reparent DistributedLevel.notify.debug( 'entity %s requesting reparent to %s, not yet created' % (entity, parentId)) entity.reparentTo(hidden) # if this parent doesn't already have another child pending, # do some setup if not self.parent2pendingChildren.has_key(parentId): self.parent2pendingChildren[parentId] = [] # do the reparent(s) once the parent is initialized def doReparent(parentId=parentId, self=self): assert self.parent2pendingChildren.has_key(parentId) parent=self.getEntity(parentId) for child in self.parent2pendingChildren[parentId]: DistributedLevel.notify.debug( 'performing pending reparent of %s to %s' % (child, parent)) child.reparentTo(parent.getNodePath()) del self.parent2pendingChildren[parentId] self.ignore(self.getEntityCreateEvent(parentId)) self.accept(self.getEntityCreateEvent(parentId), doReparent) self.parent2pendingChildren[parentId].append(entity) def showZone(self, zoneNum): zone = self.zoneNum2node[zoneNum] zone.unstash() zone.clearColor() def setColorZones(self, fColorZones): self.fColorZones = fColorZones def getColorZones(self): return self.fColorZones def hideZone(self, zoneNum): zone = self.zoneNum2node[zoneNum] if self.fColorZones: zone.unstash() zone.setColor(1,0,0) else: zone.stash() def setTransparency(self, alpha, zone=None): self.geom.setTransparency(1) if zone is None: node = self.geom else: 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 # zones' 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() prefixLen = len(DistributedLevel.FloorCollPrefix) if (name[:prefixLen] == DistributedLevel.FloorCollPrefix): try: zoneNum = int(name[prefixLen:]) except: DistributedLevel.notify.debug( 'Invalid zone 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: zoneNums = list(self.zoneNums) zoneNums.remove(LevelConstants.UberZoneNum) self.setVisibility(zoneNums) def toonEnterZone(self, zoneNum, ouchLevel=None): DistributedLevel.notify.debug('toonEnterZone%s' % zoneNum) if zoneNum != self.lastToonZone: self.lastToonZone = zoneNum print "made zone transition to %s" % zoneNum messenger.send("factoryZoneChanged", [zoneNum]) self.smallTitleText.hide() self.spawnTitleText() def camEnterZone(self, zoneNum): DistributedLevel.notify.debug('camEnterZone%s' % zoneNum) self.enterZone(zoneNum) def enterZone(self, zoneNum): DistributedLevel.notify.debug("entering zone %s" % zoneNum) if not DistributedLevel.WantVisibility: return if zoneNum == self.curZoneNum: return if zoneNum not in self.zoneNum2entId: DistributedLevel.notify.error( 'no ZoneEntity for this zone (%s)!!' % zoneNum) self.updateVisibility(zoneNum) def updateVisibility(self, zoneNum=None): """update the visibility assuming that we're in the specified zone; don't check to see if it's the zone we're already in""" if zoneNum is None: zoneNum = self.curZoneNum zoneEntId = self.zoneNum2entId[zoneNum] zoneEnt = self.getEntity(zoneEntId) # use dicts to efficiently ensure that there are no duplicates visibleZoneNums = list2dict([zoneNum]) visibleZoneNums.update(list2dict(zoneEnt.getVisibleZoneNums())) # we should not have the uberZone in the list at this point assert not 0 in visibleZoneNums if DistributedLevel.HideZones: # figure out which zones are new and which are going invisible # use dicts because it's faster to use dict.has_key(x) # than 'x in list' addedZoneNums = [] removedZoneNums = [] allVZ = dict(visibleZoneNums) allVZ.update(self.curVisibleZoneNums) for vz,dummy in allVZ.items(): new = vz in visibleZoneNums old = vz in self.curVisibleZoneNums if new and old: continue if new: addedZoneNums.append(vz) else: removedZoneNums.append(vz) # show the new, hide the old DistributedLevel.notify.debug('showing zones %s' % addedZoneNums) for az in addedZoneNums: self.showZone(az) DistributedLevel.notify.debug('hiding zones %s' % removedZoneNums) for rz in removedZoneNums: self.hideZone(rz) self.setVisibility(visibleZoneNums.keys()) self.curZoneNum = zoneNum self.curVisibleZoneNums = visibleZoneNums def setVisibility(self, vizList): """ vizList is a list of visible zone numbers. """ # convert the zone numbers into their actual zoneIds # always include Toontown and factory uberZones uberZone = self.getZoneId(zoneNum=LevelConstants.UberZoneNum) # the level itself is in the 'level zone' visibleZoneIds = [ToontownGlobals.UberZone, self.levelZone, uberZone] for vz in vizList: visibleZoneIds.append(self.getZoneId(zoneNum=vz)) assert(uniqueElements(visibleZoneIds)) DistributedLevel.notify.debug('new viz list: %s' % visibleZoneIds) toonbase.tcr.sendSetZoneMsg(self.levelZone, visibleZoneIds) def resetVisibility(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 # zones' viz lists del self.curVisibleZoneNums[0] # Make sure every zone is visible for vz,dummy in self.curVisibleZoneNums.items(): self.showZone(vz) # Redo visibility using current zone num self.updateVisibility() if __debug__: # level editing stuff def setAttribChange(self, entId, attribName, valueStr, username): """every time the spec is edited, we get this message from the AI""" value = eval(valueStr) self.levelSpec.setAttribChange(entId, attribName, value, username) def handleVisChange(self): """the zone visibility lists have changed""" Level.Level.handleVisChange(self) self.updateVisibility() def spawnTitleText(self): def getDescription(zoneId, self=self): entId = self.zoneNum2entId.get(zoneId) if entId: ent = self.entities.get(entId) if ent and hasattr(ent, 'description'): return ent.description return None description = getDescription(self.lastToonZone) if description and description != '': taskMgr.remove("titleText") self.smallTitleText.setText(description) self.titleText.setText(description) self.titleText.setColor(Vec4(*self.titleColor)) self.titleText.setFg(self.titleColor) # Only show the big title once per session. # If we've already seen it, just show the small title titleSeq = None if not self.lastToonZone in self.zonesEnteredList: self.zonesEnteredList.append(self.lastToonZone) titleSeq = Task.sequence( Task.Task(self.hideSmallTitleTextTask), Task.Task(self.showTitleTextTask), Task.pause(0.1), Task.pause(6.0), self.titleText.lerpColor(Vec4(self.titleColor[0], self.titleColor[1], self.titleColor[2], self.titleColor[3]), Vec4(self.titleColor[0], self.titleColor[1], self.titleColor[2], 0.0), 0.5), ) smallTitleSeq = Task.sequence(Task.Task(self.hideTitleTextTask), Task.Task(self.showSmallTitleTask), Task.Task(self.showSmallTitleTask)) if titleSeq: seq = Task.sequence(titleSeq, smallTitleSeq) else: seq = smallTitleSeq taskMgr.add(seq, "titleText") def showTitleTextTask(self, task): assert(DistributedLevel.notify.debug("hideTitleTextTask()")) self.titleText.show() return Task.done def hideTitleTextTask(self, task): assert(DistributedLevel.notify.debug("hideTitleTextTask()")) self.titleText.hide() return Task.done def showSmallTitleTask(self, task): # make sure large title is hidden self.titleText.hide() # show the small title self.smallTitleText.show() return Task.done def hideSmallTitleTextTask(self, task): assert(DistributedLevel.notify.debug("hideTitleTextTask()")) self.smallTitleText.hide() return Task.done # Ouch! def startOuch(self, ouchLevel, period=2): print 'startOuch %s' % ouchLevel if not hasattr(self, 'doingOuch'): def doOuch(task, self=self, ouchLevel=ouchLevel, period=period): self.b_setOuch(ouchLevel) self.lastOuchTime = globalClock.getFrameTime() taskMgr.doMethodLater(period, doOuch, DistributedLevel.OuchTaskName) # check to make sure we haven't done an ouch too recently delay = 0 if hasattr(self, 'lastOuchTime'): curFrameTime = globalClock.getFrameTime() timeSinceLastOuch = (curFrameTime - self.lastOuchTime) if timeSinceLastOuch < period: delay = period - timeSinceLastOuch if delay > 0: taskMgr.doMethodLater( period, doOuch, DistributedLevel.OuchTaskName) else: doOuch(None) self.doingOuch = 1 def stopOuch(self): if hasattr(self, 'doingOuch'): taskMgr.remove(DistributedLevel.OuchTaskName) del self.doingOuch def b_setOuch(self, penalty, anim=None): self.notify.debug('b_setOuch %s' % penalty) av = toonbase.localToon # play the stun track (flashing toon) if not av.isStunned: self.d_setOuch(penalty) self.setOuch(penalty, anim) def d_setOuch(self, penalty): self.sendUpdate("setOuch", [penalty]) def setOuch(self, penalty, anim = None): if anim == "Squish": toonbase.tcr.playGame.getPlace().fsm.request('squished') av = toonbase.localToon av.stunToon() av.playDialogueForString("!")