oldschool-toontown/toontown/spellbook/ToontownMagicWordManagerAI.py
2022-12-28 19:10:34 -04:00

309 lines
16 KiB
Python

##################################################
# The Toontown Offline Magic Word Manager
##################################################
# Author: Benjamin Frisby
# Copyright: Copyright 2020, Toontown Offline
# Credits: Benjamin Frisby, John Cote, Ruby Lord, Frank, Nick, Little Cat, Ooowoo
# License: MIT
# Version: 1.0.0
# Email: belloqzafarian@gmail.com
##################################################
from direct.directnotify import DirectNotifyGlobal
from direct.distributed import DistributedObjectAI
from otp.avatar.DistributedPlayerAI import DistributedPlayerAI
from toontown.spellbook.MagicWordConfig import *
from toontown.spellbook.MagicWordIndex import *
import json
import os
# All of the data regarding our Magic Words
MagicWordIndex = magicWordIndex.copy()
# You should only concern yourself with the following code if you want to add customization features to Magic Words
# This is only really useful in Toontown Offline, so other projects shouldn't have to really worry about it
# We allow server hosters to change a few things about the Magic Words on their server
# These are the default values generated with spellbook.json to help get them started
spellbookJsonDefaultValues = CUSTOM_SPELLBOOK_DEFAULT
# If we don't have a config directory, make it
if not os.path.exists('config/'):
os.mkdir('config/')
# If spellbook.json doesn't exist, make it
if not os.path.isfile('config/spellbook.json'):
with open('config/spellbook.json', 'w') as data:
data.write(spellbookJsonDefaultValues)
data.close()
# Now load the data from spellbook.json
with open('config/spellbook.json') as data:
spellbook = json.load(data)
# Make changes to all the Magic Words based on the data in spellbook.json
for word in spellbook['words']:
name = word['name']
accessLevel = word['access']
if accessLevel not in list(OTPGlobals.AccessLevelName2Int.keys()):
break
try:
wordInfo = MagicWordIndex[str(name.lower())]
for alias in wordInfo['aliases']:
MagicWordIndex[alias]['access'] = accessLevel
except:
pass
class ToontownMagicWordManagerAI(DistributedObjectAI.DistributedObjectAI):
notify = DirectNotifyGlobal.directNotify.newCategory('ToontownMagicWordManagerAI')
def requestExecuteMagicWord(self, affectRange, affectType, affectExtra, lastClickedAvId, magicWord):
avId = self.air.getAvatarIdFromSender()
# It's not good if we can't get the avId for whatever reason
if not avId:
self.notify.warning('requestExecuteMagicWord: Magic Word use requested but invoker avId is non-existent!')
return
# We also need the Toon as well, or else this Magic Word isn't going to do much good
toon = self.air.doId2do.get(avId)
if not toon:
self.notify.warning('requestExecuteMagicWord: Magic Word use requested but invoker avatar is non-existent!')
return
# Same thing with the Toontorial. Magic Words are strictly forbidden here
# Tell the user they can't use it because they're in the Toontorial
if hasattr(self.air, 'tutorialManager') and avId in self.air.tutorialManager.playerDict:
self.generateResponse(avId=avId, responseType="Tutorial")
return
# Our Magic Word affectRange is either SELF (the invoker) or BOTH (invoker and a target)
# Because of this, we should add the invoker to the target list
targetIds = []
if affectRange in (AFFECT_SELF, AFFECT_BOTH):
targetIds.append(avId)
# This Magic Word's affectRange is either OTHER (a target) or BOTH (invoker and a target)
# However, it's also a NORMAL affectType, so it's not as if we're targeting a zone or the whole server
# In that case, let's try to grab the single target by the lastClickedAvId provided by the invoker
lastClickedToon = None
if (affectRange in (AFFECT_OTHER, AFFECT_BOTH)) and affectType == AFFECT_NORMAL:
if lastClickedAvId:
lastClickedToon = self.air.doId2do.get(lastClickedAvId)
if lastClickedToon:
targetIds.append(lastClickedAvId)
else:
self.generateResponse(avId=avId, responseType="NoTarget")
return
# The affectType is ZONE (zone the invoker is in), SERVER (the entire server), or RANK (specified access level)
# Gather all of the Toons using whichever method this Magic Word requests
if affectType in (AFFECT_ZONE, AFFECT_SERVER, AFFECT_RANK):
toonIds = []
# Iterate over a copy of every single doId on the server
for doId in list(self.air.doId2do.keys())[:]:
do = self.air.doId2do.get(doId)
# We only care if our DistributedObject is a player that is NOT our invoker (we dealt with that earlier)
if isinstance(do, DistributedPlayerAI) and do.isPlayerControlled() and do != toon:
# Only add the Toons that are in the same zone as the invoker
if affectType == AFFECT_ZONE and do.zoneId == toon.zoneId:
toonIds.append(doId)
# Add every Toon regardless of zone
elif affectType == AFFECT_SERVER:
toonIds.append(doId)
# Only add the Toons that have the Access Level specified when the Magic Word was used
elif affectType == AFFECT_RANK and do.getAccessLevel() == affectExtra:
toonIds.append(doId)
# There were no Toons we could perform this Magic Word on, so let the invoker know that
if not toonIds and not targetIds:
self.generateResponse(avId=avId, responseType="NoTarget")
return
# Add the found Toons to the targetId list
targetIds += toonIds
# If, at this point, we still don't have any targets somehow, then let the invoker know that
if not targetIds:
self.generateResponse(avId=avId, responseType="NoTarget")
return
# Access level of the invoker
invokerAccess = int(round(toon.getAccessLevel(), -2))
# Access level of the selected target, if we have one
targetAccess = 0
if lastClickedAvId and lastClickedToon:
targetAccess = lastClickedToon.getAccessLevel()
# Now that we have all the avIds of who we want to target with this Magic Word, let's run some sanity checks
# First things first, let's make sure the invoker is allowed to target who they want to target
for targetId in targetIds:
# Of course the invoker has access to target themselves
if targetId == avId:
continue
# If our target DistributedObject doesn't exist anymore for whatever reason, just ignore them
targetToon = self.air.doId2do.get(targetId)
if not targetToon:
continue
# Get the Access Level of the target and round it to the nearest 100th
# This kind of thing is useful for roles like BUILDER, that are technically higher than USER
# They should have more perms than USERS, but shouldn't be allowed to target them
targetAccess = int(round(targetToon.getAccessLevel(), -2))
# If the Access Level of the target is greater than or equal to than that of the invoker, remove them
if targetAccess >= invokerAccess:
targetIds.remove(targetId)
continue
# Function that returns a readable name in place of the Toon's Access Level
def getAccessName(accessLevel):
return OTPGlobals.AccessLevelDebug2Name.get(OTPGlobals.AccessLevelInt2Name.get(accessLevel))
# If, after the previous check, we don't have any more targets, let's inform the invoker about it
if len(targetIds) == 0:
# If affectType is NORMAL, let the invoker know what their Access Level is compared to their target
if (affectRange in (AFFECT_OTHER, AFFECT_BOTH)) and affectType == AFFECT_NORMAL:
# Parse the Access Level of the invoker and target
parsedTargetAccess = getAccessName(targetAccess)
parsedInvokerAccess = getAccessName(invokerAccess)
# Create a nice little message to tell the invoker the difference between the Access Levels
returnValue = MAGIC_WORD_RESPONSES.get("NoAccessSingleTarget")
returnValue = returnValue.format(lastClickedToon.getName(), parsedTargetAccess, parsedInvokerAccess)
self.generateResponse(avId=avId, responseType="Success", returnValue=returnValue)
# Otherwise, just let the invoker know that everyone who was targeted was not allowed to be
else:
self.generateResponse(avId=avId, responseType="NoAccessToTarget")
return
# We're finally done determining everything related to the targets. Finally, let's get into the word itself
# We start by separating the word used from it's arguments
magicWord, args = (magicWord.split(' ', 1) + [''])[:2]
# Get the name of the word in lowercase
magicWord = magicWord.lower()
# Lookup the info for this word
magicWordInfo = MagicWordIndex[magicWord]
# Make sure the invoker has a high enough Access Level to use this Magic Word in the first place
# If they don't, them let them know about it
if toon.getAccessLevel() < OTPGlobals.AccessLevelName2Int.get(magicWordInfo['access']):
self.generateResponse(avId=avId, responseType="NoAccess")
return
# If a config option disables cheaty Magic Words and ours is deemed cheaty, let the invoker know
if hasattr(self.air, 'nonCheaty') and self.air.nonCheaty:
if not magicWordInfo['administrative']:
self.generateResponse(avId=avId, responseType="NonCheaty")
return
# If the affectRange circumstance made by the invoker is not allowed, let them know about it
# This kind of thing is good to make sure that words that shouldn't really have a particular target don't end
# up getting used in mass. For example, you don't want to use a word intended to kill a Cog Boss on other Toons
if affectRange not in magicWordInfo['affectRange']:
self.generateResponse(avId=avId, responseType="RestrictionOther")
return
# Get the arguments the Magic Word will accept
commandArgs = magicWordInfo['args']
# Determine the max and min amount of arguments the word will accept
maxArgs = len(commandArgs)
minArgs = 0
argList = args.split(None, maxArgs-1)
for argSet in commandArgs:
isRequired = argSet[ARGUMENT_REQUIRED]
if isRequired:
minArgs += 1
# If we have less arguments provided than are required, let the invoker know that
messageData = "{} argument{}"
if len(argList) < minArgs:
messageData = messageData.format(minArgs, "s" if minArgs != 1 else '')
self.generateResponse(avId=avId, responseType="NotEnoughArgs", extraMessageData=messageData)
return
# On the other hand, if we have more than what we need, tell them that instead
elif len(argList) > maxArgs:
messageData = messageData.format(maxArgs, "s" if maxArgs != 1 else '')
self.generateResponse(avId=avId, responseType="TooManyArgs", extraMessageData=messageData)
return
# If we have less arguments provided than the max, use the defaults of the ones not provided
if len(argList) < maxArgs:
for x in range(minArgs, maxArgs):
if commandArgs[x][ARGUMENT_REQUIRED] or len(argList) >= x + 1:
continue
argList.append(commandArgs[x][ARGUMENT_DEFAULT])
# Parse through all the args we had provided
parsedArgList = []
for x in range(len(argList)):
arg = argList[x]
argType = commandArgs[x][ARGUMENT_TYPE]
try:
parsedArg = argType(arg)
except:
self.generateResponse(avId=avId, responseType="BadArgs")
return
parsedArgList.append(parsedArg)
# If this is a client-sided Magic Word, execute it on the client
if magicWordInfo['execLocation'] == EXEC_LOC_CLIENT:
# We are only allowed to target ourselves with client-sided Magic Words
if len(targetIds) == 1 and avId in targetIds:
self.sendClientCommand(avId, magicWord, magicWordInfo['classname'], targetIds=targetIds,
parsedArgList=parsedArgList, affectRange=affectRange, affectType=affectType,
affectExtra=affectExtra, lastClickedAvId=lastClickedAvId)
else:
self.generateResponse(avId=avId, responseType="CannotTarget")
return
# But if it's a server-sided one, execute it on the server
else:
# Find the class associated with our Magic Word and load it
command = magicWordInfo['class']
command.loadWord(self.air, None, avId, targetIds, parsedArgList)
# Execute the Magic Word and grab a return value
returnValue = command.executeWord()
# If we have a return value, pass it over to the invoker
if returnValue:
# If the word returns more than one values, assume that the word wants to teleport the target somewhere.
if type(returnValue) is tuple:
teleportingAvId = avId
if len(returnValue) == 3:
teleportingAvId = returnValue[1]
self.sendTeleportResponse(teleportingAvId, *returnValue[-1])
self.generateResponse(avId=avId, responseType="Success", returnValue=returnValue[0])
else:
self.generateResponse(avId=avId, responseType="Success", returnValue=returnValue)
# Otherwise just throw a default response to them
else:
self.generateResponse(avId=avId, responseType="SuccessNoResp", magicWord=magicWord,
parsedArgList=parsedArgList, affectRange=affectRange, affectType=affectType,
affectExtra=affectExtra, lastClickedAvId=lastClickedAvId)
def generateResponse(self, avId, responseType="BadWord", magicWord='', parsedArgList=(), returnValue='',
affectRange=0, affectType=0, affectExtra=0, lastClickedAvId=0, extraMessageData=''):
# Pack up the arg list so it's ready to ship to the client
parsedArgList = json.dumps(parsedArgList)
# Send the invoker a response to their use of the word
self.sendUpdateToAvatarId(avId, 'generateResponse',
[responseType, magicWord, parsedArgList, returnValue, affectRange, affectType,
affectExtra, lastClickedAvId, extraMessageData])
def sendClientCommand(self, avId, word, commandName, targetIds=(), parsedArgList=(), affectRange=0, affectType=0,
affectExtra=0, lastClickedAvId=0):
# Pack up the arg list so it's ready to ship to the client
parsedArgList = json.dumps(parsedArgList)
# Execute the Magic Word on the client, because it's a client-sided Magic Word
self.sendUpdateToAvatarId(avId, "executeMagicWord", [word, commandName, targetIds, parsedArgList, affectRange,
affectType, affectExtra, lastClickedAvId])
def sendTeleportResponse(self, avId, loaderId, whereId, how, hoodId, zoneId, targetAvId):
self.sendUpdateToAvatarId(avId, "teleportResponse", [loaderId, whereId, how, hoodId, zoneId, targetAvId])