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..83de076 100644 --- a/toontown/ai/ToontownAIRepository.py +++ b/toontown/ai/ToontownAIRepository.py @@ -1,8 +1,11 @@ +import time 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 @@ -33,6 +36,7 @@ from toontown.hood.LawbotHQDataAI import LawbotHQDataAI from toontown.hood.MMHoodDataAI import MMHoodDataAI from toontown.hood.OZHoodDataAI import OZHoodDataAI from toontown.hood.TTHoodDataAI import TTHoodDataAI +from toontown.parties.ToontownTimeManager import ToontownTimeManager from toontown.pets.PetManagerAI import PetManagerAI from toontown.quest.QuestManagerAI import QuestManagerAI from toontown.racing import RaceGlobals @@ -45,6 +49,7 @@ from toontown.racing.DistributedViewPadAI import DistributedViewPadAI from toontown.racing.RaceManagerAI import RaceManagerAI from toontown.uberdog.DistributedPartyManagerAI import DistributedPartyManagerAI from toontown.safezone.SafeZoneManagerAI import SafeZoneManagerAI +from toontown.safezone import DistributedPartyGateAI from toontown.shtiker.CogPageManagerAI import CogPageManagerAI from toontown.spellbook.ToontownMagicWordManagerAI import ToontownMagicWordManagerAI from toontown.suit.SuitInvasionManagerAI import SuitInvasionManagerAI @@ -181,6 +186,11 @@ class ToontownAIRepository(ToontownInternalRepository): # Create our Cog suit manager... self.cogSuitMgr = CogSuitManagerAI(self) + # Create our Toontown time manager... + self.toontownTimeManager = ToontownTimeManager() + self.toontownTimeManager.updateLoginTimes(time.time(), time.time(), globalClock.getRealTime()) + + def createGlobals(self): """ Creates "global" (distributed) objects. @@ -406,7 +416,24 @@ class ToontownAIRepository(ToontownInternalRepository): return [] # TODO def findPartyHats(self, dnaData, zoneId): - return [] # TODO + partyHats = [] + if 'party_gate' in dnaData.getName(): + x, y, z = dnaData.getPos() + h, p, r = dnaData.getHpr() + partyHat = DistributedPartyGateAI.DistributedPartyGateAI(self) + partyHat.generateWithRequired(zoneId) + partyHats.append(partyHat) + else: + if isinstance(dnaData, DNAVisGroup): + name = dnaData.getName() + visId = int(name.split(":", 1)[0]) % 1000 + zoneId = ZoneUtil.getHoodId(zoneId) + visId + + for i in range(dnaData.getNumChildren()): + foundPartyHats = self.findPartyHats(dnaData.at(i), zoneId) + partyHats.extend(foundPartyHats) + + return partyHats def findRacingPads(self, dnaData, zoneId, area, type='racing_pad', overrideDNAZone=False): kartPads, kartPadGroups = [], [] @@ -506,3 +533,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/Cannon.py b/toontown/parties/Cannon.py index 5b57926..5b210d9 100644 --- a/toontown/parties/Cannon.py +++ b/toontown/parties/Cannon.py @@ -1,5 +1,6 @@ import math from panda3d.core import * +from panda3d.otp import Nametag, NametagFloat3d from direct.interval.MetaInterval import Sequence, Parallel from direct.interval.FunctionInterval import Func from direct.interval.LerpInterval import LerpScaleInterval, LerpColorScaleInterval 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/DistributedPartyAI.py b/toontown/parties/DistributedPartyAI.py index 842864d..ee9b071 100644 --- a/toontown/parties/DistributedPartyAI.py +++ b/toontown/parties/DistributedPartyAI.py @@ -1,5 +1,282 @@ -from direct.directnotify import DirectNotifyGlobal +# from Anesidora +#------------------------------------------------------------------------------- +# Contact: Shawn Patton +# Created: Sep 2008 +# +# Purpose: DistributedPartyAI controls message passing to the client for parties +# +#------------------------------------------------------------------------------- from direct.distributed.DistributedObjectAI import DistributedObjectAI +from toontown.parties import PartyGlobals +from toontown.parties import PartyUtils + +from toontown.parties.DistributedPartyJukeboxActivityAI import DistributedPartyJukeboxActivityAI +from toontown.parties.DistributedPartyCannonActivityAI import DistributedPartyCannonActivityAI +from toontown.parties.DistributedPartyTrampolineActivityAI import DistributedPartyTrampolineActivityAI +from toontown.parties.DistributedPartyVictoryTrampolineActivityAI import DistributedPartyVictoryTrampolineActivityAI +from toontown.parties.DistributedPartyCatchActivityAI import DistributedPartyCatchActivityAI +from toontown.parties.DistributedPartyDanceActivityAI import DistributedPartyDanceActivityAI +from toontown.parties.DistributedPartyTugOfWarActivityAI import DistributedPartyTugOfWarActivityAI +from toontown.parties.DistributedPartyFireworksActivityAI import DistributedPartyFireworksActivityAI +from toontown.parties.DistributedPartyJukebox40ActivityAI import DistributedPartyJukebox40ActivityAI +from toontown.parties.DistributedPartyDance20ActivityAI import DistributedPartyDance20ActivityAI +from toontown.parties.DistributedPartyCogActivityAI import DistributedPartyCogActivityAI +from toontown.parties.DistributedPartyWinterCatchActivityAI import DistributedPartyWinterCatchActivityAI +from toontown.parties.DistributedPartyWinterTrampolineActivityAI import DistributedPartyWinterTrampolineActivityAI +from toontown.parties.DistributedPartyWinterCogActivityAI import DistributedPartyWinterCogActivityAI +from toontown.parties.DistributedPartyValentineDanceActivityAI import DistributedPartyValentineDanceActivityAI +from toontown.parties.DistributedPartyValentineDance20ActivityAI import DistributedPartyValentineDance20ActivityAI +from toontown.parties.DistributedPartyValentineJukeboxActivityAI import DistributedPartyValentineJukeboxActivityAI +from toontown.parties.DistributedPartyValentineJukebox40ActivityAI import DistributedPartyValentineJukebox40ActivityAI +from toontown.parties.DistributedPartyValentineTrampolineActivityAI import DistributedPartyValentineTrampolineActivityAI + +ActivityIdsToClasses = { + PartyGlobals.ActivityIds.PartyJukebox : DistributedPartyJukeboxActivityAI, + PartyGlobals.ActivityIds.PartyCannon : DistributedPartyCannonActivityAI, + PartyGlobals.ActivityIds.PartyTrampoline : DistributedPartyTrampolineActivityAI, + PartyGlobals.ActivityIds.PartyVictoryTrampoline : DistributedPartyVictoryTrampolineActivityAI, + PartyGlobals.ActivityIds.PartyCatch : DistributedPartyCatchActivityAI, + PartyGlobals.ActivityIds.PartyDance : DistributedPartyDanceActivityAI, + PartyGlobals.ActivityIds.PartyTugOfWar : DistributedPartyTugOfWarActivityAI, + PartyGlobals.ActivityIds.PartyFireworks : DistributedPartyFireworksActivityAI, + PartyGlobals.ActivityIds.PartyClock : None, + PartyGlobals.ActivityIds.PartyJukebox40 : DistributedPartyJukebox40ActivityAI, + PartyGlobals.ActivityIds.PartyDance20 : DistributedPartyDance20ActivityAI, + PartyGlobals.ActivityIds.PartyCog : DistributedPartyCogActivityAI, + PartyGlobals.ActivityIds.PartyVictoryTrampoline: DistributedPartyVictoryTrampolineActivityAI, + PartyGlobals.ActivityIds.PartyWinterCatch: DistributedPartyWinterCatchActivityAI, + PartyGlobals.ActivityIds.PartyWinterTrampoline: DistributedPartyWinterTrampolineActivityAI, + PartyGlobals.ActivityIds.PartyWinterCog: DistributedPartyWinterCogActivityAI, + PartyGlobals.ActivityIds.PartyValentineDance: DistributedPartyValentineDanceActivityAI, + PartyGlobals.ActivityIds.PartyValentineDance20: DistributedPartyValentineDance20ActivityAI, + PartyGlobals.ActivityIds.PartyValentineJukebox: DistributedPartyValentineJukeboxActivityAI, + PartyGlobals.ActivityIds.PartyValentineJukebox40: DistributedPartyValentineJukebox40ActivityAI, + PartyGlobals.ActivityIds.PartyValentineTrampoline: DistributedPartyValentineTrampolineActivityAI, + } + class DistributedPartyAI(DistributedObjectAI): - notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyAI') + notify = directNotify.newCategory("DistributedPartyAI") + + def __init__(self, air, avId, zoneId, partyInfo, inviteeIds): + DistributedObjectAI.__init__(self, air) + + self.notify.debug("created with avId = %s, zoneId = %s, and partyInfo=%s" % (avId, zoneId, partyInfo)) + self.avId = avId + self.zoneId = zoneId + self.partyInfo = partyInfo + self.inviteeIds = inviteeIds + self.partyStartedTime = self.air.toontownTimeManager.getCurServerDateTime() + self.partyClockInfo = (0,0,0) + + # For the party clock, we need to set aside the x, y, and h + for activity in self.partyInfo.activityList: + if activity.activityId == PartyGlobals.ActivityIds.PartyClock: + self.partyClockInfo = (activity.x, activity.y, activity.h) + + self.hostName = "" + # try to get the host name at this point here + toon = simbase.air.doId2do.get(self.partyInfo.hostId) + if toon: + self.hostName = toon.getName() + + # Keep track of the parties state for toon's coming in to the party + self.isPartyEnding = False + self.activityObjects = [] + + def getPartyState(self): + return self.isPartyEnding + + def b_setPartyState(self, partyState): + self.isPartyEnding = partyState + self.sendUpdate("setPartyState", [partyState]) + + def generate(self): + DistributedPartyAI.notify.debug("DistParty generate: %s" % self.doId) + DistributedObjectAI.generate(self) + + self.air.writeServerEvent("party_generate",self.partyInfo.partyId, "%d|%d" % (self.doId,self.partyInfo.hostId)) + + # Log that a GM party has been generated. + try: + host = simbase.air.doId2do.get(self.partyInfo.hostId) + if host.hasGMName(): + self.air.writeServerEvent("party_generate_gm", self.partyInfo.partyId, "%s" % self.partyInfo.hostId) + assert self.notify.debug("GM-%s's party has started." % self.partyInfo.hostId) + except: + pass + + # We want to initialize all the activities that are at this party. + # We'll loop through the activityList and see if we can import, create, + # and generate the relevant AI class. This code assumes that the + # activity classes are named according to the enum + # PartyGlobals.ActivityIds + # for example: + # PartyGlobals.ActivityIds.PartyCatch would load + # DistributedPartyCatchActivityAI + for activity in self.partyInfo.activityList: + # if activity not in ActivityIdsToClasses: + # self.notify.warning('DistributedPartyAI: unknown activity: {0} continuing to next one'.format(activity)) + # continue + # Location and heading in the activityList is in party space, so + # we convert them to Panda space before passing them into the + # activities + #activityName = PartyGlobals.ActivityIds.getString(activity.activityId) + x = PartyUtils.convertDistanceFromPartyGrid(activity.x, 0) + y = PartyUtils.convertDistanceFromPartyGrid(activity.y, 1) + h = PartyUtils.convertDegreesFromPartyGrid(activity.h) + # Skip the party clock... + if activity.activityId == PartyGlobals.ActivityIds.PartyClock: + continue + + # Special case for cannon, add another cannon instead of creating + # a new instance of the cannon activity + if activity.activityId == PartyGlobals.ActivityIds.PartyCannon and \ + self.getCannonActivity(): + self.getCannonActivity().spawnCannonAt(x, y, h) + continue + + actClass = ActivityIdsToClasses[activity.activityId] + newAct = actClass(self.air, self.doId, x, y ,h) + newAct.generateWithRequired(self.zoneId) + self.activityObjects.append(newAct) + + def getCannonActivity(self): + result = None + for act in self.activityObjects: + if "DistributedPartyCannonActivityAI" in str(act.__class__): + result = act + break + return result + + def getPartyClockInfo(self): + return self.partyClockInfo + + def getAvIdsAtParty(self): + return self.air.partyManager.zoneIdToGuestAvIds[self.zoneId] + + def getPartyStartedTime(self): + return self.partyStartedTime.strftime("%Y-%m-%d %H:%M:%S") + + def getInviteeIds(self): + return self.inviteeIds + + def getPartyInfoTuple(self): + startTime = self.partyInfo.startTime + endTime = self.partyInfo.endTime + formattedActivities = [] + for activity in self.partyInfo.activityList: + oneActivity = (activity.activityId, + activity.x, + activity.y, + activity.h, + ) + formattedActivities.append(oneActivity) + formattedDecors = [] + for decor in self.partyInfo.decors: + oneDecor = (decor.decorId, + decor.x, + decor.y, + decor.h, + ) + formattedDecors.append(oneDecor) + isPrivate = self.partyInfo.isPrivate + inviteTheme = self.partyInfo.inviteTheme + + return ( + self.partyInfo.partyId, + self.partyInfo.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, + self.partyInfo.status, + ) + + def delete(self): + DistributedPartyAI.notify.debug("DistParty delete: %s" % self.doId) + self.ignoreAll() + try: + self.Party_deleted + DistributedPartyAI.notify.debug("party already deleted: %s" % self.Party_deleted) + except: + DistributedPartyAI.notify.debug("completing party delete: %s" % self.__dict__.get("zoneId")) + self.air.writeServerEvent("party_delete",self.partyInfo.partyId, "%d|%d" % (self.doId,self.partyInfo.hostId)) + self.Party_deleted = self.zoneId + for activityObj in self.activityObjects: + activityObj.requestDelete() + self.activityObjects = [] + DistributedObjectAI.delete(self) + + + def unload(self): + self.notify.debug("unload") + + def avIdEnteredParty(self, avId): + senderId = self.air.getAvatarIdFromSender() + if senderId != avId: + self.air.writeServerEvent('suspicious', senderId, 'someone else trying to enter a party for this avatar: avId = %d' % avId) + return + if not self.hostName and self.avId == self.partyInfo.hostId: + # if we don't have a hostname yet try again + toon = simbase.air.doId2do.get(self.partyInfo.hostId) + if toon: + toonName = toon.getName() + if toonName: + self.b_setHostName(toonName) + + self.sendUpdate("setAvIdsAtParty", [self.air.partyManager.zoneIdToGuestAvIds[self.zoneId]]) + totalMoney = -1 + av = simbase.air.doId2do.get(avId) + if av: + totalMoney = av.getTotalMoney() + self.air.writeServerEvent("party_enter",self.partyInfo.partyId, "%d|%d" % (avId,totalMoney)) + + def initPartyData(self): + pass + + def destroyPartyData(self): + if hasattr(self, "Party_deleted"): + DistributedPartyAI.notify.debug("destroyPartyData: party already deleted: %s" % self.Party_deleted) + return + DistributedPartyAI.notify.debug("destroyPartyData: %s" % self.__dict__.get("zoneId")) + self.releaseZoneData() + + def getHostName(self): + """Return the host name.""" + return self.hostName + + def setHostName(self, newName): + """Set the hostname on the AI only.""" + self.hostName = self.hostName + + def b_setHostName(self, newName): + """Set the hostname on the ai and client""" + self.setHostName(newName) + self.d_setHostName(newName) + + def d_setHostName(self, newName): + """Send the host name to the client.""" + self.sendUpdate("setHostName", [newName]) + + def isInActivity(self, avId): + """Return true if the avId is busy with an activity.""" + result = False + for actObj in self.activityObjects: + if actObj.isInActivity(avId): + result = True + break + return result + + + \ No newline at end of file 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/DistributedPartyActivityAI.py b/toontown/parties/DistributedPartyActivityAI.py index edfb2af..391045c 100644 --- a/toontown/parties/DistributedPartyActivityAI.py +++ b/toontown/parties/DistributedPartyActivityAI.py @@ -1,5 +1,254 @@ -from direct.directnotify import DirectNotifyGlobal -from direct.distributed.DistributedObjectAI import DistributedObjectAI +# From Anesidora +#------------------------------------------------------------------------------- +# Contact: Shawn Patton, Rob Gordon, Edmundo Ruiz (Schell Games) +# Created: Sep 2008 +# +# Purpose: DistributedPartyActivityAI is the base class for all party activities. +# It loads up the sign and lever (where applicable) +#------------------------------------------------------------------------------- +from direct.distributed import DistributedObjectAI + +from toontown.parties import PartyGlobals + +class DistributedPartyActivityAI(DistributedObjectAI.DistributedObjectAI): + """ + This is the base class for all Distributed Party Activities on the AI. A distributed + party activity constitutes of any game or area at a party that involves multiple toons + interacting with it at the same time. + + Note that a new notify category is not created here as this class expects + subclasses to create it. + """ + + def __init__(self, air, partyDoId, x, y, h, activityId, activityType): + """ + x, y and h are in Panda space, not Party Grid space. + """ + DistributedObjectAI.DistributedObjectAI.__init__(self, air) + + self.partyDoId = partyDoId + self.party = self.air.doId2do.get(partyDoId) + self.x = x + self.y = y + self.h = h + self.activityId = activityId + self.activityName = PartyGlobals.ActivityIds(self.activityId).name + self.activityType = activityType + + # Actual avatars that will play the activity + self.toonIds = [] + self.toonIdsToJellybeanRewards = {} + self.toonId2joinTime = {} + +#------------------------------------------------------------------------------- +# join/exit functions that subclasses should override +#------------------------------------------------------------------------------- + def toonJoinRequest(self): + """ + Clients call this over the wire when they want to join/start this + activity. Subclasses should override this and call sendToonJoinResponse. + """ + self.notify.error("BASE: toonJoinRequest should be overridden") + + + def toonExitRequest(self): + """ + Clients call this over the wire when they want to exit this + activity, but need AI server permission to do so. + Subclasses should override this and call sendToonExitResponse. + """ + self.notify.error("BASE: toonExitRequest should be overridden") + + + def toonReady(self): + """ + Clients call this over the wire when they are done reading the rules + and are ready to play the activity. Subclasses should override this. + """ + self.notify.error("BASE: toonReady should be overridden") + +#------------------------------------------------------------------------------- + + def toonExitDemand(self): + """ + Clients call this over the wire to inform the server that the sender + has left the activity. Use this for activities where server permission + is not required for leaving the activity. + + A default implementation is provided, which may be extended or + overridden by subclasses. + """ + senderId = self.air.getAvatarIdFromSender() + self.sendToonExitResponse(senderId, True) + + + def sendToonJoinResponse(self, toonId, joined, denialReason=PartyGlobals.DenialReasons.Default): + """ + Subclasses should call this in response to recieving a toonJoinRequest + call. + + Parameters: + toonId- id of the avatar that is being accepted/rejected + joined- True if the avatar can join this activity, False otherwise + """ + if joined: + self._addToon(toonId) + self.sendUpdate("setToonsPlaying", [self.toonIds]) + else: + self.sendUpdateToAvatarId(toonId, "joinRequestDenied", [denialReason]) + + + def sendToonExitResponse(self, toonId, exited, denialReason=PartyGlobals.DenialReasons.Default): + """ + Broadcast to all clients whether or not this toonId has left the activity. + Subclasses should call this in response to recieving a toonExitRequest + call. + Parameters: + toonId- id of the avatar that made the request to exit + exited- True if the avatar exited this activity, False otherwise + """ + if exited: + self._removeToon(toonId) + self.sendUpdate("setToonsPlaying", [self.toonIds]) + else: + self.sendUpdateToAvatarId(toonId, "exitRequestDenied", [denialReason]) + + + def isToonPlaying(self, toonId): + return(toonId in self.toonIds) + + + def removeAllToons(self): + """ + Base classes should call this when an activity is over and all toons + should exit. + """ + self.notify.debug("BASE: removeAllToons %s" % self.toonIds) + while len(self.toonIds): + self._removeToon(self.toonIds[0]) + self.sendUpdate("setToonsPlaying", [self.toonIds]) + + + def _addToon(self, toonId): + """ + Utility function for adding an avatar to the list of playing avatars. + This call should be the only way you add an toonId to self.toonIds. + """ + self.notify.debug("BASE: _addToon( toonId=%s )" % toonId) + if toonId in self.toonIds: + self.notify.warning("BASE: attempt to add toonId (%s) failed as it was already in self.toonIds" % toonId) + else: + self.toonIds.append(toonId) + self.toonId2joinTime[toonId] = globalClock.getFrameTime() + # listen for this avatar's exit event + self.acceptOnce( + self.air.getAvatarExitEvent(toonId), + self._handleUnexpectedToonExit, + extraArgs=[toonId], + ) + + + def _handleUnexpectedToonExit(self, toonId): + """ + An avatar bailed out because he lost his connection or quit + unexpectedly. Sub-classes may need to extend this function. + """ + self.notify.debug("BASE: _handleUnexpectedToonExit( toonId=%s )" % toonId) + # bypass any subclass checks and let clients know the toon has left the + # activity + self._removeToon(toonId) + self.sendUpdate("setToonsPlaying", [self.toonIds]) + + + def _removeToon(self, toonId): + """ + Utility function for removing an avatar from the list of playing avatars. + This call should be the only way you remove an toonId from + self.toonIds. + """ + self.notify.debug("BASE: _removeToon( toonId=%s )" % toonId) + if toonId not in self.toonIds: + self.notify.warning("BASE: attempt to remove toonId (%s) failed as it was not in toonIds" % toonId) + else: + self.toonIds.remove(toonId) + del self.toonId2joinTime[toonId] + # stop listening for this avatar's exit event + self.ignore(self.air.getAvatarExitEvent(toonId)) + # don't delete scores here, as we may want to display score info + # for dropped players. + + + def generate(self): + DistributedObjectAI.DistributedObjectAI.generate(self) + self.notify.debug("BASE: generate") + + + def announceGenerate(self): + DistributedObjectAI.DistributedObjectAI.announceGenerate(self) + self.notify.debug("BASE: announceGenerate") + + + def delete(self): + self.notify.debug("BASE: delete: deleting AI PartyActivity object") + self.ignoreAll() + DistributedObjectAI.DistributedObjectAI.delete(self) + + + def getPartyDoId(self): + return self.partyDoId + + + def getX(self): + return self.x + + + def getY(self): + return self.y + + + def getH(self): + return self.h + + + def issueJellybeanRewards(self): + """ + Give the participating toons their jellybean reward. + """ + now = globalClock.getFrameTime() + while len(self.toonIdsToJellybeanRewards): + self.issueJellybeanRewardToToonId(list(self.toonIdsToJellybeanRewards.keys())[0], now=now) + + def issueJellybeanRewardToToonId(self, toonId, now=None): + """ + Give a specific toon a their jellybean reward. + """ + if toonId in self.toonIdsToJellybeanRewards: + if now is None: + now = globalClock.getFrameTime() + reward = self.toonIdsToJellybeanRewards[toonId] + timePlayed = now - self.toonId2joinTime[toonId] + # reset 'joined' time to now, in case we give this toon another reward + self.toonId2joinTime[toonId] = now + self.__issueJellybeanReward(toonId, reward, timePlayed) + del self.toonIdsToJellybeanRewards[toonId] + + def __issueJellybeanReward(self, toonId, reward, timePlayed): + """ + Issue a toon a jellybean reward. + """ + # some activities, like Party Catch, can be left before it is over, + # so we do not check whether the toonId is in the list of toons in + # the activity. + toon = self.air.doId2do.get(toonId) + if toon is not None: + self.notify.debug("rewarding %d jellybeans to %s" %(reward, toon.getName())) + toon.addMoney(reward) + self.air.writeServerEvent("party_reward", toonId, "%d|%d|%d" % (self.activityId, reward, timePlayed)) + + def isInActivity(self, avId): + """Return true if the avId is busy with us.""" + result = False + if avId in self.toonIds: + result = True + return result -class DistributedPartyActivityAI(DistributedObjectAI): - notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyActivityAI') diff --git a/toontown/parties/DistributedPartyCannonAI.py b/toontown/parties/DistributedPartyCannonAI.py index 39db36c..2a50dbc 100644 --- a/toontown/parties/DistributedPartyCannonAI.py +++ b/toontown/parties/DistributedPartyCannonAI.py @@ -1,5 +1,236 @@ -from direct.directnotify import DirectNotifyGlobal -from direct.distributed.DistributedObjectAI import DistributedObjectAI +#------------------------------------------------------------------------------- +# Contact: Edmundo Ruiz (Schell Games) +# Created: Oct 2008 +# +# Purpose: DistributedPartyCannonAI handles the AI instance of a cannon at a party. +# It keeps track if a toon is inside the cannon. Note that it does not +# keep track of flying toons; that's the job of DistributedPartyCannonActivityAI +#------------------------------------------------------------------------------- -class DistributedPartyCannonAI(DistributedObjectAI): - notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyCannonAI') +from direct.distributed.ClockDelta import * +from direct.task import Task +from direct.distributed import DistributedObjectAI + +from toontown.toonbase import ToontownGlobals +from toontown.minigame import CannonGameGlobals +from toontown.minigame import Trajectory +from toontown.parties import PartyGlobals + +class DistributedPartyCannonAI(DistributedObjectAI.DistributedObjectAI): + notify = directNotify.newCategory("DistributedPartyCannonAI") + #notify.setDebug(True) + + CANNON_LIT_EVENT = "D_PARTY_CANNON_LIT" + + def __init__(self, air, activityDoId, x, y, z, h, p, r): + DistributedObjectAI.DistributedObjectAI.__init__(self, air) + + # TODO: Take this out when all cannons come from db + z = 0 + self.posHpr = [x, y, z, h, p, r] + self.toonInsideAvId = 0 + self.activityDoId = activityDoId + self.timeoutTask = None + self.rotation = 0.0 + self.angle = 0.0 + self.lit = False + self.toonEnteredTime = 0 + + def delete(self): + self.ignoreAll() + self.__stopTimeout() + DistributedObjectAI.DistributedObjectAI.delete(self) + + # Generate is never called on the AI so we do not define one + # Disable is never called on the AI so we do not define one + + # Distributed (clsend airecv) + def requestEnter(self): + avId = self.air.getAvatarIdFromSender() + # No toon inside, may enter cannon + inActivity = False + parentActivity = simbase.air.doId2do.get(self.activityDoId) + if parentActivity: + inActivity = parentActivity.party.isInActivity(avId) + if not self.isToonInside() and not inActivity: + self.__placeToonInside(avId) + else: + if self.toonInsideAvId != avId: + # don't kick out a toon already inside the cannon + self.notify.debug("requestEnter() - cannon already occupied") + self.sendUpdateToAvatarId(avId, "requestExit", []) + else: + self.notify.warning("got requestEnter from toon already inside it.") + +#=============================================================================== +# Attributes +#=============================================================================== + def isToonInside(self): + return (self.toonInsideAvId != 0) + + def getToonInsideId(self): + return self.toonInsideAvId + + def isReadyToFire(self): + return self.isToonInside() and self.lit + + def __placeToonInside(self, avId): + """ + Prepares a toon to be inside the cannon. + """ + self.notify.debug("__placeToonInside %s" % avId) + self.toonEnteredTime = globalClock.getFrameTime() + self.__stopTimeout() + self.toonInsideAvId = avId + self.notify.debug("Sending PartyGlobals.CANNON_MOVIE_LOAD for %d" % avId) + self.d_setMovie(PartyGlobals.CANNON_MOVIE_LOAD, self.toonInsideAvId) + + # Handle unexpected exit + self.acceptOnce(self.air.getAvatarExitEvent(avId), + self.__handleUnexpectedExit, extraArgs=[avId]) + self.acceptOnce("bootAvFromParty-%d" % avId, + self.__handleBootMessage, extraArgs=[avId]) + + def __clearToonInside(self): + self.notify.debug("%d __clearToonInside self.toonInsideAvId=%d" % (self.doId, self.toonInsideAvId)) + self.toonInsideAvId = 0 + self.lit = False + self.__stopTimeout() + + # Distributed (broadcast, ram) + def d_setMovie(self, mode, avId): + self.notify.debug("d_setMovie mode=%s, avId=%s" % (str(mode), str(avId))) + #if mode == PartyGlobals.CANNON_MOVIE_CLEAR: + # from otp.otpbase.PythonUtil import StackTrace + # print StackTrace() + self.sendUpdate("setMovie", [mode, avId]) + + # Distributed (aisend clrecv) + def setCannonPosition(self, zRot, angle): + avId = self.air.getAvatarIdFromSender() + # a client is sending a position update for their cannon + #self.notify.debug("setCannonPosition: %d: zRot = %d, angle = %d" % (avId, zRot, angle)) + self.rotation = zRot + self.angle = angle + self.d_updateCannonPosition(avId) + + # Distributed (broadcast ram) + def d_updateCannonPosition(self, avId): + self.sendUpdate("updateCannonPosition", [avId, self.rotation, self.angle]) + + # Distributed (required broadcast ram) + def getPosHpr(self): + return self.posHpr + + # Distributed (required broadcast ram) + # Needs this because setPartyId is dc required. + def getActivityDoId(self): + return self.activityDoId + + # Distributed (clsend airecv) + # a client is telling us that their cannon's fuse is lit + def setCannonLit(self, zRot, angle): + avId = self.air.getAvatarIdFromSender() + self.__cannonIsLit(avId, zRot, angle) + + def __cannonIsLit(self, avId, zRot, angle): + if avId == self.toonInsideAvId: + self.__stopTimeout() + self.lit = True + self.rotation = zRot + self.angle = angle + self.notify.debug("__cannonIsLit: %d: zRot = %d, angle = %d" % (avId, zRot, angle)) + # Send out that this cannon is ready to fire. + messenger.send(DistributedPartyCannonAI.CANNON_LIT_EVENT, [self.doId, self.toonEnteredTime]) + else: + self.notify.warning("__cannonIsLit but avId=%s toonInsideAvId=%s" % (str(avId), str(self.toonInsideAvId))) + + # Distributed (clsend airecv) + def setFired(self): + assert(self.notify.debug("%s setFired" % self.doId)) + senderId = self.air.getAvatarIdFromSender() + if self.lit: + self.ignore(self.air.getAvatarExitEvent(senderId)) + self.ignore("bootAvFromParty-%d" % senderId) + self.notify.debug("sending PartyGlobals.CANNON_MOVIE_CLEAR for %d" % self.toonInsideAvId) + self.d_setMovie(PartyGlobals.CANNON_MOVIE_CLEAR, self.toonInsideAvId) + self.__clearToonInside() + else: + self.notify.warning("%d setFired called but not lit" % self.doId) + + # Distributed (clsend airecv) + def setLanded(self, avId): + assert(self.notify.debug("%s setLanded" % self.doId)) + self.ignore(self.air.getAvatarExitEvent(avId)) + self.ignore("bootAvFromParty-%d" % avId) + self.notify.debug("%d sending PartyGlobals.CANNON_MOVIE_LANDED for %d" % (self.doId,avId)) + self.d_setMovie(PartyGlobals.CANNON_MOVIE_LANDED, avId) + self.__clearToonInside() + +#=============================================================================== +# Functions +#=============================================================================== + # Distributed (clsend airecv) + def setTimeout(self): + senderId = self.air.getAvatarIdFromSender() + if senderId == self.toonInsideAvId: + self.__startTimeout(PartyGlobals.CANNON_TIMEOUT + 3) + + def __startTimeout(self, timeLimit): + # Sets the timeout counter running. If __stopTimeout() is not + # called before the time expires, we'll exit the avatar. This + # prevents avatars from hanging out in the fishing spot all + # day. + self.notify.debug("__startTimeout %s" % self.taskName("timeout")) + self.__stopTimeout() + self.notify.debug("done calling __stopTimeout, really starting it %s" % self.taskName("timeout")) + self.timeoutTask = taskMgr.doMethodLater(timeLimit, + self.__handleTimeoutTask, + self.taskName("timeout")) + + def __stopTimeout(self): + """ + Stops a previously-set timeout from expiring. + """ + self.notify.debug("__stopTimeout %s" % self.taskName("timeout")) + if self.timeoutTask != None: + taskMgr.remove(self.timeoutTask) + self.timeoutTask = None + self.notify.debug("%d __stopTimeout successful" % self.doId) + else: + self.notify.debug("%d __stopTimeout no timeoutTask" % self.doId) + + def __handleTimeoutTask(self, task): + """ + Called when a timeout expires, this sends the avatar home + """ + self.notify.warning('Timeout expired! toonInside=%s' % str(self.toonInsideAvId)) + #self.__cannonIsLit(self.toonInsideAvId, self.rotation, self.angle) + self.__doExit() + return Task.done + + def __handleUnexpectedExit(self, avId): + """ + Handle in case the client crashes and the toon disappears, etc. + """ + self.notify.warning('avatar:' + str(avId) + ' has exited unexpectedly') + if self.toonInsideAvId == avId: + self.__doExit() + self.__clearToonInside() + + def __handleBootMessage(self, avId): + self.notify.warning('avatar:' + str(avId) + ' got booted.') + self.__doExit() + + def __doExit(self): + """ + Take the avatar out of the cannon because he's been in it + too long without firing. + """ + self.notify.debug("__doExit sending PartyGlobals.CANNON_MOVIE_FORCE_EXIT to %d" % self.toonInsideAvId) + self.d_setMovie(PartyGlobals.CANNON_MOVIE_FORCE_EXIT, self.toonInsideAvId) + self.__clearToonInside() + + def forceInsideToonToExit(self): + """just call __doExit.""" + self.__doExit() diff --git a/toontown/parties/DistributedPartyCannonActivityAI.py b/toontown/parties/DistributedPartyCannonActivityAI.py index 636fce3..f05db52 100644 --- a/toontown/parties/DistributedPartyCannonActivityAI.py +++ b/toontown/parties/DistributedPartyCannonActivityAI.py @@ -1,5 +1,215 @@ -from direct.directnotify import DirectNotifyGlobal -from direct.distributed.DistributedObjectAI import DistributedObjectAI +# From Anesidora +#------------------------------------------------------------------------------- +# Contact: Edmundo Ruiz (Schell Games) +# Created: Sep 2008 +# +# Purpose: DistributedPartyCannonActivityAI handles the creation of AI Dist. Cannons +# as well as keeping track of which toons are currently flying, including +# cleanup of those toons. It also listens for when a DCannonAI is lit, +# so that it can be set to fire on the client side. +#------------------------------------------------------------------------------- -class DistributedPartyCannonActivityAI(DistributedObjectAI): - notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyCannonActivityAI') +from toontown.toonbase import TTLocalizer +from toontown.toonbase import ToontownGlobals +from toontown.parties.DistributedPartyActivityAI import DistributedPartyActivityAI +from toontown.parties.DistributedPartyCannonAI import DistributedPartyCannonAI +from toontown.parties import PartyGlobals + +class DistributedPartyCannonActivityAI(DistributedPartyActivityAI): + notify = directNotify.newCategory("DistributedPartyCannonActivityAI") + #notify.setDebug(True) + + def __init__(self, air, partyDoId, x, y, h): + DistributedPartyActivityAI.__init__(self, air, partyDoId, x, y, h, PartyGlobals.ActivityIds.PartyCannon, PartyGlobals.ActivityTypes.Continuous) + + # map of cannons by cannon doId + self.cannons = {} + # map of flying toon doIds to firing cannons doIds + self.flyingToons = {} + self.flyingToonCloudsHit = {} + self.toonIdsToJellybeanRewards = {} + + # Map of cloudNumber to rgb info + self.cloudColors = {} + + def announceGenerate(self): + DistributedPartyActivityAI.announceGenerate(self) + self.__initCannons() + + def __initCannons(self): + """ + Initialize Cannon AI instances + """ + # create the first cannon. Subsequent cannons will be created through + # spawnCannonAt called from the partyAI itself. + self.spawnCannonAt(self.x, self.y, self.h) + + # Listen for when a party cannon has been lit + self.accept(DistributedPartyCannonAI.CANNON_LIT_EVENT, self.__handleFireCannon) + + + def spawnCannonAt(self, x, y, h): + cannon = DistributedPartyCannonAI(self.air, self.doId, x, y, 0, h, 0, 0) + cannon.generateWithRequired(self.zoneId) + self.cannons[cannon.doId] = cannon + + def delete(self): + self.ignoreAll() + for cannon in list(self.cannons.values()): + cannon.requestDelete() + self.cannons.clear() + self.flyingToons.clear() + self.flyingToonCloudsHit.clear() + self.toonIdsToJellybeanRewards.clear() + DistributedPartyActivityAI.delete(self) + + def __handleFireCannon(self, cannonId, timeEnteredCannon): + """ + Event handler triggered when a cannon is "lit" + Sets cannon to fire + """ + # Confirm that cannon is lit, otherwise ignore and report suspicious behavior + if cannonId in self.cannons and self.cannons[cannonId].isReadyToFire(): + cannon = self.cannons[cannonId] + toonId = cannon.getToonInsideId() + + if toonId and toonId not in self.flyingToons: + self.flyingToons[toonId] = cannon.doId + self.flyingToonCloudsHit[toonId] = 0 + self.toonIdsToJellybeanRewards[toonId] = 0 + self._addToon(toonId) + # we override toonId2Join times and start it from the time he entered the cannon + self.notify.debug("changing join time from %s to %s" % (self.toonId2joinTime[toonId], timeEnteredCannon)) + self.toonId2joinTime[toonId] = timeEnteredCannon + else: + self.notify.warning("Trying to fire toon %s who is already flying." % toonId) + toonId = cannon.getToonInsideId() + if toonId: + self.notify.warning("toon is still inside cannon, forcing him out") + cannon.forceInsideToonToExit() + return + + self.d_setCannonWillFire(cannonId, cannon.rotation, cannon.angle) + else: + if cannonId in self.cannons: + self.notify.warning("__handleFireCannon failed self.cannons[%d].isReadyToFire() = False" % cannonId) + cannon = self.cannons[cannonId] + toonId = cannon.getToonInsideId() + if toonId: + self.notify.warning("Cannon is not lit, forcing him out") + cannon.forceInsideToonToExit() + else: + self.notify.warning("__handleFireCannon failed self.cannons.has_key(%d) = False" % cannonId) + + # TODO: Write case for suspicious cannon lit call + + def _handleUnexpectedToonExit(self, toonId): + """ + Flying toon client exits, request cleanup. + """ + if toonId in self.flyingToons: + self.notify.warning("Avatar %s has exited unexpectedly." % toonId) + + # cannon_movie_force_exit will clean up the avatar on the client + # and eventually call setLanded to complete the cleanup on AI + self.d_setMovie(PartyGlobals.CANNON_MOVIE_FORCE_EXIT, toonId) + self.__cleanupFlyingToon(toonId) + DistributedPartyActivityAI._handleUnexpectedToonExit(self, toonId) + + def __cleanupFlyingToon(self, toonId): + if toonId in self.flyingToons: + self.ignore(self.air.getAvatarExitEvent(toonId)) + del self.flyingToons[toonId] + del self.flyingToonCloudsHit[toonId] + self._removeToon(toonId) + if toonId in self.toonIdsToJellybeanRewards: + del self.toonIdsToJellybeanRewards[toonId] + +#=============================================================================== +# Attributes +#=============================================================================== + + # Distributed(clsend airecv) + def cloudsColorRequest(self): + self.notify.debug("cloudsColorRequest") + senderId = self.air.getAvatarIdFromSender() + cloudColorList = [] + for key, value in list(self.cloudColors.items()): + cloudColorList.append([key, value[0], value[1], value[2]]) + + self.d_cloudsColorResponse(senderId, cloudColorList) + + def d_cloudsColorResponse(self, avId, cloudColorList): + self.notify.debug("cloudsColorResponse %s" % cloudColorList) + self.sendUpdateToAvatarId(avId, "cloudsColorResponse", [cloudColorList]) + + # Distributed (airecv clsend) + def requestCloudHit(self, cloudNumber, r, g, b): + self.notify.debug("requestCloudHit %d (%d, %d, %d)" % (cloudNumber, r, g, b)) + senderId = self.air.getAvatarIdFromSender() + + if senderId in self.flyingToonCloudsHit: + self.flyingToonCloudsHit[senderId] += 1 + addedJellyBeans = PartyGlobals.CannonJellyBeanReward + if self.air.holidayManager.isHolidayRunning(ToontownGlobals.JELLYBEAN_DAY): + addedJellyBeans *= 2 + self.toonIdsToJellybeanRewards[senderId] += addedJellyBeans + if self.toonIdsToJellybeanRewards[senderId] > PartyGlobals.CannonMaxTotalReward: + # put a cap so we don't go beyond uint8 + self.toonIdsToJellybeanRewards[senderId] = PartyGlobals.CannonMaxTotalReward + + + self.cloudColors[cloudNumber] = (r, g, b) + self.d_setCloudHit(cloudNumber, r, g, b) + + # Distributed (broadcast) + def d_setCloudHit(self, cloudNumber, r, g, b): + self.sendUpdate('setCloudHit', [cloudNumber, r, g, b]) + + # Distributed (broadcast ram) + def d_setMovie(self, mode, toonId): + """ + Broadcasts movie (state) of activity to client + """ + self.sendUpdate("setMovie", [mode, toonId]) + + # Distributed (broadcast) + def d_setCannonWillFire(self, cannonId, zRot, angle): + """ + Broadcasts that a cannon is ready to fire a toon + """ + self.sendUpdate("setCannonWillFire", [cannonId, zRot, angle]) + + # Distributed (clsend airecv) + def setLanded(self, toonId): + """ + From the client, a toon has landed. Cleanup the toon and inform all clients. + """ + self.notify.debug("%s setLanded %s" % (self.doId, toonId)) + if toonId in self.flyingToons: + + cloudsHit = self.flyingToonCloudsHit[toonId] + if cloudsHit: + jellybeansWon = self.toonIdsToJellybeanRewards[toonId] + resultsMessage = TTLocalizer.PartyCannonResults % (jellybeansWon, cloudsHit) + self.sendUpdateToAvatarId( + toonId, + "showJellybeanReward", + [jellybeansWon, self.air.doId2do[toonId].getMoney(), resultsMessage] + ) + self.issueJellybeanRewardToToonId(toonId) + + self.d_setMovie(PartyGlobals.CANNON_MOVIE_LANDED, toonId) + self.__cleanupFlyingToon(toonId) + + def isInActivity(self, avId): + """Return true if the avId is flying or inside a cannon.""" + result = False + if avId in self.flyingToons: + result = True + else: + for cannon in list(self.cannons.values()): + if cannon.getToonInsideId() == avId: + result = True + break; + return result \ No newline at end of file diff --git a/toontown/parties/DistributedPartyCatchActivityAI.py b/toontown/parties/DistributedPartyCatchActivityAI.py index 6f39ce3..ae25380 100644 --- a/toontown/parties/DistributedPartyCatchActivityAI.py +++ b/toontown/parties/DistributedPartyCatchActivityAI.py @@ -1,5 +1,355 @@ +#------------------------------------------------------------------------------- +# Contact: Shawn Patton +# Created: Sep 2008 +# +# Purpose: The Party Catch Activity is like the trolley catch activity, only +# party-wide. Toons not in an activity when the lever is pulled are +# put in this activity and anyone can join in/leave. Fruit falls +# beneath the party tree. +#------------------------------------------------------------------------------- +from direct.fsm import ClassicFSM, State +from direct.fsm import State from direct.directnotify import DirectNotifyGlobal -from direct.distributed.DistributedObjectAI import DistributedObjectAI +from direct.distributed.ClockDelta import globalClockDelta -class DistributedPartyCatchActivityAI(DistributedObjectAI): - notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyCatchActivityAI') +from toontown.toonbase import TTLocalizer +from toontown.toonbase import ToontownGlobals +from toontown.parties import PartyGlobals +from toontown.ai.ToonBarrier import ToonBarrier +from toontown.parties.DistributedPartyActivityAI import DistributedPartyActivityAI +from toontown.parties.DistributedPartyCatchActivityBase import DistributedPartyCatchActivityBase +from toontown.parties.activityFSMs import CatchActivityFSM + +class DistributedPartyCatchActivityAI(DistributedPartyActivityAI, DistributedPartyCatchActivityBase): + # Notify category for Distributed Party Activitys + notify = DirectNotifyGlobal.directNotify.newCategory("DistributedPartyCatchActivityAI") + + class Generation: + def __init__(self, generation, startTime, numPlayers): + self.generation = generation + self.startTime = startTime + self.numPlayers = numPlayers + self.caughtList = [0,] * 20 + + def __init__(self, air, partyDoId, x, y, h): + DistributedPartyActivityAI.__init__(self, air, partyDoId, x, y, h, PartyGlobals.ActivityIds.PartyCatch, PartyGlobals.ActivityTypes.HostInitiated) + self.toonIdsToScores = {} + # create state machine and set initial state + self.activityFSM = CatchActivityFSM(self) + + def getStartTimestamp(self): + return 0 + + def getGenerations(self): + return [] + + def generate(self): + DistributedPartyActivityAI.generate(self) + self._partyEndedEvent = self.air.partyManager.getPartyEndedEvent(self.party.partyInfo.hostId) + self.accept(self._partyEndedEvent, self._handlePartyEnded) + self.conclusionCountdownTask = self.uniqueName("PartyCatchConclusionCountdownTask") + self.conclusionFinishTask = self.uniqueName("PartyCatchConclusionFinishTask") + self.activityFSM.request("Idle") + # game is always running + self.activityFSM.request("Active") + + def delete(self): + DistributedPartyCatchActivityAI.notify.debug("delete") + taskMgr.remove(self.conclusionCountdownTask) + taskMgr.remove(self.conclusionFinishTask) + del self.activityFSM + self.ignore(self._partyEndedEvent) + DistributedPartyActivityAI.delete(self) + + def _handlePartyEnded(self): + # since this activity lasts for the entire party, end the activity when the party ends + # (don't wait until players get kicked out of the party) + # make sure everyone gets their reward + for toonId, reward in list(self.toonIdsToScores.items()): + reward = self.toonIdsToScores[toonId] + if reward > PartyGlobals.CatchMaxTotalReward: + # put a cap so we don't go beyond something ridiculous + reward = PartyGlobals.CatchMaxTotalReward + self.toonIdsToJellybeanRewards[toonId] = reward + self.sendUpdateToAvatarId(toonId, "showJellybeanReward", [ + reward, self.air.doId2do[toonId].getMoney(), + TTLocalizer.PartyCatchRewardMessage %(self.toonIdsToScores[toonId],reward)]) + self.toonIdsToJellybeanRewards[toonId] = reward + self.issueJellybeanRewardToToonId(toonId) + self.removeAllToons() + self.requestState('Conclusion') + + def _setUpNextGenScheduleTask(self, startT, delay=None): + if delay is None: + delay = 0. + nextGen = self._generation + 1 + task = taskMgr.doMethodLater( + delay, Functor(self._scheduleNextGeneration, startT), + 'schedNextGen-%s-%s' % (self.doId, nextGen)) + # cancel any pending generations that start after this one + for gen, item in list(self._schedTasks.items()): + if item[0] > self._lastGenerationStartTime: + taskMgr.remove(item[1]) + del self._schedTasks[gen] + self._schedTasks[nextGen] = (startT, task) + + def getNumPlayers(self): + return len(self._playerIds) + + def _scheduleNextGeneration(self, startT, task=None): + # prune out generations that are expired + tCutoff = (globalClock.getFrameTime() - self.activityStartTime) - self.generationDuration + # add a minute of wiggle room for laggy client connections + tCutoff -= 60. + genIndices = list(self._id2gen.keys()) + genIndices.sort() + for genIndex in genIndices: + timestamp = self._id2gen[genIndex].startTime + if timestamp <= tCutoff: + del self._id2gen[genIndex] + + self.calcDifficultyConstants(self.getNumPlayers()) + + # try to avoid exploits and griefing by timing this generation relative to the + # previous generation + timeDelta = startT - self._lastGenerationStartTime + quantizedTimeDelta = int(timeDelta / self.DropPeriod) * self.DropPeriod + if quantizedTimeDelta < timeDelta: + quantizedTimeDelta += self.DropPeriod + if __dev__: + assert quantizedTimeDelta >= timeDelta + quantizedStartT = self._lastGenerationStartTime + quantizedTimeDelta + + self._generation += 1 + curGenStartTime = quantizedStartT + + self._id2gen[self._generation] = self.Generation(self._generation, curGenStartTime, self.getNumPlayers()) + self._lastGenerationStartTime = curGenStartTime + + # create the data list for the network + generations = [] + genIndices = list(self._id2gen.keys()) + genIndices.sort() + for genIndex in genIndices: + timestamp = self._id2gen[genIndex].startTime + self.activityStartTime + numPlayers = self._id2gen[genIndex].numPlayers + generations.append([genIndex, globalClockDelta.localToNetworkTime(timestamp, bits=32), numPlayers]) + + self.sendUpdate('setGenerations', [generations]) + + nextStartT = curGenStartTime + self.generationDuration + # sync up the generations relative to the game start time, to preserve the + # drop frequency interest curve + nextStartT = (nextStartT / PartyGlobals.CatchActivityDuration) * PartyGlobals.CatchActivityDuration + delay = ((nextStartT + self.activityStartTime) - globalClock.getRealTime()) * .5 + delay = max(delay, 0.) + self._genSchedTask = self._setUpNextGenScheduleTask(nextStartT, delay) + +## def requestActivityStart(self): +## """ +## Called by the client when the host pulls the lever to start this game. +## """ +## DistributedPartyCatchActivityAI.notify.debug("requestActivityStart") +## senderId = self.air.getAvatarIdFromSender() +## if self.activityFSM.state == "Idle": +## self.activityFSM.request("Active") +## self.sendUpdateToAvatarId(senderId, "startRequestResponse", [int(True)]) +## else: +## self.sendUpdateToAvatarId(senderId, "startRequestResponse", [int(False)]) + + # Distributed (clsend airecv) + def toonJoinRequest(self): + DistributedPartyCatchActivityAI.notify.debug("toonJoinRequest") + senderId = self.air.getAvatarIdFromSender() + inActivity = self.party.isInActivity(senderId) + if inActivity: + # AI side don't let a toon join multiple activities + self.sendToonJoinResponse(senderId, joined=False) + return + self._playerIds.add(senderId) + if self.activityFSM.state == "Idle": + if senderId != self.party.partyInfo.hostId: + self.air.writeServerEvent('suspicious', senderId, 'non-host trying to start party catch') + return + self.sendToonJoinResponse(senderId, joined=True) + self.activityFSM.request("Active") + elif self.activityFSM.state == "Active": + # TODO: check against maximum number of players limit + self.sendToonJoinResponse(senderId, joined=True) + + def toonExitDemand(self): + DistributedPartyCatchActivityAI.notify.debug("toonJoinRequest") + toonId = self.air.getAvatarIdFromSender() + # make sure this toon is currently playing + if toonId not in self.toonIds: + return + if toonId in self.toonIdsToScores: + reward = self.toonIdsToScores[toonId] + + # if it's jelly bean day give us more jelly beans! + if self.air.holidayManager.isHolidayRunning(ToontownGlobals.JELLYBEAN_DAY): + reward *= PartyGlobals.JellyBeanDayMultiplier + + if reward > PartyGlobals.CatchMaxTotalReward: + # put a cap so we don't go beyond something ridiculous + reward = PartyGlobals.CatchMaxTotalReward + self.toonIdsToJellybeanRewards[toonId] = reward + self.sendUpdateToAvatarId(toonId, "showJellybeanReward", [ + reward, self.air.doId2do[toonId].getMoney(), + TTLocalizer.PartyCatchRewardMessage %(self.toonIdsToScores[toonId],reward)]) + del self.toonIdsToScores[toonId] + self.toonIdsToJellybeanRewards[toonId] = reward + self.issueJellybeanRewardToToonId(toonId) + DistributedPartyActivityAI.toonExitDemand(self) + if toonId in self._playerIds: + self._playerIds.remove(toonId) + # number of players changed, start a new generation of drops + self._setUpNextGenScheduleTask(globalClock.getRealTime() - self.activityStartTime) + + def sendToonJoinResponse(self, toonId, joined): + # since toons can join mid-activity, make sure to add to scores dictionary if needed + if joined: + if toonId not in self.toonIdsToScores: + self.toonIdsToScores[toonId] = 0 + DistributedPartyActivityAI.sendToonJoinResponse(self, toonId, joined) + # number of players changed, start a new generation of drops + self._setUpNextGenScheduleTask(globalClock.getRealTime() - self.activityStartTime) + + def _handleUnexpectedToonExit(self, toonId): + """ + An avatar bailed out because he lost his connection or quit + unexpectedly. + """ + DistributedPartyCatchActivityAI.notify.debug("_handleUnexpectedToonExit( toonId=%s )" % toonId) + DistributedPartyActivityAI._handleUnexpectedToonExit(self, toonId) + if toonId in self._playerIds: + self._playerIds.remove(toonId) + if toonId in self.toonIdsToScores: + del self.toonIdsToScores[toonId] + # number of players changed, start a new generation of drops + self._setUpNextGenScheduleTask(globalClock.getRealTime() - self.activityStartTime) + + # Distributed (clsend airecv) + def claimCatch(self, generation, objNum, DropObjTypeId): + if self.activityFSM.state != 'Active': + return + + # range check DropObjTypeId + if DropObjTypeId < 0 or DropObjTypeId >= len(PartyGlobals.DOTypeId2Name): + self.air.writeServerEvent('warning', DropObjTypeId, 'PartyCatchActivityAI.claimCatch DropObjTypeId out of range') + return + + gen = self._id2gen.get(generation) + if gen is None: + # generation doesn't exist yet (?) or has been deprecated + self.air.writeServerEvent('warning', generation, 'PartyCatchActivityAI.claimCatch generation is too old or doesn\'t exist yet') + return + + # sanity check; don't allow hackers to allocate unlimited memory + if objNum < 0 or objNum > 5000 or objNum >= 2*len(gen.caughtList): + # DistributedPartyCatchActivityAI.notify.debug('object num %s is too high. ignoring' % objNum) + self.air.writeServerEvent('warning', objNum, 'PartyCatchActivityAI.claimCatch objNum is too high or negative') + return + + # double the size of the caught table as needed + if objNum >= len(gen.caughtList): + gen.caughtList += [0] * len(gen.caughtList) + + # if nobody's caught this object yet, announce that it's been caught + if not gen.caughtList[objNum]: + avId = self.air.getAvatarIdFromSender() + # make sure this toon is still playing + if avId not in self._playerIds: + return + gen.caughtList[objNum] = 1 + self.sendUpdate('setObjectCaught', [avId, generation, objNum]) + # if it's a good obj, update the score + objName = PartyGlobals.DOTypeId2Name[DropObjTypeId] + DistributedPartyCatchActivityAI.notify.debug('avatar %s caught object %s: %s' % + (avId, objNum, objName)) + if PartyGlobals.Name2DropObjectType[objName].good: + self.toonIdsToScores[avId] += 1 + self.fruitsCaught += 1 + +# def reportDone(self): +# if self.activityFSM.state != 'Active': +# return +# avId = self.air.getAvatarIdFromSender() +# # all of the objects on this avatar's client have landed +# # or been caught +# DistributedPartyCatchActivityAI.notify.debug('reportDone: avatar %s is done' % avId) + + # FSM transition methods + def startIdle(self): + DistributedPartyCatchActivityAI.notify.debug("startIdle") + self.sendUpdate( + "setState", + [ + "Idle", # new state + 0, + ] + ) + + def finishIdle(self): + DistributedPartyCatchActivityAI.notify.debug("finishIdle") + + def startActive(self): + DistributedPartyCatchActivityAI.notify.debug("startActive") + self.activityStartTime = globalClock.getRealTime() + self.sendUpdate('setStartTimestamp', [ + globalClockDelta.localToNetworkTime(self.activityStartTime, bits=32),]) + self.sendUpdate( + "setState", + [ + "Active", # new state + globalClockDelta.localToNetworkTime(self.activityStartTime), + ] + ) + # populate the scores dictionary + self.toonIdsToScores.clear() + for toonId in self.toonIds: + self.toonIdsToScores[toonId] = 0 + # and keep track of how many are caught + self.fruitsCaught = 0 + # game is always running + #taskMgr.doMethodLater(PartyGlobals.CatchActivityDuration, self.requestState, self.conclusionCountdownTask, ["Conclusion"]) + self._playerIds = set() + # defines self.generationDuration + self.calcDifficultyConstants(self.getNumPlayers()) + self._id2gen = {} + self._generation = -1 + self._lastGenerationStartTime = -self.generationDuration + self._schedTasks = {} + self._setUpNextGenScheduleTask(0.) + + def requestState(self, state): + DistributedPartyCatchActivityAI.notify.debug("requestState : %s" % state) + self.activityFSM.request(state) + + def finishActive(self): + DistributedPartyCatchActivityAI.notify.debug("finishActive") + + def startConclusion(self): + DistributedPartyCatchActivityAI.notify.debug("startIdle") + self.sendUpdate( + "setState", + [ + "Conclusion", # new state + 0, + ] + ) + + def finishConclusion(self): + DistributedPartyCatchActivityAI.notify.debug("finishIdle") + + def isInActivity(self, toonId): + """Return true if the toon is doing catch.""" + result = False + if toonId in self._playerIds: + result = True + if result: + # lets do a validation check here + # they should be in sync + if not toonId in self.toonIdsToScores: + self.notify.warning("toon %d is in playerIds but not in toonIdsToScores" % toonId) + return result \ No newline at end of file diff --git a/toontown/parties/DistributedPartyCogActivityAI.py b/toontown/parties/DistributedPartyCogActivityAI.py index 0328320..26a7dcf 100644 --- a/toontown/parties/DistributedPartyCogActivityAI.py +++ b/toontown/parties/DistributedPartyCogActivityAI.py @@ -1,5 +1,223 @@ -from direct.directnotify import DirectNotifyGlobal -from direct.distributed.DistributedObjectAI import DistributedObjectAI +#=============================================================================== +# Contact: Edmundo Ruiz (Schell Games) +# Created: September 2009 +# +# Purpose: Distributed controller for Party Cog "Pinata" Activity in the AI +#=============================================================================== +from direct.showbase.PythonUtil import bound as clamp -class DistributedPartyCogActivityAI(DistributedObjectAI): - notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyCogActivityAI') +from toontown.toonbase import TTLocalizer + +from .DistributedPartyTeamActivityAI import DistributedPartyTeamActivityAI +from . import PartyGlobals +from . import PartyCogUtils + +class DistributedPartyCogActivityAI(DistributedPartyTeamActivityAI): + notify = directNotify.newCategory("DistributedPartyCogActivityAI") + + cogDistances = [0, 0, 0] # [cog0, cog1, cog2] goes from -1.0 to 1.0 + score = [0, 0] # [leftTeam, rightTeam] + toonScore = {} + + highScore = ("", 0) # toonName (in case the toon disconnects), score + + def __init__(self, air, doId, x, y ,h): + DistributedPartyTeamActivityAI.__init__( + self, air, doId, x, y, h, + PartyGlobals.ActivityIds.PartyCog, + minPlayersPerTeam=PartyGlobals.CogActivityMinPlayersPerTeam, + maxPlayersPerTeam=PartyGlobals.CogActivityMaxPlayersPerTeam, + duration=PartyGlobals.CogActivityDuration, + conclusionDuration=PartyGlobals.CogActivityConclusionDuration, + startDelay=PartyGlobals.CogActivityStartDelay, + balanceTeams=True, + calcAdvantage=True, + canSwitchTeams=True, + ) + + self.cogHeadStartZ = PartyGlobals.CogPinataHeadZ + +#=============================================================================== +# Distributed +#=============================================================================== + + # broadcast clsend airecv + def pieHitsCog(self, toonId, timestamp, hitCogNum, x, y, z, direction, hitHead): + # Count it only if it's the active state. It may be a very delayed hit, sadly. + if not self.isState("Active"): + return + + assert(self.notify.debugStateCall(self)) + + if hitHead: + if z < self.cogHeadStartZ: + senderId = self.air.getAvatarIdFromSender() + self.notify.debug("pieHitsCog suspicious behavior for toon: %s, z: %d < %d" % (toonId, z, self.cogHeadStartZ)) + self.air.writeServerEvent( + "suspicious", + senderId, + "Toon %s hits cog head in PartyCogActivity, but the head z %d is less than the expected min z %d." % + (toonId, z, self.cogHeadStartZ) + ) + + points = PartyGlobals.CogActivityHitPointsForHead + else: + points = PartyGlobals.CogActivityHitPoints + + self._addToToonScore(toonId, points) + + advantage = self.advantage[self.getTeam(toonId)] + + self._updateCogDistance(hitCogNum, direction, advantage, hitHead) + self.d_broadcastSetCogDistances() + + def d_broadcastSetCogDistances(self): + self.sendUpdate("setCogDistances", [self.cogDistances]) + + def getHighScore(self): + return self.highScore + + # broadcast ram + def d_broadcastSetHighScore(self): + self.sendUpdate("setHighScore", list(self.highScore)) + +#=============================================================================== +# Actions +#=============================================================================== + + def _updateCogDistance(self, cogNum, direction, advantage, hitHead): + if hitHead: + factor = PartyGlobals.CogPinataPushHeadFactor + else: + factor = PartyGlobals.CogPinataPushBodyFactor + + distance = self.cogDistances[cogNum] + (factor * direction * advantage) + + self.cogDistances[cogNum] = clamp(distance, -1.0, 1.0) + + def _resetCogDistances(self): + self.cogDistances = [0, 0, 0] + + def _resetScores(self): + self.toonScore.clear() + self.score = [0, 0] + + def _addToToonScore(self, toonId, points): + if toonId in self.toonScore: + self.toonScore[toonId] += points + + def _addToonToTeam(self, toonId, team): + if DistributedPartyTeamActivityAI._addToonToTeam(self, toonId, team): + self.toonScore[toonId] = 0 + + def _removeToonFromTeam(self, toonId, team): + if DistributedPartyTeamActivityAI._removeToonFromTeam(self, toonId, team) and \ + toonId in self.toonScore: + del self.toonScore[toonId] + + def _findNewHighScore(self): + """Check to see if a new high score has been made, and broadcast""" + highScoreFound = False + + for toonId, score in list(self.toonScore.items()): + if score > self.highScore[1] and toonId in self.air.doId2do: + self.highScore = (self.air.doId2do[toonId].getName(), score) + highScoreFound = True + + if highScoreFound: + self.d_broadcastSetHighScore() + +#=============================================================================== +# Utility methods +#=============================================================================== + + def computeMatchResults(self): + """Determines who was the winning team and rewards the teams accordingly.""" + # Determine how far the cogs were pushed for every team. + for distance in self.cogDistances: + if distance < 0.0: + self.score[PartyGlobals.TeamActivityTeams.LeftTeam] += abs(PartyCogUtils.getCogDistanceUnitsFromCenter(distance)) + elif distance > 0.0: + self.score[PartyGlobals.TeamActivityTeams.RightTeam] += abs(PartyCogUtils.getCogDistanceUnitsFromCenter(distance)) + + # Determine who won, who lost: + if self.score[PartyGlobals.TeamActivityTeams.LeftTeam] < self.score[PartyGlobals.TeamActivityTeams.RightTeam]: + self.losingTeam = PartyGlobals.TeamActivityTeams.LeftTeam + elif self.score[PartyGlobals.TeamActivityTeams.RightTeam] < self.score[PartyGlobals.TeamActivityTeams.LeftTeam]: + self.losingTeam = PartyGlobals.TeamActivityTeams.RightTeam + else: + self.losingTeam = PartyGlobals.TeamActivityNeitherTeam + self.winningTeam = PartyGlobals.TeamActivityNeitherTeam + + # Tie: + if self.resultIsTie(): + + for toonId in self.getToonIdsAsList(): + self.toonIdsToJellybeanRewards[toonId] = PartyGlobals.CogActivityTieBeans + + # One of the teams won: + else: + self.winningTeam = 1 - self.losingTeam + + winBeans = PartyGlobals.CogActivityWinBeans + lossBeans = PartyGlobals.CogActivityLossBeans + + # The winning team moved all of the cogs all the way: + if self.resultIsPerfect(): + winBeans = PartyGlobals.CogActivityPerfectWinBeans + lossBeans = PartyGlobals.CogActivityPerfectLossBeans + + for toonId in self.toonIds[self.winningTeam]: + self.toonIdsToJellybeanRewards[toonId] = winBeans + + for toonId in self.toonIds[self.losingTeam]: + self.toonIdsToJellybeanRewards[toonId] = lossBeans + + self._findNewHighScore() + + def getConclusionData(self): + """Formats the score into a single 32 bit data""" + return (int(self.score[0]) * 10000 + int(self.score[1])) + + def getJellybeanRewardMessage(self, toonId, reward): + message = None + + if self.resultIsTie(): + message = TTLocalizer.PartyTeamActivityRewardMessage % reward + elif self.toonIsOnWinningTeam(toonId): + # "Your team won!\n\nYou got %d jellybeans. Good job!" + message = TTLocalizer.PartyTeamActivityLocalAvatarTeamWins + "\n\n" +\ + TTLocalizer.PartyTeamActivityRewardMessage % reward + else: + message = TTLocalizer.PartyTeamActivityRewardMessage % reward + + return message + + def resultIsTie(self): + """ Returns True if the teams tied. """ + return self.losingTeam == PartyGlobals.TeamActivityNeitherTeam + + def resultIsPerfect(self): + """ Returns True if the winning team moved all of the cogs all the way. """ + return self.score[self.winningTeam] >= 3.0 * PartyGlobals.CogActivityArenaLength / 2.0 + + def toonIsOnWinningTeam(self, toonId): + """ Returns True if the given toon was on the winning team. """ + return toonId in self.toonIds[self.winningTeam] + +#=============================================================================== +# FSM +#=============================================================================== + + def startConclusion(self): + # All clients should first and foremost, synchronize cog distances. + self.d_broadcastSetCogDistances() + + DistributedPartyTeamActivityAI.startConclusion(self) + + def startWaitForEnough(self): + DistributedPartyTeamActivityAI.startWaitForEnough(self) + + self._resetScores() + self._resetCogDistances() + self.d_broadcastSetCogDistances() \ No newline at end of file diff --git a/toontown/parties/DistributedPartyDance20ActivityAI.py b/toontown/parties/DistributedPartyDance20ActivityAI.py index fdaf75d..453c455 100644 --- a/toontown/parties/DistributedPartyDance20ActivityAI.py +++ b/toontown/parties/DistributedPartyDance20ActivityAI.py @@ -1,5 +1,24 @@ -from direct.directnotify import DirectNotifyGlobal -from direct.distributed.DistributedObjectAI import DistributedObjectAI +#------------------------------------------------------------------------------- +# Contact: Edmundo Ruiz (Schell Games) +# Created: Oct 2008 +# +# Purpose: AI component that manages which toons are currently dancing, who entered +# and exited the dance floor, and broadcasts dance moves to all clients. +#------------------------------------------------------------------------------- -class DistributedPartyDance20ActivityAI(DistributedObjectAI): - notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyDance20ActivityAI') +from toontown.parties.DistributedPartyDanceActivityBaseAI import DistributedPartyDanceActivityBaseAI +from toontown.parties import PartyGlobals + +# 20 move dance floor +class DistributedPartyDance20ActivityAI(DistributedPartyDanceActivityBaseAI): + notify = directNotify.newCategory("DistributedPartyDanceActivityAI") + + def __init__(self, air, partyDoId, x, y, h): + self.notify.debug("Intializing.") + DistributedPartyDanceActivityBaseAI.__init__(self, + air, + partyDoId, + x, y, h, + PartyGlobals.ActivityIds.PartyDance20, + PartyGlobals.DancePatternToAnims20, + ) \ No newline at end of file diff --git a/toontown/parties/DistributedPartyDanceActivityAI.py b/toontown/parties/DistributedPartyDanceActivityAI.py index f88b8b8..2766751 100644 --- a/toontown/parties/DistributedPartyDanceActivityAI.py +++ b/toontown/parties/DistributedPartyDanceActivityAI.py @@ -1,5 +1,24 @@ -from direct.directnotify import DirectNotifyGlobal -from direct.distributed.DistributedObjectAI import DistributedObjectAI +#------------------------------------------------------------------------------- +# Contact: Edmundo Ruiz (Schell Games) +# Created: Oct 2008 +# +# Purpose: AI component that manages which toons are currently dancing, who entered +# and exited the dance floor, and broadcasts dance moves to all clients. +#------------------------------------------------------------------------------- -class DistributedPartyDanceActivityAI(DistributedObjectAI): - notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyDanceActivityAI') +from toontown.parties.DistributedPartyDanceActivityBaseAI import DistributedPartyDanceActivityBaseAI +from toontown.parties import PartyGlobals + +# original classic 10 move dance floor +class DistributedPartyDanceActivityAI(DistributedPartyDanceActivityBaseAI): + notify = directNotify.newCategory("DistributedPartyDanceActivityAI") + + def __init__(self, air, partyDoId, x, y, h): + self.notify.debug("Intializing.") + DistributedPartyDanceActivityBaseAI.__init__(self, + air, + partyDoId, + x, y, h, + PartyGlobals.ActivityIds.PartyDance, + PartyGlobals.DancePatternToAnims, + ) diff --git a/toontown/parties/DistributedPartyDanceActivityBase.py b/toontown/parties/DistributedPartyDanceActivityBase.py index c0c940d..e92b25f 100644 --- a/toontown/parties/DistributedPartyDanceActivityBase.py +++ b/toontown/parties/DistributedPartyDanceActivityBase.py @@ -326,14 +326,16 @@ class DistributedPartyDanceActivityBase(DistributedPartyActivity): def _requestToonState(self, toonId, state, anim): if toonId in self.dancingToonFSMs: - state = ToonDancingStates.getString(state) + state = ToonDancingStates(state) curState = self.dancingToonFSMs[toonId].getCurrentOrNextState() try: + state = str(state) + anim = str(anim) self.dancingToonFSMs[toonId].request(state, anim) except FSM.RequestDenied: self.notify.warning('could not go from state=%s to state %s' % (curState, state)) - if state == ToonDancingStates.getString(ToonDancingStates.Cleanup): + if state == ToonDancingStates(ToonDancingStates.Cleanup): self.notify.debug('deleting this fsm %s' % self.dancingToonFSMs[toonId]) del self.dancingToonFSMs[toonId] if self.localToonDanceSequence: diff --git a/toontown/parties/DistributedPartyDanceActivityBaseAI.py b/toontown/parties/DistributedPartyDanceActivityBaseAI.py index 654cdc1..a71f2b4 100644 --- a/toontown/parties/DistributedPartyDanceActivityBaseAI.py +++ b/toontown/parties/DistributedPartyDanceActivityBaseAI.py @@ -1,5 +1,124 @@ -from direct.directnotify import DirectNotifyGlobal -from direct.distributed.DistributedObjectAI import DistributedObjectAI +#------------------------------------------------------------------------------- +# Contact: Edmundo Ruiz (Schell Games) +# Created: Oct 2008 +# +# Purpose: AI component that manages which toons are currently dancing, who entered +# and exited the dance floor, and broadcasts dance moves to all clients. +#------------------------------------------------------------------------------- + +from toontown.parties.DistributedPartyActivityAI import DistributedPartyActivityAI +from toontown.parties import PartyGlobals + +class DistributedPartyDanceActivityBaseAI(DistributedPartyActivityAI): + notify = directNotify.newCategory("DistributedPartyDanceActivityBaseAI") + + def __init__(self, air, partyDoId, x, y, h, activityId, dancePatternToAnims): + self.notify.debug("Intializing.") + DistributedPartyActivityAI.__init__(self, + air, + partyDoId, + x, y, h, + activityId, + PartyGlobals.ActivityTypes.Continuous) + self.toonIdsToHeadings = {} # toon's heading when it joined + self.dancePatternToAnims = dancePatternToAnims + + def delete(self): + pass + + # Distributed (clsend airecv) + def toonJoinRequest(self): + """Gets from client when a toon enters the dance floor.""" + senderId = self.air.getAvatarIdFromSender() + self.notify.debug("Request enter %s" % senderId) + if senderId not in self.toonIds: + if self.party.isInActivity(senderId): + hToUse = 0 + toon = self.air.doId2do.get(senderId) + if toon: + hToUse = toon.getH() + self.sendToonJoinResponse(senderId, + joined = False, + h = hToUse) + else: + self.sendToonJoinResponse(senderId, + joined = True, + h = self.air.doId2do[senderId].getH()) + + else: + self.air.writeServerEvent( + "suspicious", + senderId, + "Party Dance Activity AI Join Request: Sender already in Party Dance Activity" + ) + self.notify.warning("toonJoinRequest() - Sender already in Activity") + + def sendToonJoinResponse(self, toonId, joined=True, h=0.0): + if joined: + self._addToon(toonId) + self.toonIdsToHeadings[toonId] = h + self.notify.debug("sending setToonsPlaying") + self.notify.debug(" toonIds: %s" %self.toonIds) + self.notify.debug(" toonHeadings: %s" %self.getHeadingList()) + self.sendUpdate("setToonsPlaying", [self.toonIds, self.getHeadingList()]) + else: + self.sendUpdateToAvatarId(toonId, "joinRequestDenied", [PartyGlobals.DenialReasons.SilentFail]) + + # Distributed (clsend airecv) + def toonExitRequest(self): + """ + Gets from client when a dancing toon enters the dance floor. + """ + senderId = self.air.getAvatarIdFromSender() + self.notify.debug("Request exit %s" % senderId) + if senderId in self.toonIds: + self.sendToonExitResponse(senderId, exited = True) + else: + # this case is possible when a toon lands on dance floor from the cannon + # joinRequest is never sent by client but exitRequest is sent + self.sendToonExitResponse(senderId, exited = False, denialReason=PartyGlobals.DenialReasons.SilentFail) + + + def sendToonExitResponse(self, toonId, exited, denialReason=PartyGlobals.DenialReasons.Default): + if exited: + self._removeToon(toonId) + del self.toonIdsToHeadings[toonId] + self.sendUpdate("setToonsPlaying", [self.toonIds, self.getHeadingList()]) + else: + self.sendUpdateToAvatarId(toonId, "exitRequestDenied", [denialReason]) + + def getHeadingList(self): + """ + Returns a list of initial headings for toons in the activity, in the + same order as self.toonIds + """ + headingList = [] + for toonId in self.toonIds: + headingList.append(self.toonIdsToHeadings[toonId]) + return headingList + + # Distributed (clsend airecv) + def updateDancingToon(self, state, anim): + """ + Gets from client when a dancing toon performs a dance move. + """ + senderId = self.air.getAvatarIdFromSender() + if anim != "" and anim not in list(self.dancePatternToAnims.values()): + # TODO: Log suspicious behavior? + return + self.d_setDancingToonState(senderId, state, anim) + self.notify.debug("Request dance move %s" % senderId) + + # Distributed (broadcast) + def d_setDancingToonState(self, toonId, state, anim): + self.sendUpdate("setDancingToonState", [toonId, state, anim]) + + def _handleUnexpectedToonExit(self, toonId): + """ + An avatar bailed out because he lost his connection or quit + unexpectedly. Overriding base class functionality because of the special + setToonsPlaying function in this class. + """ + self._removeToon(toonId) + self.sendUpdate("setToonsPlaying", [self.toonIds, self.getHeadingList()]) -class DistributedPartyDanceActivityBaseAI(DistributedObjectAI): - notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyDanceActivityBaseAI') diff --git a/toontown/parties/DistributedPartyFireworksActivity.py b/toontown/parties/DistributedPartyFireworksActivity.py index b1f672f..87a886a 100644 --- a/toontown/parties/DistributedPartyFireworksActivity.py +++ b/toontown/parties/DistributedPartyFireworksActivity.py @@ -29,7 +29,7 @@ class DistributedPartyFireworksActivity(DistributedPartyActivity, FireworkShowMi FireworkShowMixin.__init__(self, restorePlaygroundMusic=True, startDelay=FireworksPostLaunchDelay) def setEventId(self, eventId): - DistributedPartyFireworksActivity.notify.debug('setEventId( %s )' % FireworkShows.getString(eventId)) + DistributedPartyFireworksActivity.notify.debug('setEventId( %s )' % FireworkShows(eventId)) self.eventId = eventId def setShowStyle(self, showStyle): @@ -56,7 +56,7 @@ class DistributedPartyFireworksActivity(DistributedPartyActivity, FireworkShowMi self.rocketActor = Actor('phase_13/models/parties/rocket_model', {'launch': 'phase_13/models/parties/rocket_launch'}) rocketLocator = self.launchPadModel.find('**/rocket_locator') self.rocketActor.reparentTo(rocketLocator) - self.rocketActor.node().setBound(OmniBoundingVolume()) + self.rocketActor.node().setBounds(OmniBoundingVolume()) self.rocketActor.node().setFinal(True) effectsLocator = self.rocketActor.find('**/joint1') self.rocketExplosionEffect = RocketExplosion(effectsLocator, rocketLocator) diff --git a/toontown/parties/DistributedPartyFireworksActivityAI.py b/toontown/parties/DistributedPartyFireworksActivityAI.py index 9601a0c..92c7da8 100644 --- a/toontown/parties/DistributedPartyFireworksActivityAI.py +++ b/toontown/parties/DistributedPartyFireworksActivityAI.py @@ -1,5 +1,131 @@ -from direct.directnotify import DirectNotifyGlobal -from direct.distributed.DistributedObjectAI import DistributedObjectAI +#------------------------------------------------------------------------------- +# Contact: Rob Gordon +# Created: Oct 2008 +# +# Purpose: AI control of fireworks in a party. +# +#------------------------------------------------------------------------------- -class DistributedPartyFireworksActivityAI(DistributedObjectAI): - notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyFireworksActivityAI') +# Panda imports +from direct.distributed import ClockDelta +from direct.task import Task + +# Toontown imports +from toontown.effects.FireworkShow import FireworkShow + +# parties imports +from . import PartyGlobals +from .DistributedPartyActivityAI import DistributedPartyActivityAI +from .activityFSMs import FireworksActivityFSM + +class DistributedPartyFireworksActivityAI(DistributedPartyActivityAI): + + notify = directNotify.newCategory("DistributedPartyFireworksActivityAI") + + def __init__(self, air, partyDoId, x, y, h, eventId=PartyGlobals.FireworkShows.Summer, showStyle=0): + """ + air: instance of ToontownAIRepository + eventId: a PartyGlobals.FireworkShows value that tells us which show + to run + """ + DistributedPartyFireworksActivityAI.notify.debug("__init__") + DistributedPartyActivityAI.__init__( + self, + air, + partyDoId, + x, + y, + h, + PartyGlobals.ActivityIds.PartyFireworks, + PartyGlobals.ActivityTypes.HostInitiated, + ) + self.eventId = eventId + self.showStyle = showStyle + self.activityFSM = FireworksActivityFSM(self) + + def generate(self): + DistributedPartyFireworksActivityAI.notify.debug("generate") + self.activityFSM.request("Idle") + + def getEventId(self): + DistributedPartyFireworksActivityAI.notify.debug("getEventId") + return self.eventId + + def getShowStyle(self): + DistributedPartyFireworksActivityAI.notify.debug("getShowStyle") + return self.showStyle + + def toonJoinRequest(self): + """ + The supposed host is requesting the fireworks to start. + """ + # check that the host sent this message + senderId = self.air.getAvatarIdFromSender() + if senderId != self.party.partyInfo.hostId: + self.air.writeServerEvent('suspicious', senderId, 'A non-host with avId=%d tried to start a host activated activity.' % senderId) + return + self.activityFSM.request("Active") + + def showComplete(self, task): + DistributedPartyFireworksActivityAI.notify.debug("showComplete") + self.activityFSM.request("Disabled") + + return Task.done + + def delete(self): + DistributedPartyFireworksActivityAI.notify.debug("delete") + if hasattr(self, 'activityFSM'): + self.activityFSM.request('Disabled') + del self.activityFSM + DistributedPartyActivityAI.delete(self) + + # FSM transition methods + def startIdle(self): + DistributedPartyFireworksActivityAI.notify.debug("startIdle") + + def finishIdle(self): + DistributedPartyFireworksActivityAI.notify.debug("finishIdle") + + def startActive(self): + DistributedPartyFireworksActivityAI.notify.debug("startActive") + messenger.send( PartyGlobals.FireworksStartedEvent ) + showStartTimestamp = ClockDelta.globalClockDelta.getRealNetworkTime() + # put clients into this state + self.sendUpdate( + "setState", + [ + "Active", # new state + showStartTimestamp # when the show starts + ] + ) + # setup to transition to Disabled after the show is over + throwAwayShow = FireworkShow() + showDuration = throwAwayShow.getShowDuration( self.eventId) + showDuration += 20.0 + taskMgr.doMethodLater( + PartyGlobals.FireworksPostLaunchDelay + showDuration + PartyGlobals.FireworksTransitionToDisabledDelay, + self.showComplete, + self.taskName("waitForShowComplete"), + ) + del throwAwayShow + + def finishActive(self): + DistributedPartyFireworksActivityAI.notify.debug("finishActive") + messenger.send( PartyGlobals.FireworksFinishedEvent ) + # clean up doMethodLater + taskMgr.removeTasksMatching(self.taskName("waitForShowComplete")) + + def startDisabled(self): + DistributedPartyFireworksActivityAI.notify.debug("startDisabled") + # put clients into this state + self.sendUpdate( + "setState", + [ + "Disabled", # new state + 0 # dummy timestamp + ] + ) + + def finishDisabled(self): + DistributedPartyFireworksActivityAI.notify.debug("finishDisabled") + \ No newline at end of file diff --git a/toontown/parties/DistributedPartyJukebox40ActivityAI.py b/toontown/parties/DistributedPartyJukebox40ActivityAI.py index 7d7dec9..da92864 100644 --- a/toontown/parties/DistributedPartyJukebox40ActivityAI.py +++ b/toontown/parties/DistributedPartyJukebox40ActivityAI.py @@ -1,5 +1,25 @@ -from direct.directnotify import DirectNotifyGlobal -from direct.distributed.DistributedObjectAI import DistributedObjectAI +#------------------------------------------------------------------------------- +# Contact: Edmundo Ruiz (Schell Games) +# Created: Oct 2008 +# +# Purpose: AI component that manages which toons are currently dancing, who entered +# and exited the dance floor, and broadcasts dance moves to all clients. +#------------------------------------------------------------------------------- -class DistributedPartyJukebox40ActivityAI(DistributedObjectAI): - notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyJukebox40ActivityAI') +from toontown.parties.DistributedPartyJukeboxActivityBaseAI import DistributedPartyJukeboxActivityBaseAI + +from toontown.parties import PartyGlobals + +class DistributedPartyJukebox40ActivityAI(DistributedPartyJukeboxActivityBaseAI ): + notify = directNotify.newCategory("DistributedPartyJukeboxActivityAI") + + def __init__(self, air, partyDoId, x, y, h): + self.notify.debug("Intializing.") + DistributedPartyJukeboxActivityBaseAI.__init__(self, + air, + partyDoId, + x, y, h, + PartyGlobals.ActivityIds.PartyJukebox40, + PartyGlobals.PhaseToMusicData40, + ) + diff --git a/toontown/parties/DistributedPartyJukeboxActivityAI.py b/toontown/parties/DistributedPartyJukeboxActivityAI.py index 309a546..7ec0fb7 100644 --- a/toontown/parties/DistributedPartyJukeboxActivityAI.py +++ b/toontown/parties/DistributedPartyJukeboxActivityAI.py @@ -1,5 +1,25 @@ -from direct.directnotify import DirectNotifyGlobal -from direct.distributed.DistributedObjectAI import DistributedObjectAI +#------------------------------------------------------------------------------- +# Contact: Edmundo Ruiz (Schell Games) +# Created: Oct 2008 +# +# Purpose: AI component that manages which toons are currently dancing, who entered +# and exited the dance floor, and broadcasts dance moves to all clients. +#------------------------------------------------------------------------------- -class DistributedPartyJukeboxActivityAI(DistributedObjectAI): - notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyJukeboxActivityAI') +from toontown.parties.DistributedPartyJukeboxActivityBaseAI import DistributedPartyJukeboxActivityBaseAI + +from toontown.parties import PartyGlobals + +class DistributedPartyJukeboxActivityAI(DistributedPartyJukeboxActivityBaseAI ): + notify = directNotify.newCategory("DistributedPartyJukeboxActivityAI") + + def __init__(self, air, partyDoId, x, y, h): + self.notify.debug("Intializing.") + DistributedPartyJukeboxActivityBaseAI.__init__(self, + air, + partyDoId, + x, y, h, + PartyGlobals.ActivityIds.PartyJukebox, + PartyGlobals.PhaseToMusicData, + ) + diff --git a/toontown/parties/DistributedPartyJukeboxActivityBaseAI.py b/toontown/parties/DistributedPartyJukeboxActivityBaseAI.py index 132b59d..8d10e2c 100644 --- a/toontown/parties/DistributedPartyJukeboxActivityBaseAI.py +++ b/toontown/parties/DistributedPartyJukeboxActivityBaseAI.py @@ -1,5 +1,285 @@ -from direct.directnotify import DirectNotifyGlobal -from direct.distributed.DistributedObjectAI import DistributedObjectAI +#------------------------------------------------------------------------------- +# Contact: Edmundo Ruiz (Schell Games) +# Created: Oct 2008 +# +# Purpose: AI component that manages which toons are currently dancing, who entered +# and exited the dance floor, and broadcasts dance moves to all clients. +#------------------------------------------------------------------------------- +import random +from direct.task.Task import Task + +from toontown.parties.DistributedPartyActivityAI import DistributedPartyActivityAI +from toontown.parties import PartyGlobals +from random import randint + +class DistributedPartyJukeboxActivityBaseAI(DistributedPartyActivityAI): + notify = directNotify.newCategory("DistributedPartyJukeboxActivityAI") + + def __init__(self, air, partyDoId, x, y, h, actId, phaseToMusicData): + self.notify.debug("Intializing.") + DistributedPartyActivityAI.__init__(self, + air, + partyDoId, + x, y, h, + actId, + PartyGlobals.ActivityTypes.Continuous + ) + + # Holds the list of songs requested by each avatar. + # Format: { toonId : (phase, filename), ... } + self.phaseToMusicData = phaseToMusicData + self.toonIdToSongData = {} + # Holds the queue of songs to be played by avatarId + self.toonIdQueue = [] + + self.songPlaying = False + self.songTimerTask = None + + self.timeoutTask = None + + # listen for fireworks show starting and stopping + self.accept( PartyGlobals.FireworksStartedEvent, self.__handleFireworksStarted ) + self.accept( PartyGlobals.FireworksFinishedEvent, self.__handleFireworksFinished ) + + # this ensure the first song we hear is one of the new ones + self.randomSongPhase = 13 + + def delete(self): + self.ignoreAll() + self.__stopTimeout() + DistributedPartyActivityAI.delete(self) + + def generate(self): + DistributedPartyActivityAI.generate(self) + self.__playNextSong() + + # Distributed (clsend airecv) + def toonJoinRequest(self): + """ + Gets from client when a toon requests to use the jukebox. + only one toon can use the jukebox. + """ + senderId = self.air.getAvatarIdFromSender() + self.notify.debug("Request enter %s" % senderId) + if senderId not in self.toonIds: + joined = (len(self.toonIds) == 0) + if self.party.isInActivity(senderId): + joined = False + self.__startTimeout(PartyGlobals.JUKEBOX_TIMEOUT) + self.sendToonJoinResponse(senderId, joined) + #TODO: Add suspicious behavior case? + + # Distributed (clsend airecv) + def toonExitRequest(self): + """ + Sent from client when toon using the jukebox wishes to stop using the + jukebox. + """ + senderId = self.air.getAvatarIdFromSender() + self.notify.debug("Request exit %s" % senderId) + if senderId in self.toonIds: + self.sendToonExitResponse(senderId, True) + else: + self.sendToonExitResponse(senderId, False) + + def toonExitResponse(self, senderId, exited): + if exited: + self.__stopTimeout() + DistributedPartyActivityAI.sendToonExitResponse(self, senderId, exited) + +#=============================================================================== +# Song addition, playing, etc. +#=============================================================================== + + # Distributed (clsend airecv) + def queuedSongsRequest(self): + senderId = self.air.getAvatarIdFromSender() + + songInfoList = [] + index = -1 + for i in range(len(self.toonIdQueue)): + toonId = self.toonIdQueue[i] + if toonId == senderId: + index = i + songInfoList.append(self.toonIdToSongData[toonId]) + self.d_queuedSongsResponse(senderId, songInfoList, index) + + # Distributed + def d_queuedSongsResponse(self, senderId, songInfoList, index): + self.sendUpdateToAvatarId(senderId, + "queuedSongsResponse", + [songInfoList, index]) + + # Distributed (clsend airecv) + def setNextSong(self, nextSongInfo): + """ + Called by client when adding or replacing a song in the queue. + """ + phase = PartyGlobals.sanitizePhase(nextSongInfo[0]) + filename = nextSongInfo[1] + self.notify.debug("setNextSong %d/%s" % (phase, filename)) + + senderId = self.air.getAvatarIdFromSender() + data = self.getMusicData(phase, filename) + if data: + self.toonIdToSongData[senderId] = (phase, filename) + if senderId not in self.toonIdQueue: + self.toonIdQueue.append(senderId) + self.d_setSongInQueue(senderId, nextSongInfo) + + # Distributed (required broadcast ram) + def d_setSongPlaying(self, phase, filename, toonId=0): + self.sendUpdate("setSongPlaying", [(phase, filename), toonId]) + + # Distributed + def d_setSongInQueue(self, toonId, songInfo): + self.sendUpdateToAvatarId(toonId, "setSongInQueue", [songInfo]) + +#=============================================================================== +# Host Toon only +#=============================================================================== + + # Distributed (clsend airecv) + def moveHostSongToTopRequest(self): + self.notify.debug("moveHostSongToTopRequest") + senderId = self.air.getAvatarIdFromSender() + # TODO: Check to see if senderId is in fact party host + if senderId in self.toonIdQueue: + self.b_moveHostSongToTop(senderId) + + def moveHostSongToTop(self, hostId): + self.toonIdQueue.remove(hostId) + self.toonIdQueue.insert(0, hostId) + + def d_moveHostSongToTop(self, hostId): + self.notify.debug("d_moveHostSongToTop") + self.sendUpdateToAvatarId(hostId, "moveHostSongToTop", []) + + def b_moveHostSongToTop(self, hostId): + self.moveHostSongToTop(hostId) + self.d_moveHostSongToTop(hostId) + +#=============================================================================== +# Song scheduling +#=============================================================================== + + def __playNextSong(self): + self.notify.debug("__playNextSong") + """ + Pops song out of the queue, broadcasts new song to all clients, + and schedules next play task. + """ + self.songPlaying = False + toonId = 0 + # Play the next song in the queue + if len(self.toonIdQueue) > 0: + toonId = self.toonIdQueue[0] + songInfo = self.toonIdToSongData.get(toonId, None) + del self.toonIdToSongData[toonId] + self.toonIdQueue.remove(toonId) + + # No song in the queue? Pick a random song! + else: + songInfo = self.getRandomMusicInfo(self.randomSongPhase) + # every other random song is one of the new party songs + self.randomSongPhase = -1 + self.notify.debug("random songInfo=%s" % str(songInfo)) + + if songInfo is not None: + phase = songInfo[0] + filename = songInfo[1] + data = self.getMusicData(phase, filename) + if data: + self.notify.debug("Playing song: %s, %s" % (phase, filename)) + self.d_setSongPlaying(phase, filename, toonId) + self.__startTimerForNextSong(self.__getTimeUntilNextSong(data[1])) + self.songPlaying = True + + def __getTimeUntilNextSong(self, duration): + return (PartyGlobals.getMusicRepeatTimes(duration) * duration) + PartyGlobals.MUSIC_GAP + + def __playNextSongTask(self, task): + self.__playNextSong() + return Task.done + + def __startTimerForNextSong(self, seconds): + self.notify.debug("Playing next song in %d seconds." % seconds) + self.__stopTimerForNextSong() + self.songTimerTask = taskMgr.doMethodLater(seconds, + self.__playNextSongTask, + self.taskName("playNextSong")) + + def __stopTimerForNextSong(self): + if self.songTimerTask != None: + taskMgr.remove(self.songTimerTask) + self.songTimerTask = None + +#=============================================================================== +# Timeout +#=============================================================================== + + def __startTimeout(self, timeLimit): + """ + Sets the timeout counter running. If __stopTimeout() is not + called before the time expires, we'll exit the avatar. This + prevents avatars from hanging out in the jukebox all day. + """ + self.__stopTimeout() + self.timeoutTask = taskMgr.doMethodLater(timeLimit, + self.__handleTimeoutTask, + self.taskName("timeout")) + + def __stopTimeout(self): + """ + Stops a previously-set timeout from expiring. + """ + if self.timeoutTask != None: + taskMgr.remove(self.timeoutTask) + self.timeoutTask = None + + def __handleTimeoutTask(self, task): + """ + Called when a timeout expires, this kicks the toon out of the jukebox. + """ + self.notify.debug('Timeout expired!') + if len(self.toonIds) > 0: + self.toonExitResponse(self.toonIds[0], True) + return Task.done + + + def getRandomMusicInfo(self, phase=13): + if phase == -1: + # Get random phase + keys = list(self.phaseToMusicData.keys()) + # bias random music towards the new party songs + keys += [13,13,13,] + phase = random.choice(keys) # this is random.choice + + # From that phase, get random filename + values = list(self.phaseToMusicData[phase].keys()) + filename = values[randint(0, len(values) - 1)] + + return (phase, filename) + + + def getMusicData(self, phase, filename): + data = [] + phase = PartyGlobals.sanitizePhase(phase) + phase = self.phaseToMusicData.get(phase) + if phase: + data = phase.get(filename, []) + return data + +#=============================================================================== +# React to fireworks show +#=============================================================================== + + def __handleFireworksStarted(self): + self.notify.debug("__handleFireworksStarted") + self.__stopTimerForNextSong() + self.d_setSongPlaying(0, "", 0) + + def __handleFireworksFinished(self): + self.notify.debug("__handleFireworksFinished") + self.__playNextSong() -class DistributedPartyJukeboxActivityBaseAI(DistributedObjectAI): - notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyJukeboxActivityBaseAI') diff --git a/toontown/parties/DistributedPartyTeamActivityAI.py b/toontown/parties/DistributedPartyTeamActivityAI.py index 1a88a5b..117038b 100644 --- a/toontown/parties/DistributedPartyTeamActivityAI.py +++ b/toontown/parties/DistributedPartyTeamActivityAI.py @@ -1,5 +1,830 @@ -from direct.directnotify import DirectNotifyGlobal -from direct.distributed.DistributedObjectAI import DistributedObjectAI +#=============================================================================== +# Contact: Rob Gordon, Edmundo Ruiz (Schell Games) +# Created: October 14, 2009 +# Purpose: Base class for a session-based activity with two teams (AI side). +# +# Note that this class was originally extracted from +# DistributedPartyTugOfWarActivityAI.py +#=============================================================================== +# Config Overrides: +# party-team-activity-single-player bool +# Allows the dev to run the activity with a single toon playing in it +# +# party-team-activity-start-delay int +# How long the game should wait before it starts +# +# party-team-activity-duration int +# How long the Active states lasts in the game +#=============================================================================== +from direct.distributed import ClockDelta -class DistributedPartyTeamActivityAI(DistributedObjectAI): - notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyTeamActivityAI') +from toontown.toonbase import TTLocalizer + +from toontown.parties.DistributedPartyActivityAI import DistributedPartyActivityAI +from toontown.parties.activityFSMs import TeamActivityAIFSM +from toontown.parties import PartyGlobals + +class DistributedPartyTeamActivityAI(DistributedPartyActivityAI): + notify = directNotify.newCategory("DistributedPartyTeamActivityAI") + + def __init__( + self, + air, partyDoId, x, y, h, + activityId, + minPlayersPerTeam = PartyGlobals.TeamActivityDefaultMinPlayersPerTeam, + maxPlayersPerTeam = PartyGlobals.TeamActivityDefaultMaxPlayersPerTeam, + duration = PartyGlobals.TeamActivityDefaultDuration, + conclusionDuration = PartyGlobals.TeamActivityDefaultConclusionDuration, + startDelay = PartyGlobals.TeamActivityStartDelay, + balanceTeams = False, + calcAdvantage = False, + canSwitchTeams = False, + ): + DistributedPartyActivityAI.__init__( + self, + air, + partyDoId, + x, + y, + h, + activityId, + PartyGlobals.ActivityTypes.GuestInitiated, + ) + self.notify.debug("__init__") + + self._minPlayersPerTeam = minPlayersPerTeam + self._maxPlayersPerTeam = maxPlayersPerTeam + + # How long should the conclusion state last in seconds? + self._conclusionDuration = conclusionDuration + + # How long the active state lasts + self._duration = simbase.config.GetFloat("party-team-activity-duration", duration) + + # How long should it wait for more players before going to the active state + self._startDelay = simbase.config.GetFloat("party-team-activity-start-delay", startDelay) + + # Should it calculate whether both team have roughly an equal amount of players? + self._shouldBalanceTeams = balanceTeams + + # Should it calculate the advantage of a smaller team over a larger team? + self._shouldCalcAdvantage = calcAdvantage + + # Can players willingly switch teams before the game starts? + self._canSwitchTeams = canSwitchTeams + + # Do we allow a single player? (This is for testing purposes) + self._allowSinglePlayer = simbase.config.GetBool("party-team-activity-single-player", False) + + self.toonIds = ( + [], # doIds of toons on the left team, ordered from those nearest the water to those furthest + [], # doIds of toons on the right team, ordered from those nearest the water to those furthest + ) + + self.readyToonIds = set() + + # Keep track of which side each avatar is on for quick look up during game + self.toonIdsToTeams = {} + + # state transition request doLater + self._srTask = None + + self.activityFSM = TeamActivityAIFSM(self) + + def generate(self): + DistributedPartyActivityAI.generate(self) + self.notify.debug("generate") + + self.activityFSM.request("WaitForEnough") + + def announceGenerate(self): + DistributedPartyActivityAI.announceGenerate(self) + self.notify.debug("announceGenerate") + + def delete(self): + self.cleanupRequestLater() + + DistributedPartyActivityAI.delete(self) + +#=============================================================================== +# Actions +#=============================================================================== + + def _addToonToTeam(self, toonId, team): + """ + Checks and adds a toon to the team + + Parameters + toonId toon's doId + team team number based on PartyGlobals.TeamActivityTeams + + Returns + true if it successfully added the toon to the team + """ + self.notify.debug("_addToonToTeam") + + # check team size limits + if self.isTeamFull(team): + self.notify.error("Tried to add toonId=%s to %s when it was already full." % (toonId, self.getTeamName(team))) + # check that toonId isn't already in that team's list + elif toonId in self.toonIds[team]: + self.notify.warning("Tried to add toonId=%s to %s when it was already on that team. Ignoring request." % (toonId, self.getTeamName(team))) + else: + self.toonIds[team].append(toonId) + self.toonIdsToTeams[toonId] = team + self.toonId2joinTime[toonId] = globalClock.getFrameTime() + + # listen for this avatar's exit event + self.acceptOnce( + self.air.getAvatarExitEvent(toonId), + self._handleUnexpectedToonExit, + extraArgs=[toonId], + ) + + self.notify.debug("Added toonId=%s to %s." % (toonId, self.getTeamName(team))) + self.notify.debug("toonIds:\n\t\t%s\n\t\t%s." % (self.toonIds[0], self.toonIds[1])) + + return True + + return False + + def _removeToonFromTeam(self, toonId, team): + """ + Checks and removes a toon from a team + + Parameters + toonId toon's doId + team team number based on PartyGlobals.TeamActivityTeams + + Returns + true if it successfully removed the toon from the team + """ + self.notify.debug("_removeToonFromTeam") + + if type(team) != type(1): + self.notify.warning("Tried to remove toonId=%s from %s, but team is not an integer" % (toonId, str(team))) + elif not toonId in self.toonIds[team]: + self.notify.warning("Tried to remove toonId=%s from %s when it was not on that team. Ignoring request." % (toonId, self.getTeamName(team))) + else: + self.ignore(self.air.getAvatarExitEvent(toonId)) + self.toonIds[team].remove(toonId) + del self.toonIdsToTeams[toonId] + del self.toonId2joinTime[toonId] + + self.notify.debug("Removed toonId=%s from %s." % (toonId, self.getTeamName(team))) + self.notify.debug("toonIds:\n\t\t%s\n\t\t%s." % (self.toonIds[0], self.toonIds[1])) + + return True + + return False + + + def _removeAllToons(self): + """Removes all toons from the activity""" + + self.notify.debug("removeAllToons") + + for team in (PartyGlobals.TeamActivityTeams.LeftTeam, PartyGlobals.TeamActivityTeams.RightTeam, ): + toonIds = list(self.toonIds[team])[:] + self.notify.debug("initial toonIds for team %s = %s" % (team,toonIds)) + + for toonId in toonIds: + self.notify.debug("removing toon %s" % toonId) + self._removeToonFromTeam(toonId, team) + + self.notify.debug("self.toonIds[team] is now %s" % self.toonIds[team]) + + self.d_broadcastSetToonsPlaying() + + def balanceTeams(self): + """ + Makes the teams roughly have the same number of toons. + """ + # We assume that the left team is smaller, but later make the correction + smallerTeam = PartyGlobals.TeamActivityTeams.LeftTeam + largerTeam = PartyGlobals.TeamActivityTeams.RightTeam + + # If number of players in one team vs the other is more than 1 + if abs(len(self.toonIds[smallerTeam]) - len(self.toonIds[largerTeam])) > 1: + # We make the correction to the assumption about the smaller team + if len(self.toonIds[smallerTeam]) > len(self.toonIds[largerTeam]): + smallerTeam = largerTeam + largerTeam = 1 - smallerTeam + + shiftedToons = False + # Keep putting toons in the other team until + # the number of toons in the larger team is close to the other + # could be the same or one larger depending on the difference + while abs(len(self.toonIds[largerTeam]) - len(self.toonIds[smallerTeam])) > 1: + toondId = self.toonIds[largerTeam].pop() + self.toonIds[smallerTeam].append(toondId) + self.toonIdsToTeams[toondId] = smallerTeam + shiftedToons = True + + if self.isTeamFull(smallerTeam): + break + + # Finally send the new list out if any toons shifted teams + if shiftedToons: + self.d_broadcastSetToonsPlaying() + + def calcAdvantage(self): + """ + Calculates the advantage factor based on the team size. + For example, if a team has 3 players and the other has 4, then + the advantage will be 4/3 (1.333) for the smaller team over and 1 + for the larger team. + """ + self.resetAdvantage() # For good measure + + smallerTeam = PartyGlobals.TeamActivityTeams.LeftTeam + largerTeam = PartyGlobals.TeamActivityTeams.RightTeam + + if len(self.toonIds[smallerTeam]) == 0 or len(self.toonIds[largerTeam]) == 0: + return + + # Find the actual large team + if len(self.toonIds[smallerTeam]) > len(self.toonIds[largerTeam]): + smallerTeam = largerTeam + largerTeam = 1 - smallerTeam + + # Set the advantage + self.advantage[smallerTeam] = len(self.toonIds[largerTeam]) / float(len(self.toonIds[smallerTeam])) + self.notify.debug("Advantage set to %s" % self.advantage) + + # Send the advantage to members of the smaller team + advantage = self.advantage[smallerTeam] + + for toonId in self.toonIds[smallerTeam]: + self.d_sendSetAdvantageToAvatarId(toonId, advantage) + + def resetAdvantage(self): + """Resets the advantage for both teams back to 1.0""" + self.advantage = [1.0, 1.0] + +#=============================================================================== +# Handlers +#=============================================================================== + + def _handleUnexpectedToonExit(self, toonId): + """ + Clean up after a toon that dropped. + """ + self.notify.debug("_handleUnexpectedToonExit( toonId=%d )" % toonId) + + # do special responses based on game state + state = self.activityFSM.getCurrentOrNextState() + self.notify.debug("currentState = %s" % state) + + if state in ("WaitToStart", 'WaitForEnough'): + # remove toonId from self.toonIds and update clients + self._removeToonFromTeam(toonId, self.getTeam(toonId)) + self.d_broadcastSetToonsPlaying() + + if state != 'WaitForEnough': + # revert to WaitForEnough if necessary + if not self.hasEnoughPlayers(): + self.activityFSM.request("WaitForEnough") + + elif state in ("WaitClientsReady", "Active"): + # To keep things simple the game will kick everyone out and reset + # the game state. + self._removeAllToons() + self.activityFSM.request("WaitForEnough") + + elif state == "Conclusion": + del self.toonIdsToJellybeanRewards[toonId] + # remove toonId from self.toonIds and update clients + self._removeToonFromTeam(toonId, self.getTeam(toonId)) + self.d_broadcastSetToonsPlaying() + +#=============================================================================== +# Distributed +#=============================================================================== + + # broadcast required + # this method is for the client-side dc method setPlayersPerTeam + def getPlayersPerTeam(self): + return self._minPlayersPerTeam, self._maxPlayersPerTeam + + # broadcast required + def getDuration(self): + return self._duration + + # broadcast required + def getCanSwitchTeams(self): + return self._canSwitchTeams + + # clsend airecv + def toonJoinRequest(self, team): + """ + A toon is requesting to play tug of war. + + Parameters: + team: The team the toon is trying to join as a + PartyGlobals.TeamActivityTeams value + """ + toonId = self.air.getAvatarIdFromSender() + self.notify.debug("toonJoinRequest( toon = %s team=%s )" %(toonId, self.getTeamName(team))) + + # first make sure the toon is not in any activity + if self.party.isInActivity(toonId): + # decline the toon + self.notify.warning("rejecting toon %d trying to join team %s, but is in some other activity" % (toonId, self.getTeamName(team))) + self.sendToonJoinResponse(toonId, False, team) + + return + + # check team size limits first + if self.isTeamFull(team): + # decline the toon + self.sendToonJoinResponse(toonId, False, team, PartyGlobals.DenialReasons.Full) + + return + + state = self.activityFSM.getCurrentOrNextState() + self.notify.debug("toonJoinRequest state = " + state) + + if state == "WaitForEnough": + # add the toon + self.sendToonJoinResponse(toonId, True, team) + + # if we have enough players per team, request next state + if self.hasEnoughPlayers(): + self.activityFSM.request("WaitToStart") + elif state == "WaitToStart": + self.sendToonJoinResponse(toonId, True, team) + else: + # decline the toon + self.sendToonJoinResponse(toonId, False, team) + + def sendToonJoinResponse(self, toonId, joined, team, denialReason=PartyGlobals.DenialReasons.Default): + """ + Broadcast to all clients whether or not this toonId has joined the + activity. + + Parameters: + toonId- doId of the avatar that is being accepted/rejected + joined- True if the avatar joined this activity, False otherwise + team- which team the toon joined, as a PartyGlobals.TeamActivityTeams + value + denialReason- if the toon is being denied, the reason why + """ + # we purposely do not call the base class version as we track toons in + # this activity ourselves + if joined: + self._addToonToTeam(toonId, team) + self.d_broadcastSetToonsPlaying() + else: + self.sendUpdateToAvatarId(toonId, "joinRequestDenied", [denialReason]) + + + # clsend airecv + def toonExitRequest(self, team): + """ + Clients call this over the wire when they want to exit this + activity, but need AI server permission to do so. + + Parameters: + team- which team the toon is on, as a PartyGlobals.TeamActivityTeams + value + """ + # allow toons to leave if the activity is waiting for enough players or + # waiting to start + self.notify.debug("toonExitRequest( team=%s )" % self.getTeamName(team)) + + toonId = self.air.getAvatarIdFromSender() + state = self.activityFSM.getCurrentOrNextState() + + if state == "WaitForEnough": + # allow the toon to exit + self.sendToonExitResponse(toonId, True, team) + elif state == "WaitToStart": + # allow the toon to exit + self.sendToonExitResponse(toonId, True, team) + + # revert to WaitForEnough if necessary + if not self.hasEnoughPlayers(): + self.activityFSM.request("WaitForEnough") + else: + # decline the toon + self.sendToonExitResponse(toonId, False, team) + + def sendToonExitResponse(self, toonId, exited, team, denialReason=PartyGlobals.DenialReasons.Default): + """ + Broadcast to all clients whether or not this toonId has left the activity. + Subclasses should call this in response to recieving a toonExitRequest + call. + Parameters: + toonId- id of the avatar that made the request to exit + exited- True if the avatar exited this activity, False otherwise + """ + self.notify.debug("sendToonExitResponse(toonId=%s, exited=%s, team=%s)"%(toonId, bool(exited), self.getTeamName(team))) + + if exited: + self._removeToonFromTeam(toonId, team) + self.d_broadcastSetToonsPlaying() + else: + self.sendUpdateToAvatarId(toonId, "exitRequestDenied", [denialReason]) + + # clsend airecv + def toonSwitchTeamRequest(self): + senderId = self.air.getAvatarIdFromSender() + state = self.activityFSM.getCurrentOrNextState() + + if not self._canSwitchTeams: + self.writeServerEvent( + 'suspicious', + senderId, + "Trying to switch teams when the team activity does not allow it.") + return + + team = self.getTeam(senderId) + if team is None: # Might have exited at this point + return + + # Only allow switching before the activity starts + if state != "WaitForEnough" and state != "WaitToStart": + self.d_sendSwitchTeamRequestDeniedToAvatarId(senderId, PartyGlobals.DenialReasons.Default) + return + + otherTeam = 1 - team + + if not self.isTeamFull(otherTeam): + self.toonIds[team].remove(senderId) + self.toonIds[otherTeam].append(senderId) + self.toonIdsToTeams[senderId] = otherTeam + + self.d_broadcastSetToonsPlaying() + + # Check whether we need to change states at this point + # Depending if the conditions (like if auto balance is on) for this activity there + # may be enough players to start, or not enough. + if state == "WaitForEnough" and self.hasEnoughPlayers(): + self.activityFSM.request("WaitToStart") + elif state == "WaitToStart" and not self.hasEnoughPlayers(): + self.activityFSM.request("WaitForEnough") + else: + self.d_sendSwitchTeamRequestDeniedToAvatarId(senderId, PartyGlobals.DenialReasons.Full) + + + # ai to avatar + def d_sendSwitchTeamRequestDeniedToAvatarId(self, avId, reason): + self.sendUpdateToAvatarId( + avId, + "switchTeamRequestDenied", + [reason] + ) + + # airecv clsend + def toonReady(self): + """ + Clients call this over the wire when they are done reading the rules + and are ready to play the activity. + """ + self.notify.debug("toonReady") + + toonId = self.air.getAvatarIdFromSender() + state = self.activityFSM.getCurrentOrNextState() + + if state != "WaitClientsReady": + self.notify.warning("Got an unexpected toonReady from toonId=%s while in state %s." % (toonId, state)) + elif not self.isToonPlaying(toonId): + self.notify.warning("Got an unexpected toonReady from toonId=%s that is not currently in this activity." % toonId) + else: + # update set of ready toons + self.readyToonIds.add(toonId) + + # if we've gotten enough readies, then move on to next state + if len(self.readyToonIds) == self.getNumToonsPlaying(): + self.activityFSM.request("Active") + + return True + + return False + + + # broadcast ram + def d_broadcastSetState(self, stateName, timestamp=0, data=0): + self.sendUpdate("setState", [stateName, timestamp, data]) + + + # required broadcast ram + def d_broadcastSetToonsPlaying(self): + self.sendUpdate("setToonsPlaying", [self.toonIds[0], self.toonIds[1]]) + + + def d_sendSetAdvantageToAvatarId(self, toonId, advantage): + self.sendUpdateToAvatarId(toonId, "setAdvantage", [advantage]) + + +#=============================================================================== +# FSM +#=============================================================================== + + def isState(self, state): + """ + Checks what the current state of the activity is right now. + + Parameters + state is what state we're checking for + + Returns + true if the current or next state matches the state we're checking for + """ + return self.activityFSM.getCurrentOrNextState() == state + + def __delayedStateRequest(self, nextState): + self.activityFSM.request(nextState) + + def requestLater(self, stateName, delay, delayName): + """ + Schedule a do method later that requests a state change. + """ + self.cleanupRequestLater() + + self._srTask = taskMgr.doMethodLater( + delay, + self.__delayedStateRequest, + self.taskName(delayName), + extraArgs=[stateName], + ) + + def cleanupRequestLater(self): + """ + Cancels or cleans up the do method later that would request a state change. + """ + if self._srTask is not None: + taskMgr.remove(self._srTask) + self._srTask = None + + def startWaitForEnough(self): + """ + Wait for for the minimum number players to join the activity. + """ + self.notify.debug("startWaitForEnough") + + # put clients into this state + self.d_broadcastSetState("WaitForEnough") + + def finishWaitForEnough(self): + """ + There are enough players in the activity to start. + Transitioning to WaitToStart. + """ + self.notify.debug("finishWaitForEnough") + + + def startWaitToStart(self): + """ + Enough players are in. + + Wait a few seconds before the activity begins to let more players + come in and participate. + """ + self.notify.debug("startWaitToStart") + + if self._shouldCalcAdvantage: + self.resetAdvantage() + + # put clients into this state + self.d_broadcastSetState( + "WaitToStart", + timestamp=ClockDelta.globalClockDelta.getRealNetworkTime() + ) + + # start the game after a fixed amount of time + self.requestLater("WaitClientsReady", self._startDelay, "waitToStartTimer") + + def finishWaitToStart(self): + """ + The wait time is over and now we're displaying the rules on the clients and + waiting until everyone reads the rules. + + Or we're below the number of toons required for the activity, + so we go back to the WaitForEnough state. + """ + self.notify.debug("finishWaitToStart") + + self.cleanupRequestLater() + + + def startWaitClientsReady(self): + """ + In the clients with toons participating in this activity, we are + displaying the rules, and we are waiting for all toons to read them + before starting the activity. + """ + self.notify.debug("startWaitClientsReady") + + self.readyToonIds.clear() # reset the list of ready toons + + if self._shouldBalanceTeams: + self.balanceTeams() + + # put clients into new state + self.d_broadcastSetState("Rules") + + def finishWaitClientsReady(self): + """ + All toons have read the rules, and we transition to the Active state. + Or perhaps toons have dropped out of the activity and we're back to WaitForEnough. + """ + self.notify.debug("finishWaitClientsReady") + + + def startActive(self): + """ + The activity begins, and lasts a certain amount of minutes before jumping + to the Conclusion. + """ + self.notify.debug("startActive") + + if self._shouldCalcAdvantage: + self.calcAdvantage() + + # reset game variables + self.toonIdsToJellybeanRewards.clear() + + # put clients into this state + self.d_broadcastSetState( + "Active", + timestamp=ClockDelta.globalClockDelta.getRealNetworkTime() + ) + + # end the game after a fixed amount of time + self.requestLater("Conclusion", self._duration, "gameTimer") + + + def finishActive(self): + """ + The activity successfully ended and we are going to the conclusion, + or a participating toon dropped out of the activity and we are going + back to WaitForEnough. + """ + self.notify.debug("finishActive") + + self.cleanupRequestLater() + + + def startConclusion(self): + """ + Calculates the results and sends them to the participating clients for display. + Wait a number of seconds before transitioning out. + """ + self.notify.debug("startConclusion") + self.computeMatchResults() + + # put clients into this state + self.d_broadcastSetState( + "Conclusion", # new state + data=self.getConclusionData() + ) + + # schedule to jump to next state + self.requestLater("WaitForEnough", self._conclusionDuration, "rewardDuration") + + def finishConclusion(self): + """ + Conclusion has ended and we're going back to WaitForEnough. + """ + self.notify.debug("finishConclusion") + + self.cleanupRequestLater() + + for toonId, reward in list(self.toonIdsToJellybeanRewards.items()): + if reward > 0: + self.sendUpdateToAvatarId( + toonId, + "showJellybeanReward", [ + reward, + self.air.doId2do[toonId].getMoney(), + self.getJellybeanRewardMessage(toonId, reward) + ]) + + # since we send the toon's current money in showJellybeanReward, that needs to happen before issueJellybeanRewards + self.issueJellybeanRewards() + self._removeAllToons() + + +#=============================================================================== +# Utility +#=============================================================================== + + def getMaxPlayersPerTeam(self): + """Returns the maximum number for players allowed per team""" + return self._maxPlayersPerTeam + + def getMinPlayersPerTeam(self): + """Returns the minimum number for players allowed per team""" + return self._minPlayersPerTeam + + def getTeamName(self, team): + """ + Parameters + team is the team index (see PartyGlobals.TeamActivityTeams) + + Returns + The team name + """ + return PartyGlobals.TeamActivityTeams(team) + + + def isTeamFull(self, team): + """ + Checks if a team is full. + + Parameters + team is the team index (see PartyGlobals.TeamActivityTeams) + + Returns + true if the one of the teams is full. + """ + return (len(self.toonIds[team]) == self._maxPlayersPerTeam) + + + def hasEnoughPlayers(self): + """ + Checks to see if there are enough players to start a game. + If the config variable party-allow-single-player-teams is set, + then the test passes if there's a least one player on every team. + + Returns + true if there are enough players for starting the game. + """ + if self._allowSinglePlayer: + return (len(self.toonIds[PartyGlobals.TeamActivityTeams.LeftTeam]) >= 1 or + len(self.toonIds[PartyGlobals.TeamActivityTeams.RightTeam]) >= 1) + + elif self._shouldBalanceTeams: + # This activity has enough players if the number of players on any side + # is enough to create two teams. + # The teams are balanced before right before the game starts. + return ( + (len(self.toonIds[PartyGlobals.TeamActivityTeams.LeftTeam]) + + len(self.toonIds[PartyGlobals.TeamActivityTeams.RightTeam])) >= + (self._minPlayersPerTeam * 2) + ) + + return (len(self.toonIds[PartyGlobals.TeamActivityTeams.LeftTeam]) >= self._minPlayersPerTeam and + len(self.toonIds[PartyGlobals.TeamActivityTeams.RightTeam]) >= self._minPlayersPerTeam) + + + def computeMatchResults(self): + """ + Used to determine the Jellybean rewards. Varies per activity implementation. + """ + pass + + + def getToonsPlaying(self): + """Returns a 2-dimensional list of Toon IDs in each team.""" + return [self.toonIds[0], self.toonIds[1]] + + + def isInActivity(self, avId): + """Return true if the avId is busy with us.""" + result = False + + for toonList in self.toonIds: + if avId in toonList: + result = True + break + + return result + + + def isToonPlaying(self, toonId): + """Returns true if a toon is currently in the activity""" + return (toonId in self.getToonIdsAsList()) + + + def getToonIdsAsList(self): + """Returns a list of doId's of all toons in this activity.""" + return (self.toonIds[0] + self.toonIds[1]) + + + def getNumToonsPlaying(self): + return (len(self.toonIds[0]) + len(self.toonIds[1])) + + + def getTeam(self, toonId): + """ + Utility function that returns the team toonId is on, as a + PartyGlobals.TeamActivityTeams value, or None if toonId isn't on either team. + + Returns + team index or None + """ + for i in range(len(PartyGlobals.TeamActivityTeams)): + if self.toonIds[i].count(toonId) > 0: + return i + + return None + + def getJellybeanRewardMessage(self, toonId, reward): + """ + Returns the message that should be displayed when the game ends. + This includes how many jellybeans they made + """ + return TTLocalizer.PartyTeamActivityRewardMessage % reward diff --git a/toontown/parties/DistributedPartyTrampolineActivityAI.py b/toontown/parties/DistributedPartyTrampolineActivityAI.py index e15e909..8ab8183 100644 --- a/toontown/parties/DistributedPartyTrampolineActivityAI.py +++ b/toontown/parties/DistributedPartyTrampolineActivityAI.py @@ -1,5 +1,156 @@ -from direct.directnotify import DirectNotifyGlobal -from direct.distributed.DistributedObjectAI import DistributedObjectAI +#------------------------------------------------------------------------------- +# Contact: Jason Pratt +# Created: Oct 2008 +# +# Purpose: AI for DistributedPartyTrampolineActivity. +#------------------------------------------------------------------------------- -class DistributedPartyTrampolineActivityAI(DistributedObjectAI): - notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyTrampolineActivityAI') +from direct.task import Task +from direct.distributed import ClockDelta +from toontown.toonbase import ToontownGlobals +from toontown.parties import PartyGlobals +from toontown.ai.ToonBarrier import ToonBarrier +from toontown.parties.DistributedPartyActivityAI import DistributedPartyActivityAI +from toontown.parties.activityFSMs import TrampolineActivityFSM +from toontown.toonbase import TTLocalizer + +class DistributedPartyTrampolineActivityAI(DistributedPartyActivityAI): + notify = directNotify.newCategory("DistributedPartyTrampolineActivityAI") + + def __init__(self, air, partyDoId, x, y, h, actId=PartyGlobals.ActivityIds.PartyTrampoline): + DistributedPartyActivityAI.__init__(self, air, partyDoId, x, y, h, actId, PartyGlobals.ActivityTypes.GuestInitiated) + + self.activityFSM = TrampolineActivityFSM(self) + # bestHeightInfo is a tuple of toon's name and their height + self.bestHeightInfo = ("", 0) + self.accept("NewBestHeightInfo", self.newBestHeightInfo) + + def generate(self): + DistributedPartyTrampolineActivityAI.notify.debug("generate") + self.activityFSM.request("Idle") + + def toonJoinRequest(self): + DistributedPartyTrampolineActivityAI.notify.debug("toonJoinRequest") + senderId = self.air.getAvatarIdFromSender() + if (self.activityFSM.state == "Idle") and (len(self.toonIds) == 0) and not self.party.isInActivity(senderId): + self.sendToonJoinResponse(senderId, True) + self.activityFSM.request("Rules") + else: + self.sendToonJoinResponse(senderId, False) + + def toonReady(self): + DistributedPartyTrampolineActivityAI.notify.debug("toonReady") + senderId = self.air.getAvatarIdFromSender() + if (self.activityFSM.state == "Rules") and (senderId in self.toonIds): + self.activityFSM.request("Active") + else: + self.air.writeServerEvent("suspicious", senderId, "trampoline state not Rules or senderId not in toonIdsPlaying, in toonReady") + + def toonExitDemand(self): + DistributedPartyTrampolineActivityAI.notify.debug("toonExitDemand") + senderId = self.air.getAvatarIdFromSender() + if (self.activityFSM.state == "Active") and (senderId in self.toonIds): + self.activityFSM.request("Idle") + + def _handleUnexpectedToonExit(self, toonId): + """ + An avatar bailed out because he lost his connection or quit + unexpectedly. + """ + DistributedPartyTrampolineActivityAI.notify.debug("_handleUnexpectedToonExit( toonId=%s )" % toonId) + self.activityFSM.request("Idle") + DistributedPartyActivityAI._handleUnexpectedToonExit(self, toonId) + + def reportHeightInformation(self, height): + if height > self.bestHeightInfo[1]: + senderId = self.air.getAvatarIdFromSender() + sender = self.air.doId2do[senderId] + messenger.send("NewBestHeightInfo", [sender.getName(), height]) + + def newBestHeightInfo(self, toonName, height): + self.bestHeightInfo = (toonName, height) + self.sendUpdate("setBestHeightInfo", [toonName, height]) + + def getBestHeightInfo(self): + return self.bestHeightInfo + + def awardBeans(self, numBeansCollected, topHeight): + senderId = self.air.getAvatarIdFromSender() + + if numBeansCollected > PartyGlobals.TrampolineNumJellyBeans: + self.air.writeServerEvent("suspicious", senderId, "Player claims to have collected more jelly beans (%d) than possible." % numBeansCollected) + else: + numWon = numBeansCollected + if numWon == PartyGlobals.TrampolineNumJellyBeans: + numWon += PartyGlobals.TrampolineJellyBeanBonus + if self.air.holidayManager.isHolidayRunning(ToontownGlobals.JELLYBEAN_DAY): + numWon *= PartyGlobals.JellyBeanDayMultiplier + resultsMessage = TTLocalizer.PartyTrampolineBonusBeanResults % (numBeansCollected, PartyGlobals.TrampolineJellyBeanBonus) + else: + if self.air.holidayManager.isHolidayRunning(ToontownGlobals.JELLYBEAN_DAY): + numWon *= PartyGlobals.JellyBeanDayMultiplier + resultsMessage = TTLocalizer.PartyTrampolineBeanResults % numBeansCollected + resultsMessage += "\n\n" + TTLocalizer.PartyTrampolineTopHeightResults % topHeight + + self.toonIdsToJellybeanRewards = {senderId : numWon} + self.sendUpdateToAvatarId(senderId, "showJellybeanReward", [numWon, self.air.doId2do[senderId].getMoney(), resultsMessage]) + # since we send the toon's current money in showJellybeanReward, that needs to happen before issueJellybeanRewards + self.issueJellybeanRewards() + + def requestAnim(self, request): + self.sendUpdate("requestAnimEcho", [request]) + + def removeBeans(self, beansToRemove): + self.sendUpdate("removeBeansEcho", [beansToRemove]) + + def sessionOver(self, task): + """ + Time is up. Kick the toon off the trampoline. + """ + DistributedPartyTrampolineActivityAI.notify.debug("sessionOver") + self.sendUpdate("leaveTrampoline") + return Task.done + + # FSM transition methods + def startIdle(self): + DistributedPartyTrampolineActivityAI.notify.debug("startIdle") + # remove active toon + if len( self.toonIds ) > 0: + self.removeAllToons() + # put clients into this state + self.sendUpdate( "setState", ["Idle", # new state + 0] ) # dummy timestamp + + def finishIdle(self): + DistributedPartyTrampolineActivityAI.notify.debug("finishIdle") + + def startRules(self): + DistributedPartyTrampolineActivityAI.notify.debug("startRules") + # we do not explicitly tell clients to enter this state as the + # toonJoinResponse will prompt that change on the clients + + def finishRules(self): + DistributedPartyTrampolineActivityAI.notify.debug("finishRules") + + def startActive(self): + DistributedPartyTrampolineActivityAI.notify.debug("startActive") + # put clients into this state + self.sendUpdate( "setState", ["Active", # new state + ClockDelta.globalClockDelta.getRealNetworkTime()] ) # start time + + # setup to transition to Disabled after time is up + taskMgr.doMethodLater( + PartyGlobals.TrampolineDuration, + self.sessionOver, + self.taskName("waitForSessionOver"), + ) + + def finishActive(self): + DistributedPartyTrampolineActivityAI.notify.debug("finishActive") + # clean up doMethodLater + taskMgr.removeTasksMatching(self.taskName("waitForSessionOver")) + + def delete(self): + del self.activityFSM + self.ignore("NewBestHeightInfo") + DistributedPartyActivityAI.delete(self) diff --git a/toontown/parties/DistributedPartyTugOfWarActivity.py b/toontown/parties/DistributedPartyTugOfWarActivity.py index bcc2749..b638772 100644 --- a/toontown/parties/DistributedPartyTugOfWarActivity.py +++ b/toontown/parties/DistributedPartyTugOfWarActivity.py @@ -329,7 +329,8 @@ class DistributedPartyTugOfWarActivity(DistributedPartyTeamActivity): def __enableCollisions(self): for i in range(len(PartyGlobals.TeamActivityTeams)): - self.accept('enterTugOfWarJoinCollision%d' % i, getattr(self, '_join%s' % PartyGlobals.TeamActivityTeams.getString(i))) + teamName = PartyGlobals.TeamActivityTeams(i).name + self.accept(f'enterTugOfWarJoinCollision{i}', getattr(self, f'_join{teamName}')) def __disableCollisions(self): for i in range(len(PartyGlobals.TeamActivityTeams)): @@ -662,7 +663,7 @@ class DistributedPartyTugOfWarActivity(DistributedPartyTeamActivity): self.sendUpdate('reportFallIn', [losingTeam]) def throwTeamInWater(self, losingTeam): - self.notify.debug('throwTeamInWater( %s )' % PartyGlobals.TeamActivityTeams.getString(losingTeam)) + self.notify.debug('throwTeamInWater( %s )' % PartyGlobals.TeamActivityTeams(losingTeam)) splashSet = False for toonId in self.toonIds[losingTeam]: self.fallenToons.append(toonId) diff --git a/toontown/parties/DistributedPartyTugOfWarActivityAI.py b/toontown/parties/DistributedPartyTugOfWarActivityAI.py index fb125fd..0e3a475 100644 --- a/toontown/parties/DistributedPartyTugOfWarActivityAI.py +++ b/toontown/parties/DistributedPartyTugOfWarActivityAI.py @@ -1,5 +1,195 @@ -from direct.directnotify import DirectNotifyGlobal -from direct.distributed.DistributedObjectAI import DistributedObjectAI +#------------------------------------------------------------------------------- +# Contact: Rob Gordon, Edmundo Ruiz (Schell Games) +# Created: Oct 2008 +# +# Purpose: AI control of tug of war activity in a party. +# Changes: +# - Nov 2009 Migrated to DistributedPartyTeamActivityAI +#------------------------------------------------------------------------------- +from toontown.toonbase import ToontownGlobals +from .DistributedPartyTeamActivityAI import DistributedPartyTeamActivityAI +from . import PartyGlobals -class DistributedPartyTugOfWarActivityAI(DistributedObjectAI): - notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyTugOfWarActivityAI') +class DistributedPartyTugOfWarActivityAI(DistributedPartyTeamActivityAI): + notify = directNotify.newCategory("DistributedPartyTugOfWarActivityAI") + + MaxAbsGlobalOffset = 32 + + def __init__(self, air, partyDoId, x, y, h): + DistributedPartyTeamActivityAI.__init__( + self, + air, partyDoId, x, y, h, + PartyGlobals.ActivityIds.PartyTugOfWar, + minPlayersPerTeam = PartyGlobals.TugOfWarMinimumPlayersPerTeam, + maxPlayersPerTeam = PartyGlobals.TugOfWarMaximumPlayersPerTeam, + duration = PartyGlobals.TugOfWarDuration, + conclusionDuration = PartyGlobals.TugOfWarConclusionDuration, + startDelay = PartyGlobals.TugOfWarStartDelay, + calcAdvantage = True, + ) + self.notify.debug("__init__") + + # Add up all the players forces for each side as soon as we get a + # keyRate update from all players. + self.forceDictList = [ + {}, # left team's forces, indexed by toonId + {}, # right team's forces, indexed by toonId + ] + self.toonIdsToKeyRates = {} + self.numToonsReported = 0 + + self.globalOffset = 0.0 # how far the toons have moved from their initial position + + # Variables for determining the outcome of the game. If + # endedWithFall=True that means someone actually fell in the water. + self.endedWithFall = False + self.losingTeam = PartyGlobals.TeamActivityNeitherTeam + + # Override because this code was migrated into the base class + # And it was written assuming that it was 1v1 + if self._allowSinglePlayer: + self.notify.warning("This team activity doesn't work party-team-activity-single-player config." + + " This activity will ignore the config instead.") + self._allowSinglePlayer = False + + + # airecv clsend + def toonReady(self): + """ + Clients call this over the wire when they are done reading the rules + and are ready to play the activity. + """ + if DistributedPartyTeamActivityAI.toonReady(self): + senderId = self.air.getAvatarIdFromSender() + self.forceDictList[self.getTeam(senderId)][senderId] = 0.0 + + + def startWaitClientsReady(self): + self.globalOffset = 0.0 + self.forceDictList = [{}, {}] + + DistributedPartyTeamActivityAI.startWaitClientsReady(self) + + + def startActive(self): + # reset game variables + self.toonIdsToKeyRates.clear() + self.numToonsReported = 0 + self.losingTeam = PartyGlobals.TeamActivityNeitherTeam + self.endedWithFall = False + + DistributedPartyTeamActivityAI.startActive(self) + + + # clsend airecv + def reportKeyRateForce(self, keyRate, force): + toonId = self.air.getAvatarIdFromSender() + self.toonIdsToKeyRates[toonId] = keyRate + # sometimes the game has cleaned up and we get an old update from a client + if toonId in self.toonIdsToTeams: + self.forceDictList[self.toonIdsToTeams[toonId]][toonId] = force + + # send the keyrate for this toonId to the clients so they can update + # the toon's animation (pulling or not) + self.sendUpdate("updateToonKeyRate", [toonId, keyRate]) + + # send the current position to the clients if we have gotten all the clients forces + self.numToonsReported += 1 + if self.numToonsReported == self.getNumToonsPlaying(): + self.numToonsReported = 0 + self.calculateOffset() + self.sendUpdate("updateToonPositions", [self.globalOffset]) + + + def calculateOffset(self): + # This function totals the forces on each side of the water. Then the difference + # deltaF, between these forces is computed. This is multiplied by a constant, kMovement, + # to determine what the deltaX should be - i.e. how much the toons on each side should + # move as a result of one side applying more force than the other. + + forceTotals = [0.0, 0.0] # left team force, right team force + # total up all the toon forces on each side + for teamIndex in [0,1]: + for x in list(self.forceDictList[teamIndex].values()): + forceTotals[teamIndex] += x + + deltaF = forceTotals[1] - forceTotals[0] + deltaX = deltaF * PartyGlobals.TugOfWarMovementFactor + + self.globalOffset += deltaX + + # make sure we don't move too far + if self.globalOffset > self.MaxAbsGlobalOffset: + self.globalOffset = self.MaxAbsGlobalOffset + elif self.globalOffset < -self.MaxAbsGlobalOffset: + self.globalOffset = -self.MaxAbsGlobalOffset + + + def reportFallIn(self, losingTeam): + self.notify.debug("reportFallIn( losingTeam=%s )" % PartyGlobals.TeamActivityTeams(losingTeam)) + + if losingTeam not in PartyGlobals.TeamActivityTeams: + self.notify.warning("Got an invalid losingTeam value %d" %losingTeam) + return + + # if the losing team has already been reported and this report doesn't match + if (self.losingTeam != PartyGlobals.TeamActivityNeitherTeam) and (losingTeam != self.losingTeam): + self.notify.warning("Report of %s as the losingTeam doesn't match previously reported value. Ignoring." %PartyGlobals.TeamActivityTeams(losingTeam)) + return + + curState = self.activityFSM.getCurrentOrNextState() + if curState != "Active": + # probably late report + # must be in active state to go to conclusion + self.notify.warning("got reportFallIn but state is %s, ignoring" % curState) + return + + self.losingTeam = losingTeam + + if not self.endedWithFall: + self.endedWithFall = True + self.activityFSM.request("Conclusion") + + + def computeMatchResults(self): + """ + Determine jelly bean rewards. If neither team fell in, determine if it + was a decisive victory or too close to call (a tie). + """ + + # This seems like the easiest way to get this bean multiplier in for all these different jelly bean cases + beanMultiplier = 1 + if self.air.holidayManager.isHolidayRunning(ToontownGlobals.JELLYBEAN_DAY): + beanMultiplier = PartyGlobals.JellyBeanDayMultiplier + + + if self.endedWithFall: + winningTeam = 1 - self.losingTeam # take advantage of enum nature of PartyGlobals.TeamActivityTeams + for toonId in self.toonIds[winningTeam]: + self.toonIdsToJellybeanRewards[toonId] = PartyGlobals.TugOfWarFallInWinReward * beanMultiplier + for toonId in self.toonIds[self.losingTeam]: + self.toonIdsToJellybeanRewards[toonId] = PartyGlobals.TugOfWarFallInLossReward * beanMultiplier + else: + # if there is no obvious winner, check how far the teams moved + if abs(self.globalOffset) < PartyGlobals.TugOfWarTieThreshold: + # a tie + self.losingTeam = PartyGlobals.TeamActivityNeitherTeam + for toonId in self.getToonIdsAsList(): + self.toonIdsToJellybeanRewards[toonId] = PartyGlobals.TugOfWarTieReward * beanMultiplier + else: + if self.globalOffset > 0: + # right team wins + self.losingTeam = PartyGlobals.TeamActivityTeams.LeftTeam + else: + # left team wins + self.losingTeam = PartyGlobals.TeamActivityTeams.RightTeam + winningTeam = 1 - self.losingTeam # take advantage of enum nature of PartyGlobals.TeamActivityTeams + for toonId in self.toonIds[winningTeam]: + self.toonIdsToJellybeanRewards[toonId] = PartyGlobals.TugOfWarWinReward * beanMultiplier + for toonId in self.toonIds[self.losingTeam]: + self.toonIdsToJellybeanRewards[toonId] = PartyGlobals.TugOfWarLossReward * beanMultiplier + + + def getConclusionData(self): + return self.losingTeam + diff --git a/toontown/parties/DistributedPartyValentineDance20ActivityAI.py b/toontown/parties/DistributedPartyValentineDance20ActivityAI.py index ed5eaf2..3185aec 100644 --- a/toontown/parties/DistributedPartyValentineDance20ActivityAI.py +++ b/toontown/parties/DistributedPartyValentineDance20ActivityAI.py @@ -1,5 +1,16 @@ from direct.directnotify import DirectNotifyGlobal from direct.distributed.DistributedObjectAI import DistributedObjectAI - -class DistributedPartyValentineDance20ActivityAI(DistributedObjectAI): +from toontown.parties.DistributedPartyDanceActivityBaseAI import DistributedPartyDanceActivityBaseAI +from toontown.parties import PartyGlobals +class DistributedPartyValentineDance20ActivityAI(DistributedPartyDanceActivityBaseAI): notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyValentineDance20ActivityAI') + + def __init__(self, air, partyDoId, x, y, h): + self.notify.debug("Intializing.") + DistributedPartyDanceActivityBaseAI.__init__(self, + air, + partyDoId, + x, y, h, + PartyGlobals.ActivityIds.PartyValentineDance20, + PartyGlobals.DancePatternToAnims, + ) \ No newline at end of file diff --git a/toontown/parties/DistributedPartyValentineDanceActivityAI.py b/toontown/parties/DistributedPartyValentineDanceActivityAI.py index b40ed01..bc2a311 100644 --- a/toontown/parties/DistributedPartyValentineDanceActivityAI.py +++ b/toontown/parties/DistributedPartyValentineDanceActivityAI.py @@ -1,5 +1,15 @@ from direct.directnotify import DirectNotifyGlobal from direct.distributed.DistributedObjectAI import DistributedObjectAI - -class DistributedPartyValentineDanceActivityAI(DistributedObjectAI): +from toontown.parties.DistributedPartyDanceActivityBaseAI import DistributedPartyDanceActivityBaseAI +from toontown.parties import PartyGlobals +class DistributedPartyValentineDanceActivityAI(DistributedPartyDanceActivityBaseAI): notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyValentineDanceActivityAI') + + def __init__(self, air, partyDoId, x, y, h): + DistributedPartyDanceActivityBaseAI.__init__(self, + air, + partyDoId, + x, y, h, + PartyGlobals.ActivityIds.PartyValentineDance, + PartyGlobals.DancePatternToAnims, + ) \ No newline at end of file diff --git a/toontown/parties/DistributedPartyValentineJukebox40ActivityAI.py b/toontown/parties/DistributedPartyValentineJukebox40ActivityAI.py index a7d793f..dace66c 100644 --- a/toontown/parties/DistributedPartyValentineJukebox40ActivityAI.py +++ b/toontown/parties/DistributedPartyValentineJukebox40ActivityAI.py @@ -1,5 +1,18 @@ from direct.directnotify import DirectNotifyGlobal from direct.distributed.DistributedObjectAI import DistributedObjectAI +from toontown.parties.DistributedPartyJukeboxActivityBaseAI import DistributedPartyJukeboxActivityBaseAI +from toontown.parties import PartyGlobals -class DistributedPartyValentineJukebox40ActivityAI(DistributedObjectAI): +class DistributedPartyValentineJukebox40ActivityAI(DistributedPartyJukeboxActivityBaseAI): notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyValentineJukebox40ActivityAI') + + + def __init__(self, air, partyDoId, x, y, h): + DistributedPartyJukeboxActivityBaseAI.__init__(self, + air, + partyDoId, + x, y, h, + PartyGlobals.ActivityIds.PartyValentineJukebox40, + PartyGlobals.PhaseToMusicData40, + ) + self.notify.debug("Intializing.") \ No newline at end of file diff --git a/toontown/parties/DistributedPartyValentineJukeboxActivityAI.py b/toontown/parties/DistributedPartyValentineJukeboxActivityAI.py index 6844d59..dedc210 100644 --- a/toontown/parties/DistributedPartyValentineJukeboxActivityAI.py +++ b/toontown/parties/DistributedPartyValentineJukeboxActivityAI.py @@ -1,5 +1,26 @@ -from direct.directnotify import DirectNotifyGlobal -from direct.distributed.DistributedObjectAI import DistributedObjectAI +# ------------------------------------------------------------------------------- +# Contact: Edmundo Ruiz (Schell Games) +# Created: Oct 2008 +# +# Purpose: AI component that manages which toons are currently dancing, who entered +# and exited the dance floor, and broadcasts dance moves to all clients. +# ------------------------------------------------------------------------------- -class DistributedPartyValentineJukeboxActivityAI(DistributedObjectAI): - notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyValentineJukeboxActivityAI') +from toontown.parties.DistributedPartyJukeboxActivityBaseAI import DistributedPartyJukeboxActivityBaseAI + +from toontown.parties import PartyGlobals + +from direct.directnotify.DirectNotifyGlobal import * + +class DistributedPartyValentineJukeboxActivityAI(DistributedPartyJukeboxActivityBaseAI): + notify = directNotify.newCategory("DistributedPartyValentineJukeboxActivityAI") + + def __init__(self, air, partyDoId, x, y, h): + self.notify.debug("Intializing.") + DistributedPartyJukeboxActivityBaseAI.__init__(self, + air, + partyDoId, + x, y, h, + PartyGlobals.ActivityIds.PartyValentineJukebox, + PartyGlobals.PhaseToMusicData, + ) diff --git a/toontown/parties/DistributedPartyValentineTrampolineActivityAI.py b/toontown/parties/DistributedPartyValentineTrampolineActivityAI.py index 6012f59..f3984a4 100644 --- a/toontown/parties/DistributedPartyValentineTrampolineActivityAI.py +++ b/toontown/parties/DistributedPartyValentineTrampolineActivityAI.py @@ -1,5 +1,16 @@ from direct.directnotify import DirectNotifyGlobal from direct.distributed.DistributedObjectAI import DistributedObjectAI +from toontown.parties import PartyGlobals +from toontown.parties.DistributedPartyTrampolineActivityAI import DistributedPartyTrampolineActivityAI -class DistributedPartyValentineTrampolineActivityAI(DistributedObjectAI): +class DistributedPartyValentineTrampolineActivityAI(DistributedPartyTrampolineActivityAI): notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyValentineTrampolineActivityAI') + + def __init__(self, air, partyDoId, x, y, h): + DistributedPartyTrampolineActivityAI.__init__(self, + air, + partyDoId, + x, y, h, + PartyGlobals.ActivityIds.PartyValentineTrampoline, + ) + self.notify.debug("Intializing.") \ No newline at end of file diff --git a/toontown/parties/DistributedPartyVictoryTrampolineActivityAI.py b/toontown/parties/DistributedPartyVictoryTrampolineActivityAI.py index 41930de..dec2898 100644 --- a/toontown/parties/DistributedPartyVictoryTrampolineActivityAI.py +++ b/toontown/parties/DistributedPartyVictoryTrampolineActivityAI.py @@ -1,5 +1,13 @@ -from direct.directnotify import DirectNotifyGlobal -from direct.distributed.DistributedObjectAI import DistributedObjectAI +#------------------------------------------------------------------------------- +# Contact: Mark Wojtowicz +# Created: June 2010 +#------------------------------------------------------------------------------- -class DistributedPartyVictoryTrampolineActivityAI(DistributedObjectAI): - notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyVictoryTrampolineActivityAI') +from toontown.parties import PartyGlobals +from toontown.parties.DistributedPartyTrampolineActivityAI import DistributedPartyTrampolineActivityAI + +class DistributedPartyVictoryTrampolineActivityAI(DistributedPartyTrampolineActivityAI): + """ Reskinned trampoline for victory party holiday. """ + + def __init__(self, air, partyDoId, x, y, h): + DistributedPartyTrampolineActivityAI.__init__(self, air, partyDoId, x, y, h, actId=PartyGlobals.ActivityIds.PartyVictoryTrampoline) diff --git a/toontown/parties/DistributedPartyWinterCatchActivityAI.py b/toontown/parties/DistributedPartyWinterCatchActivityAI.py index 38a1944..46532f3 100644 --- a/toontown/parties/DistributedPartyWinterCatchActivityAI.py +++ b/toontown/parties/DistributedPartyWinterCatchActivityAI.py @@ -1,5 +1,15 @@ from direct.directnotify import DirectNotifyGlobal from direct.distributed.DistributedObjectAI import DistributedObjectAI +from toontown.parties import PartyGlobals +from toontown.parties.DistributedPartyCatchActivityAI import DistributedPartyCatchActivityAI -class DistributedPartyWinterCatchActivityAI(DistributedObjectAI): +class DistributedPartyWinterCatchActivityAI(DistributedPartyCatchActivityAI): notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyWinterCatchActivityAI') + + def __init__(self, air, partyDoId, x, y, h): + DistributedPartyCatchActivityAI.__init__(self, + air, + partyDoId, + x, y, h + ) + self.notify.debug("Intializing.") \ No newline at end of file diff --git a/toontown/parties/DistributedPartyWinterCogActivityAI.py b/toontown/parties/DistributedPartyWinterCogActivityAI.py index 0aaa08a..99a6929 100644 --- a/toontown/parties/DistributedPartyWinterCogActivityAI.py +++ b/toontown/parties/DistributedPartyWinterCogActivityAI.py @@ -1,5 +1,9 @@ from direct.directnotify import DirectNotifyGlobal from direct.distributed.DistributedObjectAI import DistributedObjectAI +from toontown.parties.DistributedPartyCogActivityAI import DistributedPartyCogActivityAI -class DistributedPartyWinterCogActivityAI(DistributedObjectAI): +class DistributedPartyWinterCogActivityAI(DistributedPartyCogActivityAI): notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyWinterCogActivityAI') + + def __init__(self, air, partyDoId, x, y, h): + DistributedPartyCogActivityAI.__init__(self, air, partyDoId, x, y, h) diff --git a/toontown/parties/DistributedPartyWinterTrampolineActivityAI.py b/toontown/parties/DistributedPartyWinterTrampolineActivityAI.py index bae8a3d..dcbd8ef 100644 --- a/toontown/parties/DistributedPartyWinterTrampolineActivityAI.py +++ b/toontown/parties/DistributedPartyWinterTrampolineActivityAI.py @@ -1,5 +1,10 @@ from direct.directnotify import DirectNotifyGlobal from direct.distributed.DistributedObjectAI import DistributedObjectAI +from .DistributedPartyTrampolineActivityAI import DistributedPartyTrampolineActivityAI +from . import PartyGlobals -class DistributedPartyWinterTrampolineActivityAI(DistributedObjectAI): +class DistributedPartyWinterTrampolineActivityAI(DistributedPartyTrampolineActivityAI): notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyWinterTrampolineActivityAI') + + def __init__(self, air, partyDoId, x, y, h, actId=PartyGlobals.ActivityIds.PartyWinterTrampoline): + DistributedPartyTrampolineActivityAI.__init__(self, air, partyDoId, x, y, h, actId) diff --git a/toontown/parties/InviteInfo.py b/toontown/parties/InviteInfo.py index f242bdf..b28138e 100644 --- a/toontown/parties/InviteInfo.py +++ b/toontown/parties/InviteInfo.py @@ -13,7 +13,7 @@ class InviteInfoBase: def __str__(self): string = 'inviteKey=%d ' % self.inviteKey string += 'partyId=%d ' % self.partyId - string += 'status=%s' % InviteStatus.getString(self.status) + string += 'status=%s' % InviteStatus(self.status) return string def __repr__(self): 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/PartyInfo.py b/toontown/parties/PartyInfo.py index 816f7cf..cb74db0 100644 --- a/toontown/parties/PartyInfo.py +++ b/toontown/parties/PartyInfo.py @@ -39,7 +39,7 @@ class PartyInfoBase: string += 'start=%s ' % self.startTime string += 'end=%s ' % self.endTime string += 'isPrivate=%s ' % self.isPrivate - string += 'inviteTheme=%s ' % InviteTheme.getString(self.inviteTheme) + string += 'inviteTheme=%s ' % InviteTheme(self.inviteTheme).name string += 'activityList=%s ' % self.activityList string += 'decors=%s ' % self.decors string += 'status=%s' % self.status 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/safezone/DistributedPartyGateAI.py b/toontown/safezone/DistributedPartyGateAI.py index 9356651..950b5d7 100644 --- a/toontown/safezone/DistributedPartyGateAI.py +++ b/toontown/safezone/DistributedPartyGateAI.py @@ -1,5 +1,76 @@ +# Referenced Anesidora to help make some functions +#------------------------------------------------------------------------------- +# Contact: Shawn Patton +# Created: Sep 2008 +# +# Purpose: AI side of the party hat which is where toon's go to access public +# parties. +#------------------------------------------------------------------------------- + from direct.directnotify import DirectNotifyGlobal from direct.distributed.DistributedObjectAI import DistributedObjectAI +from direct.task import Task +from toontown.parties import PartyGlobals class DistributedPartyGateAI(DistributedObjectAI): notify = DirectNotifyGlobal.directNotify.newCategory('DistributedPartyGateAI') + + def __init__(self, air): + DistributedObjectAI.__init__(self, air) + + def getPartyList(self, avId): + """ + Retrieves the list of all public parties for the given avatar ID (avId). + This method checks if the provided avId matches the sender's avatar ID. + If they do not match, it logs a suspicious event and returns without + sending the party list. If they match, it sends the list of all public + parties to the avatar. + Args: + avId (int): The avatar ID requesting the party list. + Returns: + None + """ + + senderId = self.air.getAvatarIdFromSender() + if avId != senderId: + self.air.writeServerEvent('suspicious', avId, 'DistributedPartyGateAI.getPartyList avId: %s does not match sender avId: %s' % (avId, senderId)) + return + self.sendUpdateToAvatarId(avId, 'listAllPublicParties', [self.air.partyManager.getAllPublicParties()]) + + def partyChoiceRequest(self, avId, shardId, zoneId): + """ + This function + 1. Checks if the avId matches the sender + 2. Gets all public parties + 3. Checks if the party is full + 4. Sends the party information to the avatar + """ + senderId = self.air.getAvatarIdFromSender() + if avId != senderId: + self.air.writeServerEvent('suspicious', avId, 'DistributedPartyGateAI.partyChoiceRequest avId: %s does not match sender avId: %s' % (avId, senderId)) + return + allPublicParties = self.air.partyManager.getAllPublicParties() + + if len(allPublicParties) == 0: + # No public parties available + self.sendUpdateToAvatarId(avId, 'partyRequestDenied', [PartyGlobals.PartyGateDenialReasons.Unavailable]) + return + else: + for party in allPublicParties: + if party[0] == shardId and party[1] == zoneId: + # Party found + if party[2] < PartyGlobals.MaxToonsAtAParty: + # Party is not full + self.sendUpdateToAvatarId(avId, 'setParty', [party]) + return + else: + # Party is full + self.sendUpdateToAvatarId(avId, 'partyRequestDenied', [PartyGlobals.PartyGateDenialReasons.Full]) + return + break # + + else: + # Party not found + self.sendUpdateToAvatarId(avId, 'partyRequestDenied', [PartyGlobals.PartyGateDenialReasons.Unavailable]) + return + \ No newline at end of file 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/toon/DistributedToonAI.py b/toontown/toon/DistributedToonAI.py index b5489fe..76966ef 100644 --- a/toontown/toon/DistributedToonAI.py +++ b/toontown/toon/DistributedToonAI.py @@ -3973,6 +3973,9 @@ class DistributedToonAI(DistributedPlayerAI.DistributedPlayerAI, DistributedSmoo partyInfo.status = PartyGlobals.PartyStatus.CanStart found = True break + else: + self.notify.info('setPartyCanStart partyId does not match partyId in hostedParties %s' % partyId) + print(self.hostedParties) if not found: self.notify.warning("setPartyCanStart can't find partyId %s" % partyId) 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..5017c8e 100644 --- a/toontown/uberdog/DistributedPartyManagerAI.py +++ b/toontown/uberdog/DistributedPartyManagerAI.py @@ -1,8 +1,1401 @@ -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]].name) + + # 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() + + + def getHostId(self, partyId): + # grab the host id from the party id + for hostId, party in self.hostAvIdToPartiesRunning.items(): + if party.partyInfo.partyId == partyId: + return hostId + diff --git a/toontown/uberdog/DistributedPartyManagerUD.py b/toontown/uberdog/DistributedPartyManagerUD.py index acff337..08f8f95 100644 --- a/toontown/uberdog/DistributedPartyManagerUD.py +++ b/toontown/uberdog/DistributedPartyManagerUD.py @@ -1,5 +1,1456 @@ -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 load(self): + """Load the current state of the partyToId dictionary from the JSON file.""" + with open(self.partyDbFilePath, 'r') as file: + self.partyToId = json.load(file) + + def putParty(self, hostId, startTime, endTime, isPrivate, inviteTheme, activities, decorations, status): + """ + Add a party to the database. + """ + self.load() + 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, + 'statusId': status + } + self.partyToId[partyId] = party + self.save() + return True + + + def getPartiesOfHost(self, hostId): + """ + Get all parties of a host. + """ + self.load() + 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. + """ + self.load() + for party in self.partyToId.values(): + if party['hostId'] == hostId and party['statusId'] == PartyGlobals.PartyStatus.Pending: + yield party + + def getPartiesAvailableToStart(self, thresholdTime): + """ + Get all parties that can start. + """ + self.load() + for party in self.partyToId.values(): + if party['startTime'] <= thresholdTime and party['statusId'] == 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. + """ + self.load() + for partyId in partyIds: + party = self.partyToId[partyId] + if future: + if party['startTime'] >= thresholdTime: + if not cancelled: + if party['statusId'] == PartyGlobals.PartyStatus.Pending: + yield party + else: + if party['statuId'] == PartyGlobals.PartyStatus.Cancelled: + yield party + else: + if party['startTime'] < thresholdTime: + if not cancelled: + if party['statusId'] == PartyGlobals.PartyStatus.Finished: + yield party + else: + if party['statusId'] == 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. + """ + self.load() + for party in self.partyToId.values(): + if party['hostId'] == hostId: + if future: + if party['startTime'] >= thresholdTime: + if not cancelled: + if party['statusId'] == PartyGlobals.PartyStatus.Pending: + yield party + else: + if party['statusId'] == PartyGlobals.PartyStatus.Cancelled: + yield party + else: + if party['startTime'] < thresholdTime: + if not cancelled: + if party['statusId'] == PartyGlobals.PartyStatus.Finished: + yield party + else: + if party['statusId'] == PartyGlobals.PartyStatus.Cancelled: + yield party + + + def getParty(self, partyId): + """ + Get a party by partyId. + """ + self.load() + return self.partyToId.get(partyId, None) + + def changePrivate(self, partyId, newPrivateStatus): + """ + Change a party to public or private. + """ + self.load() + if partyId in self.partyToId.values(): + self.partyToId[partyId]['isPrivate'] = newPrivateStatus + self.save() + return True + return False + + def deleteParty(self, partyId): + """ + Delete a party. + """ + self.load() + 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. + """ + self.load() + if partyId in self.partyToId.values(): + self.partyToId[partyId]['statusId'] = newPartyStatus + self.save() + return True + return False + + def forceFinishForStarted(self, thresholdTime): + """ + Force finish all started parties. + """ + self.load() + for party in self.partyToId.values(): + if party['endTime'] < thresholdTime and party['statusId'] == PartyGlobals.PartyStatus.Started: + party['statusId'] = PartyGlobals.PartyStatus.Finished + self.save() + + def forceNeverStartedForCanStart(self, thresholdTime): + """ + Force never started all parties that can start. + """ + self.load() + for party in self.partyToId.values(): + if party['endTime'] < thresholdTime and party['statusId'] == PartyGlobals.PartyStatus.Pending: + party['statusId'] = PartyGlobals.PartyStatus.NeverStarted + self.save() + + def getMultipleParties(self, partyIds): + """ + Get multiple parties by partyId. + """ + self.load() + for partyId in partyIds: + yield self.partyToId.get(partyId, None) + + def changeMultiplePartiesStatus(self, partyIds, newPartyStatus): + """ + Change the status of multiple parties. + """ + self.load() + for partyId in partyIds: + if partyId in self.partyToId.values(): + self.partyToId[partyId]['statusId'] = 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, + 'statusId': 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]['statusId'] = 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.info( "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) + 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 = tuple(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): + """ + Formats the party information from the given dictionary into a tuple. + Args: + partyInfoDict (dict): A dictionary containing party information with the following keys: + - 'partyId' (int): The ID of the party. + - 'hostId' (int): The ID of the host. + - 'startTime' (str): The start time of the party. + - 'endTime' (str): The end time of the party. + - 'activities' (tuple): The activities of the party. + - 'decorations' (tuple): The decorations of the party. + - 'isPrivate' (bool): A boolean indicating if the party is private. + - 'inviteTheme' (int): The theme of the invitation. + - 'statusId' (int): The status ID of the party. + Returns: + tuple: A tuple containing the formatted party information in the following order: + - partyId (int) + - hostId (int) + - startTime year (int) + - startTime month (int) + - startTime day (int) + - startTime hour (int) + - startTime minute (int) + - endTime year (int) + - endTime month (int) + - endTime day (int) + - endTime hour (int) + - endTime minute (int) + - isPrivate (bool) + - inviteTheme (int) + - formattedActivities (list of tuples): Each tuple contains 4 integers representing an activity. + - formattedDecors (list of tuples): Each tuple contains 4 integers representing a decoration. + - statusId (int) + """ + startTime = partyInfoDict['startTime'] + endTime = partyInfoDict['endTime'] + # convert the date strings to datetime + startTime = datetime.strptime(startTime, "%Y-%m-%d %H:%M:%S") + endTime = datetime.strptime(endTime, "%Y-%m-%d %H:%M:%S") + activities = partyInfoDict['activities'] + formattedActivities = [tuple(activity) for activity in activities] + + # 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) + decorations = partyInfoDict['decorations'] + formattedDecors = [tuple(decor) for decor in decorations] + 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.value and newPartyStatus == PartyGlobals.PartyStatus.Finished.value: + # 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 = tuple(self.partyDb.getPartiesOfHostThatCanStart(hostId)) + + partyFail = False + partyInfo = None + if len(hostedParties) == 0: + DistributedPartyManagerUD.notify.info( "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"] + # convert string of datetime to datetime + partyStartTime = datetime.strptime(partyStartTime, "%Y-%m-%d %H:%M:%S") + 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"] + # convert string of datetime to datetime + partyEndTime = datetime.strptime(partyEndTime, "%Y-%m-%d %H:%M:%S") + 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 = tuple(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 %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) + 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 = tuple(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')