panda3d/direct/src/level/DistributedLevel.py
2003-12-10 00:53:35 +00:00

762 lines
29 KiB
Python
Executable File

"""DistributedLevel.py: contains the DistributedLevel class"""
from ClockDelta import *
from PandaModules import *
from PythonUtil import Functor, sameElements, list2dict, uniqueElements
from IntervalGlobal import *
from ToontownMsgTypes 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'
VisChangeTaskName = 'visChange'
def __init__(self, cr):
DistributedObject.DistributedObject.__init__(self, cr)
Level.Level.__init__(self)
self.lastToonZone = 0
self.lastCamZone = 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
# we use these to track setZone requests
self.setZonesRequested = 0
self.setZonesReceived = 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 __dev__:
# if we're in dev, give the server the opportunity to send us
# a full spec
self.candidateSpec = levelSpec
self.sendUpdate('requestCurrentLevelSpec',
[hash(levelSpec),
levelSpec.entTypeReg.getHashStr()])
else:
self.privGotSpec(levelSpec)
if __dev__:
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 announceLeaving(self):
"""call this just before leaving the level; this may result in
the factory being destroyed on the AI"""
self.doneBarrier()
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.shutdownVisibility()
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 = []
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
# make sure the ouch task is stopped
self.stopOuch()
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
self.visChangedThisFrame = 0
# listen for camera-ray/floor collision events
def handleCameraRayFloorCollision(collEntry, self=self):
name = collEntry.getIntoNode().getName()
print 'camera floor ray collided with: %s' % name
prefixLen = len(DistributedLevel.FloorCollPrefix)
if (name[:prefixLen] == DistributedLevel.FloorCollPrefix):
try:
zoneNum = int(name[prefixLen:])
except:
DistributedLevel.notify.warning(
'Invalid zone floor collision node: %s'
% name)
else:
self.camEnterZone(zoneNum)
self.accept('on-floor', handleCameraRayFloorCollision)
# register our datagram handler to listen for setZone msgs
self.oldTcrHandler = toonbase.tcr.handler
toonbase.tcr.handler = self.handleDatagram
# if no viz, listen to all the zones
if not DistributedLevel.WantVisibility:
zoneNums = list(self.zoneNums)
zoneNums.remove(LevelConstants.UberZoneNum)
self.setVisibility(zoneNums)
# send out any zone changes at the end of the frame, just before
# rendering
taskMgr.add(self.visChangeTask,
self.uniqueName(DistributedLevel.VisChangeTaskName),
priority=49)
def shutdownVisibility(self):
taskMgr.remove(self.uniqueName(DistributedLevel.VisChangeTaskName))
if toonbase.tcr.handler == self.handleDatagram:
toonbase.tcr.handler = self.oldTcrHandler
del self.oldTcrHandler
def getSetZoneCompleteEvent(self, num):
return self.uniqueName('setZoneComplete-%s' % num)
def getNextSetZoneCompleteEvent(self):
return self.uniqueName('setZoneComplete-%s' % self.setZonesRequested)
def handleDatagram(self, msgType, di):
if msgType == CLIENT_DONE_SET_ZONE_RESP:
# snoop to see what zone we're talking about
di2 = DatagramIterator(di)
zone = di2.getUint32()
if zone != self.levelZone:
self.notify.warning('got setZoneComplete for unknown zone %s' %
zone)
else:
self.notify.info('setZone #%s complete' % self.setZonesReceived)
messenger.send(self.getSetZoneCompleteEvent(
self.setZonesReceived))
self.setZonesReceived += 1
if self.oldTcrHandler is None:
toonbase.tcr.handleUnexpectedMsgType(msgType, di)
else:
self.oldTcrHandler(msgType, di)
def toonEnterZone(self, zoneNum, ouchLevel=None):
"""
zoneNum is an int.
ouchLevel is a ??.
The avatar (and not necessarily the camera) has entered
a zone.
See camEnterZone()
"""
DistributedLevel.notify.info('toonEnterZone%s' % zoneNum)
if zoneNum != self.lastToonZone:
self.lastToonZone = zoneNum
print "toon is standing in zone %s" % zoneNum
messenger.send("factoryZoneChanged", [zoneNum])
def camEnterZone(self, zoneNum):
"""
zoneNum is an int.
The camera (and not necessarily the avatar) has entered
a zone.
See toonEnterZone()
"""
DistributedLevel.notify.info('camEnterZone%s' % zoneNum)
self.enterZone(zoneNum)
if zoneNum != self.lastCamZone:
self.lastCamZone = zoneNum
self.smallTitleText.hide()
self.spawnTitleText()
def lockVisibility(self, zoneNum=None, zoneId=None):
"""call this to lock the visibility to a particular zone
pass in either network zoneId or model zoneNum
this was added for battles in the HQ factories; if you engage a suit
in zone A with your camera in zone B, and you don't call this func,
your client will remain in zone B. If there's a door between A and B,
and it closes, zone B might disappear, along with the suit and the
battle objects.
"""
assert (zoneNum is None) or (zoneId is None)
assert not ((zoneNum is None) and (zoneId is None))
if zoneId is not None:
zoneNum = self.getZoneNumFromId(zoneId)
self.notify.info('lockVisibility to zoneNum %s' % zoneNum)
self.lockVizZone = zoneNum
self.enterZone(self.lockVizZone)
def unlockVisibility(self):
"""release the visibility lock"""
self.notify.info('unlockVisibility')
if not hasattr(self, 'lockVizZone'):
self.notify.warning('visibility already unlocked')
else:
del self.lockVizZone
self.updateVisibility()
def enterZone(self, zoneNum):
DistributedLevel.notify.info("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"""
#print 'updateVisibility %s' % globalClock.getFrameCount()
if zoneNum is None:
zoneNum = self.curZoneNum
if hasattr(self, 'lockVizZone'):
zoneNum = self.lockVizZone
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.info('showing zones %s' % addedZoneNums)
for az in addedZoneNums:
self.showZone(az)
DistributedLevel.notify.info('hiding zones %s' % removedZoneNums)
for rz in removedZoneNums:
self.hideZone(rz)
self.setVisibility(visibleZoneNums.keys())
self.setZonesRequested += 1
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.info('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()
def handleVisChange(self):
"""the zone visibility lists have changed on-the-fly"""
Level.Level.handleVisChange(self)
self.visChangedThisFrame = 1
def visChangeTask(self, task):
# this runs just before igloop; if viz lists have changed
# this frame, updates the visibility and sends out a setZoneMsg
if self.visChangedThisFrame:
self.updateVisibility()
self.visChangedThisFrame = 0
return Task.cont
if __dev__:
# 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 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.lastCamZone)
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.lastCamZone in self.zonesEnteredList:
self.zonesEnteredList.append(self.lastCamZone)
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("!")