From e321cc507057967342937b5217b0852be35b6d75 Mon Sep 17 00:00:00 2001 From: DarthM Date: Fri, 15 Nov 2024 20:57:40 -0500 Subject: [PATCH] Parties: Parties now can be planned They work for the most part now just need to get them in the calendar , the hosting tab and the notification Uses anesidora code and then a json database --- toontown/ai/RepairAvatars.py | 811 ++++++++++ toontown/ai/ToontownAIRepository.py | 72 + .../distributed/ToontownInternalRepository.py | 20 + toontown/parties/DistributedParty.py | 2 +- toontown/parties/DistributedPartyActivity.py | 6 +- toontown/parties/Party.py | 5 +- toontown/parties/PartyEditorListElement.py | 10 +- toontown/parties/PartyPlanner.py | 3 +- toontown/parties/PublicPartyGui.py | 2 +- toontown/shtiker/EventsPage.py | 6 +- toontown/uberdog/DistributedPartyManager.py | 2 +- toontown/uberdog/DistributedPartyManagerAI.py | 1391 +++++++++++++++- toontown/uberdog/DistributedPartyManagerUD.py | 1397 ++++++++++++++++- toontown/uberdog/ToontownUDRepository.py | 2 + 14 files changed, 3704 insertions(+), 25 deletions(-) create mode 100644 toontown/ai/RepairAvatars.py diff --git a/toontown/ai/RepairAvatars.py b/toontown/ai/RepairAvatars.py new file mode 100644 index 0000000..853272d --- /dev/null +++ b/toontown/ai/RepairAvatars.py @@ -0,0 +1,811 @@ +# from Anesidora +from . import DatabaseObject +from direct.showbase import DirectObject +from direct.showbase.PythonUtil import intersection +from toontown.toon import DistributedToonAI +from toontown.estate import DistributedHouseAI +from toontown.pets import DistributedPetAI +from toontown.toon import InventoryBase +from pandac.PandaModules import * +from toontown.quest import Quests +from toontown.toon import NPCToons +import time +from functools import reduce + +HEAL_TRACK = 0 +TRAP_TRACK = 1 +LURE_TRACK = 2 +SOUND_TRACK = 3 +THROW_TRACK = 4 +SQUIRT_TRACK = 5 +DROP_TRACK = 6 + +class AvatarGetter(DirectObject.DirectObject): + # Gets just one avatar at a time. You can munge properties on the + # avatar and write it back to the database. self.av is the + # avatar. + + # An incrementing sequence number unique to each getter object. + nextSequence = 1 + + def __init__(self, air): + self.air = air + self.dclass = self.air.dclassesByName['DistributedToonAI'] + self.av = None + self.gotAvatarEvent = 'AvatarGetter-%s' % (self.nextSequence) + AvatarGetter.nextSequence += 1 + + def getAvatar(self, avId, fields=None, event=None): + # Requests a particular avatar. The avatar will be requested + # from the database and stored in self.av when the response is + # heard back from the database, at some time in the future. + self.av = None + self.event = event + + self.acceptOnce(self.gotAvatarEvent, self.__gotData) + + db = DatabaseObject.DatabaseObject(self.air, avId) + db.doneEvent = self.gotAvatarEvent + if fields is None: + fields = db.getDatabaseFields(self.dclass) + elif 'setDNAString' not in fields: + # we need this to check if it's a Toon + fields.append('setDNAString') + db.getFields(fields) + print("Avatar %s requested." % avId) + + def saveAvatarAll(self): + # Writes all the fields on the current avatar back to the + # database. + db = DatabaseObject.DatabaseObject(self.air, self.av.doId) + db.storeObject(self.av) + print("Saved avatar %s." % (self.av.doId)) + + def saveAvatar(self, *fields): + # Writes only the named fields (strings passed as parameters) + # on the current avatar back to the database. + if (len(fields) == 0): + print("Specify the fields to save in the parameter list, or use saveAvatarAll().") + else: + db = DatabaseObject.DatabaseObject(self.air, self.av.doId) + db.storeObject(self.av, fields) + print("Saved %d fields on avatar %s." % (len(fields), self.av.doId)) + + def __gotData(self, db, retcode): + if retcode == 0 and 'setDNAString' in db.values: + self.av = DistributedToonAI.DistributedToonAI(self.air) + self.av.doId = db.doId + self.av.inventory = InventoryBase.InventoryBase(self.av) + self.av.teleportZoneArray = [] + db.fillin(self.av, self.dclass) + + # to prevent mem leaks, you should call toon.patchDelete at + # some point. + + print('Got avatar %s, "%s".' % (self.av.doId, self.av._name)) + if self.event is not None: + messenger.send(self.event, [self.av]) + else: + print("Could not get avatar %s, retcode = %s." % (db.doId, retcode)) + if self.event is not None: + messenger.send(self.event, [None]) + +class AvatarIterator(DirectObject.DirectObject): + + # The maximum number of outstanding requests to make to the server + # at once. + maxRequests = 20 + + # When we come to this many non-avatars in a row, assume we have + # reached the end of the database. + endOfListCount = 20 + + # The amount of time, in seconds, to elapse between displaying + # successive avatars. + printInterval = 1.0 + + # An incrementing sequence number unique to each iterator object. + nextSequence = 1 + + def __init__(self, air): + self.air = air + self.dclass = self.air.dclassesByName['DistributedToonAI'] + self.dnaDict = {} + self.nextObjId = None + self.objIdList = None # Fill this with a list of objId's to iterate through the list. + self.requested = [] + self.nonAvatar = 0 + self.gotAvatarEvent = 'AvatarIterator-%s' % (self.nextSequence) + AvatarIterator.nextSequence += 1 + + def start(self, startId = 100000000): + self.startTime = time.time() + if self.objIdList != None: + # Iterate through an explicit list + self.nextObjId = None + self.objIdIndex = 0 + else: + # Iterate through the whole database + self.nextObjId = startId + self.accept(self.gotAvatarEvent, self.__gotData) + self.requested = [] + self.lastPrintTime = 0 + self.getNextAvatar() + + def stop(self): + self.ignoreAll() + + def getNextAvatar(self): + while len(self.requested) < self.maxRequests: + if self.nextObjId != None: + db = DatabaseObject.DatabaseObject(self.air, self.nextObjId) + db.doneEvent = self.gotAvatarEvent + db.getFields(self.fieldsToGet(db)) + self.requested.append(self.nextObjId) + + if self.objIdList != None: + # Iterate through an explicit list + if self.objIdIndex >= len(self.objIdList): + # Done. + self.nextObjId = None + if len(self.requested) == 0: + self.done() + return + + self.nextObjId = int(self.objIdList[self.objIdIndex]) + self.objIdIndex += 1 + + else: + # Iterate through the whole database + self.nextObjId += 2 + + def fieldsToGet(self, db): + return db.getDatabaseFields(self.dclass) + + def __gotData(self, db, retcode): + self.requested.remove(db.doId) + if retcode == 0 and 'setMoney' in db.values: + av = DistributedToonAI.DistributedToonAI(self.air) + av.doId = db.doId + av.inventory = InventoryBase.InventoryBase(av) + av.teleportZoneArray = [] + db.fillin(av, self.dclass) + self.processAvatar(av, db) + self.nonAvatar = 0 + else: + if self.objIdList != None: + print("Not an avatar: %s" % (db.doId)) + self.nonAvatar += 1 + + if self.objIdList != None or self.nonAvatar < self.endOfListCount: + self.getNextAvatar() + elif len(self.requested) == 0: + self.stop() + self.done() + + def printSometimes(self, av): + now = time.time() + if now - self.lastPrintTime > self.printInterval: + print("%d: %s" % (av.doId, av._name)) + self.lastPrintTime = now + + def processAvatar(self, av, db): + self.printSometimes(av) + + def done(self): + now = time.time() + print("done, %s seconds." % (now - self.startTime)) + + +class HouseIterator(DirectObject.DirectObject): + + # The maximum number of outstanding requests to make to the server + # at once. + maxRequests = 20 + + # When we come to this many non-houses in a row, assume we have + # reached the end of the database. + endOfListCount = 20 + + # The amount of time, in seconds, to elapse between displaying + # successive houses. + printInterval = 1.0 + + # An incrementing sequence number unique to each iterator object. + nextSequence = 1 + + def __init__(self, air): + self.air = air + self.dclass = self.air.dclassesByName['DistributedHouseAI'] + self.dnaDict = {} + self.nextObjId = None + self.objIdList = None # Fill this with a list of objId's to iterate through the list. + self.requested = [] + self.nonHouse = 0 + self.gotHouseEvent = 'HouseIterator-%s' % (self.nextSequence) + HouseIterator.nextSequence += 1 + + def start(self, startId = 100000000): + self.startTime = time.time() + if self.objIdList != None: + # Iterate through an explicit list + self.nextObjId = None + self.objIdIndex = 0 + else: + # Iterate through the whole database + self.nextObjId = startId + self.accept(self.gotHouseEvent, self.__gotData) + self.requested = [] + self.lastPrintTime = 0 + self.getNextHouse() + + def stop(self): + self.ignoreAll() + + def getNextHouse(self): + while len(self.requested) < self.maxRequests: + if self.nextObjId != None: + db = DatabaseObject.DatabaseObject(self.air, self.nextObjId) + db.doneEvent = self.gotHouseEvent + db.getFields(self.fieldsToGet(db)) + self.requested.append(self.nextObjId) + + if self.objIdList != None: + # Iterate through an explicit list + if self.objIdIndex >= len(self.objIdList): + # Done. + self.nextObjId = None + if len(self.requested) == 0: + self.done() + return + + self.nextObjId = int(self.objIdList[self.objIdIndex]) + self.objIdIndex += 1 + + else: + # Iterate through the whole database + self.nextObjId += 2 + + def fieldsToGet(self, db): + return db.getDatabaseFields(self.dclass) + + def __gotData(self, db, retcode): + self.requested.remove(db.doId) + if retcode == 0 and ('setHouseType' in db.values or + 'setInteriorWallpaper' in db.values): + # Fill in dummy values of estateId, zoneId, and posIndex + house = DistributedHouseAI.DistributedHouseAI( + self.air, db.doId, 0, 0, 0) + db.fillin(house, self.dclass) + self.processHouse(house, db) + self.nonHouse = 0 + else: + if self.objIdList != None: + print("Not a house: %s" % (db.doId)) + self.nonHouse += 1 + + if self.objIdList != None or self.nonHouse < self.endOfListCount: + self.getNextHouse() + elif len(self.requested) == 0: + self.stop() + self.done() + + def printSometimes(self, house): + now = time.time() + if now - self.lastPrintTime > self.printInterval: + print("%d: %s" % (house.doId, house.name)) + self.lastPrintTime = now + + def processHouse(self, house, db): + self.printSometimes(house) + + def done(self): + now = time.time() + print("done, %s seconds." % (now - self.startTime)) + + +class PetIterator(DirectObject.DirectObject): + + # The maximum number of outstanding requests to make to the server + # at once. + maxRequests = 10 + + # When we come to this many non-pets in a row, assume we have + # reached the end of the database. + endOfListCount = 20 + + # The amount of time, in seconds, to elapse between displaying + # successive pets. + printInterval = 1.0 + + # An incrementing sequence number unique to each iterator object. + nextSequence = 1 + + def __init__(self, air): + self.air = air + self.dclass = self.air.dclassesByName['DistributedPetAI'] + self.dnaDict = {} + self.startId = None + self.endId = None + self.nextObjId = None + self.objIdList = None # Fill this with a list of objId's to iterate through the list. + self.requested = [] + self.nonPet = 0 + self.gotPetEvent = 'PetIterator-%s' % (self.nextSequence) + PetIterator.nextSequence += 1 + + def start(self, startId = 100000000): + self.startTime = time.time() + if self.objIdList != None: + # Iterate through an explicit list + self.nextObjId = None + self.objIdIndex = 0 + else: + # Iterate through the whole database + if self.startId == None: + self.startId = startId + self.nextObjId = self.startId + self.accept(self.gotPetEvent, self.__gotData) + self.requested = [] + self.lastPrintTime = 0 + self.getNextPet() + + def timeToStop(self): + # override this and return True when appropriate + if self.objIdList != None: + return False + return self.nonPet >= self.endOfListCount + + def stop(self): + self.ignoreAll() + + def getNextPet(self): + if self.timeToStop(): + return + while len(self.requested) < self.maxRequests: + if self.nextObjId != None: + db = DatabaseObject.DatabaseObject(self.air, self.nextObjId) + db.doneEvent = self.gotPetEvent + db.getFields(self.fieldsToGet(db)) + self.requested.append(self.nextObjId) + + if self.objIdList != None: + # Iterate through an explicit list + if self.objIdIndex >= len(self.objIdList): + # Done. + self.nextObjId = None + if len(self.requested) == 0: + self.done() + return + + self.nextObjId = int(self.objIdList[self.objIdIndex]) + self.objIdIndex += 1 + + else: + # Iterate through the whole database + self.nextObjId += 2 + + def fieldsToGet(self, db): + return db.getDatabaseFields(self.dclass) + + def __gotData(self, db, retcode): + self.requested.remove(db.doId) + if retcode == 0 and len(intersection(list(db.values.keys()), + self.fieldsToGet(None))) > 0: + pet = DistributedPetAI.DistributedPetAI(self.air) + db.fillin(pet, self.dclass) + self.processPet(pet, db) + self.nonPet = 0 + else: + if self.objIdList != None: + print("Not a pet: %s" % (db.doId)) + self.nonPet += 1 + self.getNextPet() + + if self.timeToStop() and (len(self.requested) == 0): + self.stop() + self.done() + + def printSometimes(self, pet): + now = time.time() + if now - self.lastPrintTime > self.printInterval: + percent = None + if self.objIdList != None: + percent = 100. * self.objIdIndex / len(self.objIdList) + elif self.endId is not None: + percent = 100. * ((self.nextObjId - self.startId) / + (self.endId - self.startId)) + if percent is not None: + print("%s%% complete, %s seconds" % (percent, + (now - self.startTime))) + else: + print("%d: %s, %s seconds" % (pet.doId, pet.petName, + (now - self.startTime))) + self.lastPrintTime = now + + def processPet(self, pet, db): + self.printSometimes(pet) + + def done(self): + now = time.time() + print("done, %s seconds." % (now - self.startTime)) + + +class AvatarFixer(AvatarIterator): + def processAvatar(self, av, db): + self.printSometimes(av) + + changed = av.fixAvatar() + if changed: + db2 = DatabaseObject.DatabaseObject(self.air, av.doId) + db2.storeObject(av, list(db.values.keys())) + print("%d: %s repaired (account %s)." % (av.doId, av._name, av.accountName)) + return + + numTracks = reduce(lambda a, b: a+b, av.trackArray) + hp = av.maxHp + healExp, trapExp, lureExp, soundExp, throwExp, squirtExp, dropExp = av.experience.experience + trackProgressId, trackProgress = av.getTrackProgress() + trackAccess = av.getTrackAccess() + maxMoney = av.getMaxMoney() + fixed = 0 + + for questDesc in av.quests: + + questId = questDesc[0] + rewardId = questDesc[3] + toNpc = questDesc[2] + + if (not Quests.questExists(questId)): + print('WARNING: av has quest that is not in quest dict: ', av.doId, questId) + continue + + if (questId in [160, 161, 162, 161]): + if rewardId != 100: + print(('WARNING: av has quest: %s with reward: %s' % (questId, rewardId))) + questDesc[3] = 100 + fixed = 1 + continue + + if (rewardId == 1000): + if (questId in [1100, 1101, 1102, 1103, 2500, 2501, 3500, 3501, 4500, 4501, 5500, 5501, 7500, 7501, 9500, 9501]): + # not fixing because this clothing quest is valid + break + av.removeAllTracesOfQuest(questId, rewardId) + fixed = 1 + continue + + if ((toNpc != 1000) and + (NPCToons.NPCToonDict[toNpc][5] == NPCToons.NPC_HQ)): + print(('WARNING: av has quest: %s to visit NPC_HQ: %s' % (questId, toNpc))) + print('before: ', av.quests) + questDesc[2] = Quests.ToonHQ + print('after: ', av.quests) + fixed = 1 + continue + + # If there were any quest fixes, broadcast them now + if fixed: + av.b_setQuests(av.quests) + + # Make sure they are not training any tracks they have already trained + if (trackProgressId >= 0) and (trackAccess[trackProgressId] == 1): + print ("WARNING: av training track he already has") + print("Track progress id: ", trackProgressId) + print("Track access: ", trackAccess) + print("Tier: ", av.rewardTier) + if av.rewardTier in [0, 1]: + print("ERROR: You should not be here") + elif av.rewardTier in [2, 3]: + print('sound or heal') + if av.trackArray[SOUND_TRACK] and not av.trackArray[HEAL_TRACK]: + trackProgressId = HEAL_TRACK + elif av.trackArray[HEAL_TRACK] and not av.trackArray[SOUND_TRACK]: + trackProgressId = SOUND_TRACK + else: + trackProgressId = HEAL_TRACK + av.b_setTrackProgress(trackProgressId, trackProgress) + print("Fixed trackProgressId: ", trackProgressId) + fixed = 1 + + elif av.rewardTier in [4]: + print("ERROR: You should not be here") + elif av.rewardTier in [5, 6]: + print('drop or lure') + if av.trackArray[DROP_TRACK] and not av.trackArray[LURE_TRACK]: + trackProgressId = LURE_TRACK + elif av.trackArray[LURE_TRACK] and not av.trackArray[DROP_TRACK]: + trackProgressId = DROP_TRACK + else: + trackProgressId = DROP_TRACK + av.b_setTrackProgress(trackProgressId, trackProgress) + print("Fixed trackProgressId: ", trackProgressId) + fixed = 1 + elif av.rewardTier in [7]: + print("ERROR: You should not be here") + elif av.rewardTier in [8]: + print("ERROR: You should not be here") + elif av.rewardTier in [9, 10]: + print('trap or heal, trap or sound') + if av.trackArray[SOUND_TRACK] and not av.trackArray[HEAL_TRACK]: + trackProgressId = HEAL_TRACK + elif av.trackArray[HEAL_TRACK] and not av.trackArray[SOUND_TRACK]: + trackProgressId = SOUND_TRACK + else: + trackProgressId = TRAP_TRACK + av.b_setTrackProgress(trackProgressId, trackProgress) + print("Fixed trackProgressId: ", trackProgressId) + fixed = 1 + elif av.rewardTier in [11]: + print("ERROR: You should not be here") + elif av.rewardTier in [12, 13]: + print('all sort of choices') + if not av.trackArray[HEAL_TRACK]: + trackProgressId = HEAL_TRACK + elif not av.trackArray[SOUND_TRACK]: + trackProgressId = SOUND_TRACK + elif not av.trackArray[DROP_TRACK]: + trackProgressId = DROP_TRACK + elif not av.trackArray[LURE_TRACK]: + trackProgressId = LURE_TRACK + elif not av.trackArray[TRAP_TRACK]: + trackProgressId = TRAP_TRACK + else: + print("ERROR") + av.b_setTrackProgress(trackProgressId, trackProgress) + print("Fixed trackProgressId: ", trackProgressId) + fixed = 1 + else: + print("ERROR: You should not be here") + print() + + # clean up track access + if av.fixTrackAccess(): + fixed = 1 + + # This was an unfortunate typo in Quests.py + if maxMoney == 10: + print('bad maxMoney limit == 10') + av.b_setMaxMoney(100) + # Fill er up cause we feel bad + av.b_setMoney(100) + fixed = 1 + + if av.rewardTier == 5: + if hp < 25 or hp > 34: + print('bad hp: ', end=' ') + + # Somehow they got here without choosing a track + if trackProgressId == -1: + print('bad track training in tier 5!') + print(('avId: %s, trackProgressId: %s, trackProgress: %s' % + (av.doId, trackProgressId, trackProgress))) + av.b_setQuestHistory([]) + av.b_setQuests([]) + # Make them choose again + av.b_setRewardHistory(4, []) + av.b_setTrackProgress(-1, 0) + av.fixAvatar() + av.inventory.zeroInv() + av.inventory.maxOutInv() + av.d_setInventory(av.inventory.makeNetString()) + print('new track access: ', av.trackArray) + fixed = 1 + + elif av.rewardTier == 7: + if hp < 34 or hp > 43: + print('bad hp: ', end=' ') + if trackProgressId != -1: + print('bad track training in tier 7!') + av.b_setQuestHistory([]) + av.b_setQuests([]) + av.b_setRewardHistory(7, []) + av.b_setTrackProgress(-1, 0) + av.fixAvatar() + av.inventory.zeroInv() + av.inventory.maxOutInv() + av.d_setInventory(av.inventory.makeNetString()) + fixed = 1 + + else: + # Nothing to fix here + pass + + if fixed: + db = DatabaseObject.DatabaseObject(self.air, av.doId) + db.storeObject(av) + print("Avatar repaired.") + print() + + return + +class AvatarPrinter(AvatarIterator): + + def __init__(self, air): + AvatarIterator.__init__(self, air) + self.hoodInfoStore = HoodInfoStore() + + def processAvatar(self, av, db): + numFriends = 0 + numSecretFriends = 0 + cogCount = 0 + + for friend in av.friendsList: + numFriends += 1 + if friend[1]: + numSecretFriends += 1 + + for cogTotal in av.cogCounts: + cogCount += cogTotal + + #if ((av.maxHp == 15) and + # (av.experience.experience[0] + + # av.experience.experience[1] + + # av.experience.experience[2] + + # av.experience.experience[3] + + # av.experience.experience[4] + + # av.experience.experience[5] + + # av.experience.experience[6] == 0) and + # numFriends == 0): + # # Do not count this fella + # return + + + #oldDna = self.backupDict.get(av.doId) + #newDna = av.dna.asTuple() + #if oldDna and (oldDna != newDna): + # print '================' + # print av.doId + # print oldDna + # print newDna + + #self.dnaDict[av.doId] = av.dna.asTuple() + #print av.doId, ' finished' #, av._name, "dna: ", av.dna.asTuple() + #return + #import ToonDNA + #newDNA = ToonDNA.ToonDNA() + #newDNA.newToonFromProperties(*av.dna.asTuple()) + #print 'old: ', av.dna + #print 'new: ', newDNA + + if av.doId % 10000 == 0: + print(("Working on avatar: %s" % av.doId)) + #print ("%s, %s, %s, %s, %s" % + # (av.doId, av.maxHp, len(av.hoodsVisited), len(av.safeZonesVisited), cogCount)) + self.hoodInfoStore.record(av.maxHp, len(av.hoodsVisited), len(av.safeZonesVisited)) + return + + print(("%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s" % + (av.doId, + av._name, + av.maxHp, + av.dna.head, + av.dna.topTex, + av.dna.botTex, + av.dna.armColor, + av.dna.legColor, + av.dna.headColor, + numFriends, + numSecretFriends, + len(av.hoodsVisited), + av.rewardTier, + av.trackArray, + av.trackProgressId, + av.trackProgress, + # Experience is a tuple of 6 values + av.experience.experience[0], + av.experience.experience[1], + av.experience.experience[2], + av.experience.experience[3], + av.experience.experience[4], + av.experience.experience[5], + av.experience.experience[6], + ))) + + + +class HoodInfoStore: + def __init__(self): + self.__printCount = 0 + # Init the values to 0 + self.avatarCount = {15:0, + 16:0, + 17:0, + 18:0, + 19:0, + 20:0, + } + self.hoodsVisited = {15: {1:0, 2:0, 3:0, 4:0, 5:0, 6:0}, + 16: {1:0, 2:0, 3:0, 4:0, 5:0, 6:0}, + 17: {1:0, 2:0, 3:0, 4:0, 5:0, 6:0}, + 18: {1:0, 2:0, 3:0, 4:0, 5:0, 6:0}, + 19: {1:0, 2:0, 3:0, 4:0, 5:0, 6:0}, + 20: {1:0, 2:0, 3:0, 4:0, 5:0, 6:0}, + } + self.safeZonesVisited = {15: {1:0, 2:0, 3:0, 4:0, 5:0, 6:0}, + 16: {1:0, 2:0, 3:0, 4:0, 5:0, 6:0}, + 17: {1:0, 2:0, 3:0, 4:0, 5:0, 6:0}, + 18: {1:0, 2:0, 3:0, 4:0, 5:0, 6:0}, + 19: {1:0, 2:0, 3:0, 4:0, 5:0, 6:0}, + 20: {1:0, 2:0, 3:0, 4:0, 5:0, 6:0}, + } + + def maybePrint(self): + self.__printCount += 1 + if self.__printCount % 100 == 0: + for hp, count in list(self.avatarCount.items()): + if count > 0: + print(("hp: %d count: %d hoods: %s sz: %s" % + (hp, count, self.hoodsVisited[hp], self.safeZonesVisited[hp]))) + print() + + def record(self, hp, numHoods, numSafeZones): + self.maybePrint() + if hp < 21: + self.avatarCount[hp] += 1 + self.hoodsVisited[hp][numHoods] += 1 + self.safeZonesVisited[hp][numSafeZones] += 1 + + +""" +import UtilityStart +import RepairAvatars +r = RepairAvatars.AvatarFixer(simbase.air) +r.start() + + +import UtilityStart +import RepairAvatars +r = RepairAvatars.AvatarPrinter(simbase.air) +h = RepairAvatars.HoodInfoStore() +r.start() + + + + +for avId, backupDna in backupDict.items(): + newDna = newDict.get(avId): + if newDna and (newDna != backupDna): + print "================" + print avId + print backupDna + print newDna + +from toontown.toon import DistributedToonAI +from toontown.toon import ToonDNA +from toontown.toon import InventoryBase +def fixAv(doId): + av = DistributedToonAI.DistributedToonAI(simbase.air) + av.doId = doId + print doId + av.inventory = InventoryBase.InventoryBase(av) + av.teleportZoneArray = [] + db = DatabaseObject.DatabaseObject(simbase.air, av.doId) + db.fillin(av, simbase.air.dclassesByName['DistributedToonAI']) + oldD = oldDna.get(doId) + print 'backup DNA', oldD + newD = ToonDNA.ToonDNA() + newD.newToonFromProperties(*oldD) + print ' new DNA', newD.asTuple() + av.b_setDNAString(newD.makeNetString()) + db.storeObject(av, ["setDNAString"]) + print 'done' + +def fixAv(doId): + +av = DistributedToonAI.DistributedToonAI(simbase.air) +av.doId = doId +db = DatabaseObject.DatabaseObject(simbase.air, av.doId) +db.getFields(db.getDatabaseFields(simbase.air.dclassesByName['DistributedToonAI'])) +db.fillin(av, simbase.air.dclassesByName['DistributedToonAI']) +print doId +progressId, progress = av.getTrackProgress() +trackAccess = av.getTrackAccess() +print "old progressId: %s progress: %s" % (progressId, progress) +print "trackAccess: ", trackAccess +print "Tier: ", av.rewardTier +# av.b_setTrackProgress(trackId, progress) +# db.storeObject(av, ["setTrackProgress"]) +print 'done' + +""" \ No newline at end of file diff --git a/toontown/ai/ToontownAIRepository.py b/toontown/ai/ToontownAIRepository.py index 19e549c..7d8d639 100644 --- a/toontown/ai/ToontownAIRepository.py +++ b/toontown/ai/ToontownAIRepository.py @@ -1,8 +1,10 @@ from direct.directnotify import DirectNotifyGlobal +from direct.distributed.PyDatagram import PyDatagram from panda3d.core import * from panda3d.toontown import * from otp.ai.AIZoneData import AIZoneDataStore +from otp.ai.AIMsgTypes import * from otp.ai.TimeManagerAI import TimeManagerAI from otp.distributed.OtpDoGlobals import * from toontown.ai.HolidayManagerAI import HolidayManagerAI @@ -506,3 +508,73 @@ class ToontownAIRepository(ToontownInternalRepository): def setupFiles(self): if not os.path.exists(self.dataFolder): os.mkdir(self.dataFolder) + + # From Anesidora + def sendUpdateToDoId(self, dclassName, fieldName, doId, args, channelId=None): + """ + channelId can be used as a recipient if you want to bypass the normal + airecv, ownrecv, broadcast, etc. If you don't include a channelId + or if channelId == doId, then the normal broadcast options will + be used. + + See Also: def queryObjectField + """ + dclass=self.dclassesByName.get(dclassName+self.dcSuffix) + assert dclass is not None + if channelId is None: + channelId=doId + if dclass is not None: + dg = dclass.aiFormatUpdate( + fieldName, doId, channelId, self.ourChannel, args) + self.send(dg) + + def createDgUpdateToDoId(self, dclassName, fieldName, doId, args, + channelId=None): + """ + channelId can be used as a recipient if you want to bypass the normal + airecv, ownrecv, broadcast, etc. If you don't include a channelId + or if channelId == doId, then the normal broadcast options will + be used. + + This is just like sendUpdateToDoId, but just returns + the datagram instead of immediately sending it. + """ + result = None + dclass=self.dclassesByName.get(dclassName+self.dcSuffix) + assert dclass is not None + if channelId is None: + channelId=doId + if dclass is not None: + dg = dclass.aiFormatUpdate( + fieldName, doId, channelId, self.ourChannel, args) + result = dg + return result + + def sendUpdateToGlobalDoId(self, dclassName, fieldName, doId, args): + """ + Used for sending messages from an AI directly to an + uber object. + """ + dclass = self.dclassesByName.get(dclassName) + assert dclass, 'dclass %s not found in DC files' % dclassName + dg = dclass.aiFormatUpdate( + fieldName, doId, doId, self.ourChannel, args) + self.send(dg) + + def addPostSocketClose(self, themessage): + # Time to send a register for channel message to the msgDirector + datagram = PyDatagram() +# datagram.addServerControlHeader(CONTROL_ADD_POST_REMOVE) + datagram.addInt8(1) + datagram.addChannel(CONTROL_MESSAGE) + datagram.addUint16(CONTROL_ADD_POST_REMOVE) + + datagram.addBlob(themessage.getMessage()) + self.send(datagram) + + def addPostSocketCloseUD(self, dclassName, fieldName, doId, args): + dclass = self.dclassesByName.get(dclassName) + assert dclass, 'dclass %s not found in DC files' % dclassName + dg = dclass.aiFormatUpdate( + fieldName, doId, doId, self.ourChannel, args) + self.addPostSocketClose(dg) \ No newline at end of file diff --git a/toontown/distributed/ToontownInternalRepository.py b/toontown/distributed/ToontownInternalRepository.py index 0d6f1ff..850f36c 100644 --- a/toontown/distributed/ToontownInternalRepository.py +++ b/toontown/distributed/ToontownInternalRepository.py @@ -14,3 +14,23 @@ class ToontownInternalRepository(OTPInternalRepository): return False return True + + # Anesidora + def sendUpdateToDoId(self, dclassName, fieldName, doId, args, + channelId=None): + """ + channelId can be used as a recipient if you want to bypass the normal + airecv, ownrecv, broadcast, etc. If you don't include a channelId + or if channelId == doId, then the normal broadcast options will + be used. + + See Also: def queryObjectField + """ + dclass=self.dclassesByName.get(dclassName+self.dcSuffix) + assert dclass is not None + if channelId is None: + channelId=doId + if dclass is not None: + dg = dclass.aiFormatUpdate( + fieldName, doId, channelId, self.ourChannel, args) + self.send(dg) diff --git a/toontown/parties/DistributedParty.py b/toontown/parties/DistributedParty.py index b525471..915fd86 100644 --- a/toontown/parties/DistributedParty.py +++ b/toontown/parties/DistributedParty.py @@ -378,7 +378,7 @@ class DistributedParty(DistributedObject.DistributedObject): def loadDecorations(self): self.decorationsList = [] for decorBase in self.partyInfo.decors: - self.decorationsList.append(Decoration(PartyGlobals.DecorationIds.getString(decorBase.decorId), PartyUtils.convertDistanceFromPartyGrid(decorBase.x, 0), PartyUtils.convertDistanceFromPartyGrid(decorBase.y, 1), PartyUtils.convertDegreesFromPartyGrid(decorBase.h))) + self.decorationsList.append(Decoration(PartyGlobals.DecorationIds(decorBase.decorId).name, PartyUtils.convertDistanceFromPartyGrid(decorBase.x, 0), PartyUtils.convertDistanceFromPartyGrid(decorBase.y, 1), PartyUtils.convertDegreesFromPartyGrid(decorBase.h))) def unload(self): if hasattr(self, 'decorationsList') and self.decorationsList: diff --git a/toontown/parties/DistributedPartyActivity.py b/toontown/parties/DistributedPartyActivity.py index f96e492..e71e9c0 100644 --- a/toontown/parties/DistributedPartyActivity.py +++ b/toontown/parties/DistributedPartyActivity.py @@ -20,7 +20,7 @@ class DistributedPartyActivity(DistributedObject.DistributedObject): def __init__(self, cr, activityId, activityType, wantLever = False, wantRewardGui = False): DistributedObject.DistributedObject.__init__(self, cr) self.activityId = activityId - self.activityName = PartyGlobals.ActivityIds.getString(self.activityId) + self.activityName = PartyGlobals.ActivityIds(self.activityId).name self.activityType = activityType self.wantLever = wantLever self.wantRewardGui = wantRewardGui @@ -234,9 +234,9 @@ class DistributedPartyActivity(DistributedObject.DistributedObject): def loadSign(self): actNameForSign = self.activityName if self.activityId == PartyGlobals.ActivityIds.PartyJukebox40: - actNameForSign = PartyGlobals.ActivityIds.getString(PartyGlobals.ActivityIds.PartyJukebox) + actNameForSign = PartyGlobals.ActivityIds.PartyJukebox.name elif self.activityId == PartyGlobals.ActivityIds.PartyDance20: - actNameForSign = PartyGlobals.ActivityIds.getString(PartyGlobals.ActivityIds.PartyDance) + actNameForSign = PartyGlobals.ActivityIds.PartyDance.name self.sign = self.root.attachNewNode('%sSign' % self.activityName) self.signModel = self.party.defaultSignModel.copyTo(self.sign) self.signFlat = self.signModel.find('**/sign_flat') diff --git a/toontown/parties/Party.py b/toontown/parties/Party.py index 54d12b6..a7a23ed 100644 --- a/toontown/parties/Party.py +++ b/toontown/parties/Party.py @@ -15,7 +15,6 @@ from toontown.hood import Place from toontown.hood import SkyUtil from toontown.parties import PartyPlanner from toontown.parties.DistributedParty import DistributedParty - class Party(Place.Place): notify = DirectNotifyGlobal.directNotify.newCategory('Party') @@ -230,9 +229,9 @@ class Party(Place.Place): zoneId = requestStatus['zoneId'] avId = requestStatus['avId'] shardId = requestStatus['shardId'] - if hoodId == ToontownGlobals.PartyHood and zoneId == self.getZoneId() and shardId == None: + if hoodId == PartyHood and zoneId == self.getZoneId() and shardId == None: self.fsm.request('teleportIn', [requestStatus]) - elif hoodId == ToontownGlobals.MyEstate: + elif hoodId == MyEstate: self.doneStatus = requestStatus self.getEstateZoneAndGoHome(requestStatus) else: diff --git a/toontown/parties/PartyEditorListElement.py b/toontown/parties/PartyEditorListElement.py index 6fd5162..29d057c 100644 --- a/toontown/parties/PartyEditorListElement.py +++ b/toontown/parties/PartyEditorListElement.py @@ -21,7 +21,7 @@ class PartyEditorListElement(DirectButton): (0.0, 0.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.5, 0.5, 0.5, 1.0)) - assetName = PartyGlobals.DecorationIds.getString(self.id) + assetName = PartyGlobals.DecorationIds(self.id).name if assetName == 'Hydra': assetName = 'StageSummer' geom = self.partyEditor.decorationModels.find('**/partyDecoration_%s' % assetName) @@ -49,11 +49,11 @@ class PartyEditorListElement(DirectButton): (0.0, 1.0, 0.0, 1.0), (1.0, 1.0, 0.0, 1.0), (0.5, 0.5, 0.5, 1.0)) - iconString = PartyGlobals.ActivityIds.getString(self.id) - if self.id == PartyGlobals.ActivityIds.PartyJukebox40: - iconString = PartyGlobals.ActivityIds.getString(PartyGlobals.ActivityIds.PartyJukebox) + iconString = PartyGlobals.ActivityIds(self.id).name + if self.id == PartyGlobals.ActivityIds.PartyJukebox40.name: + iconString = PartyGlobals.ActivityIds.PartyJukebox.name elif self.id == PartyGlobals.ActivityIds.PartyDance20: - iconString = PartyGlobals.ActivityIds.getString(PartyGlobals.ActivityIds.PartyDance) + iconString = PartyGlobals.ActivityIds.PartyDance.name geom = getPartyActivityIcon(self.partyEditor.activityIconsModel, iconString) scale = 0.35 geom3_color = (0.5, 0.5, 0.5, 1.0) diff --git a/toontown/parties/PartyPlanner.py b/toontown/parties/PartyPlanner.py index b598083..2b5db2a 100644 --- a/toontown/parties/PartyPlanner.py +++ b/toontown/parties/PartyPlanner.py @@ -2,6 +2,7 @@ import calendar from datetime import datetime from datetime import timedelta from panda3d.core import Vec3, Vec4, Point3, TextNode, VBase4 +from panda3d.otp import * from otp.otpbase import OTPLocalizer from direct.gui.DirectGui import DirectFrame, DirectButton, DirectLabel, DirectScrolledList, DirectCheckButton from direct.gui import DirectGuiGlobals @@ -659,7 +660,7 @@ class PartyPlanner(DirectFrame, FSM): return invitees def processAddPartyResponse(self, hostId, errorCode): - PartyPlanner.notify.debug('processAddPartyResponse : hostId=%d errorCode=%s' % (hostId, PartyGlobals.AddPartyErrorCode.getString(errorCode))) + PartyPlanner.notify.debug('processAddPartyResponse : hostId=%d errorCode=%s' % (hostId, PartyGlobals.AddPartyErrorCode(errorCode).name)) goingBackAllowed = False if errorCode == PartyGlobals.AddPartyErrorCode.AllOk: goingBackAllowed = False diff --git a/toontown/parties/PublicPartyGui.py b/toontown/parties/PublicPartyGui.py index bd7f8a7..3680ca4 100644 --- a/toontown/parties/PublicPartyGui.py +++ b/toontown/parties/PublicPartyGui.py @@ -177,7 +177,7 @@ class PublicPartyGui(DirectFrame): text = TTLocalizer.PartyActivityNameDict[activityId]['generic'] if number > 1: text += ' X %d' % number - item = DirectLabel(relief=None, text=text, text_align=TextNode.ACenter, text_scale=0.05, text_pos=(0.0, -0.15), geom_scale=0.3, geom_pos=Vec3(0.0, 0.0, 0.07), geom=PartyUtils.getPartyActivityIcon(self.activityIconsModel, PartyGlobals.ActivityIds.getString(activityId))) + item = DirectLabel(relief=None, text=text, text_align=TextNode.ACenter, text_scale=0.05, text_pos=(0.0, -0.15), geom_scale=0.3, geom_pos=Vec3(0.0, 0.0, 0.07), geom=PartyUtils.getPartyActivityIcon(self.activityIconsModel, PartyGlobals.ActivityIds(activityId).name)) self.activityList.addItem(item) return diff --git a/toontown/shtiker/EventsPage.py b/toontown/shtiker/EventsPage.py index b95da74..11481f4 100644 --- a/toontown/shtiker/EventsPage.py +++ b/toontown/shtiker/EventsPage.py @@ -190,7 +190,7 @@ class EventsPage(ShtikerPage.ShtikerPage): textForActivity = activityName else: textForActivity = '%s x %d' % (activityName, count) - iconString = PartyGlobals.ActivityIds.getString(activityBase.activityId) + iconString = PartyGlobals.ActivityIds(activityBase.activityId).name geom = getPartyActivityIcon(self.activityIconsModel, iconString) label = DirectLabel(relief=None, geom=geom, geom_scale=0.38, geom_pos=Vec3(0.0, 0.0, -0.17), text=textForActivity, text_scale=TTLocalizer.EPactivityItemLabel, text_align=TextNode.ACenter, text_pos=(-0.01, -0.43), text_wordwrap=7.0) return label @@ -201,7 +201,7 @@ class EventsPage(ShtikerPage.ShtikerPage): textForDecoration = decorationName else: textForDecoration = decorationName + ' x ' + str(count) - assetName = PartyGlobals.DecorationIds.getString(decorBase.decorId) + assetName = PartyGlobals.DecorationIds(decorBase.decorId).name if assetName == 'Hydra': assetName = 'StageSummer' label = DirectLabel(relief=None, geom=self.decorationModels.find('**/partyDecoration_%s' % assetName), text=textForDecoration, text_scale=TTLocalizer.EPdecorationItemLabel, text_align=TextNode.ACenter, text_pos=(-0.01, -0.43), text_wordwrap=7.0) @@ -277,7 +277,7 @@ class EventsPage(ShtikerPage.ShtikerPage): textOfActivity = TTLocalizer.PartyActivityNameDict[activityId]['generic'] else: textOfActivity = TTLocalizer.PartyActivityNameDict[activityId]['generic'] + ' x ' + str(countDict[activityId]) - geom = getPartyActivityIcon(self.activityIconsModel, PartyGlobals.ActivityIds.getString(activityId)) + geom = getPartyActivityIcon(self.activityIconsModel, PartyGlobals.ActivityIds(activityId).name) item = DirectLabel(relief=None, text=textOfActivity, text_align=TextNode.ACenter, text_scale=0.05, text_pos=(0.0, -0.15), geom_scale=0.3, geom_pos=Vec3(0.0, 0.0, 0.07), geom=geom) self.invitationActivityList.addItem(item) diff --git a/toontown/uberdog/DistributedPartyManager.py b/toontown/uberdog/DistributedPartyManager.py index 30a2c7e..8932ad6 100644 --- a/toontown/uberdog/DistributedPartyManager.py +++ b/toontown/uberdog/DistributedPartyManager.py @@ -68,7 +68,7 @@ class DistributedPartyManager(DistributedObject): if errorCode == PartyGlobals.AddPartyErrorCode.AllOk: base.localAvatar.setChatAbsolute('New party entered into database successfully.', CFSpeech | CFTimeout) else: - base.localAvatar.setChatAbsolute('New party creation failed : %s' % PartyGlobals.AddPartyErrorCode.getString(errorCode), CFSpeech | CFTimeout) + base.localAvatar.setChatAbsolute('New party creation failed : %s' % PartyGlobals.AddPartyErrorCode(errorCode).name, CFSpeech | CFTimeout) def requestPartyZone(self, avId, zoneId, callback): if zoneId < 0: diff --git a/toontown/uberdog/DistributedPartyManagerAI.py b/toontown/uberdog/DistributedPartyManagerAI.py index 75866e7..fcc6b0c 100644 --- a/toontown/uberdog/DistributedPartyManagerAI.py +++ b/toontown/uberdog/DistributedPartyManagerAI.py @@ -1,8 +1,1393 @@ -from direct.directnotify import DirectNotifyGlobal +import random +import sys +import time + +from direct.showbase.PythonUtil import Functor from direct.distributed.DistributedObjectAI import DistributedObjectAI +from direct.distributed.DistributedObjectGlobalAI import DistributedObjectGlobalAI +from otp.distributed import OtpDoGlobals +from toontown.parties import PartyGlobals +from toontown.parties.DistributedPartyAI import DistributedPartyAI +from toontown.parties.PartyInfo import PartyInfoAI +from toontown.ai import RepairAvatars +from toontown.toonbase import ToontownGlobals class DistributedPartyManagerAI(DistributedObjectAI): - notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyManagerAI') + """AI side class for the party manager.""" + + notify = directNotify.newCategory("DistributedPartyManagerAI") + + def __init__(self, air): + DistributedObjectAI.__init__(self, air) + self.accept("avatarEntered", self.handleAvatarEntered) + + self.allowUnreleased= False + self.canBuy = True # change this to True when boarding has had time on test + self.avIdToPartyZoneId = {} + self.hostAvIdToPartiesRunning = {} # hostAvId to DistributedPartyAIs + self.hostAvIdToAllPartiesInfo = {} # hostAvId to ( public party start time, shardId, zoneId, isPrivate, number of toons there, hostName, activityIds, partyId) + self.avIdEnteringPartyToHostIdZoneId = {} # avIds of toons entering a party to (hostId, zoneId) + self.zoneIdToGuestAvIds = {} # zoneId to list of guest avIds at that party + self.zoneIdToHostAvId = {} # Zone id's mapped to party host Id's + self.hostAvIdToClosingPartyZoneId = {} + self.hostIdToPlanningPartyZoneId = {} # Used for security checks when freeing zones that were used for planning + + # Number of seconds between spontaneous heals + self.healFrequency = 30 # seconds + + def generate(self): + """We have zone info but not required fields, register for the special.""" + # PARTY_MANAGER_UD_TO_ALL_AI will arrive on this channel + self.air.registerForChannel(OtpDoGlobals.OTP_DO_ID_TOONTOWN_PARTY_MANAGER) + DistributedObjectAI.generate(self) + + def announceGenerate(self): + DistributedObjectAI.announceGenerate(self) + + # tell uberdog we are starting up, so we can get info on the currently running public parties + # do whatever other sanity checks is necessary here + self.air.sendUpdateToDoId("DistributedPartyManager", + 'partyManagerAIStartingUp', + OtpDoGlobals.OTP_DO_ID_TOONTOWN_PARTY_MANAGER, + [self.doId, self.air.districtId] + ) + goingDownDg = self.air.createDgUpdateToDoId("DistributedPartyManager", + 'partyManagerAIGoingDown', + OtpDoGlobals.OTP_DO_ID_TOONTOWN_PARTY_MANAGER, + [self.doId, self.air.districtId] + ) + if goingDownDg: + self.air.addPostSocketClose(goingDownDg) + + + + def handleAvatarEntered(self, avatar): + """A toon just logged in, check his party information.""" + DistributedPartyManagerAI.notify.debug( "handleAvatarEntered" ) + #self.air.sendUpdateToDoId("DistributedPartyManager", + # 'avatarLoggedIn', + # OtpDoGlobals.OTP_DO_ID_TOONTOWN_PARTY_MANAGER, + # [avatar.doId]) + + def partyUpdate(self, avId): + """Force uberdog to resend all party related info from databases.""" + DistributedPartyManagerAI.notify.debug( "partyUpdate" ) + self.sendUpdate('avatarLoggedIn', [avId]) + + def sendAddParty(self, hostId, startTime, endTime, isPrivate, inviteTheme, activities, decorations, inviteeIds, costOfParty): + """Pass add party request up to uberdog.""" + DistributedPartyManagerAI.notify.debug( "sendAddParty" ) + self.air.sendUpdateToDoId("DistributedPartyManager", + 'addParty', + OtpDoGlobals.OTP_DO_ID_TOONTOWN_PARTY_MANAGER, + [self.doId, hostId, startTime, endTime, isPrivate, inviteTheme, activities, decorations, inviteeIds, costOfParty]) + # Set up a failsafe incase uberdog has crashed... + taskMgr.doMethodLater( + 5.0, + self.addPartyResponseUdToAi, + "NoResponseFromUberdog_%d_%d"%(self.doId,hostId), + [hostId,PartyGlobals.AddPartyErrorCode.DatabaseError,0] + ) + + def addPartyRequest(self, hostId, startTime, endTime, isPrivate, inviteTheme, activities, decorations, inviteeIds): + """Add a new party.""" + DistributedPartyManagerAI.notify.debug( "addPartyRequest" ) + validPartyRequest = True + senderId = self.air.getAvatarIdFromSender() + toonSender = simbase.air.doId2do.get(senderId) + if not toonSender: + # the toon is not on our district, let the party manager on that district handle it + DistributedPartyManagerAI.notify.debug('addPartyRequest toon %d is not in our district' % senderId) + return + + if hostId != senderId: + # really bad, potential hacker + self.air.writeServerEvent('suspicious', senderId, + 'trying to create party but not host : hostId = %d' % hostId) + validPartyRequest = False + + if validPartyRequest: + validPartyRequest, costOfPartyOrError = self.validatePartyAndReturnCost(hostId, startTime, endTime, isPrivate, inviteTheme, activities, decorations, inviteeIds) + + # assuming all is well, send this to uberdog, otherwise respond back immediately + if validPartyRequest: + actList = [] + for actTuple in activities: + actList.append(actTuple[0]) + decList = [] + for decTuple in decorations: + decList.append(decTuple[0]) + self.air.writeServerEvent("party_buy_attempt", hostId, "act=%s dec=%s" % ( str(actList), str(decList))) + self.sendAddParty(hostId, startTime,endTime, isPrivate, inviteTheme, activities, decorations, inviteeIds, costOfPartyOrError) + else: + DistributedPartyManagerAI.notify.debug('inValid party because : %s' % costOfPartyOrError) + self.sendAddPartyResponse(hostId, PartyGlobals.AddPartyErrorCode.ValidationError) + + def validatePartyAndReturnCost(self, hostId, startTime, endTime, isPrivate, inviteTheme, activities, decorations, inviteeIds): + DistributedPartyManagerAI.notify.debug( "validatePartyAndReturnCost" ) + # First, check to see if this is his only party that isn't cancelled or + # finished. + host = simbase.air.doId2do[hostId] + if not host.canPlanParty(): + return (False,"Other Parties") + + # TODO-parties : We need to validate startTime and endTime to make sure + # they are valid strings, or will sql do that? + try: + startTm = time.strptime(startTime, "%Y-%m-%d %H:%M:%S") + if PartyGlobals.MaxPlannedYear < startTm.tm_year: + return (False,"Start time too far in the future") + elif startTm.tm_year < PartyGlobals.MinPlannedYear: + return (False,"Start time too far in the future") + except ValueError: + return (False, "Can't parse startTime") + + try: + endTm = time.strptime(endTime, "%Y-%m-%d %H:%M:%S") + if PartyGlobals.MaxPlannedYear < endTm.tm_year: + return (False,"End time too far in the future") + elif endTm.tm_year < PartyGlobals.MinPlannedYear: + return (False,"End time too far in the future") + except ValueError: + return (False, "Can't parse endTime") + + if isPrivate not in (0,1): + return (False,"Invalid isPrivate %s" % isPrivate) + + if inviteTheme not in PartyGlobals.InviteTheme.__members__.values(): + return (False,"Invalid inviteTheme %s" % inviteTheme) + + if hasattr(simbase.air, "holidayManager"): + if ToontownGlobals.VALENTINES_DAY not in simbase.air.holidayManager.currentHolidays: + if inviteTheme == PartyGlobals.InviteTheme.Valentoons: + return (False,"Invalid inviteTheme %s" % inviteTheme) + if ToontownGlobals.VICTORY_PARTY_HOLIDAY not in simbase.air.holidayManager.currentHolidays: + if inviteTheme == PartyGlobals.InviteTheme.VictoryParty: + return (False,"Invalid inviteTheme %s" % inviteTheme) + + costOfParty = 0 + activitiesUsedDict = {} + usedGridSquares = {} # key is a tuple (x,y), value isn't that important + actSet = set([]) + for activityTuple in activities: + if activityTuple[0] not in PartyGlobals.ActivityIds.__members__.values(): + return (False,"Invalid activity id %s"%activityTuple[0]) + + activityId = activityTuple[0] + + # Check for holiday restrictions. + if activityId in PartyGlobals.VictoryPartyActivityIds: + if not simbase.air.holidayManager.isHolidayRunning(ToontownGlobals.VICTORY_PARTY_HOLIDAY): + return (False, "Can't add activity %s during Victory Party " %activityId) + if activityId in PartyGlobals.VictoryPartyReplacementActivityIds: + if simbase.air.holidayManager.isHolidayRunning(ToontownGlobals.VICTORY_PARTY_HOLIDAY): + return (False, "Can't add activity %s during Victory Party " %activityId) + + actSet.add(activityTuple[0]) + if activityTuple[0] in activitiesUsedDict: + activitiesUsedDict[activityTuple[0]] += 1 + else: + activitiesUsedDict[activityTuple[0]] = 1 + costOfParty += PartyGlobals.ActivityInformationDict[activityTuple[0]]["cost"] + if activityTuple[1] < 0 or activityTuple[1] >= PartyGlobals.PartyEditorGridSize[0]: + return (False,"Invalid activity x %s"%activityTuple[1]) + if activityTuple[2] < 0 or activityTuple[2] >= PartyGlobals.PartyEditorGridSize[1]: + return (False,"Invalid activity y %s"%activityTuple[2]) + if activityTuple[3] < 0 or activityTuple[3] > 255: + return (False,"Invalid activity h %s"%activityTuple[3]) + # check for unreleased activity + if activityTuple[0] in PartyGlobals.UnreleasedActivityIds: + self.air.writeServerEvent('suspicious', hostId, "trying to buy unreleased activity %s" % + PartyGlobals.ActivityIds(activityTuple[0]).name) + self.notify.warning("%d trying to buy unreleased activity %s" % + (hostId, PartyGlobals.ActivityIds(activityTuple[0]).name)) + if not self.allowUnreleasedServer(): + return (False, "Activity %s is not released" % + PartyGlobals.ActivityIds.get[[0]]) + + # check if the grid squares are valid + gridSize = PartyGlobals.ActivityInformationDict[activityId]["gridsize"] + centerGridX = activityTuple[1] + centerGridY = activityTuple[2] + # y has 14 at the north side (top) of the party editor + yRange = self.computeGridYRange(centerGridY, gridSize[1]) + xRange = self.computeGridXRange(centerGridX, gridSize[0]) + for curGridY in yRange: + for curGridX in xRange: + squareToTest = (curGridX, curGridY) + if squareToTest in usedGridSquares: + self.notify.debug("activitities=%s decor=%s usedGridSquares=%s" % + (str(activities), + str(decorations), + str(usedGridSquares))) + return (False, "Grid Square %s is used twice by %s and %s" % + (str(squareToTest), + str(activityId), + str(usedGridSquares[squareToTest]))) + else: + usedGridSquares[squareToTest]="activity-%d"%activityId + + # Check to see if an activity is used too many times + for id in PartyGlobals.ActivityIds: + if id in activitiesUsedDict: + if activitiesUsedDict[id] > PartyGlobals.ActivityInformationDict[id]["limitPerParty"]: + return (False,"Too many of activity %s"%id) + + # Check for mutually exclusive activities + for mutuallyExclusiveTuples in PartyGlobals.MutuallyExclusiveActivities: + mutSet = set(mutuallyExclusiveTuples) + inter = mutSet.intersection(actSet) + if len(inter) > 1: + return (False, "Mutuallly exclusive activites %s" % str(inter)) + + decorationsUsedDict = {} + for decorationTuple in decorations: + decorId = decorationTuple[0] + if decorId not in PartyGlobals.DecorationIds: + return (False,"%s is not a valid decoration" % decorId) + # Check if decorId is a holiday specific decoration. + decorName = PartyGlobals.DecorationIds(decorId).name + if (decorName == "HeartTarget") \ + or (decorName == "HeartBanner") \ + or (decorName == "FlyingHeart"): + if not simbase.air.holidayManager.isHolidayRunning(ToontownGlobals.VALENTINES_DAY): + return (False, "Can't add ValenToons decoration %s" % decorId) + if decorId in PartyGlobals.VictoryPartyDecorationIds: + if not simbase.air.holidayManager.isHolidayRunning(ToontownGlobals.VICTORY_PARTY_HOLIDAY): + return (False, "Can't add Victory Party decoration %s" % decorId) + elif decorId in PartyGlobals.VictoryPartyReplacementDecorationIds: + if simbase.air.holidayManager.isHolidayRunning(ToontownGlobals.VICTORY_PARTY_HOLIDAY): + return (False, "Can't add decoration during Victory Party %s" % decorId) + + if decorationTuple[0] in decorationsUsedDict: + decorationsUsedDict[decorationTuple[0]] += 1 + else: + decorationsUsedDict[decorationTuple[0]] = 1 + costOfParty += PartyGlobals.DecorationInformationDict[decorationTuple[0]]["cost"] + if decorationTuple[1] < 0 or decorationTuple[1] >= PartyGlobals.PartyEditorGridSize[0]: + return (False,"Invalid decoration X %s" % decorationTuple[1]) + if decorationTuple[2] < 0 or decorationTuple[2] >= PartyGlobals.PartyEditorGridSize[1]: + return (False,"Invalid decoration Y %s" % decorationTuple[2]) + if decorationTuple[3] < 0 or decorationTuple[3] > 255: + return (False,"Invalid decoration H %s" % decorationTuple[3]) + # check for unreleased decoration + if decorationTuple[0] in PartyGlobals.UnreleasedDecorationIds: + self.air.writeServerEvent('suspicious', hostId, "trying to buy unreleased decoration %s" % + PartyGlobals.DecorationIds(decorationTuple[0]).name) + self.notify.warning("%d trying to buy unreleased decoration %s" % + (hostId, PartyGlobals.DecorationIds(decorationTuple[0]).name)) + if not self.allowUnreleasedServer(): + return (False, "Decoration %s is not released" % + PartyGlobals.DecorationIds(decorationTuple[0]).name) + # check if the grid squares are valid + gridSize = PartyGlobals.DecorationInformationDict[decorId]["gridsize"] + centerGridX = decorationTuple[1] + centerGridY = decorationTuple[2] + # y has 14 at the north side (top) of the party editor + yRange = self.computeGridYRange(centerGridY, gridSize[1]) + xRange = self.computeGridXRange(centerGridX, gridSize[0]) + for curGridY in yRange: + for curGridX in xRange: + squareToTest = (curGridX, curGridY) + if squareToTest in usedGridSquares: + self.notify.debug("activitities=%s decor=%s usedGridSquares=%s" % + (str(activities), + str(decorations), + str(usedGridSquares))) + return (False, "decor Grid Square %s is used twice" % str(squareToTest)) + else: + usedGridSquares[squareToTest]="decor-%d"%decorId + + # Check to see if a decoration is used too many times + for id in PartyGlobals.DecorationIds: + if id in decorationsUsedDict: + if decorationsUsedDict[id] > PartyGlobals.DecorationInformationDict[id]["limitPerParty"]: + return (False,"Decoration %s used too many times." % id) + + # Can I afford this party, really? + if costOfParty > host.getTotalMoney(): + return (False,"Party too expensive, cost = %d"%costOfParty) + + # Can't have parties that have 0 empty grid squares. + if len(usedGridSquares) >= PartyGlobals.AvailableGridSquares: + return (False,"Party uses %s grid squares." % len(usedGridSquares)) + + # Wow, you passed all the tests I can think of, ship it! + return (True, costOfParty) + + #### + ## Grid range computation note: + ## We must round with negative values otherwise for center=0, size=3, the + ## result will be [1, 0] when we expect [1, 0, -1]. + ## The range without rounding: range(int(1.5), int(-1.5), -1) + ## The range with rounding: range(int(1.5), int(-2), -1) + ## Not a problem with center>=2 in this example: + ## The range without rounding: range(int(3.5), int(0.5), -1) + ## The range with rounding: range(int(3.5), int(0), -1) + #### + + def computeGridYRange(self, centerGridY, size): + result = [] + if size == 1: + result = [centerGridY] + else: + result = list(range(int(centerGridY + size/2), + int(centerGridY - (size/2)), + -1)) + + # The result list should be the same size as given. + assert len(result) == size, "Bad result range: c=%s s=%s result=%s" % (centerGridY, size, result) + + return result + + def computeGridXRange(self, centerGridX, size): + result = [] + if size == 1: + result = [centerGridX] + else: + result = list(range(int(centerGridX + size/2), + int(centerGridX - (size/2)), + -1)) + + # The result list should be the same size as given. + assert len(result) == size, "Bad result range: c=%s s=%s result=%s" % (centerGridX, size, result) + + return result + + def sendAddPartyResponse(self, hostId, errorCode): + """Tell the client if he's add party request got accepted.""" + self.sendUpdateToAvatarId(hostId, "addPartyResponse", [hostId, errorCode]) + + def markInviteReadButNotReplied(self, inviteKey): + """Just flag the invite as read in the database.""" + self.air.sendUpdateToDoId("DistributedPartyManager", + 'markInviteAsReadButNotReplied', + OtpDoGlobals.OTP_DO_ID_TOONTOWN_PARTY_MANAGER, + [self.doId, inviteKey] + ) + + def respondToInviteFromMailbox(self, context, inviteKey, newStatus, mailboxDoId): + """Send invite response to uberdog.""" + DistributedPartyManagerAI.notify.debug( "respondToInvite" ) + senderId = self.air.getAvatarIdFromSender() + toonSender = simbase.air.doId2do.get(senderId) + if not toonSender: + # the toon is not on our district, let the party manager on that district handle it + DistributedPartyManagerAI.notify.debug('respondToInviteFromMailbox toon %d is not in our district' % senderId) + return + self.air.sendUpdateToDoId("DistributedPartyManager", + 'respondToInvite', + OtpDoGlobals.OTP_DO_ID_TOONTOWN_PARTY_MANAGER, + [self.doId, mailboxDoId, context, inviteKey, newStatus] + ) + + def respondToInviteResponse(self, mailboxDoId, context, inviteKey, retcode, newStatus): + """UD responding to our invite change.""" + DistributedPartyManagerAI.notify.debug( "respondToInviteResponse" ) + mailboxAI = simbase.air.doId2do.get(mailboxDoId) + if mailboxAI: + if newStatus == PartyGlobals.InviteStatus.Rejected: + mailboxAI.respondToRejectInviteCallback(context, inviteKey, retcode) + else: + mailboxAI.respondToAcceptInviteCallback(context, inviteKey, retcode) + + def addPartyResponseUdToAi(self, hostId, errorCode, costOfParty): + """Handle uberdog responding to our addParty message.""" + taskMgr.remove("NoResponseFromUberdog_%d_%d"%(self.doId,hostId)) + if errorCode == PartyGlobals.AddPartyErrorCode.AllOk: + host = simbase.air.doId2do.get(hostId) + self.air.writeServerEvent("party_buy", hostId,"%d" % costOfParty) + if host : + host.takeMoney(costOfParty, bUseBank = True) + else: + # Woah, did he just get a free party? Someone + # bought a party, and while the uberdog was putting it in the + # database, they logged out... how can we make sure the money + # gets taken out? + self.deductMoneyFromOfflineToon(hostId, costOfParty) + + self.sendAddPartyResponse(hostId, errorCode) + + def changePrivateRequest(self, partyId, newPrivateStatus): + """Handle the client requesting to make a party public/private.""" + senderId = self.air.getAvatarIdFromSender() + toonSender = simbase.air.doId2do.get(senderId) + if not toonSender: + # the toon is not on our district, let the party manager on that district handle it + DistributedPartyManagerAI.notify.debug('changePrivateRequest toon %d is not in our district' % senderId) + return + + errorCode = self.partyFieldChangeValidate(partyId) + if errorCode != PartyGlobals.ChangePartyFieldErrorCode.AllOk: + # immediately say we have an error then return + self.sendUpdateToAvatarId(senderId,'changePrivateResponse', + [partyId, newPrivateStatus, errorCode]) + return + + # do whatever other sanity checks is necessary here + self.air.sendUpdateToDoId("DistributedPartyManager", + 'changePrivateRequestAiToUd', + OtpDoGlobals.OTP_DO_ID_TOONTOWN_PARTY_MANAGER, + [self.doId, partyId, newPrivateStatus] + ) + + def partyFieldChangeValidate(self, partyId): + """Do common validation when changing private and status fields for a party.""" + senderId = self.air.getAvatarIdFromSender() + errorCode = PartyGlobals.ChangePartyFieldErrorCode.AllOk + toon = simbase.air.doId2do.get(senderId) + if not toon: + # we don't have the toon for some reason + errorCode = PartyGlobals.ChangePartyFieldErrorCode.ValidationError + return errorCode + + hostingThisParty = False + for party in toon.hostedParties: + if partyId == party.partyId: + hostingThisParty = True + break + + if not hostingThisParty: + # the toon is not hosting this partyId + # really bad, potential hacker + self.air.writeServerEvent('suspicious', senderId, + 'trying to change field of party %s but not the host' % partyId) + errorCode = PartyGlobals.ChangePartyFieldErrorCode.ValidationError + return errorCode + + if party.hostId != senderId: + # really bad, potential hacker + self.air.writeServerEvent('suspicious', senderId, + 'trying to change field of party %s but not the host' % partyId) + errorCode = PartyGlobals.ChangePartyFieldErrorCode.ValidationError + return errorCode + + return errorCode + + def changePrivateResponseUdToAi(self, hostId, partyId, newPrivateStatus, errorCode): + """Handle the Uberdog telling us if the change private succeeded or not.""" + if errorCode == PartyGlobals.ChangePartyFieldErrorCode.AllOk: + if hostId in self.air.doId2do: + av = self.air.doId2do[hostId] + for partyInfo in av.hostedParties: + if partyInfo.partyId == partyId: + partyInfo.isPrivate = newPrivateStatus + if hostId in self.hostAvIdToAllPartiesInfo: + self.hostAvIdToAllPartiesInfo[hostId][3] = newPrivateStatus + + self.sendUpdateToAvatarId(hostId, "changePrivateResponse", [partyId, newPrivateStatus, errorCode]) + + def changePartyStatusRequest(self, partyId, newPartyStatus): + """Handle the client requesting to change the party status.""" + senderId = self.air.getAvatarIdFromSender() + toonSender = simbase.air.doId2do.get(senderId) + if not toonSender: + # the toon is not on our district, let the party manager on that district handle it + DistributedPartyManagerAI.notify.debug('changePartyStatusRequest toon %d not in our district' % senderId) + return + errorCode = self.partyFieldChangeValidate(partyId) + if errorCode != PartyGlobals.ChangePartyFieldErrorCode.AllOk: + # immediately say we have an error then return + self.sendUpdateToAvatarId(senderId,'changePartyStatusResponse', + [partyId, newPartyStatus, errorCode, 0]) + return + + # do whatever other sanity checks is necessary here + self.air.sendUpdateToDoId("DistributedPartyManager", + 'changePartyStatusRequestAiToUd', + OtpDoGlobals.OTP_DO_ID_TOONTOWN_PARTY_MANAGER, + [self.doId, partyId, newPartyStatus] + ) + + def changePartyStatusResponseUdToAi(self, hostId, partyId, newPartyStatus, errorCode): + """Handle the Uberdog telling us if the change partyStatus succeeded or not.""" + beansRefunded = 0 + if hostId in self.air.doId2do: + av = self.air.doId2do[hostId] + for partyInfo in av.hostedParties: + if partyInfo.partyId == partyId: + partyInfo.status = newPartyStatus + if newPartyStatus == PartyGlobals.PartyStatus.Cancelled: + beansRefunded = self.getCostOfParty(partyInfo) + beansRefunded = int(PartyGlobals.PartyRefundPercentage * beansRefunded) + av.addMoney(beansRefunded) + self.air.writeServerEvent("party_cancel", hostId, "%d|%d|%d|%d" % (beansRefunded, partyId, newPartyStatus, errorCode)) + self.sendUpdateToAvatarId(hostId, "changePartyStatusResponse", [partyId, newPartyStatus, errorCode, beansRefunded]) + + def getCostOfParty(self, partyInfo): + newCost = 0 + for activityBase in partyInfo.activityList: + newCost += PartyGlobals.ActivityInformationDict[activityBase.activityId]["cost"] + for decorBase in partyInfo.decors: + newCost += PartyGlobals.DecorationInformationDict[decorBase.decorId]["cost"] + return newCost + + def getAllPublicParties(self): + allParties = list(self.hostAvIdToAllPartiesInfo.values()) + allParties.sort() + returnParties = [] + curGmTime = time.time() + for partyInfo in allParties: + # If the party is private, just continue, don't append it. + if partyInfo[3]: + continue + # We want to return a list that has positive time and isPrivate + minLeft = int( (PartyGlobals.DefaultPartyDuration *60) - ( curGmTime - partyInfo[0]) / 60.0) + if minLeft <= 0: + continue + returnParties.append(partyInfo[1:3] + partyInfo[4:7] + [minLeft]) + DistributedPartyManagerAI.notify.debug("getAllPublicParties : %s" % returnParties) + return returnParties + + def updateToPublicPartyCountUdToAllAi(self, hostId, newCount): + """ + The count has changed on a public party. + """ + DistributedPartyManagerAI.notify.debug("updateToPublicPartyCountUdToAllAi : hostId=%s newCount=%s"%(hostId, newCount)) + if hostId in self.hostAvIdToAllPartiesInfo: + self.hostAvIdToAllPartiesInfo[hostId][4] = newCount + + def partyHasFinishedUdToAllAi(self, hostId): + """ + This party has finished, it may not have been mine, so just update my + public party information. + """ + if hostId in self.hostAvIdToAllPartiesInfo: + del self.hostAvIdToAllPartiesInfo[hostId] + + def updateToPublicPartyInfoUdToAllAi(self, hostId, time, shardId, zoneId, isPrivate, numberOfGuests, hostName, activityIds, partyId): + """ + There is an update to a public party, might not be on this AI so just + update the public party information. + """ + DistributedPartyManagerAI.notify.debug("updateToPublicPartyInfoUdToAllAi : hostId=%s time=%s shardId=%s zoneId=%s isPrivate=%s numberOfGuests=%s hostName=%s"%(hostId, time, shardId, zoneId, isPrivate, numberOfGuests, hostName)) + self.hostAvIdToAllPartiesInfo[hostId] = [time, shardId, zoneId, isPrivate, numberOfGuests, hostName, activityIds, partyId] + + def delete(self): + DistributedPartyManagerAI.notify.debug("BASE: delete: deleting DistributedPartyManagerAI object") + self.ignoreAll() + DistributedObjectGlobalAI.DistributedObjectGlobalAI.delete(self) + for party in list(self.hostAvIdToPartiesRunning.values()): + party.requestDelete() + del self.avIdToPartyZoneId + del self.hostAvIdToPartiesRunning + del self.hostAvIdToAllPartiesInfo + del self.avIdEnteringPartyToHostIdZoneId + del self.zoneIdToGuestAvIds + del self.zoneIdToHostAvId + del self.hostAvIdToClosingPartyZoneId + del self.hostIdToPlanningPartyZoneId + + ## ----------------------------------------------------------- + ## Zone allocation and enter code + ## ----------------------------------------------------------- + + # Gets the party zone based on the host's avatar ID + def getPartyZone(self, hostId, zoneId, planningParty): + DistributedPartyManagerAI.notify.debug("getPartyZone: hostId=%s zoneId=%s planningParty=%s" % (hostId, zoneId, planningParty)) + # Get the party the avatar is in. + # If the party is running in this ai, and sender is allowed to go + # (public or invited, and not full), then return the party zone. + # If no party is running right now, and the sender is the avatar + # Then look for the party info and check if he's the owner. + # If he's the owner, then create the party, and return the created party zone + # If he's not the owner, maybe he's got a valid zone already and he's + # looking to join a party coming from a public party gate + # Otherwise fail + + senderId = self.air.getAvatarIdFromSender() + + # If we're planning a party, we need to give them a zone to plan in, but + # we don't need to create a DistributedParty/AI and add them to all the + # dictionaries. + if planningParty: + if hostId != senderId: + self.air.writeServerEvent('suspicious', senderId, 'trying to plan party but not host : hostId = %d' % hostId) + self.__sendNoPartyZoneToClient(senderId) + return + # let's allocate a zone for the client to plan the party in + zoneId = self.air.allocateZone() + # We'll need to free it later, and we want to make sure we're freeing + # the right zone, so let's remember it. + self.hostIdToPlanningPartyZoneId[senderId] = zoneId + DistributedPartyManagerAI.notify.debug("getPartyZone : Avatar %s is planning party in zone %s" % (senderId, zoneId)) + self.sendUpdateToAvatarId(senderId, "receivePartyZone", [senderId, 0, zoneId]) + return + + # If they have a zoneId, that means they came from a public party gate + # or they are teleporting directly to a toon in a party or they are the + # host returning to their own party. + if zoneId > 0 : + if zoneId not in self.zoneIdToHostAvId: + # this party is gone, you'cant go to it + self.notify.warning("Trying to go to a party that is gone. zoneId=%s" % zoneId) + self.__sendNoPartyZoneToClient(senderId) + return + + partyHostId = self.zoneIdToHostAvId[zoneId] + if partyHostId in self.hostAvIdToClosingPartyZoneId: + # This party is closing, you can't go to it. + self.notify.warning("Trying to go to a party that is closing. hostId=%s" % partyHostId) + self.__sendNoPartyZoneToClient(senderId) + return + + if partyHostId != senderId: + # If I'm not the host, check to see if the party is private + if self.hostAvIdToPartiesRunning[partyHostId].partyInfo.isPrivate: + # This is a private party, check the invitee list + if senderId not in self.hostAvIdToPartiesRunning[partyHostId].inviteeIds: + # The senderId is not on the invitee list of a private party + # so they can't attend, sorry. + self.__sendNoPartyZoneToClient(senderId) + return + self.__addReferences(senderId, partyHostId) + self.__waitForToonToEnterParty(senderId, partyHostId, zoneId) + self.__sendPartyZoneToClient(senderId, partyHostId) + return + + self.__enterParty(senderId, hostId) + + avPartyZoneId = self.avIdToPartyZoneId.get(hostId) + senderPartyZoneId = self.avIdToPartyZoneId.get(senderId) + + isSenderHost = False + isHostStartingParty = False + + # Else if sender is host, then toon might be starting a party + if senderId == hostId: + isSenderHost = True + + # If the host got here and there's no party for him, then start a party. + # TODO-parties: Double check on the party shard dict to make sure that toon's party is not happening somewhere else. + if ( + (senderId not in self.hostAvIdToPartiesRunning) and + (senderId not in self.hostAvIdToClosingPartyZoneId) + ): + # The host is starting a party, let's clear him out of the avIdToPartyZoneId + #self.clearPartyZoneId(senderId) # this doesn't clear him out of guests + self.__exitParty(senderId) + isHostStartingParty = True + # Else sender is visiting or attending a party at this shard: + else: + # The party we are visiting is not in this shard + if avPartyZoneId is None: + self.notify.warning("Avatar is not at a party in this shard.") + # SDN: tell the client and do something more graceful + # make sure we don't give this guy toonups + try: + # stop tooning up this visitor, he hasn't reached the party yet + av = self.air.doId2do[senderId] + av.stopToonUp() + except: + DistributedPartyManagerAI.notify.debug("couldn't stop toonUpTask for av %s" % self.air.getAvatarIdFromSender()) + self.__sendNoPartyZoneToClient(senderId) + return + + # If sender was at a party: + if senderPartyZoneId is not None: + # Check if toon is teleporting somewhere else in the same party: + # No need to update the party zone information in this case. + try: + if senderPartyZoneId == avPartyZoneId and not isHostStartingParty: + DistributedPartyManagerAI.notify.debug("We are staying in the same zone %s, don't delete." % senderPartyZoneId) + self.__sendPartyZoneToClient(senderId, hostId) + return + else: + DistributedPartyManagerAI.notify.debug("Sender is goint to a different party. Party av zone = %s, sender zone = %s." % (avPartyZoneId, senderPartyZoneId)) + except: + DistributedPartyManagerAI.notify.debug("Sender is not teleporting to the same party.") + + # At this point, toon is at a party and is going/creating a different party + if senderPartyZoneId != avPartyZoneId: + if hostId in self.hostAvIdToClosingPartyZoneId: + # This party is closing, you can't go to it. + self.notify.warning("Trying to go to a party that is closing. hostId=%s" % hostId) + self.__sendNoPartyZoneToClient(senderId) + return + else: + self.notify.debug("644 calling exitParty for %s" % senderId) + self.__exitParty(senderId) + + # Sender is host and he's starting a party, then start the party: + if isSenderHost and isHostStartingParty: + if self.checkHostHasPartiesThatCanStart(hostId): + # We need to ask DistributedPartyManagerUD for the party information + # self.partyInfoOfHostResponseUdToAi will be called with the response + self.notify.debug("starting party Asking uberdog for party ifnromation hostId=%d" % hostId) + self.air.sendUpdateToDoId( + "DistributedPartyManager", + 'partyInfoOfHostRequestAiToUd', + OtpDoGlobals.OTP_DO_ID_TOONTOWN_PARTY_MANAGER, + [self.doId, hostId] + ) + taskMgr.doMethodLater( + 3.0, + self.partyInfoOfHostFailedResponseUdToAi, + "UberdogTimedOut_%d"%hostId, + [hostId] + ) + else: + # save a round trip asking the uberdog, fail immediately + self.air.writeServerEvent('suspicious', senderId, + 'trying to start a party when none can start %d' % senderId) + self.notify.warning('suspicious %d trying to start a party when none can start' % senderId) + self.__sendNoPartyZoneToClient(senderId) + return + + + # We have a toon visiting another toon's party or a host visiting an + # already started party, update dicts + else: + # Note: hostId is host of the party sender is trying to go to + if hostId in self.hostAvIdToClosingPartyZoneId: + # This party is closing, you can't go to it. + self.notify.warning("Trying to go to a party that is closing. hostId=%s" % hostId) + self.__sendNoPartyZoneToClient(senderId) + return + self.__addReferences(senderId, hostId) + zoneId = self.avIdToPartyZoneId[hostId] + self.__waitForToonToEnterParty(senderId, hostId, zoneId) + self.__sendPartyZoneToClient(senderId, hostId) + + def checkHostHasPartiesThatCanStart(self, hostId): + """Return True if the host has any party that can start.""" + result = False + toon = simbase.air.doId2do.get(hostId) + if toon: + hostedParties = toon.hostedParties + for partyInfo in hostedParties: + # it must not be cancelle or finished + if partyInfo.status in (PartyGlobals.PartyStatus.Cancelled, + PartyGlobals.PartyStatus.Finished): + continue + curServerTime = self.air.toontownTimeManager.getCurServerDateTimeForComparison() + if curServerTime < partyInfo.startTime: + # the party is still in the future + continue + if partyInfo.endTime < curServerTime: + # party end time has passed + continue + # if we get here we have at least 1 party that could start + result = True + break + else: + result = True + self.notify.warning("checkHostedParties could not find toon %d " % hostId) + return result + + + def getAvEnterEvent(self): + return 'avatarEnterParty' + + def getAvExitEvent(self, avId=None): + # listen for all exits or a particular exit + # event args: + # if avId given: none + # if avId not given: avId, hostId, zoneId + if avId is None: + return 'avatarExitParty' + else: + return 'avatarExitParty-%s' % avId + + def __enterParty(self, avId, hostId): + # Tasks that should always get called when entering a party + + # Handle unexpected exit + self.acceptOnce(self.air.getAvatarExitEvent(avId), + self.__handleUnexpectedExit, extraArgs=[avId]) + + def __waitForToonToEnterParty(self, avId, hostId, zoneId): + if avId in self.avIdEnteringPartyToHostIdZoneId: + self.notify.warning( + '__waitForToonToEnterParty(avId=%s, ownerId=%s, zoneId=%s): ' + '%s already in avIdToPendingEnter. overwriting' % ( + avId, hostId, zoneId, avId)) + self.avIdEnteringPartyToHostIdZoneId[avId] = (hostId, zoneId) + self.accept(DistributedObjectAI.staticGetLogicalZoneChangeEvent(avId), + Functor(self.__toonChangedZone, avId)) + + def __toonLeftBeforeArrival(self, avId): + if avId not in self.avIdEnteringPartyToHostIdZoneId: + self.notify.warning('__toonLeftBeforeArrival: av %s not in table' % + avId) + return + hostId, zoneId = self.avIdEnteringPartyToHostIdZoneId[avId] + self.notify.warning( + '__toonLeftBeforeArrival: av %s left server before arriving in ' + 'party (host=%s, zone=%s)' % (avId, hostId, zoneId)) + del self.avIdEnteringPartyToHostIdZoneId[avId] + + # When toon changes zone, check if toon has finally entered party + def __toonChangedZone(self, avId, newZoneId, oldZoneId): + #DistributedPartyManagerAI.notify.debug('_toonChangedZone(avId=%s, newZoneId=%s, oldZoneId=%s)' % (avId, newZoneId, oldZoneId)) + if avId not in self.avIdEnteringPartyToHostIdZoneId: + self.notify.warning('__toonChangedZone: av %s not in table' % + avId) + return + av = self.air.doId2do.get(avId) + if not av: + self.notify.warning('__toonChangedZone(%s): av not present' % avId) + return + hostId, zoneId = self.avIdEnteringPartyToHostIdZoneId[avId] + if newZoneId == zoneId: + del self.avIdEnteringPartyToHostIdZoneId[avId] + self.ignore(DistributedObjectAI.staticGetLogicalZoneChangeEvent(avId)) + self.announceToonEnterPartyZone(avId, hostId, zoneId) + + def announceToonEnterPartyZone(self, avId, hostId, zoneId): + """ + announce to the rest of the system that a toon is entering a party + """ + DistributedPartyManagerAI.notify.debug('announceToonEnterPartyZone: %s %s %s' % (avId, hostId, zoneId)) + + av = self.air.doId2do[avId] + + # Toonup + av.startToonUp(self.healFrequency) + + if avId == hostId: + # We have to tell the uberdog that we've started a new party so it can + # update all the other AIs with public party info (but only host does this) + if avId not in self.hostAvIdToAllPartiesInfo: + self.air.sendUpdateToDoId( + "DistributedPartyManager", + 'partyHasStartedAiToUd', + OtpDoGlobals.OTP_DO_ID_TOONTOWN_PARTY_MANAGER, + [self.doId, self.hostAvIdToPartiesRunning[hostId].partyInfo.partyId, self.air.districtId, zoneId, av.getName()] + ) + + # tell host to whisper all his guests that the party has started. + # We have the host do this as host already has access to guest list. + av.sendUpdate( "announcePartyStarted", [self.hostAvIdToPartiesRunning[hostId].partyInfo.partyId] ) + + messenger.send(self.getAvEnterEvent(), [avId, hostId, zoneId]) + # Tell the uberdog about the new count + self.air.sendUpdateToDoId( + "DistributedPartyManager", + 'toonHasEnteredPartyAiToUd', + OtpDoGlobals.OTP_DO_ID_TOONTOWN_PARTY_MANAGER, + [hostId], + ) + + def announceToonExitPartyZone(self, avId, hostId, zoneId): + """ announce to the rest of the system that a toon is exiting + a party """ + EstateManagerAI.notify.debug('announceToonExitPartyZone: %s %s %s' % + (avId, hostId, zoneId)) + messenger.send(self.getAvExitEvent(avId)) + messenger.send(self.getAvExitEvent(), [avId, hostId, zoneId]) + + # Return a running distributed party based on the Zone id: + def getRunningPartyFromZoneId(self, zoneId): + hostId = self.zoneIdToHostAvId.get(zoneId) + if hostId: + return self.hostAvIdToPartiesRunning.get(hostId) + return None + + # Send Party Zone information back to the client + def __sendPartyZoneToClient(self, avId, hostId): + self.notify.warning("__sendPartyZoneToClient called with avId=%d, hostId=%d" % (avId, hostId)) + try: + zoneId = self.avIdToPartyZoneId[avId] + partyId = self.hostAvIdToPartiesRunning[hostId].partyInfo.partyId + self.sendUpdateToAvatarId(avId, "receivePartyZone", [hostId, partyId, zoneId]) + except: + self.notify.warning("__sendPartyZoneToClient : zone did not exist for party host %d, and visitor %d" % (hostId, avId)) + self.sendUpdateToAvatarId(avId, "receivePartyZone", [0, 0, 0]) + + # Send empty Party Zone information back to the client + # This is called, for example, in case a toon teleports to a party not + # running on this shard or if the uberdog doesn't think this party can be + # created. + def __sendNoPartyZoneToClient(self, avId): + self.notify.warning("__sendNoPartyZoneToClient : not sending avId %d to a party." % avId) + self.sendUpdateToAvatarId(avId, "receivePartyZone", [0, 0, 0]) + + def partyInfoOfHostFailedResponseUdToAi(self, hostId): + """ + A host tried to create a party that it wasn't time to create, ie, + something fishy went down. Reply that the party creation failed. + If you want to test parties using magic words, be sure to set + allow-random-party-creation 1 in your config overrides. + """ + self.notify.warning("Host with avId %d tried to create a party it wasn't time for." % hostId) + taskMgr.remove("UberdogTimedOut_%d"%hostId) + self.__sendNoPartyZoneToClient(hostId) + + def partyInfoOfHostResponseUdToAi(self, partyInfoTuple, inviteeIds): + """ + Called by UD after it has gathered the relevant information for this + party. + """ + assert self.notify.debugStateCall(self) + taskMgr.remove("UberdogTimedOut_%d"%partyInfoTuple[1]) + # Note this method will send the zone info back to the client + # after it creates the party + partyInfo = PartyInfoAI(*partyInfoTuple) + # request an available zone to have this party in + zoneId = self.air.allocateZone() + + # remove any references to host in parties he's currently attending + # in case he start a party within another party + self.__exitParty(partyInfo.hostId) + + # Host is attending the party, too + self.setPartyZoneId(partyInfo.hostId, zoneId) + + # start a ref count for this zone id + self.zoneIdToGuestAvIds[zoneId] = [] + self.zoneIdToHostAvId[zoneId] = partyInfo.hostId + self.handleGetPartyInfo(partyInfo, inviteeIds) + + def __addReferences(self, senderId, hostId): + DistributedPartyManagerAI.notify.debug("__addReferences : senderId = %s hostId = %s" % (senderId, hostId)) + party = self.hostAvIdToPartiesRunning.get(hostId) + if party is not None: + self.setPartyZoneId(senderId, party.zoneId) + ref = self.zoneIdToGuestAvIds.get(party.zoneId) + if ref is not None: + if not senderId in ref: + ref.append(senderId) + else: + self.zoneIdToGuestAvIds[party.zoneId] = [senderId] + + def __removeReferences(self, avId, zoneId): + try: + self.clearPartyZoneId(avId) + self.zoneIdToGuestAvIds[zoneId].remove(avId) + except: + DistributedPartyManagerAI.notify.debug("we weren't in the zoneIdToGuestAvIds for %s." % zoneId) + pass + + def setPartyZoneId(self, avId, zoneId): + self.avIdToPartyZoneId[avId] = zoneId + frame = sys._getframe(1) + lineno = frame.f_lineno + defName = frame.f_code.co_name + DistributedPartyManagerAI.notify.debug("%s(%s):Added %s:%s" % (defName, lineno, avId, zoneId)) + + def clearPartyZoneId(self, avId, zoneIdFromClient = None): + """Clear avId to partyzoneId dict, if zoneIdFromClient is not none, do it only if they match.""" + if avId not in self.avIdToPartyZoneId: + return + zoneId = self.avIdToPartyZoneId[avId] + frame = sys._getframe(1) + lineno = frame.f_lineno + defName = frame.f_code.co_name + removeFromDict = False + if zoneIdFromClient != None: + if zoneId == zoneIdFromClient: + removeFromDict =True + else: + self.notify.debug("zoneIdFromClient=%s AI thinks he's at zone %s, not removing" % + (zoneIdFromClient, zoneId)) + else: + removeFromDict = True + + if removeFromDict: + DistributedPartyManagerAI.notify.debug("%s(%s):Removed %s:%s" % (defName, lineno, avId, self.avIdToPartyZoneId[avId])) + del self.avIdToPartyZoneId[avId] + + def handleGetPartyInfo(self, partyInfo, inviteeIds): + DistributedPartyManagerAI.notify.debug("handleGetPartyInfo for host %s" % partyInfo.hostId) + # this function is called after the party data is pulled + # from the database. the DistributedPartyAI object is initialized + # here + + # Note: this function is only called by the host of the party. + + # there is a chance that the owner will already have left (by + # closing the window). We need to handle that gracefully. + if partyInfo.hostId not in self.avIdToPartyZoneId: + self.notify.warning("Party Zone info was requested, but the guest left before it could be recived: %d" % estateId) + return + + # create the DistributedPartyAI object for this hostId + if partyInfo.hostId in self.hostAvIdToPartiesRunning: + self.notify.warning("Already have distobj %s, not generating again" % (partyInfo.partyId)) + else: + self.notify.info('start party %s init, owner=%s, frame=%s' % + (partyInfo.partyId, partyInfo.hostId, globalClock.getFrameCount())) + + partyZoneId = self.avIdToPartyZoneId[partyInfo.hostId] + partyAI = DistributedPartyAI(self.air, partyInfo.hostId, partyZoneId, partyInfo, inviteeIds) + + partyAI.generateOtpObject( + parentId=self.air.districtId, + zoneId=partyZoneId, + ) + partyAI.initPartyData() + self.hostAvIdToPartiesRunning[partyInfo.hostId] = partyAI + + self.__addReferences(partyInfo.hostId, partyInfo.hostId) + + # We need to kick guests out when the party is closing and not allow + # anyone else in. Send a message to guests to leave + # Also, alert uberdog. + taskMgr.doMethodLater( + PartyGlobals.DefaultPartyDuration * 3600.0, + self.__setPartyEnded, + "DistributedPartyManagerAI_PartyEnding_%d" % partyZoneId, + [partyInfo.hostId,partyZoneId] + ) + + # Boot the guests + taskMgr.doMethodLater( + PartyGlobals.DefaultPartyDuration * 3600.0 + PartyGlobals.DelayBeforeAutoKick, + self.__bootGuests, + "DistributedPartyManagerAI_BootGuests_%d" % partyZoneId, + [partyInfo.hostId,partyZoneId] + ) + + # We need to clean this party up after everybody is gone + taskMgr.doMethodLater( + PartyGlobals.DefaultPartyDuration * 3600.0 + PartyGlobals.DelayBeforeAutoKick + 10.0, + self.__cleanupParty, + "DistributedPartyManagerAI_CleanUpPartyZone_%d" % partyZoneId, + [partyInfo.hostId,partyZoneId] + ) + + self.notify.info('Finished creating party : partyId %s init, host = %s' % (partyInfo.partyId, partyInfo.hostId)) + + # Now that the zone is set up, send the notification back to + # the client. + zoneId = self.avIdToPartyZoneId[partyInfo.hostId] + self.__sendPartyZoneToClient(partyInfo.hostId, partyInfo.hostId) + self.__waitForToonToEnterParty(partyInfo.hostId, partyInfo.hostId, zoneId) + + def requestShardIdZoneIdForHostId(self, hostId): + """ + Request from either a host of an already started party or a guest of + a party for the shardId and zoneId of that host's party. + """ + senderId = self.air.getAvatarIdFromSender() + if hostId in self.hostAvIdToAllPartiesInfo: + shardId = self.hostAvIdToAllPartiesInfo[hostId][1] + zoneId = self.hostAvIdToAllPartiesInfo[hostId][2] + self.sendUpdateToAvatarId(senderId, 'sendShardIdZoneIdToAvatar', [shardId, zoneId]) + else: + # The host's id is not in our dictionary... that most likely means + # that the AI server has crashed, send back 0 + self.sendUpdateToAvatarId(senderId, 'sendShardIdZoneIdToAvatar', [0, 0]) + + ## ----------------------------------------------------------- + ## Cleanup and exit functions + ## ----------------------------------------------------------- + + def exitParty(self, zoneId): + senderId = self.air.getAvatarIdFromSender() + DistributedPartyManagerAI.notify.debug("exitParty(%s)" % senderId) + # This function is called from client in the normal case, + # such as teleporting out, door out, exiting the game, etc + self.__exitParty(senderId, zoneId) + + def __handleUnexpectedExit(self, avId): + DistributedPartyManagerAI.notify.debug("we got an unexpected exit on av: %s: deleting." % avId) + taskMgr.remove("estateToonUp-" + str(avId)) + if avId in self.avIdEnteringPartyToHostIdZoneId: + self.__toonLeftBeforeArrival(avId) + if avId in self.avIdToPartyZoneId: + self.__exitParty(avId) + else: + DistributedPartyManagerAI.notify.debug("unexpected exit and %s is not in avIdToPartyZoneId" % avId) + return None + + def __exitParty(self, avId, zoneIdFromClient = None): + DistributedPartyManagerAI.notify.debug("__exitParty(%d)" % avId) + DistributedPartyManagerAI.notify.info("__exitParty(%d)" % avId) + # This is called whenever avId leaves a party. + # Just remove references of avId from the party + avZoneId = self.avIdToPartyZoneId.get(avId) + if zoneIdFromClient != None: + # We get a very weird case when you're starting a party from another party + if avZoneId != zoneIdFromClient: + self.notify.debug("overriding avZoneId to %s" % zoneIdFromClient) + avZoneId = zoneIdFromClient + + partyId = -1 + isPlanning = True + if avZoneId is not None: + isPlanning = False + self.clearPartyZoneId(avId, zoneIdFromClient) + if avZoneId in self.zoneIdToGuestAvIds: + if avId in self.zoneIdToGuestAvIds[avZoneId]: + self.notify.debug("removing guest %d from zone %d" % (avId, avZoneId)) + self.zoneIdToGuestAvIds[avZoneId].remove(avId) + else: + DistributedPartyManagerAI.notify.debug("wasn't in zoneIdToGuestAvIds list: %s, %s" % (avZoneId, avId)) + else: + DistributedPartyManagerAI.notify.debug("wasn't in zoneIdToGuestAvIds: %s, %s" % (avZoneId, avId)) + # Tell the uberdog that this host's party has lost a guest + hostAvId = self.zoneIdToHostAvId.get(avZoneId) + if hostAvId: + self.air.sendUpdateToDoId( + "DistributedPartyManager", + 'toonHasExitedPartyAiToUd', + OtpDoGlobals.OTP_DO_ID_TOONTOWN_PARTY_MANAGER, + [self.zoneIdToHostAvId[avZoneId]] + ) + info = self.hostAvIdToAllPartiesInfo.get(hostAvId) + if info: + partyId = info[7] + + else: + self.notify.warning("__exitParty() avZoneId=%d not in self.zoneIdToHostAvId" % avZoneId) + + else: + DistributedPartyManagerAI.notify.debug("__exitParty can't find zone for %d" % avId) + + totalMoney = -1 + # stop the healing + if avId in self.air.doId2do: + # Find the avatar + av = self.air.doId2do[avId] + # Stop healing them + av.stopToonUp() + totalMoney = av.getTotalMoney() + + if not isPlanning: + self.air.writeServerEvent("party_exit", partyId,"%d|%d" % (avId, totalMoney)) + + def freeZoneIdFromPlannedParty(self, hostId, zoneId): + """ Free a zone that was allocated for the planning of a party """ + senderId = self.air.getAvatarIdFromSender() + if senderId != hostId: + self.air.writeServerEvent('suspicious', senderId, 'someone else trying to free a zone for this avatar: hostId = %d' % hostId) + return + if hostId in self.hostIdToPlanningPartyZoneId: + DistributedPartyManagerAI.notify.debug("freeZoneIdFromPlannedParty : freeing zone : hostId = %d, zoneId = %d" % (hostId, zoneId)) + self.air.deallocateZone(self.hostIdToPlanningPartyZoneId[hostId]) + del self.hostIdToPlanningPartyZoneId[hostId] + return + else: + self.notify.warning('suspicious senderId=%d trying to free a zone that this avatar did not allocate: hostId = %d' % (senderId,hostId)) + self.air.writeServerEvent('suspicious', senderId, 'trying to free a zone that this avatar did not allocate: hostId = %d' % hostId) + return + + def __cleanupParty(self, hostId, zoneId): + DistributedPartyManagerAI.notify.debug("__cleanupParty hostId = %d, zoneId = %d" % (hostId, zoneId)) + + self.clearPartyZoneId(hostId) + if hostId in self.hostAvIdToClosingPartyZoneId: + del self.hostAvIdToClosingPartyZoneId[hostId] + if zoneId in self.zoneIdToHostAvId: + del self.zoneIdToHostAvId[zoneId] + + # give our zoneId back to the air + self.air.deallocateZone(zoneId) + + # delete party grounds from state server + self.__deleteParty(hostId) + + # stop listening for unexpectedExit + self.ignore(self.air.getAvatarExitEvent(hostId)) + + if zoneId in self.zoneIdToGuestAvIds: + del self.zoneIdToGuestAvIds[zoneId] + + def __deleteParty(self, hostId): + # remove all our objects from the stateserver + DistributedPartyManagerAI.notify.debug("__deleteParty(hostId=%s)" % hostId) + + # delete from state server + if hostId in self.hostAvIdToPartiesRunning: + if self.hostAvIdToPartiesRunning[hostId] != None: + self.hostAvIdToPartiesRunning[hostId].destroyPartyData() + DistributedPartyManagerAI.notify.debug('DistributedPartyAI requestDelete, doId=%d' % getattr(self.hostAvIdToPartiesRunning[hostId], 'doId')) + self.hostAvIdToPartiesRunning[hostId].requestDelete() + del self.hostAvIdToPartiesRunning[hostId] + + def __bootGuests(self, hostId, zoneId): + DistributedPartyManagerAI.notify.debug("__bootGuests (hostId=%s zoneId=%s)" % (hostId,zoneId)) + try: + # we need a copy of the list, otherwise we skip some people in booting out + visitors = self.zoneIdToGuestAvIds[zoneId][:] + for avId in visitors: + # people get left behind in the party if we boot the host first + if avId == hostId: + continue + self.notify.debug("booting %d from zone %d host=%d" %(avId, zoneId, hostId)) + self.__bootAv(avId, zoneId, hostId) + if hostId in visitors: + self.__bootAv(hostId, zoneId, hostId) + self.notify.debug("booting %d from zone %d host=%d" %(avId, zoneId, hostId)) + except: + # refCount might have already gotten deleted + pass + + def __bootAv(self, avId, zoneId, hostId): + # Let anyone who might be doing something with this avatar in the party + assert self.notify.debugStateCall(self) + messenger.send("bootAvFromParty-"+str(avId)) + self.__exitParty(avId) + # Pass the message to the client, who will pass it to the PartyHood + self.sendUpdateToAvatarId(avId, "sendAvToPlayground", [avId, 1]) # 0 is a warning, 1 is final + + def getPartyEndedEvent(self, hostId): + return 'partyEnded-%s' % hostId + + def __setPartyEnded(self, hostId, zoneId): + """ + This party has ended, so no one can go to it, and we'll + warn people there to get out! + """ + DistributedPartyManagerAI.notify.debug("__setPartyEnded (hostId=%s zoneId=%s)" % (hostId,zoneId)) + # Tell uberdog about it so it can update hostAvIdToAllPartiesInfo + self.air.sendUpdateToDoId( + "DistributedPartyManager", + 'changePartyStatusRequestAiToUd', + OtpDoGlobals.OTP_DO_ID_TOONTOWN_PARTY_MANAGER, + [self.doId, self.hostAvIdToPartiesRunning[hostId].partyInfo.partyId, PartyGlobals.PartyStatus.Finished] + ) + + self.hostAvIdToClosingPartyZoneId[hostId] = zoneId + + messenger.send(self.getPartyEndedEvent(hostId)) + + # Warn guests they have to leave + guests = self.zoneIdToGuestAvIds.get(zoneId) + if guests: + for avId in guests: + # Pass the message to the client, who will pass it to the PartyHood + self.sendUpdateToAvatarId(avId, "sendAvToPlayground", [avId, 0]) # 0 is a warning, 1 is final + + self.hostAvIdToPartiesRunning[hostId].b_setPartyState(True) + + def testMsgUdToAllAi(self): + """Try receiving a UD to all AI msg.""" + self.notify.debugStateCall(self) + pass + + def forceCheckStart(self): + """Force the uberdog party manager to do an immediate check for which parties can start.""" + self.air.sendUpdateToDoId( + "DistributedPartyManager", + 'forceCheckStart', + OtpDoGlobals.OTP_DO_ID_TOONTOWN_PARTY_MANAGER, + [] + ) + + def allowUnreleasedServer(self): + """Return do we allow player to buy unreleased activities and decorations on the client.""" + return self.allowUnreleased + + def setAllowUnreleaseServer(self, newValue): + """Set if we allow player to buy unreleased activities and decorations on the client.""" + self.allowUnreleased = newValue + + def toggleAllowUnreleasedServer(self): + """Toggle allow unreleased on the client, then return the new value.""" + self.allowUnreleased = not self.allowUnreleased + return self.allowUnreleased def canBuyParties(self): - return False # TODO + """Return do we allow player to buy parties.""" + return self.canBuy + + def setCanBuyParties(self, newValue): + """Set if we allow player to buy unreleased activities and decorations on the client.""" + self.canBuy= newValue + + def toggleCanBuyParties(self): + """Toggle allow unreleased on the client, then return the new value.""" + self.canBuy= not self.canBuy + return self.canBuy + + def partyManagerUdStartingUp(self): + """The uberdog is restarting, tell it about parties running on this district.""" + for hostId in self.hostAvIdToAllPartiesInfo: + if hostId not in self.hostAvIdToPartiesRunning: + self.notify.warning('hostId %d is in self.hostAvIdToAllPartiesInfo but not in self.hostAvIdToPartiesRunning' % hostId) + # really check we have a DistributedPartyAI for the host + continue + partyInfo = self.hostAvIdToAllPartiesInfo[hostId] + shardId = partyInfo[1] + if shardId == self.air.districtId: + startTime = partyInfo[0] + zoneId = partyInfo[2] + isPrivate = partyInfo[3] + numberOfGuests = partyInfo[4] + hostName = partyInfo[5] + activityIds = partyInfo[6] + partyId = partyInfo[7] + + self.air.sendUpdateToDoId( + "DistributedPartyManager", + 'updateAllPartyInfoToUd', + OtpDoGlobals.OTP_DO_ID_TOONTOWN_PARTY_MANAGER, + [hostId, startTime, shardId, zoneId, isPrivate, numberOfGuests, + hostName, activityIds, partyId] + ) + + + def magicWordEnd(self, senderId): + """End the party prematurely as the sender said a magic word.""" + # first test if we are hosting a party + partyZoneId = self.avIdToPartyZoneId.get(senderId) + if not partyZoneId: + return "%d not in self.avIdToPartyZoneId" % senderId + + hostId = self.zoneIdToHostAvId.get(partyZoneId) + if hostId != senderId: + return "sender %d is not host (%d is)" % (senderId, hostId) + + # nuke the old tasks + taskMgr.remove("DistributedPartyManagerAI_PartyEnding_%d"%partyZoneId) + taskMgr.remove("DistributedPartyManagerAI_BootGuests_%d"%partyZoneId) + taskMgr.remove("DistributedPartyManagerAI_CleanUpPartyZone_%d"%partyZoneId) + + # now start up new tasks to end the party right now + taskMgr.doMethodLater(0.1, self.__setPartyEnded, "DistributedPartyManagerAI_PartyEnding_%d"%partyZoneId, [hostId,partyZoneId]) + kickDelay = simbase.config.GetInt("party-kick-delay",PartyGlobals.DelayBeforeAutoKick) + taskMgr.doMethodLater(0.1 + kickDelay, self.__bootGuests, "DistributedPartyManagerAI_BootGuests_%d"%partyZoneId, [hostId,partyZoneId]) + taskMgr.doMethodLater(0.1 + kickDelay + 10.0, self.__cleanupParty, "DistributedPartyManagerAI_CleanUpPartyZone_%d"%partyZoneId, [hostId,partyZoneId]) + + return ("Party Zone %d Ending Soon" % partyZoneId) + + + def deductMoneyFromOfflineToon(self, toonId, cost): + """Deduct the cost of the party from an offline toon.""" + # it's possible for someone to alt f4 out in between the time it takes for + # the uberdog to respond to AI that buying the party was a success + ag = RepairAvatars.AvatarGetter(self.air) + event = 'gotOfflineToon-%s' % toonId + ag.getAvatar(toonId, fields=['setName', 'setMaxHp', + 'setMaxMoney', + 'setMaxBankMoney', + 'setMoney', + 'setBankMoney'], + event = event) + self.acceptOnce(event, Functor(self.gotOfflineToon, cost = cost, toonId = toonId)) + + def gotOfflineToon(self, toon, cost, toonId): + """Handle a response to our request to get an offline toon, deduct the money from him.""" + if toon is None: + # prevent mem leak + self.notify.warning("gotOfflineToon - toon %s not found. buying a party for free!cost=%s" + % (toonId, cost)) + self.air.writeServerEvent('suspicious', toonId, + "gotOfflineToon - toon %s not found. buying a party for free!cost=%s" + % (toonId, cost)) + return + + totalMoney = toon.getTotalMoney() + result = toon.takeMoney(cost, bUseBank = True) + if result: + newTotalMoney = toon.getTotalMoney() + self.notify.info("gotOfflineToon - deducting %s from offline toon %s newTotalMoney=%s" + % (cost, toonId,newTotalMoney)) + else: + self.notify.warning("gotOfflineToon - Host %s got away with buying a party he can't afford! totalMoney=%s cost=%s" + % (toonId,totalMoney, cost)) + self.air.writeServerEvent('suspicious', toonId, + "gotOfflineToon - Host %s got away with buying a party he can't afford! totalMoney=%s cost=%s" + % (toonId,totalMoney, cost)) + + + # takeMoney is doing a b_setMoney, so that gets written into the otp database + # db = DatabaseObject.DatabaseObject(self.air, toon.doId) + # db.storeObject(toon, ["setMoney", "setBankMoney"]) + + # prevent mem leak + # as far as I can tell we don't need this, ~aigarbage reports 0 cycles + # toon.patchDelete() \ No newline at end of file diff --git a/toontown/uberdog/DistributedPartyManagerUD.py b/toontown/uberdog/DistributedPartyManagerUD.py index acff337..c60d1e3 100644 --- a/toontown/uberdog/DistributedPartyManagerUD.py +++ b/toontown/uberdog/DistributedPartyManagerUD.py @@ -1,5 +1,1394 @@ -from direct.directnotify import DirectNotifyGlobal -from direct.distributed.DistributedObjectUD import DistributedObjectUD -class DistributedPartyManagerUD(DistributedObjectUD): - notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyManagerUD') +# taken from Anesidora + +# PartyDB and inviteDB - DarthMDev + +import time +import math +import json +import os +from direct.distributed.DistributedObjectGlobalUD import DistributedObjectGlobalUD +from direct.directnotify import DirectNotifyGlobal +from direct.distributed.AsyncRequest import AsyncRequest +from otp.distributed import OtpDoGlobals +#from toontown.uberdog.PartiesUdLog import partiesUdLog +from toontown.toonbase import ToontownGlobals +from toontown.parties import PartyGlobals +from toontown.parties import PartyUtils +from toontown.ai.ToontownAIMsgTypes import PARTY_MANAGER_UD_TO_ALL_AI +from datetime import timedelta # Used for testing, to create random test party +from datetime import datetime # Used for testing, to create random test party + +class PartyDb: + """ + PartyDB is the base class for all party database interface implementations. + """ + def __init__(self, partyManager): + self.partyManager = partyManager + # setup partyToId dictionary + self.partyDbFilePath = config.GetString('partydb-local-file', 'astron/databases/parties.json') + # Load the JSON file if it exists. + if os.path.exists(self.partyDbFilePath): + with open(self.partyDbFilePath, 'r') as file: + self.partyToId = json.load(file) + else: + # If not, create a blank file. + self.partyToId = {} + with open(self.partyDbFilePath, 'w') as file: + json.dump(self.partyToId, file) + + + + def save(self): + """Save the current state of the partyToId dictionary to the JSON file.""" + with open(self.partyDbFilePath, 'w') as file: + json.dump(self.partyToId, file) + + def putParty(self, hostId, startTime, endTime, isPrivate, inviteTheme, activities, decorations, status): + """ + Add a party to the database. + """ + partyId = len(self.partyToId) + 1 # Generate a new party ID + party = { + 'partyId': partyId, + 'hostId': hostId, + 'startTime': startTime, + 'endTime': endTime, + 'isPrivate': isPrivate, + 'inviteTheme': inviteTheme, + 'activities': activities, + 'decorations': decorations, + 'status': status + } + self.partyToId[partyId] = party + self.save() + return True + + + def getPartiesOfHost(self, hostId): + """ + Get all parties of a host. + """ + for party in self.partyToId.values(): + if party['hostId'] == hostId: + yield party + + + def getPartiesOfHostThatCanStart(self, hostId): + """ + Get all parties of a host that can start. + """ + for party in self.partyToId.values(): + if party['hostId'] == hostId and party['status'] == PartyGlobals.PartyStatus.Pending: + yield party + + def getPartiesAvailableToStart(self, thresholdTime): + """ + Get all parties that can start. + """ + for party in self.partyToId.values(): + if party['startTime'] <= thresholdTime and party['status'] == PartyGlobals.PartyStatus.Pending: + yield party + + def getPrioritizedParties(self, partyIds, thresholdTime, limit, future, cancelled): + """ + Get a prioritized list of parties. + + partyIds: list of partyIds to consider + thresholdTime: the time to compare against + limit: the maximum number of parties to return + future: whether to consider future or past parties + cancelled: whether to consider cancelled or finished parties + + Returns a list of partyInfo dictionaries. + """ + for partyId in partyIds: + party = self.partyToId[partyId] + if future: + if party['startTime'] > thresholdTime: + if not cancelled: + if party['status'] == PartyGlobals.PartyStatus.Pending: + yield party + else: + if party['status'] == PartyGlobals.PartyStatus.Cancelled: + yield party + else: + if party['startTime'] < thresholdTime: + if not cancelled: + if party['status'] == PartyGlobals.PartyStatus.Finished: + yield party + else: + if party['status'] == PartyGlobals.PartyStatus.Cancelled: + yield party + + + def getHostPrioritizedParties(self, hostId, thresholdTime, limit, future, cancelled): + """ + Get a prioritized list of parties of a host. + + hostId: the host to consider + thresholdTime: the time to compare against + limit: the maximum number of parties to return + future: whether to consider future or past parties + cancelled: whether to consider cancelled or finished parties + + Returns a list of partyInfo dictionaries. + """ + for party in self.partyToId.values(): + if party['hostId'] == hostId: + if future: + if party['startTime'] > thresholdTime: + if not cancelled: + if party['status'] == PartyGlobals.PartyStatus.Pending: + yield party + else: + if party['status'] == PartyGlobals.PartyStatus.Cancelled: + yield party + else: + if party['startTime'] < thresholdTime: + if not cancelled: + if party['status'] == PartyGlobals.PartyStatus.Finished: + yield party + else: + if party['status'] == PartyGlobals.PartyStatus.Cancelled: + yield party + + + def getParty(self, partyId): + """ + Get a party by partyId. + """ + return self.partyToId.get(partyId, None) + + def changePrivate(self, partyId, newPrivateStatus): + """ + Change a party to public or private. + """ + if partyId in self.partyToId.values(): + self.partyToId[partyId]['isPrivate'] = newPrivateStatus + self.save() + return True + return False + + def deleteParty(self, partyId): + """ + Delete a party. + """ + if partyId in self.partyToId.values(): + del self.partyToId[partyId] + self.save() + return True + return False + + def changePartyStatus(self, partyId, newPartyStatus): + """ + Change the status of a party. + """ + if partyId in self.partyToId.values(): + self.partyToId[partyId]['status'] = newPartyStatus + self.save() + return True + return False + + def forceFinishForStarted(self, thresholdTime): + """ + Force finish all started parties. + """ + for party in self.partyToId.values(): + if party['startTime'] < thresholdTime and party['status'] == PartyGlobals.PartyStatus.Started.name: + party['status'] = PartyGlobals.PartyStatus.Finished.name + self.save() + + def forceNeverStartedForCanStart(self, thresholdTime): + """ + Force never started all parties that can start. + """ + for party in self.partyToId.values(): + if party['startTime'] <= thresholdTime and party['status'] == PartyGlobals.PartyStatus.Pending: + party['status'] = PartyGlobals.PartyStatus.NeverStarted + self.save() + + def getMultipleParties(self, partyIds): + """ + Get multiple parties by partyId. + """ + for partyId in partyIds: + yield self.partyToId.get(partyId, None) + + def changeMultiplePartiesStatus(self, partyIds, newPartyStatus): + """ + Change the status of multiple parties. + """ + for partyId in partyIds: + if partyId in self.partyToId.values(): + self.partyToId[partyId]['status'] = newPartyStatus + self.save() + +class InviteDB(): + """ + InviteDB is the base class for all invite database interface implementations. + """ + def __init__(self, partyManager): + self.partyMsanager = partyManager + self.inviteDbFilePath = config.GetString('invitedb-local-file', 'astron/databases/invites.json') + # Load the JSON file if it exists. + if os.path.exists(self.inviteDbFilePath): + with open(self.inviteDbFilePath, 'r') as file: + self.inviteToId = json.load(file) + else: + # If not, create a blank file. + self.inviteToId = {} + with open(self.inviteDbFilePath, 'w') as file: + json.dump(self.inviteToId, file) + + def save(self): + """Save the current state of the inviteToId dictionary to the JSON file.""" + with open(self.inviteDbFilePath, 'w') as file: + json.dump(self.inviteToId, file) + + def putInvite(self, partyId, inviteeId): + """ + Add an invite to the database. + """ + inviteKey = len(self.inviteToId) + 1 + invite = { + 'inviteKey': inviteKey, + 'partyId': partyId, + 'inviteeId': inviteeId, + 'status': PartyGlobals.InviteStatus.NotRead + } + self.inviteToId[inviteKey] = invite + self.save() + + def getInvites(self, avatarId): + """ + Get all invites for an avatar. + """ + for invite in self.inviteToId: + if invite['inviteeId'] == avatarId: + yield invite + + def getOneInvite(self, inviteKey): + """ + Get one invite by inviteKey. + """ + return self.inviteToId.get(inviteKey, None) + + def updateInvite(self, inviteKey, newStatus): + """ + Update the status of an invite. + """ + if inviteKey in self.inviteToId: + self.inviteToId[inviteKey]['status'] = newStatus + self.save() + return True + return False + + def getReplies(self, partyId): + """ + Get all replies for a party. + """ + for invite in self.inviteToId: + if invite['partyId'] == partyId: + yield invite + + def deleteInvite(self, inviteKey): + """ + Delete an invite. + """ + if inviteKey in self.inviteToId: + del self.inviteToId[inviteKey] + self.save() + return True + return False + + def getInviteesOfParty(self, partyId): + """ + Get all invitees of a party. + """ + for invite in self.inviteToId: + if invite['partyId'] == partyId: + yield invite + +class DistributedPartyManagerUD(DistributedObjectGlobalUD): + """UD side class for the party manager.""" + + # WARNING this is a global OTP object + # DistributedPartyManagerAI is NOT! + # Hence the use of sendUpdateToDoId when sending back to AI + + notify = DirectNotifyGlobal.directNotify.newCategory("DistributedPartyManagerUD") + + def __init__(self, air): + DistributedObjectGlobalUD.__init__(self, air) + # self.printlog = partiesUdLog("PartiesUdMonitor","localhost",12346) + + # avId is key, if present, avatar is online + self.isAvatarOnline = {} + + self.hostAvIdToAllPartiesInfo = {} + # 0 1 2 3 + # hostAvId to ( shardId, zoneId, isPrivate, number of toons there, + # 4 5 6 7 + # hostName,activityIds, actualStartTime, partyId) + + # The uberdog has the database, and knows when every party in every shard + # is allowed to start, or rather, when the 'go' button is activated. So, + # every 15 minutes (parties can only start on increments of 15 minutes) + # we'll check and see what parties are allowed to start and make the calls + # to enable their go buttons. We'll do the 1st check a minute in... + taskMgr.doMethodLater(60, self._checkForPartiesStarting, "DistributedPartyManagerUD_checkForPartiesStarting" ) + + + + self.partyDb = PartyDb(self) + + + self.inviteDb = InviteDB(self) + + # in minutes, how often do we check if a party can start + self.startPartyFrequency = config.GetFloat('start-party-frequency', PartyGlobals.UberdogCheckPartyStartFrequency) + + # The uberdog has the database, we need to check if party has been started but never finished + # We'll do the 1st check a 1 second in... + self.partiesSanityCheckFrequency = config.GetInt('parties-sanity-check-frequency', + PartyGlobals.UberdogPartiesSanityCheckFrequency) + taskMgr.doMethodLater(1, self._sanityCheckParties, "DistributedPartyManagerUD_sanityCheckParties") + + def announceGenerate(self): + DistributedObjectGlobalUD.announceGenerate(self) + self.accept("avatarOnlinePlusAccountInfo", self.avatarOnlinePlusAccountInfo, []) + self.accept("avatarOffline", self.avatarOffline, []) + # assuming we are restarting, tell all the AIs so they can reply back with their + # currently running parties + self.sendUpdateToAllAis("partyManagerUdStartingUp", []) + + + def avatarLoggedIn(self, avatarId): + """Handle an avatar just logging in.""" + # Note this is no longer sent by the AI but is instead in response to + # avatarOnlinePlusAccountInfo from otp_server + # for now we get all the invites, then send them across the wire to the client. + DistributedPartyManagerUD.notify.debug( "avatarLoggedIn( avaterId=%d )" % avatarId ) + # we are blasting everything for now + partyIds, partyInfo = self._updateInvites( avatarId ) + + # we've sent invites, send party details related to those invites + self._updateInvitedToParties( avatarId, partyIds, partyInfo ) + + # send out the details of the parties he's hosting + hostedPartyIds, hostedPartyInfo = self._updateHostedParties( avatarId ) + + # send out replies to his parties + self._updatePartyReplies(avatarId, hostedPartyIds, hostedPartyInfo) + + def addParty(self, pmDoId, hostId, startTime, endTime, isPrivate, inviteTheme, activities, decorations, inviteeIds, costOfParty): + """Add a party to the the invite and party dbs.""" + DistributedPartyManagerUD.notify.debug( "addParty( hostId=%d, startTime=%s, endTime=%s, isPrivate=%s, inviteTheme=%s, invitees=%s... )" %(hostId, startTime, endTime, isPrivate, PartyGlobals.InviteTheme(inviteTheme)._name_ ,str(inviteeIds)) ) + putSucceeded = self.partyDb.putParty(hostId, startTime, endTime, isPrivate, inviteTheme, activities, decorations, PartyGlobals.PartyStatus.Pending.name) + if not putSucceeded: + DistributedPartyManagerUD.notify.warning( "putParty call for party with hostID %s failed." % hostId ) + # TODO having too many parties is not the only reason putParty can fail + # add those cases too + # case 1: too many decors + self.sendAddPartyResponse(pmDoId, hostId, PartyGlobals.AddPartyErrorCode.TooManyHostedParties) + return + + errorCode = PartyGlobals.AddPartyErrorCode.AllOk + partiesTuple = list(self.partyDb.getPartiesOfHost(hostId)) + if len(partiesTuple) > 0: + partyId = partiesTuple[-1]['partyId'] # TODO-parties: is getting the -1 index guranteed to get the party we just pushed to the database? + # send out the details of the parties he's hosting + hostedPartyIds, hostedPartyInfo = self._updateHostedParties(hostId) + + # Send out updates to invitees + for inviteeId in inviteeIds: + self.inviteDb.putInvite(partyId, inviteeId) + if self.isOnline(inviteeId): + # update invitee's invites + partyIds, partyInfo = self._updateInvites( inviteeId ) + # update invitee's InvitedTo parties + self._updateInvitedToParties( inviteeId, partyIds, partyInfo ) + + # send out replies to his parties + self._updatePartyReplies(hostId, [partyId], hostedPartyInfo) + else: + DistributedPartyManagerUD.notify.warning( "Unable to find a party for hostId %s in the party database." % hostId ) + errorCode = PartyGlobals.AddPartyErrorCode.DatabaseError + self.sendAddPartyResponse(pmDoId, hostId, errorCode, costOfParty) + + def markInviteAsReadButNotReplied( self, partyManagerDoId, inviteKey): + """Just mark the invite as read in the database.""" + invite = self.inviteDb.getOneInvite(inviteKey) + if not invite: + # how the heck did this happen, inviteKey isn't there + DistributedPartyManagerUD.notify.warning('markInviteAsReadButNotReplied inviteKey=%s not found in inviteDb' % inviteKey) + return + + # verify the party is still there + partyId = invite[0]['partyId'] + party = self.partyDb.getParty(partyId) + if not party: + return + + updateResult = self.inviteDb.updateInvite(inviteKey, PartyGlobals.InviteStatus.ReadButNotReplied) + self.updateHostAndInviteeStatus(inviteKey, partyId, invite, party, PartyGlobals.InviteStatus.ReadButNotReplied ) + + def updateHostAndInviteeStatus(self, inviteKey, partyId, invite, party, newStatus): + """Tell the invitee and host toons of the change in inviteStatus.""" + # tell the Invitee DistributedToon + inviteeId = invite[0]['guestId'] + + DistributedPartyManagerUD.notify.debug( "Calling DistributedToon::updateInvite( inviteKey=%s, newStatus=%s ) across the network with inviteeId %d." %(inviteKey, PartyGlobals.InviteStatus(newStatus).name, inviteeId ) ) + self.air.sendUpdateToDoId( + "DistributedToon", + "updateInvite", + inviteeId, + [inviteKey, newStatus], + ) + + # tell the host, he might not be logged in + hostId = party[0]['hostId'] + + DistributedPartyManagerUD.notify.debug( "Calling DistributedToon::updateReply( partyId=%d, inviteeId=%d, newStatus=%s ) across the network with hostId %d." %(partyId, inviteeId, PartyGlobals.InviteStatus(newStatus).name, hostId ) ) + self.air.sendUpdateToDoId( + "DistributedToon", + "updateReply", + hostId, + [partyId, inviteeId, newStatus], + ) + + def respondToInvite(self, partyManagerDoId, mailboxDoId, context, inviteKey, newStatus): + """Handle accepting/rejecting an invite.""" + DistributedPartyManagerUD.notify.debug( "respondToInvite( partyManagerDoId=%d, mailboxDoId=%d, ..., inviteKey=%d, newStatus=%s )" %(partyManagerDoId, mailboxDoId, inviteKey, PartyGlobals.InviteStatus(newStatus).name ) ) + replyToChannelAI = self.air.getSenderReturnChannel() + retcode = ToontownGlobals.P_InvalidIndex + invite = self.inviteDb.getOneInvite(inviteKey) + if not invite: + # how the heck did this happen, inviteKey isn't there + DistributedPartyManagerUD.notify.warning('inviteKey=%s not found in inviteDb' % inviteKey) + self.air.sendUpdateToDoId( + "DistributedPartyManager", + "respondToInviteResponse", + partyManagerDoId, + [mailboxDoId, context, inviteKey, retcode, newStatus], + ) + return + + # verify the party is still there + partyId = invite[0]['partyId'] + party = self.partyDb.getParty(partyId) + if not party: + self.air.sendUpdateToDoId( + "DistributedPartyManager", + "respondToInviteResponse", + partyManagerDoId, + [mailboxDoId, context, inviteKey, ToontownGlobals.P_PartyNotFound, newStatus], + ) + return + + # we have a valid party and invite, update the status + # TODO updateResult is always empty, do we need to verify the update took? + updateResult = self.inviteDb.updateInvite(inviteKey, newStatus) + + self.air.sendUpdateToDoId( + "DistributedPartyManager", + "respondToInviteResponse", + partyManagerDoId, + [mailboxDoId, context, inviteKey, ToontownGlobals.P_ItemAvailable, newStatus] + ) + + # tell the invitee and host he accepted/rejected + self.updateHostAndInviteeStatus(inviteKey, partyId, invite, party, newStatus) + + def sendAddPartyResponse(self, pmDoId, hostId, errorCode, costOfParty=0): + """Tell the AI if all went well or if there's a problem adding the party.""" + self.air.sendUpdateToDoId( + "DistributedPartyManager", + 'addPartyResponseUdToAi', + pmDoId, + [hostId, errorCode, costOfParty], + ) + + def _updateInvites(self, avatarId): + """ + Push invites and setInviteMailNotify across to the DistributedToon. + + Returns a list of prioritized partyIds and the partyInfo that avatarId is invited to. + """ + DistributedPartyManagerUD.notify.debug( "_updateInvites( avatarId=%d )" % avatarId ) + + invitesTuple = self.inviteDb.getInvites(avatarId) + DistributedPartyManagerUD.notify.debug( "Found %d invites for avatarId %d in the invite database." % (len(invitesTuple), avatarId) ) + + # with 8 bytes inviteKey, 8 bytes partyId, 1 byte status = 17 bytes for the 1 invite + # 64kb / 17 = 3855 + # the party info related to these invites is the limiting factor + # However cancelled parties will show up in this list + # if we really want to be 100% sure we can pull the parties from the database + # and examine them one by one. + + # But an extremely large number should cover it, say 1000 + invitesTuple = invitesTuple[-PartyGlobals.MaxSetInvites:] + + # ok we really need to examine the parties + # since we need to figure out the correct value for inviteMailNotify + # we can have an invite that's not read, so it will trigger as a new invite in the + # mailbox, but since it's so far in the future the partyInvitedTo is not sent + # we get the case of a mailbox being flagged but having nothing in it! + partyIds = [inviteInfo['partyId'] for inviteInfo in invitesTuple] + + prioritizedPartyIds, prioritizedPartyInfo = self.reprioritizeParties(partyIds, PartyGlobals.MaxSetPartiesInvitedTo) + + formattedInvites = [] + partyIds = [] + numOld = 0 + numNew = 0 + for item in invitesTuple: + partyId = item['partyId'] + if partyId not in prioritizedPartyIds: + # skip this invite, too far in the past or in the future + continue + inviteKey = item['inviteId'] + status = item['statusId'] + if status == PartyGlobals.InviteStatus.NotRead: + numNew += 1 + elif status == PartyGlobals.InviteStatus.ReadButNotReplied: + numOld += 1 + # send even the rejected invites, it will show up in invites tab + formattedInvites.append( (inviteKey, partyId, status) ) + partyIds.append(partyId) + + DistributedPartyManagerUD.notify.debug( "Calling DistributedToon::setInvites across the network with avatarId %d. Sending %d formatted invites." %(avatarId, len(formattedInvites) ) ) + self.air.sendUpdateToDoId( + "DistributedToon", + "setInvites", + avatarId, + [formattedInvites], + ) + + # we let DistributedToon.updateInviteMailNotify() properly + # set the right value for inviteMailNotify now instead of uberdog doing it here + + return prioritizedPartyIds, prioritizedPartyInfo + + def reprioritizeParties(self, partyIds, limit): + """Return a prioritized list of partyIds and the associated partyInfo.""" + thresholdTime = self.getThresholdTime() + futurePendingParties = () + futureCancelledParties = () + pastFinishedParties =() + pastCancelledParties = () + prioritizedPartyIds = [] + prioritizedPartyInfo = () + + futurePendingParties = tuple(self.partyDb.getPrioritizedParties(\ + partyIds, + thresholdTime.strftime("%Y-%m-%d %H:%M:%S"), + limit, + future = True, + cancelled = False)) + self.notify.debug('futurePendingParties = %s' % str(futurePendingParties)) + prioritizedPartyIds += [partyInfo['partyId'] for partyInfo in futurePendingParties] + prioritizedPartyInfo += futurePendingParties + slotsLeft = limit - len(futurePendingParties) + + if slotsLeft > 0: + futureCancelledParties = tuple(self.partyDb.getPrioritizedParties(\ + partyIds, + thresholdTime.strftime("%Y-%m-%d %H:%M:%S"), + slotsLeft, + future = True, + cancelled = True)) + #self.notify.debug('futureCancelledParties = %s' % str(futureCancelledParties)) + prioritizedPartyIds += [partyInfo['partyId'] for partyInfo in futureCancelledParties] + prioritizedPartyInfo += futureCancelledParties + slotsLeft -= len(futureCancelledParties) + if slotsLeft > 0: + pastFinishedParties = tuple(self.partyDb.getPrioritizedParties(\ + partyIds, + thresholdTime.strftime("%Y-%m-%d %H:%M:%S"), + slotsLeft, + future = False, + cancelled = False)) + #self.notify.debug('pastFinishedParties = %s' % str(pastFinishedParties)) + prioritizedPartyIds += [partyInfo['partyId'] for partyInfo in pastFinishedParties] + prioritizedPartyInfo += pastFinishedParties + slotsLeft -= len(pastFinishedParties) + if slotsLeft > 0: + pastCancelledParties = tuple(self.partyDb.getPrioritizedParties(\ + partyIds, + thresholdTime.strftime("%Y-%m-%d %H:%M:%S"), + slotsLeft, + future = False, + cancelled = True)) + #self.notify.debug('pastCancelledParties = %s' % str(pastCancelledParties)) + prioritizedPartyIds += [partyInfo['partyId'] for partyInfo in pastCancelledParties] + prioritizedPartyInfo += pastCancelledParties + + # prioritizedPartyIds should have everything, prioritizing pending parties in the future + # then cancelled parties in the future, then started parties in the past + # then cancelled parties in the past + + return prioritizedPartyIds, prioritizedPartyInfo + + def _updateInvitedToParties(self, avatarId, passedPartyIds, passedPartyInfo): + """ + Push information about parties that avatarId is invited to across to the DistributedToon. + + partyIds: list of partyIds that avatarId is invited to + """ + partyIds = passedPartyIds + partyInfo = passedPartyInfo + DistributedPartyManagerUD.notify.debug( "_updateInvitedToParties( avatarId=%d, partyIds=%s )" %(avatarId, partyIds) ) + if partyInfo == None: + partyIds, partyInfo = self.reprioritizeParties(passedPartyIds, PartyGlobals.MaxSetPartiesInvitedTo) + + formattedPartiesInvitedTo = [] + formattedPartiesSize = 0 + for partyInfoDict in partyInfo: + formattedPartyInfo = self.getFormattedPartyInfo(partyInfoDict) + partyInfoSize = self._getPartyInfoSize(formattedPartyInfo) + formattedPartiesSize += partyInfoSize + # A full party info can be as big as 383 bytes, and we can only send 16KB over the wire. + # So we clip off any party after 15.8 KB (we leave some leeway for any extra info) + if (formattedPartiesSize < 15800): + formattedPartiesInvitedTo.append(formattedPartyInfo) + else: + break + + DistributedPartyManagerUD.notify.debug( "Calling DistributedToon::setPartiesInvitedTo across the network with avatarId %d. Sending %d formatted parties." %(avatarId, len(formattedPartiesInvitedTo) ) ) + self.air.sendUpdateToDoId( + "DistributedToon", + "setPartiesInvitedTo", + avatarId, + [formattedPartiesInvitedTo], + ) + + def getThresholdTime(self): + """Return the server threshold time for high priority parties.""" + # Some parties could have started recently. + # threshold time, let's get the current server time, subtract default party time + # and then subtract it again to get the threshold + thresholdTime = self.air.toontownTimeManager.getCurServerDateTime() + thresholdTime += timedelta(hours = -(2*PartyGlobals.DefaultPartyDuration )) + + return thresholdTime + + def getFormattedPartyInfo(self, partyInfoDict): + startTime = partyInfoDict['startTime'] + endTime = partyInfoDict['endTime'] + activitiesStr = partyInfoDict['activities'].decode() + formattedActivities = [] + for i in range (int(len(activitiesStr) / 4)): + oneActivity = (ord(activitiesStr[i*4]), + ord(activitiesStr[i*4 + 1]), + ord(activitiesStr[i*4 + 2]), + ord(activitiesStr[i*4 + 3]) + ) + formattedActivities.append(oneActivity) + decorStr = partyInfoDict['decorations'] + formattedDecors = [] + for i in range(int(len(decorStr) / 4)): + oneDecor = (ord(decorStr[i*4]), + ord(decorStr[i*4 + 1]), + ord(decorStr[i*4 + 2]), + ord(decorStr[i*4 + 3]) + ) + formattedDecors.append(oneDecor) + isPrivate = partyInfoDict['isPrivate'] + inviteTheme = partyInfoDict['inviteTheme'] + + return( + partyInfoDict['partyId'], + partyInfoDict['hostId'], + startTime.year, + startTime.month, + startTime.day, + startTime.hour, + startTime.minute, + endTime.year, + endTime.month, + endTime.day, + endTime.hour, + endTime.minute, + isPrivate, + inviteTheme, + formattedActivities, + formattedDecors, + partyInfoDict['statusId'] + ) + + def reprioritizeHostedParties(self, hostId, limit): + """Return a prioritized list of partyIds and the associated partyInfo.""" + thresholdTime = self.getThresholdTime() + futurePendingParties = () + futureCancelledParties = () + pastFinishedParties =() + pastCancelledParties = () + prioritizedPartyIds = [] + prioritizedPartyInfo = () + + futurePendingParties = tuple(self.partyDb.getHostPrioritizedParties(\ + hostId, + thresholdTime.strftime("%Y-%m-%d %H:%M:%S"), + limit, + future = True, + cancelled = False)) + self.notify.debug('futurePendingParties = %s' % str(futurePendingParties)) + prioritizedPartyIds += [partyInfo['partyId'] for partyInfo in futurePendingParties] + prioritizedPartyInfo += futurePendingParties + slotsLeft = limit - len(futurePendingParties) + + if slotsLeft > 0: + futureCancelledParties = tuple(self.partyDb.getHostPrioritizedParties(\ + hostId, + thresholdTime.strftime("%Y-%m-%d %H:%M:%S"), + slotsLeft, + future = True, + cancelled = True)) + self.notify.debug('futureCancelledParties = %s' % str(futureCancelledParties)) + prioritizedPartyIds += [partyInfo['partyId'] for partyInfo in futureCancelledParties] + prioritizedPartyInfo += futureCancelledParties + slotsLeft -= len(futureCancelledParties) + if slotsLeft > 0: + pastFinishedParties = tuple(self.partyDb.getHostPrioritizedParties(\ + hostId, + thresholdTime.strftime("%Y-%m-%d %H:%M:%S"), + slotsLeft, + future = False, + cancelled = False)) + self.notify.debug('pastFinishedParties = %s' % str(pastFinishedParties)) + prioritizedPartyIds += [partyInfo['partyId'] for partyInfo in pastFinishedParties] + prioritizedPartyInfo += pastFinishedParties + slotsLeft -= len(pastFinishedParties) + if slotsLeft > 0: + pastCancelledParties = tuple(self.partyDb.getHostPrioritizedParties(\ + hostId, + thresholdTime.strftime("%Y-%m-%d %H:%M:%S"), + slotsLeft, + future = False, + cancelled = True)) + self.notify.debug('pastCancelledParties = %s' % str(pastCancelledParties)) + prioritizedPartyIds += [partyInfo['partyId'] for partyInfo in pastCancelledParties] + prioritizedPartyInfo += pastCancelledParties + + # prioritizedPartyIds should have everything, prioritizing pending parties in the future + # then cancelled parties in the future, then started parties in the past + # then cancelled parties in the past + + return prioritizedPartyIds, prioritizedPartyInfo + + + def _updateHostedParties(self, avatarId): + """ + Push information about parties that avatarId is hosting across to the DistributedToon. + + Returns a list of hostedPartyIds + """ + DistributedPartyManagerUD.notify.debug( "_updateHostedParties( avatarId=%d )" % avatarId ) + hostedPartyIds, hostedParties = self.reprioritizeHostedParties(avatarId, PartyGlobals.MaxSetHostedParties) + + formattedHostedParties = [] + formattedPartiesSize = 0 + for partyInfoDict in hostedParties: + if partyInfoDict['startTime'] and partyInfoDict['endTime']: + + formattedPartyInfo = self.getFormattedPartyInfo(partyInfoDict) + partyInfoSize = self._getPartyInfoSize(formattedPartyInfo) + formattedPartiesSize += partyInfoSize + # A full party info can be as big as 383 bytes, and we can only send 16KB over the wire. + # So we clip off any party after 15.8 KB (we leave some leeway for any extra info) + if (formattedPartiesSize < 15800): + formattedHostedParties.append(formattedPartyInfo) + else: + break + else: + self.notify.warning("partyId=%s has an invalid start or end time startTime=%s endTime=%s" % \ + ( str(partyInfoDict["partyId"]), + str(partyInfoDict['startTime']), + str(partyInfoDict['endTime']) + )) + + DistributedPartyManagerUD.notify.debug( "Calling DistributedToon::setHostedParties across the network with avatarId %d. Sending %d formatted parties." %(avatarId, len(formattedHostedParties) ) ) + self.air.sendUpdateToDoId( + "DistributedToon", + "setHostedParties", + avatarId, + [formattedHostedParties], + ) + + return hostedPartyIds, hostedParties + + def _updatePartyReplies(self, avatarId, hostedPartyIds, hostedParties): + """ + Look up replies to all parties avatarId is hosting from the database + and push them across to DistributedToon. + """ + DistributedPartyManagerUD.notify.debug( "_updatePartyReplies( avatarId=%d, hostedPartyIds=%s )" %(avatarId, hostedPartyIds) ) + thresholdTime = self.getThresholdTime() + formattedRepliesForAllParties = [] + for index, partyId in enumerate( hostedPartyIds): + if index >= len(hostedParties): + self.notify.warning(f'skipping len(hostedPartyIds)={len(hostedPartyIds)} != len(hostedParties)={len(hostedParties)}') + continue + gotCorrectPartyInfo = True + partyInfoDict = hostedParties[index] + if partyInfoDict['partyId'] != partyId: + gotCorrectPartyInfo = False + for hostedInfo in hostedParties: + if hostedInfo['partyId'] == partyId: + gotCorrectPartyInfo = True + partyInfoDict = hostedInfo + break + + if not gotCorrectPartyInfo: + self.notify.warning('partyId =%d not in hostedPartyIds' % partyId) + continue + + getRepliesForThisParty = True + # we only need replies for parties in the future that are not cancelled + # temporarily turned off as shticker book is not happy + #if partyInfoDict['statusId'] != PartyGlobals.PartyStatus.Cancelled and \ + # thresholdTime < partyInfoDict['startTime']: + # getRepliesForThisParty = True + + if getRepliesForThisParty: + formattedReplies = [] + replies = self.inviteDb.getReplies(partyId) + for oneReply in replies: + formattedReplies.append(( + oneReply['guestId'], + oneReply['statusId'] + )) + formattedRepliesForAllParties.append( (partyId, formattedReplies) ) + DistributedPartyManagerUD.notify.debug( "Calling DistributedToon::setPartyReplies across the network with avatarId %d. Sending %d formatted replies." %(avatarId, len(formattedRepliesForAllParties) ) ) + self.air.sendUpdateToDoId( + "DistributedToon", + "setPartyReplies", + avatarId, + [formattedRepliesForAllParties], + ) + + def changePrivateRequestAiToUd(self, pmDoId, partyId, newPrivateStatus): + """Handle AI requesting to change a party to public or private.""" + errorCode = PartyGlobals.ChangePartyFieldErrorCode.AllOk + + # verify the party is still there + party = self.partyDb.getParty(partyId) + if not party: + errorCode = PartyGlobals.ChangePartyFieldErrorCode.DatabaseError + self.air.sendUpdateToDoId( + "DistributedPartyManager", + 'changePrivateResponseUdToAi', + pmDoId, + [0, partyId, newPrivateStatus, errorCode ], + ) + return + + if party[0]['statusId'] == PartyGlobals.PartyStatus.Started: + errorCode = PartyGlobals.ChangePartyFieldErrorCode.AlreadyStarted + self.air.sendUpdateToDoId( + "DistributedPartyManager", + 'changePrivateResponseUdToAi', + pmDoId, + [party[0]['hostId'], partyId, newPrivateStatus, errorCode ], + ) + return + + + # TODO updateResult is always empty, do we need to verify the update took? + updateResult = self.partyDb.changePrivate(partyId, newPrivateStatus) + + self.air.sendUpdateToDoId( + "DistributedPartyManager", + 'changePrivateResponseUdToAi', + pmDoId, + [ party[0]['hostId'], partyId, newPrivateStatus, errorCode ], + ) + + # TODO do we need to send out partiesInvitedTo again? + + def changePartyStatusRequestAiToUd(self, pmDoId, partyId, newPartyStatus): + """Handle AI requesting to change the party status.""" + DistributedPartyManagerUD.notify.debug("changePartyStatusRequestAiToUd partyId = %s, newPartyStatus = %s" % (partyId, newPartyStatus)) + errorCode = PartyGlobals.ChangePartyFieldErrorCode.AllOk + + # verify the party is still there + party = self.partyDb.getParty(partyId) + if not party: + errorCode = PartyGlobals.ChangePartyFieldErrorCode.DatabaseError + self.air.sendUpdateToDoId( + "DistributedPartyManager", + 'changePartyStatusResponseUdToAi', + pmDoId, + [0, partyId, newPartyStatus, errorCode ], + ) + + return errorCode + partyDict = party[0] + # Check to see if this is a party that has finished + if partyDict["statusId"] == PartyGlobals.PartyStatus.Started and newPartyStatus == PartyGlobals.PartyStatus.Finished: + # It's over, send word to all the AIs so they can update for their public party gates + if partyDict["hostId"] in self.hostAvIdToAllPartiesInfo: + self.sendUpdateToAllAis("partyHasFinishedUdToAllAi", [partyDict["hostId"]]) + del self.hostAvIdToAllPartiesInfo[partyDict["hostId"]] + + # TODO updateResult is always empty, do we need to verify the update took? + updateResult = self.partyDb.changePartyStatus(partyId, newPartyStatus) + self.air.sendUpdateToDoId( + "DistributedPartyManager", + 'changePartyStatusResponseUdToAi', + pmDoId, + [ partyDict['hostId'], partyId, newPartyStatus, errorCode ], + ) + + return errorCode + # TODO do we need to send out partiesInvitedTo again? + + def partyInfoOfHostRequestAiToUd(self, pmDoId, hostId): + """ + A host is trying to create a party, check to see if this host has a + party available and, if so, return the party info and inviteeIds + """ + DistributedPartyManagerUD.notify.debug( "partyInfoOfHostRequestAiToUd( pmDoId=%d, hostId=%d )" %(pmDoId, hostId) ) + + # Query the database, get the info! + hostedParties = list(self.partyDb.getPartiesOfHostThatCanStart(hostId)) + + partyFail = False + partyInfo = None + if len(hostedParties) == 0: + DistributedPartyManagerUD.notify.debug( "partyInfoOfHostRequestAiToUd : party failed because avatar is not hosting any parties." ) + partyFail = True + else: + curServerDateTime = self.air.toontownTimeManager.getCurServerDateTime() + partyInfoDict = hostedParties[0] + # Check to see if this party's startTime is before the current time + # Note: Must make partyInfoDict["startTime"]'s time aware of any + # time offsets by creating a new datetime based on it but + # using the ToontownTimeManager's serverTimeZone info + partyStartTime = partyInfoDict["startTime"] + partyStartTime = datetime( + partyStartTime.year, + partyStartTime.month, + partyStartTime.day, + partyStartTime.hour, + partyStartTime.minute, + tzinfo=self.air.toontownTimeManager.serverTimeZone, + ) + curServerDateTime = datetime( + curServerDateTime.year, + curServerDateTime.month, + curServerDateTime.day, + curServerDateTime.hour, + curServerDateTime.minute, + tzinfo=self.air.toontownTimeManager.serverTimeZone, + ) + if partyStartTime <= curServerDateTime: + pass + else: + DistributedPartyManagerUD.notify.debug("partyInfoOfHostRequestAiToUd : party failed because avatar's party's start time has not passed yet.") + DistributedPartyManagerUD.notify.debug(" startTime = %s, servertime = %s" % (partyStartTime, curServerDateTime)) + partyFail = True + + partyEndTime = partyInfoDict["endTime"] + partyEndTime = datetime( + partyEndTime.year, + partyEndTime.month, + partyEndTime.day, + partyEndTime.hour, + partyEndTime.minute, + tzinfo=self.air.toontownTimeManager.serverTimeZone, + ) + if partyEndTime < curServerDateTime: + DistributedPartyManagerUD.notify.debug("partyInfoOfHostRequestAiToUd : party failed because avatar's party's end time has already passed.") + DistributedPartyManagerUD.notify.debug(" endTime = %s, servertime = %s" % (partyEndTime, curServerDateTime)) + partyFail = True + + if partyFail: + # Something is fishy... this host is not allowed to start this party now or has no parties planned + randomPartyCreationAllowed = uber.config.GetBool('allow-random-party-creation', 0) + if not randomPartyCreationAllowed: + self.air.sendUpdateToDoId( + "DistributedPartyManager", + 'partyInfoOfHostFailedResponseUdToAi', + pmDoId, + [hostId], + ) + return + else: + # We're allowed to create random parties for testing purposes + # We'll base the partyId on the current time... + curServerDateTime = self.air.toontownTimeManager.getCurServerDateTime() + partyDuration = timedelta(hours=PartyGlobals.DefaultPartyDuration) + endTime = curServerDateTime + partyDuration + partyId = int(time.time()) + partyId = int(str(partyId)[1:]) + DistributedPartyManagerUD.notify.debug( "partyInfoOfHostRequestAiToUd : Creating random test party with partyId %d" % partyId) + activities = [] + # Let's make one of each activity, arranged in a circle/oval in the party grounds + numActivities = len(PartyGlobals.ActivityIds) + circleStep = (2*math.pi)/numActivities + xRadius = 60.0 + yRadius = 80.0 + for i in range(numActivities): + # these are unsigned 8 bit ints (0-255) + activities += "%s%s%s%s"%( + chr(i), + chr(PartyUtils.convertDistanceToPartyGrid(math.cos(i*circleStep)*xRadius, 0)), + chr(PartyUtils.convertDistanceToPartyGrid(math.sin(i*circleStep)*yRadius, 1)), + chr(PartyUtils.convertDegreesToPartyGrid((i*circleStep*180)/math.pi + 270.0)) + ) + partyInfoDict = { + "partyId" : partyId, + "hostId" : hostId, + "startTime" : curServerDateTime,#.strftime("%Y-%m-%d %H:%M:%S"), + "endTime" : endTime,#.strftime("%Y-%m-%d %H:%M:%S"), + "isPrivate" : False, + "inviteTheme" : 0, + "activities" : activities, + "decorations" : [], + "statusId" : 0, + } + + # Form the list of inviteeIds + inviteeIds = [] + inviteeDict = self.inviteDb.getInviteesOfParty(partyInfoDict["partyId"]) + if inviteeDict is not None: + for info in inviteeDict: + inviteeIds.append(info['guestId']) + + # Send the party info back to the AI who requested it + self.air.sendUpdateToDoId( + "DistributedPartyManager", + 'partyInfoOfHostResponseUdToAi', + pmDoId, + [self.getFormattedPartyInfo(partyInfoDict), inviteeIds], + ) + + def _checkForPartiesStarting(self, task): + """ Called every 15 minutes to alert hosts to parties that can start """ + DistributedPartyManagerUD.notify.debug( "_checkForPartiesStarting : Checking for parties starting..." ) + curServerDateTime = self.air.toontownTimeManager.getCurServerDateTime() + # force started parties to finished if they've gone for for too long + self.forceFinishedForStarted() + # first mark as never started parties who went past the end time + self.forceNeverStartedForCanStart() + + partiesStartingTuples = self.partyDb.getPartiesAvailableToStart(curServerDateTime.strftime("%Y-%m-%d %H:%M:%S")) + # Now we know the partyIds and hostIds of parties that can start, let's + # send those directly out to the DistributedToons who can use them! + for infoDict in partiesStartingTuples: + self.notify.debug('%d can start party %d' % (infoDict['hostId'], infoDict['partyId'])) + self.air.sendUpdateToDoId( + "DistributedToon", + "setPartyCanStart", + infoDict['hostId'], + [infoDict['partyId']], + ) + timeToNextCheck = ((self.startPartyFrequency - (curServerDateTime.minute % self.startPartyFrequency)) * 60) - curServerDateTime.second + 1 + self.notify.debug("timeToNextCheck=%s" % timeToNextCheck) + if task: + taskMgr.doMethodLater(timeToNextCheck, self._checkForPartiesStarting, "DistributedPartyManagerUD_checkForPartiesStarting" ) + else: + # if we got here through ~party checkStart, don't schedule another check + self.notify.debug("not rescheduling self._checkForPartiesStarting") + + def _sanityCheckParties(self, task): + """ Called every 60 minutes to check the database for started but never finished parties """ + self.notify.debug( "_sanityCheckParties :..." ) + self.forceFinishedForStarted() + # check is now done every 5 minutes as part of check parties starting + # taskMgr.doMethodLater(self.partiesSanityCheckFrequency * 60, self._sanityCheckParties, "DistributedPartyManagerUD_sanityCheckParties") + + def forceFinishedForStarted(self): + """check the database for started but never finished parties.""" + curServerDateTime = self.air.toontownTimeManager.getCurServerDateTime() + thresholdTime = curServerDateTime + timedelta(hours = -(PartyGlobals.DefaultPartyDuration )) + result = self.partyDb.forceFinishForStarted(thresholdTime.strftime("%Y-%m-%d %H:%M:%S")) + # first make sure result isn't an empty or invalid list + if not result: + return + for info in result: + partyId = info['partyId'] + hostId = info['hostId'] + if self.isOnline(hostId): + status = PartyGlobals.PartyStatus.Finished + self.sendNewPartyStatus(hostId, partyId, status) + + def forceNeverStartedForCanStart(self): + curServerDateTime = self.air.toontownTimeManager.getCurServerDateTime() + result = self.partyDb.forceNeverStartedForCanStart(curServerDateTime.strftime("%Y-%m-%d %H:%M:%S")) + # first make sure result isn't an empty list or invalid list + if not result: + return + for info in result: + partyId = info['partyId'] + hostId = info['hostId'] + if self.isOnline(hostId): + status = PartyGlobals.PartyStatus.NeverStarted + self.sendNewPartyStatus(hostId, partyId, status) + + def sendNewPartyStatus(self, avatarId, partyId, newStatus): + """Tell a toon a party status has changed.""" + DistributedPartyManagerUD.notify.debug( "Calling DistributedToon::sendNewPartyStatus across the network with avatarId %d. partyId=%d newStatus=%d." %(avatarId, partyId, newStatus ) ) + self.air.sendUpdateToDoId( + "DistributedToon", + "setPartyStatus", + avatarId, + [partyId, newStatus], + ) + + def toonHasEnteredPartyAiToUd(self, hostId): + """ This gets called when a toon enters a party. """ + DistributedPartyManagerUD.notify.debug("toonHasEnteredPartyAiToUd : someone entered hostIds %s party"%hostId) + if hostId in self.hostAvIdToAllPartiesInfo: + self.hostAvIdToAllPartiesInfo[hostId][3] += 1 + if self.hostAvIdToAllPartiesInfo[hostId][3] >= 0: + self.sendUpdateToAllAis("updateToPublicPartyCountUdToAllAi", [hostId, self.hostAvIdToAllPartiesInfo[hostId][3]]) + + def toonHasExitedPartyAiToUd(self, hostId): + """ This gets called when a toon exits a party. """ + DistributedPartyManagerUD.notify.debug("toonHasExitedPartyAiToUd : someone exited hostIds %s party"%hostId) + if hostId in self.hostAvIdToAllPartiesInfo: + self.hostAvIdToAllPartiesInfo[hostId][3] -= 1 + if self.hostAvIdToAllPartiesInfo[hostId][3] >= 0: + self.sendUpdateToAllAis("updateToPublicPartyCountUdToAllAi", [hostId, self.hostAvIdToAllPartiesInfo[hostId][3]]) + + def partyHasStartedAiToUd(self, pmDoId, partyId, shardId, zoneId, hostName): + """ + This gets called by an AI when a party is started, updates + hostAvIdToAllPartiesInfo for use by other AIs and their public party + gates. + """ + DistributedPartyManagerUD.notify.debug("partyHasStartedAiToUd : pmDoId=%s partyId=%s shardId=%s zoneId=%s hostName=%s " % (pmDoId, partyId, shardId, zoneId, hostName)) + errorCode = self.changePartyStatusRequestAiToUd(pmDoId, partyId, PartyGlobals.PartyStatus.Started.name) + if errorCode != PartyGlobals.ChangePartyFieldErrorCode.AllOk: + return + party = self.partyDb.getParty(partyId) + partyInfo = party[0] + activityIds = [] + for i in range(len(partyInfo["activities"])): + if i%4 == 0: + activityIds.append(partyInfo["activities"][i]) + # we can not rely on globalClock.getRealTime() as that depends on when the process is started + # and will definitely be different between the uberdog and AI + actualStartTime = int(time.time()) + self.hostAvIdToAllPartiesInfo[partyInfo["hostId"]] = [shardId, zoneId, partyInfo["isPrivate"], 0, hostName, activityIds,actualStartTime, partyId] + self.sendUpdateToAllAis("updateToPublicPartyInfoUdToAllAi", [partyInfo["hostId"], actualStartTime, shardId, zoneId, partyInfo["isPrivate"], 0, hostName, activityIds, partyId]) + self.informInviteesPartyHasStarted(partyId) + + def sendUpdateToAllAis(self, message, args): + pass + #TODO figure out alternative for PARTY_MANAGER_UD_TO_ALL_AI + #dg = self.dclass.aiFormatUpdateMsgType( + # message, self.doId, self.doId, self.air.ourChannel, PARTY_MANAGER_UD_TO_ALL_AI, args) + #self.air.send(dg) + + def sendTestMsg(self): + """Send a test msg to all AIs to prove it can be done.""" + fieldName = 'testMsgUdToAllAi' + args = [] + dg = self.dclass.aiFormatUpdateMsgType( + fieldName, self.doId, self.doId, self.air.ourChannel, PARTY_MANAGER_UD_TO_ALL_AI, args) + self.air.send(dg) + + def forceCheckStart(self): + """Do an immediate check which parties can start.""" + self._checkForPartiesStarting(None) + + def avatarOnlinePlusAccountInfo(self,avatarId,accountId,playerName, + playerNameApproved,openChatEnabled, + createFriendsWithChat,chatCodeCreation): + # otp server is telling us an avatar just logged in + # this is far far better than having the AI be the one to tell us + assert self.notify.debugCall() + assert avatarId + + self.notify.debug("avatarOnlinePlusAccountInfo") + self.avatarLoggedIn(avatarId) + self.markAvatarOnline(avatarId) + + def avatarOffline(self, avatarId): + """Handle otp_server telling us an avatar is offline.""" + self.markAvatarOffline(avatarId) + + + def markAvatarOnline(self, avatarId): + """Mark an avatar as online.""" + + if avatarId in self.isAvatarOnline: + assert self.notify.debug( + "\n\nWe got a duplicate avatar online notice %s"%(avatarId,)) + if avatarId and avatarId not in self.isAvatarOnline: + self.isAvatarOnline[avatarId]=True + + def markAvatarOffline(self, avatarId): + """Mark an avatar as offline.""" + self.isAvatarOnline.pop(avatarId,None) + + def isOnline(self, avatarId): + """Return True if an avatar is online.""" + result = avatarId in self.isAvatarOnline + return result + + def handleInterruptedPartiesOnShard(self, shardId): + """Tell other shards the parties on this shard are gone, set party status back to CanStart.""" + # figure out which partyIds are running on that shard + assert self.notify.debugStateCall(self) + interruptedParties = [] + interruptedPartiesToCanStart = [] + interruptedPartiesToFinished = [] + interruptedHostIds = [] + for hostId in self.hostAvIdToAllPartiesInfo: + partyInfo = self.hostAvIdToAllPartiesInfo[hostId] + if partyInfo[0] == shardId: + interruptedParties.append(partyInfo[7]) + interruptedHostIds.append(hostId) + + # TODO is it possible for a toon to get back online before we hit this point? + # Currently if the current server time is past party end time, he is SOL and can't start a party + curServerTime = self.air.toontownTimeManager.getCurServerDateTime() + interruptedInfo = list(self.partyDb.getMultipleParties(interruptedParties)) + for info in interruptedInfo: + endTime = info["endTime"] + endTime = datetime( + endTime.year, + endTime.month, + endTime.day, + endTime.hour, + endTime.minute, + tzinfo=self.air.toontownTimeManager.serverTimeZone, + ) + if endTime < curServerTime: + interruptedPartiesToFinished.append(info["partyId"]) + else: + interruptedPartiesToCanStart.append(info["partyId"]) + + if interruptedPartiesToCanStart: + self.notify.debug('setting these parties to CanStart %s' % interruptedPartiesToCanStart) + if interruptedPartiesToFinished: + self.notify.debug('setting these parties to Finished %s' % interruptedPartiesToFinished) + + # the toon just got kicked out, he will probably want to go back and restart + self.partyDb.changeMultiplePartiesStatus(interruptedPartiesToCanStart, + PartyGlobals.PartyStatus.CanStart) + self.partyDb.changeMultiplePartiesStatus(interruptedPartiesToFinished, + PartyGlobals.PartyStatus.Finished) + + for index,hostId in enumerate(interruptedHostIds): + if self.isOnline(hostId): + partyId = interruptedParties[index] + if partyId in interruptedPartiesToCanStart: + status = PartyGlobals.PartyStatus.CanStart + else: + status = PartyGlobals.PartyStatus.Finished + self.sendNewPartyStatus(hostId, partyId, status) + + # tell all AI servers the party has finished since it was interrupted + for hostId in interruptedHostIds: + self.sendUpdateToAllAis("partyHasFinishedUdToAllAi", [hostId]) + del self.hostAvIdToAllPartiesInfo[hostId] + + + def partyManagerAIStartingUp(self, pmDoId, shardId): + """An AI server is starting up (or restarting) , send him all public parties running.""" + # if this shardId is starting up, it implies that all parties running on this + # shard have been interrupted + assert self.notify.debugStateCall(self) + # we still need this check just in case uberdog was accidentally shut down + # before the AI servers + self.handleInterruptedPartiesOnShard(shardId) + + for hostId in self.hostAvIdToAllPartiesInfo: + publicInfo = self.hostAvIdToAllPartiesInfo[hostId] + numToons = publicInfo[3] + if numToons <0: + numToons = 0 + self.air.sendUpdateToDoId( + "DistributedPartyManager", + 'updateToPublicPartyInfoUdToAllAi', + pmDoId, + [hostId, publicInfo[6], publicInfo[0], publicInfo[1], publicInfo[2], numToons, + publicInfo[4], publicInfo[5], publicInfo[7]], + ) + + def partyManagerAIGoingDown(self, pmDoId, shardId): + """An AI server is going down, interupt parties appropriately""" + # if this shardId is going down, it implies that all parties running on this + # shard have been interrupted + assert self.notify.debugStateCall(self) + self.handleInterruptedPartiesOnShard(shardId) + + + def updateAllPartyInfoToUd(self, hostId, startTime, shardId, zoneId, isPrivate, numberOfGuests, \ + hostName, activityIds, partyId): + """Handle an AI server telling us all the information about a party running on him.""" + if hostId in self.hostAvIdToAllPartiesInfo: + self.notify.warning("hostId %s already in self.hostAvIdToAllPartiesInfo %s" % ( + hostId, self.hostAvIdToAllPartiesInfo[hostId])) + + self.hostAvIdToAllPartiesInfo[hostId] = [ + shardId, zoneId, isPrivate, numberOfGuests, + hostName, activityIds, startTime, partyId] + + def informInviteesPartyHasStarted(self, partyId): + """The host has started his party, tell the invitees.""" + # WARNING since this is not sent through a ram field, if the toon switches + # districts the AI on the other district could have the party status wrong. + # To do it 100% safe we'd need to do a _updateInvites and _updatePartiesInvitedTo + # but those are fairly expensive operations, let's just try this for now + inviteeDict = self.inviteDb.getInviteesOfParty(partyId) + for info in inviteeDict: + avId = info['guestId'] + if self.isOnline(avId): + self.sendNewPartyStatus( avId, partyId, PartyGlobals.PartyStatus.Started) + + def _getPartyInfoSize(self, partyInfo): + """ + Calculate the size of the party info and return the value in bytes. + This is the format of the party info from toon.dc: + struct party{ + uint64 partyId; - 8 bytes + uint32 hostId; - 4 bytes + uint16 startYear; - 2 bytes + uint8 startMonth; - 1 byte + uint8 startDay; - 1 byte + uint8 startHour; - 1 byte + uint8 startMinute; - 1 byte + uint16 endYear; - 2 bytes + uint8 endMonth; - 1 byte + uint8 endDay; - 1 byte + uint8 endHour; - 1 byte + uint8 endMinute; - 1 byte + uint8 isPrivate; - 1 byte + uint8 inviteTheme; - 1 byte + activity activities[]; - 4 bytes * numberOfActivities + decoration decors[]; - 4 bytes * numberOfDecors + uint8 status; - 1 byte + }; + So the basic party info size is: + partyInfoSize = (27 + 4*numberOfActivities + 4*numberOfDecors) bytes + + Note: We assume that the party info format in toon.dc won't change. + Please change this method and calculation if the format changes. + """ + activities = partyInfo[14] + decors = partyInfo[15] + basePartySize = 27 + numActivities = 0 + numDecors = 0 + + if (type(activities) == type([])): + numActivities = len(activities) + else: + self.notify.warning("partyId=%s has an incorrect partyInfo format for activities" %str(partyInfo[0])) + + if (type(decors) == type([])): + numDecors = len(decors) + else: + self.notify.warning("partyId=%s has an incorrect partyInfo format for decors" %str(partyInfo[0])) + + partyInfoSize = basePartySize + (4 * numActivities) + (4 * numDecors) + return partyInfoSize \ No newline at end of file diff --git a/toontown/uberdog/ToontownUDRepository.py b/toontown/uberdog/ToontownUDRepository.py index 9ee9b85..dcd6a48 100644 --- a/toontown/uberdog/ToontownUDRepository.py +++ b/toontown/uberdog/ToontownUDRepository.py @@ -45,3 +45,5 @@ class ToontownUDRepository(ToontownInternalRepository): if __astron__: # Create our Astron login manager... self.astronLoginManager = self.generateGlobalObject(OTP_DO_ID_ASTRON_LOGIN_MANAGER, 'AstronLoginManager') + # create our DistributedPartyManagerUD + self.partyManager = self.generateGlobalObject(OTP_DO_ID_TOONTOWN_PARTY_MANAGER, 'DistributedPartyManager')