from direct.directnotify import DirectNotifyGlobal from direct.distributed.DistributedObjectAI import DistributedObjectAI from direct.fsm.FSM import FSM from toontown.estate.DistributedEstateAI import DistributedEstateAI from toontown.estate.DistributedHouseAI import DistributedHouseAI from toontown.toon import ToonDNA import HouseGlobals import functools class LoadHouseFSM(FSM): notify = DirectNotifyGlobal.directNotify.newCategory('LoadHouseFSM') def __init__(self, mgr, estate, houseIndex, toon, callback): FSM.__init__(self, 'LoadHouseFSM') self.mgr = mgr self.estate = estate self.houseIndex = houseIndex self.toon = toon self.callback = callback self.done = False def start(self): # We have a few different cases here: if self.toon is None: # Case #1: There isn't a Toon in that estate slot. Make a blank house. # Because this state completes so fast, we'll use taskMgr to delay # it until the next iteration. This solves re-entrancy problems. taskMgr.doMethodLater(0.0, self.demand, 'makeBlankHouse-%s' % id(self), extraArgs=['MakeBlankHouse']) return self.houseId = self.toon.get('setHouseId', [0])[0] if self.houseId == 0: # Case #2: There is a Toon, but no setHouseId. Gotta make one. self.demand('CreateHouse') else: # Case #3: Toon with a setHouseId. Load it. self.demand('LoadHouse') def enterMakeBlankHouse(self): self.house = DistributedHouseAI(self.mgr.air) self.house.setHousePos(self.houseIndex) self.house.setColor(self.houseIndex) self.house.generateWithRequired(self.estate.zoneId) self.estate.houses[self.houseIndex] = self.house self.demand('Off') def enterCreateHouse(self): style = ToonDNA.ToonDNA() style.makeFromNetString(self.toon['setDNAString'][0]) self.mgr.air.dbInterface.createObject( self.mgr.air.dbId, self.mgr.air.dclassesByName['DistributedHouseAI'], { 'setName' : [self.toon['setName'][0]], 'setAvatarId' : [self.toon['ID']], 'setGender': [0 if style.getGender() == 'm' else 1] }, self.__handleCreate) def __handleCreate(self, doId): if self.state != 'CreateHouse': return # Update the avatar's houseId: av = self.mgr.air.doId2do.get(self.toon['ID']) if av: av.b_setHouseId(doId) else: self.mgr.air.dbInterface.updateObject( self.mgr.air.dbId, self.toon['ID'], self.mgr.air.dclassesByName['DistributedToonAI'], {'setHouseId': [doId]}) self.houseId = doId self.demand('LoadHouse') def enterLoadHouse(self): # Activate the house: self.mgr.air.sendActivate(self.houseId, self.mgr.air.districtId, self.estate.zoneId, self.mgr.air.dclassesByName['DistributedHouseAI'], {'setHousePos': [self.houseIndex], 'setColor': [self.houseIndex], 'setName': [self.toon['setName'][0]], 'setAvatarId': [self.toon['ID']]}) # Now we wait for the house to show up... We do this by hanging a messenger # hook which the DistributedHouseAI throws once it spawns. self.acceptOnce('generate-%d' % self.houseId, self.__gotHouse) def __gotHouse(self, house): self.house = house house.initializeInterior() self.estate.houses[self.houseIndex] = self.house self.demand('Off') def exitLoadHouse(self): self.ignore('generate-%d' % self.houseId) def enterOff(self): self.done = True self.callback(self.house) class LoadPetFSM(FSM): def __init__(self, mgr, estate, toon, callback): FSM.__init__(self, 'LoadPetFSM') self.mgr = mgr self.estate = estate self.toon = toon self.callback = callback self.done = False def start(self): self.petId = self.toon['setPetId'][0] if not self.petId in self.mgr.air.doId2do: self.mgr.air.sendActivate(self.petId, self.mgr.air.districtId, self.estate.zoneId) self.acceptOnce('generate-%d' % self.petId, self.__generated) else: self.__generated(self.mgr.air.doId2do[self.petId]) def __generated(self, pet): self.pet = pet self.estate.pets.append(pet) self.demand('Off') def enterOff(self): self.done = True self.callback(self.pet) class LoadEstateFSM(FSM): def __init__(self, mgr, callback): FSM.__init__(self, 'LoadEstateFSM') self.mgr = mgr self.callback = callback self.estate = None def start(self, accountId, zoneId): self.accountId = accountId self.zoneId = zoneId self.demand('QueryAccount') def enterQueryAccount(self): self.mgr.air.dbInterface.queryObject(self.mgr.air.dbId, self.accountId, self.__gotAccount) def __gotAccount(self, dclass, fields): if self.state != 'QueryAccount': return # We must have aborted or something... if dclass != self.mgr.air.dclassesByName['AccountAI']: self.mgr.notify.warning('Account %d has non-account dclass %d!' % (self.accountId, dclass)) self.demand('Failure') return self.accountFields = fields self.estateId = fields.get('ESTATE_ID', 0) self.demand('QueryToons') def enterQueryToons(self): self.toonIds = self.accountFields.get('ACCOUNT_AV_SET', [0]*6) self.toons = {} for index, toonId in enumerate(self.toonIds): if toonId == 0: self.toons[index] = None continue self.mgr.air.dbInterface.queryObject( self.mgr.air.dbId, toonId, functools.partial(self.__gotToon, index=index)) def __gotToon(self, dclass, fields, index): if self.state != 'QueryToons': return # We must have aborted or something... if dclass != self.mgr.air.dclassesByName['DistributedToonAI']: self.mgr.notify.warning('Account %d has avatar %d with non-Toon dclass %d!' % (self.accountId, self.toonIds[index], dclass)) self.demand('Failure') return fields['ID'] = self.toonIds[index] self.toons[index] = fields if len(self.toons) == 6: self.__gotAllToons() def __gotAllToons(self): # Okay, we have all of our Toons, now we can proceed with estate! if self.estateId: # We already have an estate, load it! self.demand('LoadEstate') else: # We don't have one yet, make one! self.demand('CreateEstate') def enterCreateEstate(self): # We have to ask the DB server to construct a blank estate object... self.mgr.air.dbInterface.createObject( self.mgr.air.dbId, self.mgr.air.dclassesByName['DistributedEstateAI'], {}, self.__handleEstateCreate) def __handleEstateCreate(self, estateId): if self.state != 'CreateEstate': return # We must have aborted or something... self.estateId = estateId self.demand('StoreEstate') def enterStoreEstate(self): # store the estate in account # congrats however wrote this for forgetting it! self.mgr.air.dbInterface.updateObject( self.mgr.air.dbId, self.accountId, self.mgr.air.dclassesByName['AccountAI'], {'ESTATE_ID': self.estateId}, {'ESTATE_ID': 0}, self.__handleStoreEstate) def __handleStoreEstate(self, fields): if fields: self.notify.warning("Failed to associate Estate %d with account %d, loading anyway." % (self.estateId, self.accountId)) self.demand('LoadEstate') def enterLoadEstate(self): # Activate the estate: fields = {} for i, toon in enumerate(self.toonIds): fields['setSlot%dToonId' % i] = (toon,) self.mgr.air.sendActivate(self.estateId, self.mgr.air.districtId, self.zoneId, self.mgr.air.dclassesByName['DistributedEstateAI'], fields) # Now we wait for the estate to show up... We do this by hanging a messenger # hook which the DistributedEstateAI throws once it spawns. self.acceptOnce('generate-%d' % self.estateId, self.__gotEstate) def __gotEstate(self, estate): self.estate = estate estate.pets = [] # Gotcha! Now we need to load houses: self.demand('LoadHouses') def exitLoadEstate(self): self.ignore('generate-%d' % self.estateId) def enterLoadHouses(self): self.houseFSMs = [] for houseIndex in xrange(6): fsm = LoadHouseFSM(self.mgr, self.estate, houseIndex, self.toons[houseIndex], self.__houseDone) self.houseFSMs.append(fsm) fsm.start() def __houseDone(self, house): if self.state != 'LoadHouses': # We aren't loading houses, so we probably got cancelled. Therefore, # the only sensible thing to do is simply destroy the house. house.requestDelete() return # A houseFSM just finished! Let's see if all of them are done: if all(houseFSM.done for houseFSM in self.houseFSMs): self.demand('LoadPets') def enterLoadPets(self): self.petFSMs = [] for houseIndex in xrange(6): toon = self.toons[houseIndex] if toon and toon['setPetId'][0] != 0: fsm = LoadPetFSM(self.mgr, self.estate, toon, self.__petDone) self.petFSMs.append(fsm) fsm.start() if not self.petFSMs: taskMgr.doMethodLater(0, lambda: self.demand('Finished'), 'nopets', extraArgs=[]) def __petDone(self, pet): if self.state != 'LoadPets': pet.requestDelete() return # A petFSM just finished! Let's see if all of them are done: if all(petFSM.done for petFSM in self.petFSMs): self.demand('Finished') def enterFinished(self): self.callback(True) def enterFailure(self): self.cancel() self.callback(False) def cancel(self): if self.estate: self.estate.destroy() self.estate = None self.demand('Off') class EstateManagerAI(DistributedObjectAI): notify = DirectNotifyGlobal.directNotify.newCategory("EstateManagerAI") def __init__(self, air): DistributedObjectAI.__init__(self, air) self.estate2toons = {} self.toon2estate = {} self.estate2timeout = {} self.zoneId2owner = {} def getEstateZone(self, avId): senderId = self.air.getAvatarIdFromSender() accId = self.air.getAccountIdFromSender() toon = self.air.doId2do.get(senderId) if not toon: self.air.writeServerEvent('suspicious', senderId, 'Sent getEstateZone() but not on district!') return # If there's an avId included, then the Toon is interested in visiting a # friend. We do NOT load the estate, we simply see if it's already up... if avId and avId != senderId: av = self.air.doId2do.get(avId) if av and av.dclass == self.air.dclassesByName['DistributedToonAI']: estate = self._lookupEstate(av) if estate: # Yep, there it is! avId = estate.owner.doId zoneId = estate.zoneId self._mapToEstate(toon, estate) self._unloadEstate(toon) # In case they're doing estate->estate TP. self.sendUpdateToAvatarId(senderId, 'setEstateZone', [avId, zoneId]) # Bummer, couldn't find avId at an estate... self.sendUpdateToAvatarId(senderId, 'setEstateZone', [0, 0]) return # The Toon definitely wants to go to his own estate... estate = getattr(toon, 'estate', None) if estate: # They already have an estate loaded, so let's just return it: self._mapToEstate(toon, toon.estate) self.sendUpdateToAvatarId(senderId, 'setEstateZone', [senderId, estate.zoneId]) # If a timeout is active, cancel it: if estate in self.estate2timeout: self.estate2timeout[estate].remove() del self.estate2timeout[estate] return if getattr(toon, 'loadEstateFSM', None): # We already have a loading operation underway; ignore this second # request since the first operation will setEstateZone() when it # finishes anyway. return zoneId = self.air.allocateZone() def estateLoaded(success): if success: toon.estate = toon.loadEstateFSM.estate toon.estate.owner = toon self._mapToEstate(toon, toon.estate) self.sendUpdateToAvatarId(senderId, 'setEstateZone', [senderId, zoneId]) else: # Estate loading failed??! self.sendUpdateToAvatarId(senderId, 'setEstateZone', [0, 0]) # And I guess we won't need our zoneId anymore... self.air.deallocateZone(zoneId) del self.zoneId2owner[zoneId] toon.loadEstateFSM = None self.acceptOnce(self.air.getAvatarExitEvent(toon.doId), self._unloadEstate, extraArgs=[toon]) self.zoneId2owner[zoneId] = avId toon.loadEstateFSM = LoadEstateFSM(self, estateLoaded) toon.loadEstateFSM.start(accId, zoneId) def exitEstate(self): senderId = self.air.getAvatarIdFromSender() toon = self.air.doId2do.get(senderId) if not toon: self.air.writeServerEvent('suspicious', senderId, 'Sent exitEstate() but not on district!') return self._unmapFromEstate(toon) self._unloadEstate(toon) def _unloadEstate(self, toon): if getattr(toon, 'estate', None): estate = toon.estate if estate not in self.estate2timeout: self.estate2timeout[estate] = \ taskMgr.doMethodLater(HouseGlobals.BOOT_GRACE_PERIOD, self._cleanupEstate, estate.uniqueName('emai-cleanup-task'), extraArgs=[estate]) self._sendToonsToPlayground(toon.estate, 0) # This is a warning only... if getattr(toon, 'loadEstateFSM', None): self.air.deallocateZone(toon.loadEstateFSM.zoneId) toon.loadEstateFSM.cancel() toon.loadEstateFSM = None self.ignore(self.air.getAvatarExitEvent(toon.doId)) def _cleanupEstate(self, estate): # Boot all Toons from estate: self._sendToonsToPlayground(estate, 1) # Clean up toon<->estate mappings... for toon in self.estate2toons.get(estate, []): try: del self.toon2estate[toon] except KeyError: pass try: del self.estate2toons[estate] except KeyError: pass # Clean up timeout, if it exists: if estate in self.estate2timeout: del self.estate2timeout[estate] # Destroy estate and unmap from owner: estate.destroy() estate.owner.estate = None # Destroy pets: for pet in estate.pets: pet.requestDelete() estate.pets = [] # Free estate's zone: self.air.deallocateZone(estate.zoneId) del self.zoneId2owner[estate.zoneId] def _sendToonsToPlayground(self, estate, reason): for toon in self.estate2toons.get(estate, []): self.sendUpdateToAvatarId(toon.doId, 'sendAvToPlayground', [toon.doId, reason]) def _mapToEstate(self, toon, estate): self._unmapFromEstate(toon) self.estate2toons.setdefault(estate, []).append(toon) self.toon2estate[toon] = estate if hasattr(toon, 'enterEstate'): toon.enterEstate(estate.owner.doId, estate.zoneId) def _unmapFromEstate(self, toon): estate = self.toon2estate.get(toon) if not estate: return del self.toon2estate[toon] try: self.estate2toons[estate].remove(toon) except (KeyError, ValueError): pass if hasattr(toon, 'exitEstate'): toon.exitEstate() def _lookupEstate(self, toon): return self.toon2estate.get(toon) def getOwnerFromZone(self, zoneId): return self.zoneId2owner.get(zoneId, 0) def getEstateZones(self, ownerId): estate = self._lookupEstate(self.air.doId2do.get(ownerId)) if estate: return [estate.zoneId] return []