ai: Attempts to connect now

This commit is contained in:
Open Toontown 2022-12-17 00:15:31 -05:00
parent aaafd22c6d
commit 54cd9c8736
9 changed files with 2016 additions and 6 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()