From 54cd9c873678b4e9ae14544ddf49adbae48aa03d Mon Sep 17 00:00:00 2001 From: Open Toontown <57279094+opentoontown@users.noreply.github.com> Date: Sat, 17 Dec 2022 00:15:31 -0500 Subject: [PATCH] ai: Attempts to connect now --- otp/ai/MagicWordManagerAI.py | 596 ++++++++++++++++++++++++++ toontown/ai/ToontownAIRepository.py | 12 +- toontown/ai/ToontownGroupManager.py | 154 +++++++ toontown/effects/FireworkManagerAI.py | 126 ++++++ toontown/fishing/BingoManagerAI.py | 564 ++++++++++++++++++++++++ toontown/fishing/FishManagerAI.py | 198 +++++++++ toontown/toon/NPCDialogue.py | 177 ++++++++ toontown/toon/NPCDialogueManagerAI.py | 55 +++ toontown/uberdog/DataStoreAIClient.py | 140 ++++++ 9 files changed, 2016 insertions(+), 6 deletions(-) create mode 100644 otp/ai/MagicWordManagerAI.py create mode 100644 toontown/ai/ToontownGroupManager.py create mode 100644 toontown/effects/FireworkManagerAI.py create mode 100644 toontown/fishing/BingoManagerAI.py create mode 100644 toontown/fishing/FishManagerAI.py create mode 100644 toontown/toon/NPCDialogue.py create mode 100644 toontown/toon/NPCDialogueManagerAI.py create mode 100644 toontown/uberdog/DataStoreAIClient.py diff --git a/otp/ai/MagicWordManagerAI.py b/otp/ai/MagicWordManagerAI.py new file mode 100644 index 0000000..83de4e1 --- /dev/null +++ b/otp/ai/MagicWordManagerAI.py @@ -0,0 +1,596 @@ +from .AIBaseGlobal import * +from pandac.PandaModules import * +from direct.distributed import DistributedObjectAI +from direct.directnotify import DirectNotifyGlobal +from otp.otpbase import OTPGlobals +from direct.showbase import PythonUtil, GarbageReport, ContainerReport, MessengerLeakDetector +from direct.showbase import ContainerLeakDetector +from direct.showbase.PythonUtil import Functor, DelayedCall, formatTimeCompact +import string +import time +import re +from direct.task import Task + +class MagicWordManagerAI(DistributedObjectAI.DistributedObjectAI): + notify = DirectNotifyGlobal.directNotify.newCategory("MagicWordManagerAI") + + supportSuperchat = simbase.config.GetBool('support-superchat', 0) + supportRename = simbase.config.GetBool('support-rename', 0) + + # Fill in by subclass + GameAvatarClass = None + + # This will hold the local namespace we evaluate '~ai' messages + # within. + ExecNamespace = { } + + def __init__(self, air): + DistributedObjectAI.DistributedObjectAI.__init__(self, air) + + def setMagicWord(self, word, avId, zoneId, signature): + senderId = self.air.getAvatarIdFromSender() + + sender = self.air.doId2do.get(senderId, None) + if sender: + if senderId == avId: + sender = "%s/%s(%s)" % (sender.accountName, sender.name, senderId) + else: + sender = "%s/%s(%s) (for %d)" % (sender.accountName, sender.name, senderId, avId) + else: + sender = "Unknown avatar %d" % (senderId) + + self.notify.info("%s (%s) just said the magic word: %s" % (sender, signature, word)) + self.air.writeServerEvent('magic-word', senderId, "%s|%s|%s" % (sender, signature, word)) + if self.air.doId2do.has_key(avId): + av = self.air.doId2do[avId] + + try: + self.doMagicWord(word, av, zoneId, senderId) + except: + response = PythonUtil.describeException(backTrace = 1) + self.notify.warning("Ignoring error in magic word:\n%s" % response) + self.down_setMagicWordResponse(senderId, response) + else: + self.notify.info("Don't know avatar %d." % (avId)) + + def wordIs(self, word, w): + return word == w or word[:(len(w)+1)] == ('%s ' % w) + + def getWordIs(self, word): + # bind a word to self.wordIs and return a callable obj + return Functor(self.wordIs, word) + + def doMagicWord(self, word, av, zoneId, senderId): + wordIs = self.getWordIs(word) + + if wordIs("~rename"): + if (not self.supportRename): + self.notify.warning("Rename is not supported for %s, requested by %d" % (av.name, senderId)) + else: + name = string.strip(word[8:]) + if name == "": + response = "No name." + else: + av.d_setName(name) + + elif wordIs("~badname"): + self.notify.warning("Renaming inappropriately named toon %s (doId %d)." % (av.name, av.doId)) + name = "toon%d" % (av.doId % 1000000) + av.d_setName(name) + + elif wordIs("~chat"): + if (not self.supportSuperchat) and (senderId != av.doId): + self.notify.warning("Super chat is not supported for %s, requested by %d" % (av.name, senderId)) + else: + av.d_setCommonChatFlags(OTPGlobals.CommonChat) + self.notify.debug("Giving common chat permission to " + av.name) + elif wordIs("~superchat"): + if not self.supportSuperchat: + self.notify.warning("Super chat is not supported for " + av.name) + else: + av.d_setCommonChatFlags(OTPGlobals.SuperChat) + self.notify.debug("Giving super chat permission to " + av.name) + elif wordIs("~nochat"): + av.d_setCommonChatFlags(0) + self.notify.debug("Removing special chat permissions for " + av.name) + + elif wordIs("~listen"): + if (not self.supportSuperchat) and (senderId != av.doId): + self.notify.warning("Listen is not supported for %s, requested by %d" % (av.name, senderId)) + else: + # This is a client-side word. + if (senderId != av.doId): + self.sendUpdateToAvatarId(av.doId, 'setMagicWord', [word, av.doId, zoneId]) + + elif wordIs("~fix"): + anyChanged = av.fixAvatar() + if anyChanged: + response = "avatar fixed." + else: + response = "avatar does not need fixing." + self.down_setMagicWordResponse(senderId, response) + + self.down_setMagicWordResponse(senderId, response) + + elif wordIs("~who all"): + str = '' + for obj in self.air.doId2do.values(): + if hasattr(obj, "accountName"): + str += '%s %s\n' % (obj.accountName, obj.name) + if not str: + str = "No avatars." + self.down_setMagicWordResponse(senderId, str) + + elif wordIs("~ouch"): + if av.hp < 1: + av.b_setHp(0) + av.toonUp(1) + else: + av.b_setHp(1) + self.notify.debug("Only 1 hp for " + av.name) + elif wordIs("~sad"): + av.b_setHp(0) + self.notify.debug("Only 0 hp for " + av.name) + elif wordIs("~dead"): + av.takeDamage(av.hp) + self.notify.debug(av.name + " is dead") + elif wordIs("~waydead"): + av.takeDamage(av.hp) + av.b_setHp(-100) + self.notify.debug(av.name + " is way dead") + elif wordIs("~toonup"): + av.toonUp(av.maxHp) + self.notify.debug("Full heal for " + av.name) + elif wordIs('~hp'): + args = word.split() + hp = int(args[1]) + av.b_setHp(hp) + self.notify.debug('Set hp to %s for %s' % (hp, av.name)) + + elif wordIs("~ainotify"): + args = word.split() + n = Notify.ptr().getCategory(args[1]) + n.setSeverity( + {'error': NSError, + 'warning': NSWarning, + 'info': NSInfo, + 'debug': NSDebug, + 'spam': NSSpam,}[args[2]]) + + elif wordIs("~ghost"): + # Toggle ghost mode. Ghost mode == 2 indicates a magic + # word was the source. + if av.ghostMode: + av.b_setGhostMode(0) + else: + av.b_setGhostMode(2) + + elif wordIs('~immortal'): + # ~immortal toggles immortal mode on and off + # ~immortal 0/1 and ~immortal on/off sets the mode explicitly + args = word.split() + invalid = False + if len(args) > 1 and args[1] in ('0', 'off'): + immortal = False + elif len(args) > 1 and args[1] in ('1', 'on'): + immortal = True + elif len(args) > 1: + invalid = True + else: + immortal = not av.immortalMode + + if invalid: + self.down_setMagicWordResponse(senderId, 'unknown argument %s' % args[1]) + else: + # immortality + av.setImmortalMode(immortal) + if av.immortalMode: + response = 'immortality ON' + else: + response = 'immortality OFF' + self.down_setMagicWordResponse(senderId, response) + + elif wordIs("~dna"): + # Fiddle with your dna. + self.doDna(word, av, zoneId, senderId) + + elif wordIs('~ai'): + # Execute an arbitrary Python command on the AI. + command = string.strip(word[3:]) + self.notify.warning("Executing command '%s' from %s" % (command, senderId)) + text = self.__execMessage(command)[:simbase.config.GetInt("ai-debug-length",300)] + self.down_setMagicWordResponse( + senderId, text) + + elif wordIs('~ud'): + # Execute an arbitrary Python command on the ud. + print(word) + channel,command = re.match("~ud ([0-9]+) (.+)", word).groups() + channel = int(channel) + if(simbase.air.doId2do.get(channel)): + self.notify.warning("Passing command '%s' to %s from %s" % (command, channel, senderId)) + + try: + simbase.air.doId2do[channel].sendUpdate("execCommand", [command, self.doId, senderId, zoneId]) + except: + pass + + elif wordIs('~aiobjects'): + args = word.split() + from direct.showbase import ObjectReport + report = ObjectReport.ObjectReport('AI ~objects') + + if 'all' in args: + self.notify.info('printing full object set...') + report.getObjectPool().printObjsByType(printReferrers='ref' in args) + + if hasattr(self, 'baselineObjReport'): + self.notify.info('calculating diff from baseline ObjectReport...') + self.lastDiff = self.baselineObjReport.diff(report) + self.lastDiff.printOut(full=('diff' in args or 'dif' in args)) + + if 'baseline' in args or not hasattr(self, 'baselineObjReport'): + self.notify.info('recording baseline ObjectReport...') + if hasattr(self, 'baselineObjReport'): + self.baselineObjReport.destroy() + self.baselineObjReport = report + + self.down_setMagicWordResponse(senderId, 'objects logged') + + elif wordIs('~aiobjecthg'): + import gc + objs = gc.get_objects() + type2count = {} + for obj in objs: + tn = safeTypeName(obj) + type2count.setdefault(tn, 0) + type2count[tn] += 1 + count2type = invertDictLossless(type2count) + counts = count2type.keys() + counts.sort() + counts.reverse() + for count in counts: + print('%s: %s' % (count, count2type[count])) + self.down_setMagicWordResponse(senderId, '~aiobjecthg complete') + + elif wordIs('~aicrash'): + # TODO: require a typed explanation in production + # if we call notify.error directly, the magic word mgr will catch it + # self.notify.error doesn't seem to work either + DelayedCall(Functor(simbase.air.notify.error, '~aicrash: simulating an AI crash')) + + elif wordIs('~aicontainers'): + args = word.split() + limit = 30 + if 'full' in args: + limit = None + ContainerReport.ContainerReport('~aicontainers', log=True, limit=limit, threaded=True) + + elif wordIs('~aigarbage'): + args = word.split() + # it can take a LOOONG time to print out the garbage referrers and referents + # by reference (as opposed to by number) + full = ('full' in args) + safeMode = ('safe' in args) + verbose = ('verbose' in args) + delOnly = ('delonly' in args) + def handleGarbageDone(senderId, garbageReport): + self.down_setMagicWordResponse(senderId, 'garbage logged, %s AI cycles' % garbageReport.getNumCycles()) + # This does a garbage collection and dumps the list of leaked (uncollectable) objects to the AI log. + GarbageReport.GarbageReport('~aigarbage', fullReport=full, verbose=verbose, log=True, threaded=True, + doneCallback=Functor(handleGarbageDone, senderId), safeMode=safeMode, delOnly=delOnly) + + elif wordIs("~creategarbage"): + args = word.split() + num = 1 + if len(args) > 1: + num = int(args[1]) + GarbageReport._createGarbage(num) + self.down_setMagicWordResponse(senderId, 'leaked garbage created') + + elif wordIs('~leaktask'): + def leakTask(task): + return task.cont + taskMgr.add(leakTask, uniqueName('leakedTask')) + leakTask = None + self.down_setMagicWordResponse(senderId, 'leaked task created') + + elif wordIs('~aileakmessage'): + MessengerLeakDetector._leakMessengerObject() + self.down_setMagicWordResponse(senderId, 'messenger leak object created') + + elif wordIs('~leakContainer'): + ContainerLeakDetector._createContainerLeak() + self.down_setMagicWordResponse(senderId, 'leak container task created') + + elif wordIs('~aipstats'): + args = word.split() + hostname = None + port = None + if len(args) > 1: + hostname = args[1] + if len(args) > 2: + port = int(args[2]) + # make sure pstats is enabled + simbase.wantStats = 1 + Task.TaskManager.pStatsTasks = 1 + result = simbase.createStats(hostname, port) + connectionName = '%s' % hostname + if port is not None: + connectionName += ':%s' % port + if result: + response = 'connected AI pstats to %s' % connectionName + else: + response = 'could not connect AI pstats to %s' % connectionName + self.down_setMagicWordResponse(senderId, response) + + elif wordIs('~aiprofile'): + args = word.split() + if len(args) > 1: + num = int(args[1]) + else: + num = 5 + session = taskMgr.getProfileSession('~aiprofile') + session.setLogAfterProfile(True) + taskMgr.profileFrames(num, session) + self.down_setMagicWordResponse(senderId, 'profiling %s AI frames...' % num) + + elif wordIs('~aiframeprofile'): + args = word.split() + wasOn = bool(taskMgr.getProfileFrames()) + if len(args) > 1: + setting = bool(int(args[1])) + else: + setting = not wasOn + taskMgr.setProfileFrames(setting) + self.down_setMagicWordResponse( + senderId, + 'AI frame profiling %s%s' % (choice(setting, 'ON', 'OFF'), + choice(wasOn == setting, ' already', ''))) + + elif wordIs('~aitaskprofile'): + args = word.split() + wasOn = bool(taskMgr.getProfileTasks()) + if len(args) > 1: + setting = bool(int(args[1])) + else: + setting = not wasOn + taskMgr.setProfileTasks(setting) + self.down_setMagicWordResponse( + senderId, + 'AI task profiling %s%s' % (choice(setting, 'ON', 'OFF'), + choice(wasOn == setting, ' already', ''))) + + elif wordIs('~aitaskspikethreshold'): + from direct.task.TaskProfiler import TaskProfiler + args = word.split() + if len(args) > 1: + threshold = float(args[1]) + response = 'AI task spike threshold set to %ss' % threshold + else: + threshold = TaskProfiler.GetDefaultSpikeThreshold() + response = 'AI task spike threshold reset to %ss' % threshold + TaskProfiler.SetSpikeThreshold(threshold) + self.down_setMagicWordResponse(senderId, response) + + elif wordIs('~ailogtaskprofiles'): + args = word.split() + if len(args) > 1: + name = args[1] + else: + name = None + taskMgr.logTaskProfiles(name) + response = 'logged AI task profiles%s' % choice(name, ' for %s' % name, '') + self.down_setMagicWordResponse(senderId, response) + + elif wordIs('~aitaskprofileflush'): + args = word.split() + if len(args) > 1: + name = args[1] + else: + name = None + taskMgr.flushTaskProfiles(name) + response = 'flushed AI task profiles%s' % choice(name, ' for %s' % name, '') + self.down_setMagicWordResponse(senderId, response) + + elif wordIs('~aiobjectcount'): + simbase.air.printObjectCount() + self.down_setMagicWordResponse(senderId, 'logging AI distributed object count...') + + elif wordIs('~aitaskmgr'): + print(taskMgr) + self.down_setMagicWordResponse(senderId, 'logging AI taskMgr...') + + elif wordIs('~aijobmgr'): + print(jobMgr) + self.down_setMagicWordResponse(senderId, 'logging AI jobMgr...') + + elif wordIs('~aijobtime'): + args = word.split() + if len(args) > 1: + time = float(args[1]) + else: + time = None + response = '' + if time is None: + time = jobMgr.getDefaultTimeslice() + time = time * 1000. + response = 'reset AI jobMgr timeslice to %s ms' % time + else: + response = 'set AI jobMgr timeslice to %s ms' % time + time = time / 1000. + jobMgr.setTimeslice(time) + self.down_setMagicWordResponse(senderId, response) + + elif wordIs('~aidetectleaks'): + started = self.air.startLeakDetector() + self.down_setMagicWordResponse(senderId, + choice(started, + 'AI leak detector started', + 'AI leak detector already started', + )) + + elif wordIs('~aitaskthreshold'): + args = word.split() + if len(args) > 1.: + threshold = float(args[1]) + else: + threshold = None + response = '' + if threshold is None: + threshold = taskMgr.DefTaskDurationWarningThreshold + response = 'reset AI task duration warning threshold to %s' % threshold + else: + response = 'set AI task duration warning threshold to %s' % threshold + taskMgr.setTaskDurationWarningThreshold(threshold) + self.down_setMagicWordResponse(senderId, response) + + elif wordIs('~aimessenger'): + print(messenger) + self.down_setMagicWordResponse(senderId, 'logging AI messenger...') + + elif wordIs('~requestdeleted'): + requestDeletedDOs = self.air.getRequestDeletedDOs() + response = '%s requestDeleted AI objects%s' % ( + len(requestDeletedDOs), choice(len(requestDeletedDOs), ', logging...', '')) + s = '~requestDeleted: [' + for do, age in requestDeletedDOs: + s += '[%s, %s]' % (do.__class__.__name__, age) + s += ']' + self.notify.info(s) + if len(requestDeletedDOs): + response += '\noldest: %s, %s' % ( + requestDeletedDOs[0][0].__class__.__name__, + formatTimeCompact(requestDeletedDOs[0][1])) + self.down_setMagicWordResponse(senderId, response) + + elif wordIs('~aigptc'): + args = word.split() + if len(args) > 1. and hasattr(self.cr, 'leakDetector'): + gptcJob = self.cr.leakDetector.getPathsToContainers( + '~aigptc', args[1], Functor(self._handleGPTCfinished, senderId, args[1])) + else: + self.down_setMagicWordResponse(senderId, 'error') + + elif wordIs('~aigptcn'): + args = word.split() + if len(args) > 1. and hasattr(self.cr, 'leakDetector'): + gptcnJob = self.cr.leakDetector.getPathsToContainersNamed( + '~aigptcn', args[1], Functor(self._handleGPTCNfinished, senderId, args[1])) + else: + self.down_setMagicWordResponse(senderId, 'error') + + else: + # The word is not an AI-side magic word. If the sender is + # different than the target avatar, then pass the magic + # word down to the target client-side MagicWordManager to + # execute a client-side magic word. + # MPG this gets done in child class + #if (senderId != av.doId): + # self.sendUpdateToAvatarId(av.doId, 'setMagicWord', [word, av.doId, zoneId]) + return 0 + return 1 + + # MPG define in child class + """ + def doDna(self, word, av, zoneId, senderId): + # Handle the ~dna magic word: change your dna + + # Strip of the "~dna" part; everything else is parameters to + # AvatarDNA.updateToonProperties. + parms = string.strip(word[4:]) + + # Get a copy of the avatar's current DNA. + dna = ToonDNA.ToonDNA(av.dna.makeNetString()) + + # Modify it according to the user's parameter selection. + eval("dna.updateToonProperties(%s)" % (parms)) + + av.b_setDNAString(dna.makeNetString()) + response = "%s" % (dna.asTuple(),) + + self.down_setMagicWordResponse(senderId, response) + """ + + def _handleGPTCfinished(self, senderId, ct, gptcJob): + self.down_setMagicWordResponse(senderId, 'aigptc(%s) finished' % ct) + + def _handleGPTCNfinished(self, senderId, cn, gptcnJob): + self.down_setMagicWordResponse(senderId, 'aigptcn(%s) finished' % cn) + + def __execMessage(self, message): + if not self.ExecNamespace: + # Import some useful variables into the ExecNamespace initially. + exec('from pandac.PandaModules import *' in globals(), self.ExecNamespace) + #self.importExecNamespace() + + # Now try to evaluate the expression using ChatInputNormal.ExecNamespace as + # the local namespace. + try: + return str(eval(message, globals(), self.ExecNamespace)) + + except SyntaxError: + # Maybe it's only a statement, like "x = 1", or + # "import math". These aren't expressions, so eval() + # fails, but they can be exec'ed. + try: + exec(message in globals(), self.ExecNamespace) + return 'ok' + except: + exception = sys.exc_info()[0] + extraInfo = sys.exc_info()[1] + if extraInfo: + return str(extraInfo) + else: + return str(exception) + except: + exception = sys.exc_info()[0] + extraInfo = sys.exc_info()[1] + if extraInfo: + return str(extraInfo) + else: + return str(exception) + + def down_setMagicWordResponse(self, avId, response): + """down_setMagicWordResponse(self, avId, string response) + + Send a response to the avatar who said the magic word. + """ + self.sendUpdateToAvatarId(avId, 'setMagicWordResponse', [response]) + + def setWho(self, avIds): + # Sent by the client in response to ~who. + str = '' + for avId in avIds: + obj = self.air.doId2do.get(avId, None) + if not obj: + self.air.writeServerEvent('suspicious', avId, 'MagicWordManager.setWho not a valid avId: %s' % avId) + return + elif obj.__class__ == self.GameAvatarClass: + str += '%s %s\n' % (obj.accountName, obj.name) + if not str: + str = "No avatars." + + senderId = self.air.getAvatarIdFromSender() + self.down_setMagicWordResponse(senderId, str) + +class FakeAv: + # fake avatar object that we can pass in to prevent magic words from crashing + def __init__(self, senderId): + self.hp = 100 + self.doId = senderId + self.name = 'FakeAv' + def b_setHp(*args): + pass + def b_setMojo(*args): + pass + def toonUp(*args): + pass + +def magicWord(mw, av=None, zoneId=0, senderId=0): + if av is None: + av = FakeAv(senderId) + simbase.air.magicWordManager.doMagicWord(mw, av, zoneId, senderId) + +import builtins +builtins.magicWord = magicWord diff --git a/toontown/ai/ToontownAIRepository.py b/toontown/ai/ToontownAIRepository.py index 47e9bf6..59a13b1 100644 --- a/toontown/ai/ToontownAIRepository.py +++ b/toontown/ai/ToontownAIRepository.py @@ -15,7 +15,7 @@ from . import ToontownMagicWordManagerAI from toontown.tutorial import TutorialManagerAI from toontown.catalog import CatalogManagerAI from otp.ai import TimeManagerAI -import WelcomeValleyManagerAI +from . import WelcomeValleyManagerAI from toontown.building import DistributedBuildingMgrAI from toontown.building import DistributedTrophyMgrAI from toontown.estate import DistributedBankMgrAI @@ -39,7 +39,7 @@ from toontown.coghq import FactoryManagerAI from toontown.coghq import MintManagerAI from toontown.coghq import LawOfficeManagerAI from toontown.coghq import CountryClubManagerAI -import NewsManagerAI +from . import NewsManagerAI from toontown.hood import ZoneUtil from toontown.fishing import DistributedFishingPondAI from toontown.safezone import DistributedFishingSpotAI @@ -51,7 +51,7 @@ from toontown.toon import NPCToons from toontown.safezone import ButterflyGlobals from toontown.estate import EstateManagerAI from toontown.suit import SuitInvasionManagerAI -import HolidayManagerAI +from . import HolidayManagerAI from toontown.effects import FireworkManagerAI from toontown.coghq import CogSuitManagerAI from toontown.coghq import PromotionManagerAI @@ -73,10 +73,10 @@ import string import os import time -import DatabaseObject +from . import DatabaseObject from direct.distributed.PyDatagram import PyDatagram from direct.distributed.PyDatagramIterator import PyDatagramIterator -from ToontownAIMsgTypes import * +from .ToontownAIMsgTypes import * from otp.otpbase.OTPGlobals import * from toontown.distributed.ToontownDistrictAI import ToontownDistrictAI #from otp.distributed.DistributedDirectoryAI import DistributedDirectoryAI @@ -89,7 +89,7 @@ from toontown.parties import ToontownTimeManager from toontown.coderedemption.TTCodeRedemptionMgrAI import TTCodeRedemptionMgrAI from toontown.distributed.NonRepeatableRandomSourceAI import NonRepeatableRandomSourceAI -import ToontownGroupManager +from . import ToontownGroupManager if __debug__: import pdb diff --git a/toontown/ai/ToontownGroupManager.py b/toontown/ai/ToontownGroupManager.py new file mode 100644 index 0000000..290e7c0 --- /dev/null +++ b/toontown/ai/ToontownGroupManager.py @@ -0,0 +1,154 @@ +from toontown.toonbase.ToontownGlobals import * +from otp.otpbase import OTPGlobals + +# these are array indexs +# since there are no structs +GROUPMEMBER = 0 +GROUPINVITE = 1 + +class ToontownGroupManager: + + def __init__(self): + self.groupLists = [] + self.avIdDict = {} + #group is [[members],[invitees]] + + def cleanup(self): + self.groupLists = [] + self.avIdDict = {} + + def inviteToGroup(self, memberId, joinerId): + #print("Group Manager Invite to group") + group = self.getGroup(memberId) + joinerGroup = self.getGroup(joinerId) + if group and ((joinerId in group[GROUPMEMBER]) or (joinerId in group[GROUPINVITE])): + #joiner is already in the group + #print ("joiner in same group") + return None + elif joinerGroup and len(joinerGroup) > 1: + #joiner is in another group + #print ("joiner in another group") + return None + if group: + # lookup the member's group and add the joiner + group[GROUPINVITE].append(joinerId) + #self.avIdDict[joiner] = group + #print ("added joiner to group") + else: + # there is no group so add a new one + newGroupList = [[memberId],[joinerId]] + self.avIdDict[memberId] = newGroupList + self.groupLists.append(newGroupList) + group = newGroupList + #print ("creating new group") + #tell each group member that the joiner has been invited + for avId in group[GROUPMEMBER]: + if avId != joinerId: + avatar = simbase.air.doId2do.get(avId) + if avatar: + avatar.sendUpdate("receiveInvitePosted", [joinerId]) + + #send the invite message to joinerId + avatar = simbase.air.doId2do.get(joinerId) + if avatar: + avatar.sendUpdate("receiveGroupInvite", [group[GROUPMEMBER]]) + return group + + def useInviteToJoin(self, memberIdList, joinerId): + memberId = memberIdList[0] + joinerGroup = self.getGroup(joinerId) + if joinerGroup: + if len(joinerGroup) > 1: + return + else: + # if the joiner is in a group by themselves, remove that group + self.removeFromGroup(joinerId) + + group = self.getGroup(memberId) + if group and (joinerId in group[GROUPINVITE]): + group[GROUPINVITE].remove(joinerId) + group[GROUPMEMBER].append(joinerId) + self.avIdDict[joinerId] = group + + for avId in group[GROUPMEMBER]: + avatar = simbase.air.doId2do.get(avId) + if avatar: + if avId != joinerId: + avatar.sendUpdate("receiveJoinGroup", [[joinerId]]) + else: + avatar.sendUpdate("receiveJoinGroup", [group[GROUPMEMBER]]) + else: + #TODO tell the joiner that the group invitee was invalid + return + + def removeInvitation(self, memberId, joinerId): + group = self.getGroup(memberId) + if group: + #tell each group member that the invitation has been retracted + for avId in group[GROUPMEMBER]: + if avId != joinerId: + avatar = simbase.air.doId2do.get(avId) + if avatar: + avatar.sendUpdate("receiveInviteRemoved", [joinerId]) + + #tell the joiner that the invitation has been retracted + avatar = simbase.air.doId2do.get(joinerId) + if avatar: + avatar.sendUpdate("receiveGroupInviteRetract", [memberId]) + + + if (joinerId in group[GROUPMEMBER]): + #joiner is already in the group do nothing + pass + elif (joinerId in group[GROUPINVITE]): + group[GROUPINVITE].remove(joinerId) + else: + #there was no invitation + pass + + + def removeFromGroup(self, leaverId): + print("removeFromGroup") + group = self.getGroup(leaverId) + if group and (leaverId in group[GROUPMEMBER]): + print("Group found for %s" % (leaverId)) + #send everyone in the group a message memberId has left + for avId in group[GROUPMEMBER]: + if avId != leaverId: + avatar = simbase.air.doId2do.get(avId) + if avatar: + avatar.sendUpdate("receiveLeaveGroup", [[leaverId]]) + else: + avatar = simbase.air.doId2do.get(avId) + if avatar: + avatar.sendUpdate("receiveLeaveGroup", [group[GROUPMEMBER]]) + #remove the memberId from the groupList + group[GROUPMEMBER].remove(leaverId) + else: + #print("No group found for %s" % (leaverId)) + pass + + + # if the group is empty remove it + if group and len(group[GROUPMEMBER]) <= 1: + self.groupLists.remove(group) + for member in group[GROUPMEMBER]: + if self.avIdDict.has_key(member): + self.avIdDict.pop(member) + # clear the member's group affiliation + if self.avIdDict.has_key(leaverId): + self.avIdDict.pop(leaverId) + + def getGroup(self, memberId): + if self.avIdDict.has_key(memberId): + return self.avIdDict[memberId] + else: + return None + + + + + + + + \ No newline at end of file diff --git a/toontown/effects/FireworkManagerAI.py b/toontown/effects/FireworkManagerAI.py new file mode 100644 index 0000000..7b3ed4d --- /dev/null +++ b/toontown/effects/FireworkManagerAI.py @@ -0,0 +1,126 @@ +from direct.directnotify import DirectNotifyGlobal +import random +from direct.task import Task +from . import DistributedFireworkShowAI +from toontown.ai import HolidayBaseAI +from . import FireworkShow +from toontown.toonbase.ToontownGlobals import DonaldsDock, ToontownCentral, \ + TheBrrrgh, MinniesMelodyland, DaisyGardens, OutdoorZone, GoofySpeedway, DonaldsDreamland +import time + +class FireworkManagerAI(HolidayBaseAI.HolidayBaseAI): + """ + Manages Fireworks holidays + """ + + notify = DirectNotifyGlobal.directNotify.newCategory('FireworkManagerAI') + + zoneToStyleDict = { + # Donald's Dock + DonaldsDock : 5, + # Toontown Central + ToontownCentral : 0, + # The Brrrgh + TheBrrrgh : 4, + # Minnie's Melodyland + MinniesMelodyland : 3, + # Daisy Gardens + DaisyGardens : 1, + # Acorn Acres + OutdoorZone : 0, + # GS + GoofySpeedway : 0, + # Donald's Dreamland + DonaldsDreamland : 2, + } + + def __init__(self, air, holidayId): + HolidayBaseAI.HolidayBaseAI.__init__(self, air, holidayId) + # Dict from zone to DistFireworkShow objects + self.fireworkShows = {} + self.waitTaskName = 'waitStartFireworkShows' + + def start(self): + self.notify.info("Starting firework holiday: %s" % (time.ctime())) + self.waitForNextShow() + + def stop(self): + self.notify.info("Stopping firework holiday: %s" % (time.ctime())) + taskMgr.remove(self.waitTaskName) + self.stopAllShows() + + def startAllShows(self, task): + for hood in self.air.hoods: + showType = self.zoneToStyleDict.get(hood.canonicalHoodId) + if showType is not None: + self.startShow(hood.zoneId, showType) + + self.waitForNextShow() + return Task.done + + def waitForNextShow(self): + currentTime = time.localtime() + currentMin = currentTime[4] + currentSec = currentTime[5] + waitTime = ((60 - currentMin) * 60) - currentSec + self.notify.debug("Waiting %s seconds until next show" % (waitTime)) + taskMgr.doMethodLater(waitTime, self.startAllShows, self.waitTaskName) + + def startShow(self, zone, showType = -1, magicWord = 0): + """ + Start a show of showType in this zone. + Returns 1 if a show was successfully started. + Warns and returns 0 if a show was already running in this zone. + There can only be one show per zone. + """ + if self.fireworkShows.has_key(zone): + self.notify.warning("startShow: already running a show in zone: %s" % (zone)) + return 0 + self.notify.debug("startShow: zone: %s showType: %s" % (zone, showType)) + # Create a show, passing ourselves in so it can tell us when + # the show is over + show = DistributedFireworkShowAI.DistributedFireworkShowAI(self.air, self) + show.generateWithRequired(zone) + self.fireworkShows[zone] = show + # Currently needed to support legacy fireworks + if simbase.air.config.GetBool('want-old-fireworks', 0) or magicWord == 1: + show.d_startShow(showType, showType) + else: + show.d_startShow(self.holidayId, showType) + # Success! + return 1 + + def stopShow(self, zone): + """ + Stop a firework show in this zone. + Returns 1 if it did stop a show, warns and returns 0 if there is not one + """ + if not self.fireworkShows.has_key(zone): + self.notify.warning("stopShow: no show running in zone: %s" % (zone)) + return 0 + self.notify.debug("stopShow: zone: %s" % (zone)) + show = self.fireworkShows[zone] + del self.fireworkShows[zone] + show.requestDelete() + # Success! + return 1 + + def stopAllShows(self): + """ + Stop all firework shows this manager knows about in all zones. + Returns number of shows stopped by this command. + """ + numStopped = 0 + for zone, show in self.fireworkShows.items(): + self.notify.debug("stopAllShows: zone: %s" % (zone)) + show.requestDelete() + numStopped += 1 + self.fireworkShows.clear() + return numStopped + + def isShowRunning(self, zone): + """ + Is there currently a show running in this zone? + """ + return self.fireworkShows.has_key(zone) + diff --git a/toontown/fishing/BingoManagerAI.py b/toontown/fishing/BingoManagerAI.py new file mode 100644 index 0000000..4047400 --- /dev/null +++ b/toontown/fishing/BingoManagerAI.py @@ -0,0 +1,564 @@ +################################################################# +# class: BingoManagerAI.py +# +# Purpose: Manages the Bingo Night Holiday for all ponds in all +# hoods. It generates PondBingoManagerAI objects for +# every pond and shuts them down respectively. In +# addition, it should handle all Stat collection such +# as top jackpot of the night, top bingo players, and +# so forth. +# +# Note: Eventually, this will derive from the HolidayBase class +# and run each and ever Bingo Night, whenever that has +# been decided upon. +################################################################# + +################################################################# +# Direct Specific Modules +################################################################# +from direct.distributed import DistributedObjectAI +from direct.distributed.ClockDelta import * +from direct.directnotify import DirectNotifyGlobal +from direct.showbase import PythonUtil +from direct.task import Task + +################################################################# +# Toontown Specific Modules +################################################################# +from toontown.estate import DistributedEstateAI +from toontown.fishing import BingoGlobals +from toontown.fishing import DistributedFishingPondAI +from toontown.fishing import DistributedPondBingoManagerAI +from direct.showbase import RandomNumGen +from toontown.toonbase import ToontownGlobals +from toontown.hood import ZoneUtil + +################################################################# +# Python Specific Modules +################################################################# +import pickle +import os +import time + +################################################################# +# Globals and Constants +################################################################# +TTG = ToontownGlobals +BG = BingoGlobals + +class BingoManagerAI(object): +# __metaclass__ = PythonUtil.Singleton + notify = DirectNotifyGlobal.directNotify.newCategory("BingoManagerAI") + #notify.setDebug(True) + #notify.setInfo(True) + serverDataFolder = simbase.config.GetString('server-data-folder', "") + + DefaultReward = { TTG.DonaldsDock: [BG.MIN_SUPER_JACKPOT, 1], + TTG.ToontownCentral: [BG.MIN_SUPER_JACKPOT, 1], + TTG.TheBrrrgh: [BG.MIN_SUPER_JACKPOT, 1], + TTG.MinniesMelodyland: [BG.MIN_SUPER_JACKPOT, 1], + TTG.DaisyGardens: [BG.MIN_SUPER_JACKPOT, 1], + TTG.DonaldsDreamland: [BG.MIN_SUPER_JACKPOT, 1], + TTG.MyEstate: [BG.MIN_SUPER_JACKPOT, 1] } + + ############################################################ + # Method: __init__ + # Purpose: This method initializes the BingoManagerAI object + # and generates the PondBingoManagerAI. + # Input: air - The AI Repository. + # Output: None + ############################################################ + def __init__(self, air): + self.air = air + + # Dictionaries for quick reference to the DPMAI + self.doId2do = {} + self.zoneId2do = {} + self.hood2doIdList = { TTG.DonaldsDock: [], + TTG.ToontownCentral: [], + TTG.TheBrrrgh: [], + TTG.MinniesMelodyland: [], + TTG.DaisyGardens: [], + TTG.DonaldsDreamland: [], + TTG.MyEstate: [] } + + self.__hoodJackpots = {} + self.finalGame = BG.NORMAL_GAME + self.shard = str(air.districtId) + self.waitTaskName = 'waitForIntermission' + + # Generate the Pond Bingo Managers + self.generateBingoManagers() + + ############################################################ + # Method: start + # Purpose: This method "starts" each PondBingoManager for + # the Bingo Night Holidy. + # Input: None + # Output: None + ############################################################ + def start(self): + # Iterate through keys and change into "active" state + # for the pondBingoManagerAI + self.notify.info("Starting Bingo Night Event: %s" % (time.ctime())) + self.air.bingoMgr = self + # Determine current time so that we can gracefully handle + # an AI crash or reboot during Bingo Night. + currentMin = time.localtime()[4] + self.timeStamp = globalClockDelta.getRealNetworkTime() + initState = ((currentMin < BG.HOUR_BREAK_MIN) and ['Intro'] or ['Intermission'])[0] + + # CHEATS + #initState = 'Intermission' + for do in self.doId2do.values(): + do.startup(initState) + self.waitForIntermission() + + # tell everyone bingo night is starting + simbase.air.newsManager.bingoStart() + + + ############################################################ + # Method: stop + # Purpose: This method begins the process of shutting down + # bingo night. It is called whenever the + # BingoNightHolidayAI is told to close for the + # evening. + # Input: None + # Output: None + ############################################################ + def stop(self): + self.__startCloseEvent() + + ############################################################ + # Method: __shutdown + # Purpose: This method performs the actual shutdown sequence + # for the pond bingo manager. By this point, all + # of the PondBingoManagerAIs should have shutdown + # so we can safely close. + # Input: None + # Output: None + ############################################################ + def shutdown(self): + self.notify.info('__shutdown: Shutting down BingoManager') + + # tell everyone bingo night is stopping + simbase.air.newsManager.bingoEnd() + + if self.doId2do: + #self.notify.warning('__shutdown: Not all PondBingoManagers have shutdown! Manual Shutdown for Memory sake.') + for bingoMgr in self.doId2do.values(): + self.notify.info("__shutdown: shutting down PondBinfoManagerAI in zone %s" % bingoMgr.zoneId) + bingoMgr.shutdown() + self.doId2do.clear() + del self.doId2do + + self.air.bingoMgr = None + del self.air + del self.__hoodJackpots + + ############################################################ + # Method: __resumeBingoNight + # Purpose: This method resumes Bingo Night after an + # an intermission has taken place. This should + # start on the hour. + # Input: None + # Output: None + ############################################################ + def __resumeBingoNight(self, task): + self.__hoodJackpots = self.load() + for bingoMgr in self.doId2do.values(): + if bingoMgr.isGenerated(): + if self.finalGame: + bingoMgr.setFinalGame(self.finalGame) + bingoMgr.resumeBingoNight() + timeToWait = BG.getGameTime(BG.BLOCKOUT_CARD) + BG.TIMEOUT_SESSION + 5.0 + taskMgr.doMethodLater(timeToWait, self.__handleSuperBingoClose, 'SuperBingoClose') + + # If we have another game after this, then do not want to generate a + # new task to wait for the next intermission. + return Task.done + + ############################################################ + # Method: __handleSuperBingoClose + # Purpose: This method is responsible for logging the + # current hood jackpot amounts to the .jackpot + # "database" file. In addition, it initiates the + # shutdown of the BingoManagerAI if the final + # game of the evening has been played. + # Input: task - a task that is spawned by a doMethodLater + # Output: None + ############################################################ + def __handleSuperBingoClose(self, task): + # Save Jackpot Data to File + self.notify.info("handleSuperBingoClose: Saving Hood Jackpots to DB") + self.notify.info("handleSuperBingoClose: hoodJackpots %s" %(self.__hoodJackpots)) + for hood in self.__hoodJackpots.keys(): + if self.__hoodJackpots[hood][1]: + self.__hoodJackpots[hood][0] += BG.ROLLOVER_AMOUNT + # clamp it if it exceeds jackpot total + if self.__hoodJackpots[hood][0] > BG.MAX_SUPER_JACKPOT: + self.__hoodJackpots[hood][0] = BG.MAX_SUPER_JACKPOT + else: + self.__hoodJackpots[hood][1] = BG.MIN_SUPER_JACKPOT + + taskMgr.remove(task) + self.save() + if self.finalGame: + self.shutdown() + return + + self.waitForIntermission() + + ############################################################ + # Method: __handleIntermission + # Purpose: This wrapper method tells the intermission to + # start. + # Input: task - a task that is spawned by a doMethodLater + # Output: None + ############################################################ + def __handleIntermission(self, task): + self.__startIntermission() + + ############################################################ + # Method: getIntermissionTime + # Purpose: This method returns the time of when an + # intermission began. It is meant to provide a + # fairly accurate time countdown for the clients. + # Input: None + # Output: returns the timestamp of intermission start + ############################################################ + def getIntermissionTime(self): + return self.timeStamp + + ############################################################ + # Method: __startIntermission + # Purpose: This method is responsible for starting the + # hourly intermission for bingo night. + # Input: None + # Output: None + ############################################################ + def __startIntermission(self): + for bingoMgr in self.doId2do.values(): + bingoMgr.setFinalGame(BG.INTERMISSION) + + if not self.finalGame: + currentTime = time.localtime() + currentMin = currentTime[4] + currentSec = currentTime[5] + + # Calculate time until the next hour + waitTime = (60-currentMin)*60 - currentSec + sec = (currentMin - BG.HOUR_BREAK_MIN)*60 + currentSec + self.timeStamp = globalClockDelta.getRealNetworkTime() - sec + self.notify.info('__startIntermission: Timestamp %s'%(self.timeStamp)) + else: + # In case someone should decide that bingo night does not end on the hour, ie 30 past, + # then this will allow a five minute intermission to sync up the PBMgrAIs for the + # final game. + waitTime = BG.HOUR_BREAK_SESSION + self.timeStamp = globalClockDelta.getRealNetworkTime() + + self.waitTaskName = 'waitForEndOfIntermission' + self.notify.info('__startIntermission: Waiting %s seconds until Bingo Night resumes.' %(waitTime)) + taskMgr.doMethodLater(waitTime, self.__resumeBingoNight, self.waitTaskName) + return Task.done + + ############################################################ + # Method: __waitForIntermission + # Purpose: This method is responsible for calculating the + # wait time for the hourly intermission for bingo + # night. + # Input: None + # Output: None + ############################################################ + def waitForIntermission(self): + currentTime = time.localtime() + currentMin = currentTime[4] + currentSec = currentTime[5] + + # Calculate Amount of time needed for one normal game of Bingo from the + # Waitcountdown all the way to the gameover. (in secs) + if currentMin >= BG.HOUR_BREAK_MIN: + # If the AI starts during bingo night and after the intermission start(a crash or scheduled downtime), + # then immediately start the intermission to sync all the clients up for the next hour. + self.__startIntermission() + else: + waitTime = ((BG.HOUR_BREAK_MIN - currentMin)*60) - currentSec + self.waitTaskName = 'waitForIntermission' + self.notify.info("Waiting %s seconds until Final Game of the Hour should be announced." % (waitTime)) + taskMgr.doMethodLater(waitTime, self.__handleIntermission, self.waitTaskName) + + ############################################################ + # Method: generateBingoManagers + # Purpose: This method creates a PondBingoManager for each + # pond that is found within the hoods. It searches + # through each hood for pond objects and generates + # the corresponding ManagerAI objects. + # Input: None + # Output: None + ############################################################ + def generateBingoManagers(self): + # Create DPBMAI for all ponds in all hoods. + for hood in self.air.hoods: + self.createPondBingoMgrAI(hood) + + # Create DPBMAI for every pond in every active estate. + for estateAI in self.air.estateMgr.estate.values(): + self.createPondBingoMgrAI(estateAI) + + ############################################################ + # Method: addDistObj + # Purpose: This method adds the newly created Distributed + # object to the BingoManagerAI doId2do list for + # easy reference. + # Input: distObj + # Output: None + ############################################################ + def addDistObj(self, distObj): + self.notify.debug("addDistObj: Adding %s : %s" % (distObj.getDoId(), distObj.zoneId)) + self.doId2do[distObj.getDoId()] = distObj + self.zoneId2do[distObj.zoneId] = distObj + + def __hoodToUse(self, zoneId): + hood = ZoneUtil.getCanonicalHoodId(zoneId) + if hood >= TTG.DynamicZonesBegin: + hood = TTG.MyEstate + + return hood + + ############################################################ + # Method: createPondBingoMgrAI + # Purpose: This method generates PBMgrAI instances for + # each pond found in the specified hood. A hood + # may be an estate or an actual hood. + # Input: hood - HoodDataAI or EstateAI object. + # dynamic - Will be 1 only if an Estate was generated + # after Bingo Night has started. + # Output: None + ############################################################ + def createPondBingoMgrAI(self, hood, dynamic=0): + if hood.fishingPonds == None: + self.notify.warning("createPondBingoMgrAI: hood doesn't have any ponds... were they deleted? %s" % hood) + return + + for pond in hood.fishingPonds: + # First, optain hood id based on zone id that the pond is located in. + hoodId = self.__hoodToUse(pond.zoneId) + if hoodId not in self.hood2doIdList: + # for now don't start it for minigolf zone and outdoor zone + continue + + bingoMgr = DistributedPondBingoManagerAI.DistributedPondBingoManagerAI(self.air, pond) + bingoMgr.generateWithRequired(pond.zoneId) + + self.addDistObj(bingoMgr) + if hasattr(hood, "addDistObj"): + hood.addDistObj(bingoMgr) + pond.setPondBingoManager(bingoMgr) + + # Add the PBMgrAI reference to the hood2doIdList. + self.hood2doIdList[hoodId].append(bingoMgr.getDoId()) + + # Dynamic if this method was called when an estate was generated after + # Bingo Night has started. + if dynamic: + self.startDynPondBingoMgrAI(bingoMgr) + + ############################################################ + # Method: startDynPondBingoMgrAI + # Purpose: This method determines what state a Dynamic + # Estate PBMgrAI should start in, and then it tells + # the PBMgrAI to start. + # Input: bingoMgr - PondBongoMgrAI Instance + # Output: None + ############################################################ + def startDynPondBingoMgrAI(self, bingoMgr): + currentMin = time.localtime()[4] + + # If the dynamic estate is generated before the intermission starts + # and it is not the final game of the night, then the PBMgrAI should start + # in the WaitCountdown state. Otherwise, it should start in the intermission + # state so that it can sync up with all of the other Estate PBMgrAIs for the + # super bingo game. + initState = (((currentMin < BG.HOUR_BREAK_MIN) and (not self.finalGame)) and ['WaitCountdown'] or ['Intermission'])[0] + bingoMgr.startup(initState) + + ############################################################ + # Method: removePondBingoMgrAI + # Purpose: This method generates PBMgrAI instances for + # each pond found in the specified hood. A hood + # may be an estate or an actual hood. + # Input: doId - the doId of the PBMgrAI that should be + # removed from the dictionaries. + # Output: None + ############################################################ + def removePondBingoMgrAI(self, doId): + if self.doId2do.has_key(doId): + zoneId = self.doId2do[doId].zoneId + self.notify.info('removePondBingoMgrAI: Removing PondBingoMgrAI %s' %(zoneId)) + hood = self.__hoodToUse(zoneId) + self.hood2doIdList[hood].remove(doId) + del self.zoneId2do[zoneId] + del self.doId2do[doId] + else: + self.notify.debug('removeBingoManager: Attempt to remove invalid PondBingoManager %s' % (doId)) + + ############################################################ + # Method: SetFishForPlayer + # Purpose: This method adds the newly created Distributed + # object to the BingoManagerAI doId2do list for + # easy reference. + # Input: distObj + # Output: None + ############################################################ + def setAvCatchForPondMgr(self, avId, zoneId, catch): + self.notify.info('setAvCatchForPondMgr: zoneId %s' %(zoneId)) + if self.zoneId2do.has_key(zoneId): + self.zoneId2do[zoneId].setAvCatch(avId, catch) + else: + self.notify.info('setAvCatchForPondMgr Failed: zoneId %s' %(zoneId)) + + ############################################################ + # Method: getFileName + # Purpose: This method constructs the jackpot filename for + # a particular shard. + # Input: None + # Output: returns jackpot filename + ############################################################ + def getFileName(self): + """Figure out the path to the saved state""" + f = "%s%s.jackpot" % (self.serverDataFolder, self.shard) + return f + + ############################################################ + # Method: saveTo + # Purpose: This method saves the current jackpot ammounts + # to the specified file. + # Input: file - file to save jackpot amounts + # Output: None + ############################################################ + def saveTo(self, file): + pickle.dump(self.__hoodJackpots, file) + + ############################################################ + # Method: save + # Purpose: This method determines where to save the jackpot + # amounts. + # Input: None + # Output: None + ############################################################ + def save(self): + """Save data to default location""" + try: + fileName = self.getFileName() + backup = fileName+ '.jbu' + if os.path.exists(fileName): + os.rename(fileName, backup) + file = open(fileName, 'w') + file.seek(0) + self.saveTo(file) + file.close() + if os.path.exists(backup): + os.remove(backup) + except EnvironmentError: + self.notify.warning(str(sys.exc_info()[1])) + + ############################################################ + # Method: loadFrom + # Purpose: This method loads the jackpot amounts from the + # specified file. + # Input: File - file to load amount from + # Output: returns a dictionary of the jackpots for this shard + ############################################################ + def loadFrom(self, file): + # Default Jackpot Amount + jackpots = self.DefaultReward + try: + jackpots = pickle.load(file) + except EOFError: + pass + return jackpots + + ############################################################ + # Method: load + # Purpose: This method determines where to load the jackpot + # amounts. + # Input: None + # Output: None + ############################################################ + def load(self): + """Load Jackpot data from default location""" + fileName = self.getFileName() + try: + file = open(fileName+'.jbu', 'r') + if os.path.exists(fileName): + os.remove(fileName) + except IOError: + try: + file = open(fileName) + except IOError: + # Default Jackpot Amount + return self.DefaultReward + + file.seek(0) + jackpots = self.loadFrom(file) + file.close() + return jackpots + + ############################################################ + # Method: getSuperJackpot + # Purpose: This method returns the super jackpot amount for + # the specified zone. It calculates which hood + # the zone is in and returns the shared jackpot + # amount for that hood. + # Input: zoneId - retrieve jackpot for this zone's hood + # Output: returns jackpot for hood that zoneid is found in + ############################################################ + def getSuperJackpot(self, zoneId): + hood = self.__hoodToUse(zoneId) + self.notify.info('getSuperJackpot: hoodJackpots %s \t hood %s' % (self.__hoodJackpots, hood)) + return self.__hoodJackpots.get(hood, [BG.MIN_SUPER_JACKPOT])[0] + + ############################################################ + # Method: __startCloseEvent + # Purpose: This method starts to close Bingo Night down. One + # more super card game will be played at the end + # of the hour(unless the times are changed). + # Input: None + # Output: None + ############################################################ + def __startCloseEvent(self): + self.finalGame = BG.CLOSE_EVENT + + if self.waitTaskName == 'waitForIntermission': + taskMgr.remove(self.waitTaskName) + self.__startIntermission() + + ############################################################ + # Method: handleSuperBingoWin + # Purpose: This method handles a victory when a super + # bingo game has been one. It updates the jackpot + # amount and tells each of the other ponds in that + # hood that they did not win. + # Input: zoneId - pond who won the bingo game. + # Output: None + ############################################################ + def handleSuperBingoWin(self, zoneId): + # Reset the Jackpot and unmark the dirty bit. + + hood = self.__hoodToUse(zoneId) + self.__hoodJackpots[hood][0] = self.DefaultReward[hood][0] + self.__hoodJackpots[hood][1] = 0 + + # tell everyone who won + #simbase.air.newsManager.bingoWin(zoneId) + + # Tell the other ponds that they did not win and should handle the loss + for doId in self.hood2doIdList[hood]: + distObj = self.doId2do[doId] + if distObj.zoneId != zoneId: + self.notify.info("handleSuperBingoWin: Did not win in zone %s" %(distObj.zoneId)) + distObj.handleSuperBingoLoss() + + diff --git a/toontown/fishing/FishManagerAI.py b/toontown/fishing/FishManagerAI.py new file mode 100644 index 0000000..fa48014 --- /dev/null +++ b/toontown/fishing/FishManagerAI.py @@ -0,0 +1,198 @@ +from otp.ai.AIBaseGlobal import * +from direct.task import Task +from direct.directnotify import DirectNotifyGlobal +from toontown.quest import Quests +from toontown.toon import NPCToons +import random +from direct.showbase import PythonUtil +from . import FishGlobals +from toontown.toonbase import TTLocalizer +from . import FishBase +from . import FishGlobals +from toontown.hood import ZoneUtil +from toontown.toonbase import ToontownGlobals + + +class FishManagerAI: + + notify = DirectNotifyGlobal.directNotify.newCategory("FishManagerAI") + + def __init__(self, air): + self.air = air + + def __chooseItem(self, av, zoneId): + rodId = av.getFishingRod() + rand = random.random() * 100.0 + for cutoff in FishGlobals.SortedProbabilityCutoffs: + if rand <= cutoff: + itemType = FishGlobals.ProbabilityDict[cutoff] + self.notify.debug("__chooseItem: %s" % (itemType)) + return itemType + self.notify.warning("Somehow we did not choose an item, returning boot") + return FishGlobals.BootItem + + def __chooseFish(self, av, zoneId): + rodId = av.getFishingRod() + branchZone = ZoneUtil.getCanonicalBranchZone(zoneId) + success, genus, species, weight = FishGlobals.getRandomFishVitals(branchZone, rodId) + return (success, genus, species, weight) + + def recordCatch(self, avId, zoneId, pondZoneId): + # Chooses an item to find in the water. Returns a (code, + # item) pair where code indicates the type of item, and item + # is the particular item id. + av = self.air.doId2do.get(avId) + if not av: + return (None, None) + + #check to see if bingo cheating is turned on + if av.bingoCheat: + # Fished up a boot + self.notify.debug("av: %s caught the boot" % (avId)) + rodId = av.getFishingRod() + self.air.writeServerEvent("fishedBoot", avId, "%s|%s" % (rodId, zoneId)) + self.air.handleAvCatch(avId, pondZoneId, FishGlobals.BingoBoot) + return (FishGlobals.BootItem, None) + + # First, check for a quest item. + item = self.air.questManager.findItemInWater(av, zoneId) + if item: + self.notify.debug("av: %s caught quest item: %s" % (avId, item)) + # Write to server logs + rodId = av.getFishingRod() + self.air.writeServerEvent("fishedQuestItem", avId, "%s|%s|%s" % (rodId, zoneId, item)) + return (FishGlobals.QuestItem, item) + + # Ok, no quest item, now let's see what you pull out + # Could be a fish, jellybeans, a boot, shirts, etc + itemType = self.__chooseItem(av, zoneId) + + if itemType == FishGlobals.FishItem: + # Choose which fish (this may come back with the boot too) + success, genus, species, weight = self.__chooseFish(av, zoneId) + if success: + # Ok, you found a fish + self.air.handleAvCatch(avId, pondZoneId, (genus, species)) + fish = FishBase.FishBase(genus, species, weight) + # do you already have one like it? + inTank = av.fishTank.hasFish(genus, species) + if inTank: + # TODO: This is a bit wasteful to loop through the fish tank twice + hasBiggerAlready = av.fishTank.hasBiggerFish(genus, species, weight) + else: + # If we already know it is not in the tank, no sense searching around + # to see if we have one bigger + hasBiggerAlready = 0 + added = av.addFishToTank(fish) + if added: + self.notify.debug("av: %s caught fish: %s %s %s" % + (avId, genus, species, weight)) + # See what the collect result will be + # NOTE: this does not actually collect the fish. The NPC + # fisherman does that work + collectResult = av.fishCollection.getCollectResult(fish) + + # Catch a fish, get a heal + # Nope - changed my mind. Fishing does not heal anymore + # It is strange to be able to heal out on the streets, and while + # you are in the playground you heal anyways + # av.toonUp(FishGlobals.HealAmount) + + # Write to server logs + rodId = av.getFishingRod() + self.air.writeServerEvent("fishedFish", avId, "%s|%s|%s|%s|%s|%s" % + (rodId, zoneId, genus, species, weight, fish.getValue())) + if collectResult == FishGlobals.COLLECT_NO_UPDATE: + return (FishGlobals.FishItem, fish) + elif collectResult == FishGlobals.COLLECT_NEW_ENTRY: + # If it is not in our tank also, it really is a new entry + if not inTank: + return (FishGlobals.FishItemNewEntry, fish) + # Ok, we already have one in our tank. If we do not already + # have a bigger one it is a new record + elif not hasBiggerAlready: + return (FishGlobals.FishItemNewRecord, fish) + # Otherwise, just a normal catch + else: + return (FishGlobals.FishItem, fish) + elif collectResult == FishGlobals.COLLECT_NEW_RECORD: + # If we have one of these in our tank, check to see if + # it is bigger. If the one we already have in our tank is bigger + # then we do not get a new record set. + if hasBiggerAlready: + # No new record, we already have this fish + # beat, but it is still in our tank - we have + # not sold it yet so it is not in our + # collection. + return (FishGlobals.FishItem, fish) + else: + return (FishGlobals.FishItemNewRecord, fish) + else: + self.notify.error("unrecognized collectResult: %s" % (collectResult)) + else: + self.notify.debug("av: %s is over the tank limit" % (avId)) + return (FishGlobals.OverTankLimit, None) + else: + # If you did not choose a fish, you get the boot + self.notify.debug("av: %s tried to catch fish, but got the boot" % (avId)) + rodId = av.getFishingRod() + self.air.writeServerEvent("fishedBoot", avId, "%s|%s" % (rodId, zoneId)) + self.air.handleAvCatch(avId, pondZoneId, FishGlobals.BingoBoot) + return (FishGlobals.BootItem, None) + elif itemType == FishGlobals.BootItem: + # Fished up a boot + self.notify.debug("av: %s caught the boot" % (avId)) + rodId = av.getFishingRod() + self.air.writeServerEvent("fishedBoot", avId, "%s|%s" % (rodId, zoneId)) + self.air.handleAvCatch(avId, pondZoneId, FishGlobals.BingoBoot) + return (FishGlobals.BootItem, None) + elif itemType == FishGlobals.JellybeanItem: + # Fished up some jellybeans + rodId = av.getFishingRod() + jellybeanAmount = FishGlobals.Rod2JellybeanDict[rodId] + av.addMoney(jellybeanAmount) + self.notify.debug("av: %s caught %s jellybeans" % (avId, jellybeanAmount)) + self.air.writeServerEvent("fishedJellybeans", avId, "%s|%s|%s" % (rodId, zoneId, jellybeanAmount)) + return (FishGlobals.JellybeanItem, jellybeanAmount) + + def creditFishTank(self, av): + """ + Do all the work of selling the tank and updating the collection. + Also updates your trophy status and maxHP if needed. + Returns 1 if you earned a trophy, 0 if you did not. + """ + assert(self.notify.debug("creditFishTank av: %s is selling all fish" % (av.getDoId()))) + oldBonus = int(len(av.fishCollection)/FishGlobals.FISH_PER_BONUS) + + # give the avatar jellybeans in exchange for his fish + value = av.fishTank.getTotalValue() + av.addMoney(value) + + # update the avatar collection for each fish + for fish in av.fishTank.fishList: + av.fishCollection.collectFish(fish) + + # clear out the fishTank + av.b_setFishTank([],[],[]) + + # update the collection in the database + av.d_setFishCollection(*av.fishCollection.getNetLists()) + + newBonus = int(len(av.fishCollection)/FishGlobals.FISH_PER_BONUS) + if newBonus > oldBonus: + self.notify.info("avatar %s gets a bonus: old: %s, new: %s" % (av.doId, oldBonus, newBonus)) + oldMaxHp = av.getMaxHp() + newMaxHp = min(ToontownGlobals.MaxHpLimit, oldMaxHp + newBonus - oldBonus) + av.b_setMaxHp(newMaxHp) + # Also, give them a full heal + av.toonUp(newMaxHp) + # update the av's trophy list + newTrophies = av.getFishingTrophies() + trophyId = len(newTrophies) + newTrophies.append(trophyId) + av.b_setFishingTrophies(newTrophies) + self.air.writeServerEvent("fishTrophy", av.doId, "%s" % (trophyId)) + return 1 + else: + assert(self.notify.debug("avatar %s no bonus: old: %s, new: %s" % (av.doId, oldBonus, newBonus))) + return 0 diff --git a/toontown/toon/NPCDialogue.py b/toontown/toon/NPCDialogue.py new file mode 100644 index 0000000..354d602 --- /dev/null +++ b/toontown/toon/NPCDialogue.py @@ -0,0 +1,177 @@ +from toontown.toonbase import TTLocalizer +from direct.distributed.ClockDelta import * +from direct.directnotify import DirectNotifyGlobal +from . import DistributedNPCToonBaseAI +from direct.task import Task +import random + +class NPCDialogue: + """ + The NPC dialogue for a given topic and set of participants + """ + + notify = DirectNotifyGlobal.directNotify.newCategory("NPCDialogue") + + def __init__(self, participant, dialogueTopic): + self.participants = {} + + if dialogueTopic in TTLocalizer.toontownDialogues: + self.topic = dialogueTopic + else: + self.notify.warning("Dialogue does not exist: %s" %dialogueTopic) + self.topic = TTLocalizer.BoringTopic + + self.conversation = TTLocalizer.toontownDialogues[self.topic] + + if participant and isinstance(participant, DistributedNPCToonBaseAI.DistributedNPCToonBaseAI): + self.addParticipant(participant) + self.participantProgress = 0 + self.currentParticipant = participant.npcId + else: + self.notify.warning("Participant does not exist: %s" %participant) + self.participantProgress = 0 + self.currrentParticipant = None + + def calcMaxNumMsgs(self): + """ + Find the participant that has the most number of things to say + """ + self.maxNumMsgs = 0 + for participant, spiel in self.conversation.items(): + if len(spiel)>self.maxNumMsgs and participant[1] in self.participants: + self.maxNumMsgs = len(spiel) + + def getTopic(self): + """ + Accessor function for topic + """ + return self.topic + + def addParticipant(self, participant): + """ + Add a new participant + """ + if self.getNumParticipants() > self.getMaxParticipants(): + return False + if participant: + for partPos in self.conversation.keys(): + if partPos[1] == participant.npcId: + if not (participant.npcId in self.participants): + self.participants[participant.npcId] = [participant] + else: + if participant not in self.participants[participant.npcId]: + self.participants[participant.npcId].append(participant) + else: + self.notify.warning("Participant: %s already in the conversation" %participant) + self.calcMaxNumMsgs() + return True + self.notify.warning("Participant: %s should not be in conversation" %participant) + return False + + def removeParticipant(self, participant): + """ + Remove a participant + """ + if participant.npcId in self.participants: + if participant.npcId == self.currentParticipant: + self.getNextParticipant() + self.participants[participant.npcId].remove(participant) + if self.participants[participant.npcId] == []: + del self.participants[participant.npcId] + self.calcMaxNumMsgs() + return True + return False + + def getNextParticipant(self): + while 1: + self.currentParticipant = self.calcNextParticipant() + if self.currentParticipant in self.participants: + break + + def calcNextParticipant(self): + """ + Returns the next in line to talk + """ + nextParticipant = None + + for partPos in self.conversation.keys(): + if partPos[1] == self.currentParticipant: + nextPos = partPos[0]+1 + break + if nextPos>len(self.conversation): + nextPos = 1 + self.participantProgress = (self.participantProgress+1)%self.maxNumMsgs + for partPos in self.conversation.keys(): + if partPos[0] == nextPos: + nextParticipant = partPos[1] + return nextParticipant + + return self.currentParticipant + + + def getMaxParticipants(self): + """ + Number of conversation pieces provided in TTLocalizer + """ + return len(self.conversation) + + def getNumParticipants(self): + """ + Returns the number of NPC's currently participating + """ + return len(self.participants) + + def isRunning(self): + if taskMgr.hasTaskNamed("Dialogue"+self.topic): + return True + + def start(self): + """ + Start up a dialogue amongst the participants + """ + self.nextChatTime = 0 + + taskMgr.add(self.__blather, "Dialogue"+self.topic) + + return True + + def stop(self): + """ + This conversation is over! + """ + taskMgr.remove("Dialogue"+self.topic) + + def __blather(self, task): + """ + Speak in turn + """ + now = globalClock.getFrameTime() + if now < self.nextChatTime: + return Task.cont + + if not self.currentParticipant: + return Task.done + + # Increment the participantProgress + for partPos in self.conversation.keys(): + if partPos[1] == self.currentParticipant: + convKey = partPos + break + if self.participantProgress >= len(self.conversation[convKey]): + self.getNextParticipant() + return Task.cont + + # Select the current spiel + #msg = self.conversation[self.participants[self.currentParticipant]][self.participantProgress] + + for participant in self.participants[self.currentParticipant]: + chatFlags = CFSpeech | CFTimeout + + participant.sendUpdate("setChat", [self.topic, convKey[0], convKey[1], self.participantProgress, chatFlags]) + + self.getNextParticipant() + + # Delay before next message + self.nextChatTime = now + 5.0 + + return Task.cont \ No newline at end of file diff --git a/toontown/toon/NPCDialogueManagerAI.py b/toontown/toon/NPCDialogueManagerAI.py new file mode 100644 index 0000000..1eedc18 --- /dev/null +++ b/toontown/toon/NPCDialogueManagerAI.py @@ -0,0 +1,55 @@ +from toontown.toonbase import TTLocalizer +from direct.directnotify import DirectNotifyGlobal +from . import NPCDialogue + +class NPCDialogueManagerAI: + """ + Create and distroy dialogues here. + """ + + notify = DirectNotifyGlobal.directNotify.newCategory("NPCDialogueManagerAI") + + def __init__(self): + self.dialogues = [] + + def createNewDialogue(self, participant, dialogueTopic): + """ + Create a new dialogue + """ + dialogue = NPCDialogue.NPCDialogue(participant, dialogueTopic) + result = dialogue.start() + if result: + self.dialogues.append(dialogue) + return result + + def requestDialogue(self, participant, dialogueTopic): + """ + Request to be added to the dialogue: dialogueTopic + """ + for dialogue in self.dialogues: + if dialogue.getTopic() == dialogueTopic: + result = dialogue.addParticipant(participant) + if result and not dialogue.isRunning(): + result = dialogue.start() + return result + + result = self.createNewDialogue(participant, dialogueTopic) + return result + + def leaveDialogue(self, participant, dialogueTopic): + """ + Stop participating in this dialogue + """ + result = False + for dialogue in self.dialogues: + if dialogue.getTopic() == dialogueTopic: + result = dialogue.removeParticipant(participant) + + if dialogue.getNumParticipants() == 0: + dialogue.stop() + try: + self.dialogues.remove(dialogue) + except: + self.notify.warning("Couldn't find the dialogue: %s" %dialogue) + + return result \ No newline at end of file diff --git a/toontown/uberdog/DataStoreAIClient.py b/toontown/uberdog/DataStoreAIClient.py new file mode 100644 index 0000000..87fb17d --- /dev/null +++ b/toontown/uberdog/DataStoreAIClient.py @@ -0,0 +1,140 @@ +from direct.directnotify.DirectNotifyGlobal import directNotify +from toontown.uberdog import DataStoreGlobals +from direct.showbase.DirectObject import DirectObject +import pickle + +class DataStoreAIClient(DirectObject): + """ + This class should be instantiated by any class that needs to + access an Uberdog data store. + + The client, as it is now, has the ability to create and destroy + DataStores on the Uberdog. This is mainly provided for backwards + compatibility with the Toontown architecture where the logic has + already been written for the AI side of things. + + For example, the HolidayManagerAI is something that could feasably + be run on the Uberdog, however it's already well established on + the AI. For this reason, we'll allow the HolidayManagerAI to + create and destroy data stores as it needs to. + + All it takes is one request to the Uberdog to carry out one of + these operations. Any further requests for data to an already + destroyed store will go unanswered. + + In the future, we should make attempts to keep the create/destroy + control on the Uberdog. That way, we have only one point of control + rather than several various AIs who may not be entirely in sync. + """ + + notify = directNotify.newCategory('DataStoreAIClient') + wantDsm = simbase.config.GetBool('want-ddsm', 1) + + def __init__(self,air,storeId,resultsCallback): + """ + storeId is a unique identifier to the type of store + the client wishes to connect to. There will only be + one store of this type on the Uberdog at any given time. + + resultsCallback is a function that accepts one argument, + the results returned from a query. The format of this + result argument is defined in the store's class definition. + """ + + if self.wantDsm: + self.__storeMgr = air.dataStoreManager + self.__storeId = storeId + self.__resultsCallback = resultsCallback + self.__storeClass = DataStoreGlobals.getStoreClass(storeId) + self.__queryTypesDict = self.__storeClass.QueryTypes + self.__queryStringDict = dict(zip(self.__queryTypesDict.values(), + self.__queryTypesDict.keys())) + self.__enabled = False + + def openStore(self): + """ + Attempt to connect to the store defined by the storeId in the + __init__() function. If no store of this type is present on + the Uberdog, the store is created at this time. Queries can now + be sent to the store and replies from the store will be processed + by the client. + """ + if self.wantDsm: + self.__storeMgr.startStore(self.__storeId) + self.__startClient() + + def closeStore(self): + """ + This client will no longer receive results from the store. Also, + the store, if present on the Uberdog, will now be shutdown and all + data destroyed. Do not use this method unless you are sure that + the data is no longer needed by this, or any other, client. + """ + if self.wantDsm: + self.__stopClient() + self.__storeMgr.stopStore(self.__storeId) + + def isOpen(self): + return self.__enabled + + def getQueryTypes(self): + return self.__queryTypesDict.keys() + + def getQueryTypeString(self,qId): + return self.__queryStringDict.get(qId,None) + + def sendQuery(self,queryTypeString,queryData): + """ + Sends a query to the data store. The format of the query is + defined in the store's class definition. + """ + if self.__enabled: + qId = self.__queryTypesDict.get(queryTypeString,None) + if qId is not None: + query = (qId,queryData) + # pack the data to be sent to the Uberdog store. + pQuery = pickle.dumps(query) + self.__storeMgr.queryStore(self.__storeId,pQuery) + else: + self.notify.debug('Tried to send invalid query type: \'%s\'' % (queryTypeString,)) + else: + self.notify.warning('Client currently stopped. \'%s\' query will fail.' % (queryTypeString,)) + + def receiveResults(self,data): + """ + Upon receiving a query, the store will respond with a result. + This function will call the resultsCallback function with the + result data as its sole argument. Try to treat the + resultsCallback function as an event that is fired whenever + the client receives data from the store. + """ + # unpack the results from the Uberdog store. + + if data == 'Store not found': + self.notify.debug('%s not present on uberdog. Query dropped.' %(self.__storeClass.__name__,)) + else: + results = pickle.loads(data) + self.__resultsCallback(results) + + def __startClient(self): + """ + Allow the client to send queries and receive results from its + associated data store. + """ + self.accept('TDS-results-%d'%self.__storeId,self.receiveResults) + self.__enabled = True + + def __stopClient(self): + """ + Disallow the client from sending queries and receiving results + from its associated data store. + """ + self.ignoreAll() + self.__enabled = False + + def deleteBackupStores(self): + """ + Delete any backed up stores from previous year's + """ + if self.wantDsm: + self.__storeMgr.deleteBackupStores()