DistributedLevel.py 20 KB

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