DistributedLevel.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. """DistributedLevel.py: contains the DistributedLevel class"""
  2. from ClockDelta import *
  3. from PythonUtil import Functor, sameElements, list2dict, uniqueElements
  4. import ToontownGlobals
  5. import DistributedObject
  6. import Level
  7. import DirectNotifyGlobal
  8. import EntityCreator
  9. import OnscreenText
  10. import Task
  11. class DistributedLevel(DistributedObject.DistributedObject,
  12. Level.Level):
  13. """DistributedLevel"""
  14. notify = DirectNotifyGlobal.directNotify.newCategory('DistributedLevel')
  15. WantVisibility = config.GetBool('level-visibility', 1)
  16. HideZones = config.GetBool('level-hidezones', 1)
  17. # TODO: move level-model stuff to LevelMgr or FactoryLevelMgr?
  18. FloorCollPrefix = 'zoneFloor'
  19. def __init__(self, cr):
  20. DistributedObject.DistributedObject.__init__(self, cr)
  21. Level.Level.__init__(self)
  22. self.lastToonZone = 0
  23. self.titleColor = (1,1,1,1)
  24. self.titleText = OnscreenText.OnscreenText(
  25. "",
  26. fg = self.titleColor,
  27. shadow = (0,0,0,1),
  28. font = ToontownGlobals.getSuitFont(),
  29. pos = (0,-0.5),
  30. scale = 0.16,
  31. drawOrder = 0,
  32. mayChange = 1,
  33. )
  34. self.smallTitleText = OnscreenText.OnscreenText(
  35. "",
  36. fg = self.titleColor,
  37. font = ToontownGlobals.getSuitFont(),
  38. pos = (0.65,0.9),
  39. scale = 0.08,
  40. drawOrder = 0,
  41. mayChange = 1,
  42. bg = (.5,.5,.5,.5),
  43. align = TextNode.ARight,
  44. )
  45. self.zonesEnteredList = []
  46. def generate(self):
  47. DistributedLevel.notify.debug('generate')
  48. DistributedObject.DistributedObject.generate(self)
  49. # this dict stores entity reparents if the parent hasn't been
  50. # created yet
  51. self.parent2ChildIds = {}
  52. # if the AI sends us a full spec, it will be put here
  53. self.curSpec = None
  54. # Most (if not all) of the timed entities of levels
  55. # run on looping intervals that are started once based on
  56. # the level's start time.
  57. # This sync request is *NOT* guaranteed to finish by the time
  58. # the entities get created.
  59. # We should listen for any and all time-sync events and re-sync
  60. # all our entities at that time.
  61. toonbase.tcr.timeManager.synchronize('DistributedLevel.generate')
  62. # add factory menu to SpeedChat
  63. toonbase.localToon.chatMgr.chatInputSpeedChat.addFactoryMenu()
  64. # the real required fields
  65. def setLevelZoneId(self, zoneId):
  66. # this is the zone that the level is in; we should listen to this
  67. # zone the entire time we're in here
  68. self.levelZone = zoneId
  69. # "required" fields (these ought to be required fields, but
  70. # the AI level obj doesn't know the data values until it has been
  71. # generated.)
  72. def setZoneIds(self, zoneIds):
  73. DistributedLevel.notify.debug('setZoneIds: %s' % zoneIds)
  74. self.zoneIds = zoneIds
  75. def setStartTimestamp(self, timestamp):
  76. DistributedLevel.notify.debug('setStartTimestamp: %s' % timestamp)
  77. self.startTime = globalClockDelta.networkToLocalTime(timestamp,bits=32)
  78. def setScenarioIndex(self, scenarioIndex):
  79. self.scenarioIndex = scenarioIndex
  80. # ugly hack: we treat a few DC fields as if they were required,
  81. # and use 'levelAnnounceGenerate()' in place of regular old
  82. # announceGenerate(). Note that we have to call
  83. # gotAllRequired() in the last 'faux-required' DC update
  84. # handler. If you add another field, move this to the last one.
  85. self.privGotAllRequired()
  86. def privGotAllRequired(self):
  87. self.levelAnnounceGenerate()
  88. def levelAnnounceGenerate(self):
  89. pass
  90. def initializeLevel(self, levelSpec):
  91. """subclass should call this as soon as it's located its level spec.
  92. Must be called after obj has been generated."""
  93. if __debug__:
  94. # if we're in debug, give the server the opportunity to send us
  95. # a full spec
  96. self.candidateSpec = levelSpec
  97. self.sendUpdate('requestCurrentLevelSpec',
  98. [hash(levelSpec),
  99. hash(levelSpec.entTypeReg)])
  100. else:
  101. self.privGotSpec(levelSpec)
  102. if __debug__:
  103. def setSpecDeny(self, reason):
  104. DistributedLevel.notify.error(reason)
  105. def setSpecSenderDoId(self, doId):
  106. DistributedLevel.notify.debug('setSpecSenderDoId: %s' % doId)
  107. blobSender = toonbase.tcr.doId2do[doId]
  108. def setSpecBlob(specBlob, blobSender=blobSender, self=self):
  109. blobSender.sendAck()
  110. from LevelSpec import LevelSpec
  111. spec = eval(specBlob)
  112. if spec is None:
  113. spec = self.candidateSpec
  114. del self.candidateSpec
  115. self.privGotSpec(spec)
  116. if blobSender.isComplete():
  117. setSpecBlob(blobSender.getBlob())
  118. else:
  119. evtName = self.uniqueName('specDone')
  120. blobSender.setDoneEvent(evtName)
  121. self.acceptOnce(evtName, setSpecBlob)
  122. def privGotSpec(self, levelSpec):
  123. Level.Level.initializeLevel(self, self.doId, levelSpec,
  124. self.scenarioIndex)
  125. # all of the local entities have been created now.
  126. # TODO: have any of the distributed entities been created at this point?
  127. # there should not be any pending reparents left at this point
  128. # TODO: is it possible for a local entity to be parented to a
  129. # distributed entity? I think so!
  130. assert len(self.parent2ChildIds) == 0
  131. # make sure the zoneNums from the model match the zoneNums from
  132. # the zone entities
  133. assert sameElements(self.zoneNums, self.zoneNum2entId.keys())
  134. # load stuff
  135. self.initVisibility()
  136. def createEntityCreator(self):
  137. """Create the object that will be used to create Entities.
  138. Inheritors, override if desired."""
  139. return EntityCreator.EntityCreator(level=self)
  140. def onEntityTypePostCreate(self, entType):
  141. """listen for certain entity types to be created"""
  142. Level.Level.onEntityTypePostCreate(self, entType)
  143. # NOTE: these handlers are private in order to avoid overriding
  144. # similar handlers in base classes
  145. if entType == 'levelMgr':
  146. self.__handleLevelMgrCreated()
  147. def __handleLevelMgrCreated(self):
  148. # as soon as the levelMgr has been created, load up the model
  149. # and extract zone info
  150. self.geom = self.levelMgr.geom
  151. def findNumberedNodes(baseString, model=self.geom, self=self):
  152. # finds nodes whose name follows the pattern 'baseString#'
  153. # where there are no characters after #
  154. # returns dictionary that maps # to node
  155. potentialNodes = model.findAllMatches(
  156. '**/%s*' % baseString).asList()
  157. num2node = {}
  158. for potentialNode in potentialNodes:
  159. name = potentialNode.getName()
  160. DistributedLevel.notify.debug('potential match for %s: %s' %
  161. (baseString, name))
  162. try:
  163. num = int(name[len(baseString):])
  164. except:
  165. continue
  166. num2node[num] = potentialNode
  167. return num2node
  168. # find the zones in the model and fix them up
  169. self.zoneNum2node = findNumberedNodes('Zone')
  170. # add the UberZone to the table
  171. self.zoneNum2node[0] = self.geom
  172. self.zoneNums = self.zoneNum2node.keys()
  173. self.zoneNums.sort()
  174. DistributedLevel.notify.debug('zones: %s' % self.zoneNums)
  175. # fix up the floor collisions for walkable zones *before*
  176. # any entities get put under the model
  177. for zoneNum,zoneNode in self.zoneNum2node.items():
  178. # if this is a walkable zone, fix up the model
  179. allColls = zoneNode.findAllMatches('**/+CollisionNode').asList()
  180. # which of them, if any, are floors?
  181. floorColls = []
  182. for coll in allColls:
  183. bitmask = coll.node().getIntoCollideMask()
  184. if not (bitmask & ToontownGlobals.FloorBitmask).isZero():
  185. floorColls.append(coll)
  186. if len(floorColls) > 0:
  187. # rename the floor collision nodes, and make sure no other
  188. # nodes under the ZoneNode have that name
  189. floorCollName = '%s%s' % (DistributedLevel.FloorCollPrefix,
  190. zoneNum)
  191. others = zoneNode.findAllMatches(
  192. '**/%s' % floorCollName).asList()
  193. for other in others:
  194. other.setName('%s_renamed' % floorCollName)
  195. for floorColl in floorColls:
  196. floorColl.setName(floorCollName)
  197. # listen for zone enter events from floor collisions
  198. def handleZoneEnter(collisionEntry,
  199. self=self, zoneNum=zoneNum):
  200. # eat the collisionEntry
  201. self.toonEnterZone(zoneNum)
  202. self.accept('enter%s' % floorCollName, handleZoneEnter)
  203. # hack in another doorway
  204. dw = self.geom.attachNewNode('Doorway27')
  205. dw.setPos(-49.4,86.7,19.26)
  206. dw.setH(0)
  207. # find the doorway nodes
  208. self.doorwayNum2Node = findNumberedNodes('Doorway')
  209. def announceGenerate(self):
  210. DistributedLevel.notify.debug('announceGenerate')
  211. DistributedObject.DistributedObject.announceGenerate(self)
  212. def disable(self):
  213. DistributedLevel.notify.debug('disable')
  214. # geom is owned by the levelMgr
  215. if hasattr(self, 'geom'):
  216. del self.geom
  217. self.destroyLevel()
  218. DistributedObject.DistributedObject.disable(self)
  219. self.ignoreAll()
  220. # NOTE: this should be moved to FactoryInterior
  221. if self.smallTitleText:
  222. self.smallTitleText.cleanup()
  223. self.smallTitleText = None
  224. if self.titleText:
  225. self.titleText.cleanup()
  226. self.titleText = None
  227. self.zonesEnteredList = []
  228. # NOTE: this should be moved to ZoneEntity.disable
  229. toonbase.localToon.chatMgr.chatInputSpeedChat.removeFactoryMenu()
  230. def delete(self):
  231. DistributedLevel.notify.debug('delete')
  232. DistributedObject.DistributedObject.delete(self)
  233. # remove factory menu to SpeedChat
  234. toonbase.localToon.chatMgr.chatInputSpeedChat.removeFactoryMenu()
  235. def getDoorwayNode(self, doorwayNum):
  236. # returns node that doors should parent themselves to
  237. return self.doorwayNum2Node[doorwayNum]
  238. def getZoneNode(self, zoneNum):
  239. return self.zoneNum2node[zoneNum]
  240. def requestReparent(self, entity, parentId):
  241. assert(entity.entId != parentId)
  242. parent = self.getEntity(parentId)
  243. if parent is not None:
  244. # parent has already been created
  245. entity.reparentTo(parent.getNodePath())
  246. else:
  247. # parent hasn't been created yet; schedule the reparent
  248. DistributedLevel.notify.debug(
  249. 'entity %s requesting reparent to %s, not yet created' %
  250. (entity, parentId))
  251. entId = entity.entId
  252. entity.reparentTo(hidden)
  253. # if this parent doesn't already have another child pending,
  254. # do some setup
  255. if not self.parent2ChildIds.has_key(parentId):
  256. self.parent2ChildIds[parentId] = []
  257. # do the reparent once the parent is initialized
  258. def doReparent(parentId=parentId, self=self):
  259. assert self.parent2ChildIds.has_key(parentId)
  260. parent=self.getEntity(parentId)
  261. for entId in self.parent2ChildIds[parentId]:
  262. entity=self.getEntity(entId)
  263. DistributedLevel.notify.debug(
  264. 'performing pending reparent of %s to %s' %
  265. (entity, parent))
  266. entity.reparentTo(parent.getNodePath())
  267. del self.parent2ChildIds[parentId]
  268. self.accept(self.getEntityCreateEvent(parentId), doReparent)
  269. self.parent2ChildIds[parentId].append(entId)
  270. def showZone(self, zoneNum):
  271. self.zoneNum2node[zoneNum].show()
  272. def hideZone(self, zoneNum):
  273. self.zoneNum2node[zoneNum].hide()
  274. def setTransparency(self, alpha, zone=None):
  275. self.geom.setTransparency(1)
  276. if zone is None:
  277. node = self.geom
  278. else:
  279. node = self.zoneNum2node[zone]
  280. node.setAlphaScale(alpha)
  281. def initVisibility(self):
  282. # start out with every zone visible, since none of the zones have
  283. # been hidden
  284. self.curVisibleZoneNums = list2dict(self.zoneNums)
  285. # the UberZone is always visible, so it's not included in the
  286. # zones' viz lists
  287. del self.curVisibleZoneNums[0]
  288. # we have not entered any zone yet
  289. self.curZoneNum = None
  290. # listen for camera-ray/floor collision events
  291. def handleCameraRayFloorCollision(collEntry, self=self):
  292. name = collEntry.getIntoNode().getName()
  293. prefixLen = len(DistributedLevel.FloorCollPrefix)
  294. if (name[:prefixLen] == DistributedLevel.FloorCollPrefix):
  295. try:
  296. zoneNum = int(name[prefixLen:])
  297. except:
  298. DistributedLevel.notify.debug(
  299. 'Invalid zone floor collision node: %s'
  300. % name)
  301. else:
  302. self.camEnterZone(zoneNum)
  303. self.accept('on-floor', handleCameraRayFloorCollision)
  304. # if no viz, listen to all the zones
  305. if not DistributedLevel.WantVisibility:
  306. zoneNums = list(self.zoneNums)
  307. zoneNums.remove(Level.Level.uberZoneNum)
  308. self.setVisibility(zoneNums)
  309. def toonEnterZone(self, zoneNum):
  310. DistributedLevel.notify.debug('toonEnterZone%s' % zoneNum)
  311. if zoneNum != self.lastToonZone:
  312. self.lastToonZone = zoneNum
  313. print "made zone transition to %s" % zoneNum
  314. messenger.send("factoryZoneChanged", [zoneNum])
  315. self.smallTitleText.hide()
  316. self.spawnTitleText()
  317. def camEnterZone(self, zoneNum):
  318. DistributedLevel.notify.debug('camEnterZone%s' % zoneNum)
  319. self.enterZone(zoneNum)
  320. def enterZone(self, zoneNum):
  321. DistributedLevel.notify.debug("entering zone %s" % zoneNum)
  322. if not DistributedLevel.WantVisibility:
  323. return
  324. if zoneNum == self.curZoneNum:
  325. return
  326. zoneEntId = self.zoneNum2entId[zoneNum]
  327. zoneSpec = self.levelSpec.getEntitySpec(zoneEntId)
  328. # use dicts to efficiently ensure that there are no duplicates
  329. visibleZoneNums = list2dict([zoneNum])
  330. visibleZoneNums.update(list2dict(zoneSpec['visibility']))
  331. if DistributedLevel.HideZones:
  332. # figure out which zones are new and which are going invisible
  333. # use dicts because it's faster to use dict.has_key(x)
  334. # than 'x in list'
  335. addedZoneNums = []
  336. removedZoneNums = []
  337. allVZ = dict(visibleZoneNums)
  338. allVZ.update(self.curVisibleZoneNums)
  339. for vz,None in allVZ.items():
  340. new = vz in visibleZoneNums
  341. old = vz in self.curVisibleZoneNums
  342. if new and old:
  343. continue
  344. if new:
  345. addedZoneNums.append(vz)
  346. else:
  347. removedZoneNums.append(vz)
  348. # show the new, hide the old
  349. DistributedLevel.notify.debug('showing zones %s' % addedZoneNums)
  350. for az in addedZoneNums:
  351. self.showZone(az)
  352. DistributedLevel.notify.debug('hiding zones %s' % removedZoneNums)
  353. for rz in removedZoneNums:
  354. self.hideZone(rz)
  355. self.setVisibility(visibleZoneNums.keys())
  356. self.curZoneNum = zoneNum
  357. self.curVisibleZoneNums = visibleZoneNums
  358. def setVisibility(self, vizList):
  359. # accepts list of visible zone numbers
  360. # convert the zone numbers into their actual zoneIds
  361. # always include Toontown and factory uberZones
  362. uberZone = self.getZoneId(zoneNum=Level.Level.UberZoneNum)
  363. # the level itself is in the 'level zone'
  364. visibleZoneIds = [ToontownGlobals.UberZone, self.levelZone, uberZone]
  365. for vz in vizList:
  366. visibleZoneIds.append(self.getZoneId(zoneNum=vz))
  367. assert(uniqueElements(visibleZoneIds))
  368. DistributedLevel.notify.debug('new viz list: %s' % visibleZoneIds)
  369. toonbase.tcr.sendSetZoneMsg(self.levelZone, visibleZoneIds)
  370. if __debug__:
  371. # level editing stuff
  372. def setAttribChange(self, entId, attribName, valueStr, username):
  373. """every time the spec is edited, we get this message
  374. from the AI"""
  375. value = eval(valueStr)
  376. self.levelSpec.setAttribChange(entId, attribName, value, username)
  377. def spawnTitleText(self):
  378. def getDescription(zoneId, self=self):
  379. entId = self.zoneNum2entId.get(zoneId)
  380. if entId:
  381. ent = self.entities.get(entId)
  382. if ent and hasattr(ent, 'description'):
  383. return ent.description
  384. return None
  385. description = getDescription(self.lastToonZone)
  386. if description and description != '':
  387. taskMgr.remove("titleText")
  388. self.smallTitleText.setText(description)
  389. self.titleText.setText(description)
  390. self.titleText.setColor(Vec4(*self.titleColor))
  391. self.titleText.setFg(self.titleColor)
  392. # Only show the big title once per session.
  393. # If we've already seen it, just show the small title
  394. titleSeq = None
  395. if not self.lastToonZone in self.zonesEnteredList:
  396. self.zonesEnteredList.append(self.lastToonZone)
  397. titleSeq = Task.sequence(
  398. Task.Task(self.hideSmallTitleTextTask),
  399. Task.Task(self.showTitleTextTask),
  400. Task.pause(0.1),
  401. Task.pause(6.0),
  402. self.titleText.lerpColor(Vec4(self.titleColor[0],
  403. self.titleColor[1],
  404. self.titleColor[2],
  405. self.titleColor[3]),
  406. Vec4(self.titleColor[0],
  407. self.titleColor[1],
  408. self.titleColor[2],
  409. 0.0),
  410. 0.5),
  411. )
  412. smallTitleSeq = Task.sequence(Task.Task(self.hideTitleTextTask),
  413. Task.Task(self.showSmallTitleTask),
  414. Task.Task(self.showSmallTitleTask))
  415. if titleSeq:
  416. seq = Task.sequence(titleSeq, smallTitleSeq)
  417. else:
  418. seq = smallTitleSeq
  419. taskMgr.add(seq, "titleText")
  420. def showTitleTextTask(self, task):
  421. assert(DistributedLevel.notify.debug("hideTitleTextTask()"))
  422. self.titleText.show()
  423. return Task.done
  424. def hideTitleTextTask(self, task):
  425. assert(DistributedLevel.notify.debug("hideTitleTextTask()"))
  426. self.titleText.hide()
  427. return Task.done
  428. def showSmallTitleTask(self, task):
  429. # make sure large title is hidden
  430. self.titleText.hide()
  431. # show the small title
  432. self.smallTitleText.show()
  433. return Task.done
  434. def hideSmallTitleTextTask(self, task):
  435. assert(DistributedLevel.notify.debug("hideTitleTextTask()"))
  436. self.smallTitleText.hide()
  437. return Task.done