From 0f7a02e1d3b872f91780e310efe680b5fbc33adc Mon Sep 17 00:00:00 2001 From: Darren Ranalli Date: Mon, 6 Oct 2003 22:50:39 +0000 Subject: [PATCH] AI sends edited spec to clients, level is in its own zone to prevent race conditions --- direct/src/level/DistributedLevel.py | 111 +++++++++++++++++-------- direct/src/level/DistributedLevelAI.py | 35 +++++--- direct/src/level/EntityTypeRegistry.py | 2 + direct/src/level/EntityTypes.py | 1 - direct/src/level/Level.py | 7 ++ direct/src/level/ZoneEntityAI.py | 11 +-- 6 files changed, 113 insertions(+), 54 deletions(-) diff --git a/direct/src/level/DistributedLevel.py b/direct/src/level/DistributedLevel.py index 43a710dc3f..6827c79b9c 100755 --- a/direct/src/level/DistributedLevel.py +++ b/direct/src/level/DistributedLevel.py @@ -51,13 +51,16 @@ class DistributedLevel(DistributedObject.DistributedObject, self.zonesEnteredList = [] def generate(self): - self.notify.debug('generate') + DistributedLevel.notify.debug('generate') DistributedObject.DistributedObject.generate(self) # this dict stores entity reparents if the parent hasn't been # created yet self.parent2ChildIds = {} + # 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. @@ -70,38 +73,77 @@ class DistributedLevel(DistributedObject.DistributedObject, # add factory menu to SpeedChat toonbase.localToon.chatMgr.chatInputSpeedChat.addFactoryMenu() + # 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 + # "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) + DistributedLevel.notify.debug('setZoneIds: %s' % zoneIds) self.zoneIds = zoneIds def setStartTimestamp(self, timestamp): - self.notify.debug('setStartTimestamp: %s' % 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 these DC fields as if they were required, + # 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 - # levelAnnounceGenerate() in the last 'faux-required' DC update + # gotAllRequired() in the last 'faux-required' DC update # handler. If you add another field, move this to the last one. - self.levelAnnounceGenerate() + if __debug__: + # if we're in debug, ask the server if it wants to send us + # a full spec + self.sendUpdate('requestCurrentLevelSpec', []) + else: + self.gotAllRequired() + if __debug__: + 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 + self.curSpec = eval(specBlob) + self.gotAllRequired() + + if blobSender.isComplete(): + setSpecBlob(blobSender.getBlob()) + else: + evtName = self.uniqueName('specDone') + blobSender.setDoneEvent(evtName) + self.acceptOnce(evtName, setSpecBlob) + + def gotAllRequired(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.""" - Level.Level.initializeLevel(self, self.doId, - levelSpec, self.scenarioIndex) + # if the AI sent us a full spec, use it instead + if self.curSpec is not None: + levelSpec = self.curSpec + 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? - # all of the entities have been created now. # 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.parent2ChildIds) == 0 # make sure the zoneNums from the model match the zoneNums from # the zone entities @@ -137,7 +179,7 @@ class DistributedLevel(DistributedObject.DistributedObject, num2node = {} for potentialNode in potentialNodes: name = potentialNode.getName() - self.notify.debug('potential match for %s: %s' % + DistributedLevel.notify.debug('potential match for %s: %s' % (baseString, name)) try: num = int(name[len(baseString):]) @@ -155,7 +197,7 @@ class DistributedLevel(DistributedObject.DistributedObject, self.zoneNums = self.zoneNum2node.keys() self.zoneNums.sort() - self.notify.debug('zones: %s' % self.zoneNums) + DistributedLevel.notify.debug('zones: %s' % self.zoneNums) # fix up the floor collisions for walkable zones *before* # any entities get put under the model @@ -196,20 +238,21 @@ class DistributedLevel(DistributedObject.DistributedObject, self.doorwayNum2Node = findNumberedNodes('Doorway') def announceGenerate(self): - self.notify.debug('announceGenerate') + DistributedLevel.notify.debug('announceGenerate') DistributedObject.DistributedObject.announceGenerate(self) def disable(self): - self.notify.debug('disable') + DistributedLevel.notify.debug('disable') # geom is owned by the levelMgr - del self.geom + if hasattr(self, 'geom'): + del self.geom self.destroyLevel() DistributedObject.DistributedObject.disable(self) self.ignoreAll() - # NOTE: this should be moved to FactoryInterior + # NOTE: this should be moved to FactoryInterior if self.smallTitleText: self.smallTitleText.cleanup() self.smallTitleText = None @@ -218,11 +261,11 @@ class DistributedLevel(DistributedObject.DistributedObject, self.titleText = None self.zonesEnteredList = [] - # NOTE: this should be moved to ZoneEntity.disable + # NOTE: this should be moved to ZoneEntity.disable toonbase.localToon.chatMgr.chatInputSpeedChat.removeFactoryMenu() def delete(self): - self.notify.debug('delete') + DistributedLevel.notify.debug('delete') DistributedObject.DistributedObject.delete(self) # remove factory menu to SpeedChat toonbase.localToon.chatMgr.chatInputSpeedChat.removeFactoryMenu() @@ -242,7 +285,7 @@ class DistributedLevel(DistributedObject.DistributedObject, entity.reparentTo(parent.getNodePath()) else: # parent hasn't been created yet; schedule the reparent - self.notify.debug( + DistributedLevel.notify.debug( 'entity %s requesting reparent to %s, not yet created' % (entity, parentId)) @@ -260,7 +303,7 @@ class DistributedLevel(DistributedObject.DistributedObject, parent=self.getEntity(parentId) for entId in self.parent2ChildIds[parentId]: entity=self.getEntity(entId) - self.notify.debug( + DistributedLevel.notify.debug( 'performing pending reparent of %s to %s' % (entity, parent)) entity.reparentTo(parent.getNodePath()) @@ -302,8 +345,9 @@ class DistributedLevel(DistributedObject.DistributedObject, try: zoneNum = int(name[prefixLen:]) except: - self.notify.debug('Invalid zone floor collision node: %s' - % name) + DistributedLevel.notify.debug( + 'Invalid zone floor collision node: %s' + % name) else: self.camEnterZone(zoneNum) self.accept('on-floor', handleCameraRayFloorCollision) @@ -315,7 +359,7 @@ class DistributedLevel(DistributedObject.DistributedObject, self.setVisibility(zoneNums) def toonEnterZone(self, zoneNum): - self.notify.debug('toonEnterZone%s' % zoneNum) + DistributedLevel.notify.debug('toonEnterZone%s' % zoneNum) if zoneNum != self.lastToonZone: self.lastToonZone = zoneNum print "made zone transition to %s" % zoneNum @@ -324,11 +368,11 @@ class DistributedLevel(DistributedObject.DistributedObject, self.spawnTitleText() def camEnterZone(self, zoneNum): - self.notify.debug('camEnterZone%s' % zoneNum) + DistributedLevel.notify.debug('camEnterZone%s' % zoneNum) self.enterZone(zoneNum) def enterZone(self, zoneNum): - self.notify.debug("entering zone %s" % zoneNum) + DistributedLevel.notify.debug("entering zone %s" % zoneNum) if not DistributedLevel.WantVisibility: return @@ -360,10 +404,10 @@ class DistributedLevel(DistributedObject.DistributedObject, else: removedZoneNums.append(vz) # show the new, hide the old - self.notify.debug('showing zones %s' % addedZoneNums) + DistributedLevel.notify.debug('showing zones %s' % addedZoneNums) for az in addedZoneNums: self.showZone(az) - self.notify.debug('hiding zones %s' % removedZoneNums) + DistributedLevel.notify.debug('hiding zones %s' % removedZoneNums) for rz in removedZoneNums: self.hideZone(rz) @@ -376,14 +420,15 @@ 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(zoneNum=Level.Level.UberZoneNum) - visibleZoneIds = [ToontownGlobals.UberZone, factoryUberZone] + uberZone = self.getZoneId(zoneNum=Level.Level.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)) - self.notify.debug('new viz list: %s' % visibleZoneIds) + DistributedLevel.notify.debug('new viz list: %s' % visibleZoneIds) - toonbase.tcr.sendSetZoneMsg(factoryUberZone, visibleZoneIds) + toonbase.tcr.sendSetZoneMsg(self.levelZone, visibleZoneIds) if __debug__: # level editing stuff @@ -455,12 +500,12 @@ class DistributedLevel(DistributedObject.DistributedObject, taskMgr.add(seq, "titleText") def showTitleTextTask(self, task): - assert(self.notify.debug("hideTitleTextTask()")) + assert(DistributedLevel.notify.debug("hideTitleTextTask()")) self.titleText.show() return Task.done def hideTitleTextTask(self, task): - assert(self.notify.debug("hideTitleTextTask()")) + assert(DistributedLevel.notify.debug("hideTitleTextTask()")) self.titleText.hide() return Task.done @@ -472,7 +517,7 @@ class DistributedLevel(DistributedObject.DistributedObject, return Task.done def hideSmallTitleTextTask(self, task): - assert(self.notify.debug("hideTitleTextTask()")) + assert(DistributedLevel.notify.debug("hideTitleTextTask()")) self.smallTitleText.hide() return Task.done diff --git a/direct/src/level/DistributedLevelAI.py b/direct/src/level/DistributedLevelAI.py index 7e1eee4abf..6f0a78f4ff 100755 --- a/direct/src/level/DistributedLevelAI.py +++ b/direct/src/level/DistributedLevelAI.py @@ -1,5 +1,6 @@ """DistributedLevelAI.py: contains the DistributedLevelAI class""" +from AIBaseGlobal import * from ClockDelta import * import DistributedObjectAI import Level @@ -15,7 +16,9 @@ class DistributedLevelAI(DistributedObjectAI.DistributedObjectAI, def __init__(self, air, zoneId): DistributedObjectAI.DistributedObjectAI.__init__(self, air) Level.Level.__init__(self) - self.uberZoneId = zoneId + # this is one of the required fields + self.zoneId = zoneId + self.hasBeenEdited = 0 def generate(self, levelSpec): self.notify.debug('generate') @@ -27,6 +30,12 @@ class DistributedLevelAI(DistributedObjectAI.DistributedObjectAI, self.sendUpdate('setStartTimestamp', [self.startTimestamp]) self.sendUpdate('setScenarioIndex', [self.scenarioIndex]) + def getLevelZoneId(self): + """no entities should be generated in the level's zone; it causes + nasty race conditions on the client if there are entities in the + same zone with the level""" + return self.zoneId + def delete(self): self.notify.debug('delete') self.destroyLevel() @@ -45,8 +54,7 @@ class DistributedLevelAI(DistributedObjectAI.DistributedObjectAI, wc = WeightedChoice.WeightedChoice(lol) scenarioIndex = wc.choose()[1] - Level.Level.initializeLevel(self, self.doId, - levelSpec, scenarioIndex) + Level.Level.initializeLevel(self, self.doId, levelSpec, scenarioIndex) def createEntityCreator(self): """Create the object that will be used to create Entities. @@ -83,14 +91,17 @@ class DistributedLevelAI(DistributedObjectAI.DistributedObjectAI, # send a copy to the client-side level obj self.sendUpdate('setAttribChange', [entId, attribName, valueStr]) + self.hasBeenEdited = 1 - def getCurrentLevelSpec(self): - """returns the complete, current spec, including any edits""" - return self.levelSpec + def requestCurrentLevelSpec(self): + senderId = self.air.msgSender + spec = self.levelSpec + specStr = repr(spec) - """ - def getSpecOverride(self): - # This is the value we'll send until someone actually edits - # the level - return repr(None) - """ + import DistributedLargeBlobSenderAI + largeBlob = DistributedLargeBlobSenderAI.\ + DistributedLargeBlobSenderAI( + self.air, self.zoneId, senderId, specStr, + useDisk=simbase.config.GetBool('spec-by-disk', 0)) + self.sendUpdateToAvatarId(senderId, + 'setSpecSenderDoId', [largeBlob.doId]) diff --git a/direct/src/level/EntityTypeRegistry.py b/direct/src/level/EntityTypeRegistry.py index aecde212e7..668a319dda 100755 --- a/direct/src/level/EntityTypeRegistry.py +++ b/direct/src/level/EntityTypeRegistry.py @@ -39,6 +39,7 @@ class EntityTypeRegistry: def getAttribNames(self, entityTypeName): """ returns ordered list of attribute names for entity type """ + assert entityTypeName in self.typeName2class # TODO: precompute this attribDescs = self.typeName2class[entityTypeName]._attribDescs attribNames = [] @@ -48,6 +49,7 @@ class EntityTypeRegistry: def getAttribDescs(self, entityTypeName): """ returns dict of attribName -> attribDescriptor """ + assert entityTypeName in self.typeName2class # TODO: precompute this attribDescs = self.typeName2class[entityTypeName]._attribDescs attribNames = self.getAttribNames(entityTypeName) diff --git a/direct/src/level/EntityTypes.py b/direct/src/level/EntityTypes.py index 163df8dc1f..7ffc422a86 100755 --- a/direct/src/level/EntityTypes.py +++ b/direct/src/level/EntityTypes.py @@ -9,7 +9,6 @@ class Entity: ('comment', ''), ) - class ActiveCell(Entity): type = 'activeCell' attribs = ( diff --git a/direct/src/level/Level.py b/direct/src/level/Level.py index 33bea55a44..2052da85ab 100755 --- a/direct/src/level/Level.py +++ b/direct/src/level/Level.py @@ -35,6 +35,7 @@ class 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 @@ -65,7 +66,13 @@ class Level: self.levelSpec.setAttribChangeEventName(self.getAttribChangeEvent()) self.accept(self.getAttribChangeEvent(), self.handleAttribChange) + self.initialized = 1 + + def isInitialized(self): + return self.initialized + def destroyLevel(self): + self.initialized = 0 if hasattr(self, 'createdEntities'): # destroy the entities in reverse order while len(self.createdEntities) > 0: diff --git a/direct/src/level/ZoneEntityAI.py b/direct/src/level/ZoneEntityAI.py index c8609dd293..4c8e9dbddc 100755 --- a/direct/src/level/ZoneEntityAI.py +++ b/direct/src/level/ZoneEntityAI.py @@ -6,14 +6,9 @@ class ZoneEntityAI(ZoneEntityBase.ZoneEntityBase): def __init__(self, level, entId): ZoneEntityBase.ZoneEntityBase.__init__(self, level, entId) - if self.isUberZone(): - print 'uberZone' - # the uberzone is already allocated - self.setZoneId(self.level.uberZoneId) - else: - # allocate a network zoneId for this zone - # there is error checking in air.allocateZone - self.setZoneId(self.level.air.allocateZone()) + # allocate a network zoneId for this zone + # there is error checking in air.allocateZone + self.setZoneId(self.level.air.allocateZone()) def destroy(self): if not self.isUberZone():