spellbook: Implement magic word system

This commit is contained in:
Little Cat 2021-07-07 03:51:22 -03:00
parent 20ee51972c
commit 44a90f07f8
No known key found for this signature in database
GPG key ID: 68CC2B4D0E5AF41C
12 changed files with 922 additions and 5 deletions

View file

@ -70,7 +70,7 @@ from toontown.estate import DistributedHouse/AI
from toontown.estate import DistributedHouseInterior/AI from toontown.estate import DistributedHouseInterior/AI
from toontown.estate import DistributedGarden/AI from toontown.estate import DistributedGarden/AI
from toontown.shtiker import DeleteManager/AI from toontown.shtiker import DeleteManager/AI
from toontown.ai import ToontownMagicWordManager/AI from toontown.spellbook import ToontownMagicWordManager/AI
from toontown.ai import NewsManager/AI from toontown.ai import NewsManager/AI
from toontown.shtiker import PurchaseManager/AI from toontown.shtiker import PurchaseManager/AI
from toontown.shtiker import NewbiePurchaseManager/AI from toontown.shtiker import NewbiePurchaseManager/AI
@ -1406,8 +1406,10 @@ dclass DeleteManager : DistributedObject {
setInventory(blob) airecv clsend; setInventory(blob) airecv clsend;
}; };
dclass ToontownMagicWordManager : MagicWordManager { dclass ToontownMagicWordManager : DistributedObject {
requestTeleport(string, string, uint32, uint32, uint32); requestExecuteMagicWord(int8, int8, int16, uint32, string) airecv clsend;
executeMagicWord(string, string, uint32[], blob, int8, int8, int16, uint32);
generateResponse(string, string, blob, string, int8, int8, int16, uint32, string);
}; };
struct weeklyCalendarHoliday { struct weeklyCalendarHoliday {
@ -3265,4 +3267,3 @@ dclass DistributedTrashcanZeroMgr : DistributedPhaseEventMgr {
dclass DistributedSillyMeterMgr : DistributedPhaseEventMgr { dclass DistributedSillyMeterMgr : DistributedPhaseEventMgr {
}; };

View file

@ -457,3 +457,6 @@ class DistributedPlayer(DistributedAvatar.DistributedAvatar, PlayerBase.PlayerBa
def setAccessLevel(self, accessLevel): def setAccessLevel(self, accessLevel):
self.accessLevel = accessLevel self.accessLevel = accessLevel
def getAccessLevel(self):
return self.accessLevel

View file

@ -113,6 +113,9 @@ class DistributedPlayerAI(DistributedAvatarAI.DistributedAvatarAI, PlayerBase.Pl
def setAccessLevel(self, accessLevel): def setAccessLevel(self, accessLevel):
self.accessLevel = accessLevel self.accessLevel = accessLevel
def getAccessLevel(self):
return self.accessLevel
def d_setFriendsList(self, friendsList): def d_setFriendsList(self, friendsList):
self.sendUpdate('setFriendsList', [friendsList]) self.sendUpdate('setFriendsList', [friendsList])

View file

@ -617,7 +617,7 @@ class TalkAssistant(DirectObject.DirectObject):
def sendOpenTalk(self, message): def sendOpenTalk(self, message):
error = None error = None
if base.cr.wantMagicWords and len(message) > 0 and message[0] == '~': if base.cr.magicWordManager and base.localAvatar.getAccessLevel() > OTPGlobals.AccessLevelName2Int.get('NO_ACCESS') and len(message) > 0 and message[0] == base.cr.magicWordManager.chatPrefix:
messenger.send('magicWord', [message]) messenger.send('magicWord', [message])
self.receiveDeveloperMessage(message) self.receiveDeveloperMessage(message)
else: else:

View file

@ -44,6 +44,7 @@ from toontown.racing.DistributedViewPadAI import DistributedViewPadAI
from toontown.racing.RaceManagerAI import RaceManagerAI from toontown.racing.RaceManagerAI import RaceManagerAI
from toontown.safezone.SafeZoneManagerAI import SafeZoneManagerAI from toontown.safezone.SafeZoneManagerAI import SafeZoneManagerAI
from toontown.shtiker.CogPageManagerAI import CogPageManagerAI from toontown.shtiker.CogPageManagerAI import CogPageManagerAI
from toontown.spellbook.ToontownMagicWordManagerAI import ToontownMagicWordManagerAI
from toontown.suit.SuitInvasionManagerAI import SuitInvasionManagerAI from toontown.suit.SuitInvasionManagerAI import SuitInvasionManagerAI
from toontown.toon import NPCToons from toontown.toon import NPCToons
from toontown.toonbase import ToontownGlobals from toontown.toonbase import ToontownGlobals
@ -84,6 +85,7 @@ class ToontownAIRepository(ToontownInternalRepository):
self.catalogManager = None self.catalogManager = None
self.trophyMgr = None self.trophyMgr = None
self.safeZoneManager = None self.safeZoneManager = None
self.magicWordManager = None
self.zoneTable = {} self.zoneTable = {}
self.dnaStoreMap = {} self.dnaStoreMap = {}
self.dnaDataMap = {} self.dnaDataMap = {}
@ -202,6 +204,10 @@ class ToontownAIRepository(ToontownInternalRepository):
self.safeZoneManager = SafeZoneManagerAI(self) self.safeZoneManager = SafeZoneManagerAI(self)
self.safeZoneManager.generateWithRequired(OTP_ZONE_ID_MANAGEMENT) self.safeZoneManager.generateWithRequired(OTP_ZONE_ID_MANAGEMENT)
# Generate our magic word manager...
self.magicWordManager = ToontownMagicWordManagerAI(self)
self.magicWordManager.generateWithRequired(OTP_ZONE_ID_MANAGEMENT)
def generateHood(self, hoodConstructor, zoneId): def generateHood(self, hoodConstructor, zoneId):
# Bossbot HQ doesn't use DNA, so we skip over that. # Bossbot HQ doesn't use DNA, so we skip over that.
if zoneId != ToontownGlobals.BossbotHQ: if zoneId != ToontownGlobals.BossbotHQ:

View file

@ -106,6 +106,7 @@ class ToontownClientRepository(OTPClientRepository.OTPClientRepository):
self.streetSign = None self.streetSign = None
self.furnitureManager = None self.furnitureManager = None
self.objectManager = None self.objectManager = None
self.magicWordManager = None
self.friendsMap = {} self.friendsMap = {}
self.friendsOnline = {} self.friendsOnline = {}
self.friendsMapPending = 0 self.friendsMapPending = 0

View file

@ -0,0 +1,86 @@
##################################################
# 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
##################################################
OUTGOING_CHAT_MESSAGE_NAME = 'magicWord'
CLICKED_NAMETAG_MESSAGE_NAME = 'clickedNametag'
FOCUS_OUT_MESSAGE_NAME = 'focusOut'
PREFIX_DEFAULT = '~'
PREFIX_ALLOWED = ['~', '?', '/', '<', ':', ';']
if config.GetBool('exec-chat', False):
PREFIX_ALLOWED.append('>')
WIZARD_DEFAULT = 'Spellbook'
MAGIC_WORD_SUCCESS_PHRASES = ['Alakazam!', 'Voila!', 'Ta-da!', 'Presto!', 'Abracadabra!']
MAGIC_WORD_RESPONSES = {
"SuccessNoResp": 'response will be randomly selected from MAGIC_WORD_SUCCESS_PHRASES',
"Success": 'response will be provided by magic word',
"Teleporting": 'Yikes! Don\'t use Magic words while switching zones!',
"OtherTeleporting": 'Your target is currently switching zones!',
"BadWord": 'Uh-oh! This Magic Word doesn\'t exist!',
"CloseWord": 'This Magic Word doesn\'t exist! Did you mean {}?',
"NoEffect": "This word doesn't affect anybody!",
"BadTarget": 'Invalid target specified!',
"NoAccessToTarget": "You don't have a high enough Access Level to target them!",
"NoAccessSingleTarget": "You don't have a high enough Access Level to target {}! Their Access Level: {}. Your Access Level: {}.",
"NoTarget": 'Unable to find a target!',
"NoAccess": 'Your Toon does not have enough power to use this Magic Word!',
"NotEnoughArgs": 'This command takes at least {}!',
"TooManyArgs": 'This command takes at most {}!',
"BadArgs": 'These arguments are of the wrong type!',
"CannotTarget": 'You cannot target other players with this Magic Word!',
"Locked": 'You are temporarily locked down and cannot use Magic Words.',
"RestrictionOther": 'You may only target one other player with this Magic Word!',
'NonCheaty': 'You cannot use cheaty Magic Words at this time!',
'Tutorial': 'You cannot use Magic Words in the Toontorial!'
}
MAGIC_WORD_NO_RESPONSE = "...I don't know how to respond!"
HAS_EXTRA_MESSAGE_DATA = ["NotEnoughArgs", "TooManyArgs", "CloseWord"]
MAGIC_WORD_DEFAULT_DESC = 'A simple Magic Word.'
MAGIC_WORD_DEFAULT_ADV_DESC = 'This Magic Word does a lot of things, because reasons.'
AFFECT_TYPES = ['singular', 'zone', 'server', 'rank']
AFFECT_TYPES_NAMES = ['Everyone in this zone', 'The entire server', 'Everyone with an Access Level of']
AFFECT_NONE = -1
AFFECT_SELF = 0
AFFECT_OTHER = 1
AFFECT_BOTH = 2
AFFECT_NORMAL = 0
AFFECT_ZONE = 1
AFFECT_SERVER = 2
AFFECT_RANK = 3
GROUP_AFFECTS = [AFFECT_ZONE, AFFECT_SERVER, AFFECT_RANK]
EXEC_LOC_INVALID = -1
EXEC_LOC_CLIENT = 0
EXEC_LOC_SERVER = 1
ARGUMENT_NAME = 0
ARGUMENT_TYPE = 1
ARGUMENT_REQUIRED = 2
ARGUMENT_DEFAULT = 3
CUSTOM_SPELLBOOK_DEFAULT = '''{
"words":
[
{
"name": "SetPos",
"access": "MODERATOR"
},
{
"name": "GetPos",
"access": "MODERATOR"
}
]
}
'''

View file

@ -0,0 +1,253 @@
##################################################
# 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
##################################################
import collections, types
from direct.distributed.ClockDelta import *
from direct.interval.IntervalGlobal import *
from libotp import NametagGroup, WhisperPopup
from otp.otpbase import OTPLocalizer
from otp.otpbase import OTPGlobals
from . import MagicWordConfig
import time, random, re, json
magicWordIndex = collections.OrderedDict()
class MagicWord:
notify = DirectNotifyGlobal.directNotify.newCategory('MagicWord')
# Whether this Magic word should be considered "hidden"
# If your Toontown source has a page for Magic Words in the Sthickerbook, this will be useful for that
hidden = False
# Whether this Magic Word is an administrative command or not
# Good for config settings where you want to disable cheaty Magic Words, but still want moderation ones
administrative = False
# List of names that will also invoke this word - a setHP magic word might have "hp", for example
# A Magic Word will always be callable with its class name, so you don't have to put that in the aliases
aliases = None
# Description of the Magic Word
# If your Toontown source has a page for Magic Words in the Sthickerbook, this will be useful for that
desc = MagicWordConfig.MAGIC_WORD_DEFAULT_DESC
# Advanced description that gives the user a lot more information than normal
# If your Toontown source has a page for Magic Words in the Sthickerbook, this will be useful for that
advancedDesc = MagicWordConfig.MAGIC_WORD_DEFAULT_ADV_DESC
# Default example with for commands with no arguments set
# If your Toontown source has a page for Magic Words in the Sthickerbook, this will be useful for that
example = ""
# The minimum access level required to use this Magic Word
accessLevel = 'MODERATOR'
# A restriction on the Magic Word which sets what kind or set of Distributed Objects it can be used on
# By default, a Magic Word can affect everyone
affectRange = [MagicWordConfig.AFFECT_SELF, MagicWordConfig.AFFECT_OTHER, MagicWordConfig.AFFECT_BOTH]
# Where the magic word will be executed -- EXEC_LOC_CLIENT or EXEC_LOC_SERVER
execLocation = MagicWordConfig.EXEC_LOC_INVALID
# List of all arguments for this word, with the format [(type, isRequired), (type, isRequired)...]
# If the parameter is not required, you must provide a default argument: (type, False, default)
arguments = None
def __init__(self):
if self.__class__.__name__ != "MagicWord":
self.aliases = self.aliases if self.aliases is not None else []
self.aliases.insert(0, self.__class__.__name__)
self.aliases = [x.lower() for x in self.aliases]
self.arguments = self.arguments if self.arguments is not None else []
if len(self.arguments) > 0:
for arg in self.arguments:
argInfo = ""
if not arg[MagicWordConfig.ARGUMENT_REQUIRED]:
argInfo += "(default: {0})".format(arg[MagicWordConfig.ARGUMENT_DEFAULT])
self.example += "[{0}{1}] ".format(arg[MagicWordConfig.ARGUMENT_NAME], argInfo)
self.__register()
def __register(self):
for wordName in self.aliases:
if wordName in magicWordIndex:
self.notify.error('Duplicate Magic Word name or alias detected! Invalid name: {}'. format(wordName))
magicWordIndex[wordName] = {'class': self,
'classname': self.__class__.__name__,
'hidden': self.hidden,
'administrative': self.administrative,
'aliases': self.aliases,
'desc': self.desc,
'advancedDesc': self.advancedDesc,
'example': self.example,
'execLocation': self.execLocation,
'access': self.accessLevel,
'affectRange': self.affectRange,
'args': self.arguments}
def loadWord(self, air=None, cr=None, invokerId=None, targets=None, args=None):
self.air = air
self.cr = cr
self.invokerId = invokerId
self.targets = targets
self.args = args
def executeWord(self):
executedWord = None
validTargets = len(self.targets)
for avId in self.targets:
invoker = None
toon = None
if self.air:
invoker = self.air.doId2do.get(self.invokerId)
toon = self.air.doId2do.get(avId)
elif self.cr:
invoker = self.cr.doId2do.get(self.invokerId)
toon = self.cr.doId2do.get(avId)
if hasattr(toon, "getName"):
name = toon.getName()
else:
name = avId
if not self.validateTarget(toon):
if len(self.targets) > 1:
validTargets -= 1
continue
return "{} is not a valid target!".format(name)
# TODO: Should we implement locking?
# if toon.getLocked() and not self.administrative:
# if len(self.targets) > 1:
# validTargets -= 1
# continue
# return "{} is currently locked. You can only use administrative commands on them.".format(name)
if invoker.getAccessLevel() <= toon.getAccessLevel() and toon != invoker:
if len(self.targets) > 1:
validTargets -= 1
continue
targetAccess = OTPGlobals.AccessLevelDebug2Name.get(OTPGlobals.AccessLevelInt2Name.get(toon.getAccessLevel()))
invokerAccess = OTPGlobals.AccessLevelDebug2Name.get(OTPGlobals.AccessLevelInt2Name.get(invoker.getAccessLevel()))
return "You don't have a high enough Access Level to target {0}! Their Access Level: {1}. Your Access Level: {2}.".format(name, targetAccess, invokerAccess)
if self.execLocation == MagicWordConfig.EXEC_LOC_CLIENT:
self.args = json.loads(self.args)
executedWord = self.handleWord(invoker, avId, toon, *self.args)
# If you're only using the Magic Word on one person and there is a response, return that response
if executedWord and len(self.targets) == 1:
return executedWord
# If the amount of targets is higher than one...
elif validTargets > 0:
# And it's only 1, and that's yourself, return None
if validTargets == 1 and self.invokerId in self.targets:
return None
# Otherwise, state how many targets you executed it on
return "Magic Word successfully executed on %s target(s)." % validTargets
else:
return "Magic Word unable to execute on any targets."
def validateTarget(self, target):
if self.air:
from toontown.toon.DistributedToonAI import DistributedToonAI
return isinstance(target, DistributedToonAI)
elif self.cr:
from toontown.toon.DistributedToon import DistributedToon
return isinstance(target, DistributedToon)
return False
def handleWord(self, invoker, avId, toon, *args):
raise NotImplementedError
class SetHP(MagicWord):
aliases = ["hp", "setlaff", "laff"]
desc = "Sets the target's current laff."
advancedDesc = "This Magic Word will change the current amount of laff points the target has to whichever " \
"value you specify. You are only allowed to specify a value between -1 and the target's maximum " \
"laff points. If you specify a value less than 1, the target will instantly go sad unless they " \
"are in Immortal Mode."
execLocation = MagicWordConfig.EXEC_LOC_SERVER
arguments = [("hp", int, True)]
def handleWord(self, invoker, avId, toon, *args):
hp = args[0]
if not -1 <= hp <= toon.getMaxHp():
return "Can't set {0}'s laff to {1}! Specify a value between -1 and {0}'s max laff ({2}).".format(
toon.getName(), hp, toon.getMaxHp())
if hp <= 0 and toon.immortalMode:
return "Can't set {0}'s laff to {1} because they are in Immortal Mode!".format(toon.getName(), hp)
toon.b_setHp(hp)
return "{}'s laff has been set to {}.".format(toon.getName(), hp)
class SetMaxHP(MagicWord):
aliases = ["maxhp", "setmaxlaff", "maxlaff"]
desc = "Sets the target's max laff."
advancedDesc = "This Magic Word will change the maximum amount of laff points the target has to whichever value " \
"you specify. You are only allowed to specify a value between 15 and 137 laff points."
execLocation = MagicWordConfig.EXEC_LOC_SERVER
arguments = [("maxhp", int, True)]
def handleWord(self, invoker, avId, toon, *args):
maxhp = args[0]
if not 15 <= maxhp <= 137:
return "Can't set {}'s max laff to {}! Specify a value between 15 and 137.".format(toon.getName(), maxhp)
toon.b_setMaxHp(maxhp)
toon.toonUp(maxhp)
return "{}'s max laff has been set to {}.".format(toon.getName(), maxhp)
class ToggleOobe(MagicWord):
aliases = ["oobe"]
desc = "Toggles the out of body experience mode, which lets you move the camera freely."
advancedDesc = "This Magic Word will toggle what is known as 'Out Of Body Experience' Mode, hence the name " \
"'Oobe'. When this mode is active, you are able to move the camera around with your mouse- " \
"though your camera will still follow your Toon. You can also toggle this mode by pressing the " \
"'F4' key, or whichever other keybind you have set."
execLocation = MagicWordConfig.EXEC_LOC_CLIENT
def handleWord(self, invoker, avId, toon, *args):
base.oobe()
return "Oobe mode has been toggled."
class ToggleRun(MagicWord):
aliases = ["run"]
desc = "Toggles run mode, which gives you a faster running speed."
advancedDesc = "This Magic Word will toggle Run Mode. When this mode is active, the target can run around at a " \
"very fast speed. This running speed stacks with other speed multipliers, such as the one given" \
"by the 'SetSpeed' Magic Word. You will automatically toggle Run Mode by using the 'EnableGod' " \
"Magic Word."
execLocation = MagicWordConfig.EXEC_LOC_CLIENT
def handleWord(self, invoker, avId, toon, *args):
from direct.showbase.InputStateGlobal import inputState
inputState.set('debugRunning', not inputState.isSet('debugRunning'))
return "Run mode has been toggled."
# Instantiate all classes defined here to register them.
# A bit hacky, but better than the old system
for item in list(globals().values()):
if isinstance(item, type) and issubclass(item, MagicWord):
i = item()

View file

@ -0,0 +1,262 @@
##################################################
# 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 DistributedObject
from libotp import WhisperPopup
from otp.otpbase.OTPGlobals import *
from toontown.friends import FriendHandle
from toontown.spellbook.MagicWordConfig import *
from toontown.spellbook.MagicWordIndex import *
from toontown.toon import Toon
import json
import random
import string
MagicWordIndex = magicWordIndex.copy()
class ToontownMagicWordManager(DistributedObject.DistributedObject):
notify = DirectNotifyGlobal.directNotify.newCategory('ToontownMagicWordManager')
neverDisable = 1
def __init__(self, cr):
DistributedObject.DistributedObject.__init__(self, cr)
base.cr.magicWordManager = self
# The default chat prefix we use to determine if said phrase is a Magic Word
self.chatPrefix = PREFIX_DEFAULT
# The default name of the "wizard" that returns responses when executing Magic Words
self.wizardName = WIZARD_DEFAULT
# Keep track of the last clicked avatar for targeting purposes
self.lastClickedAvId = 0
def announceGenerate(self):
DistributedObject.DistributedObject.announceGenerate(self)
# Only use a custom Magic Word activator if the index is allowed
# TODO: Uncomment after adding settings support
activatorIndex = 0 # base.settings.getInt('game', 'magic-word-activator', 0)
if 0 <= activatorIndex <= (len(PREFIX_ALLOWED) - 1):
self.chatPrefix = PREFIX_ALLOWED[activatorIndex]
# Accept events such as outgoing chat messages and clicking on nametags
self.accept(OUTGOING_CHAT_MESSAGE_NAME, self.checkMagicWord)
self.accept(CLICKED_NAMETAG_MESSAGE_NAME, self.__handleClickedNametag)
self.accept(FOCUS_OUT_MESSAGE_NAME, self.__handleFocusOutNametag)
def disable(self):
DistributedObject.DistributedObject.disable(self)
# Ignore the events we were accepting earlier
self.ignore(OUTGOING_CHAT_MESSAGE_NAME)
self.ignore(CLICKED_NAMETAG_MESSAGE_NAME)
self.ignore(FOCUS_OUT_MESSAGE_NAME)
def setChatPrefix(self, chatPrefix):
self.chatPrefix = chatPrefix
def __handleClickedNametag(self, avatar):
if avatar:
# Make sure the nametag we clicked on is a Toon
if isinstance(avatar, Toon.Toon) or isinstance(avatar, FriendHandle.FriendHandle):
# Store the avId of our target
self.lastClickedAvId = avatar.getDoId()
return
# Clear our target avId
self.lastClickedAvId = 0
def __handleFocusOutNametag(self):
# We've clicked off of a nametag, so reset the target avId
self.lastClickedAvId = 0
def checkMagicWord(self, magicWord):
# Well, this obviously isn't a Magic Word if it doesn't begin with our prefix
if not magicWord.startswith(self.chatPrefix):
return
# We don't even have access to be using Magic Words in the first place
if base.localAvatar.getAccessLevel() < OTPGlobals.AccessLevelName2Int.get('MODERATOR'):
self.generateResponse(responseType="NoAccess")
return
# Using Magic Words while teleporting or going through tunnels is scary
if base.localAvatar.getTransitioning():
self.generateResponse(responseType="Teleporting")
return
# We're allowed to use the Magic Word then, so let's proceed
self.handleMagicWord(magicWord)
def generateResponse(self, responseType, magicWord='', args=None, returnValue=None, affectRange=None,
affectType=None, affectExtra=None, lastClickedAvId=None, extraMessageData = None):
# Generate and send the response to the use of our Magic Word
response = self.generateMagicWordResponse(responseType, magicWord, args, returnValue, affectRange, affectType,
affectExtra, lastClickedAvId, extraMessageData)
base.localAvatar.setSystemMessage(0, self.wizardName + ': ' + response, WhisperPopup.WTSystem)
self.notify.info(response)
def generateMagicWordResponse(self, responseType, magicWord, args, returnValue, affectRange, affectType,
affectExtra, lastClickedAvId, extraMessageData):
# Start out with a blank response
response = ''
# If our Magic Word was a success but has no return value, just send a randomized success phrase
if responseType == "SuccessNoResp" and magicWord:
successExclaim = random.choice(MAGIC_WORD_SUCCESS_PHRASES)
response += successExclaim
return response
# We had a successful Magic Word and also got a return value, so let's just use that
elif responseType == "Success":
response += returnValue
return response
# Guess it wasn't a success, so let's grab our response via the given code
response += MAGIC_WORD_RESPONSES.get(responseType, MAGIC_WORD_NO_RESPONSE)
# If we want to provide extra info, format the response
if responseType in MagicWordConfig.HAS_EXTRA_MESSAGE_DATA:
response = response.format(extraMessageData)
return response
def handleMagicWord(self, magicWord):
# By default, our Magic Word affects nobody- not even ourself!
affectRange = AFFECT_NONE
# A normal affect type- we aren't trying to target all Toons in the zone, server, or a specific Access Level
affectType = AFFECT_NORMAL
# Only used for determining what Access Level will be targeted, if we decide to target a specific one
affectExtra = -1
# Used for determining what the affectRange is- it counts the amount of activators uses (ranges 1-3)
for x in range(3):
if magicWord.startswith(self.chatPrefix * (3 - x)):
affectRange = 2 - x
break
# If so some reason our affectRange is still none, we can't go any further
if affectRange == AFFECT_NONE:
self.generateResponse(responseType="NoEffect")
return
# Our affectRange is other, meaning we want to target someone- let's make sure we're allowed to
elif affectRange == AFFECT_OTHER:
# If they don't exist, why would we want to continue?
toon = base.cr.doId2do.get(self.lastClickedAvId)
if not toon:
return
# Like earlier, Magic Words are no good if used while moving between zones
if toon.getTransitioning():
self.generateResponse(responseType="OtherTeleporting")
return
# Get how many activators were used in this Magic Word execution
activatorLength = affectRange + 1
# The Magic word without the activators
magicWordNoPrefix = magicWord[activatorLength:]
# Iterate through the affectType strings and see if any of them were used (e.g. zone, server, or rank)
for type in AFFECT_TYPES:
if magicWordNoPrefix.startswith(type):
magicWordNoPrefix = magicWordNoPrefix[len(type):]
affectType = AFFECT_TYPES.index(type)
break
# Calculate the Access Level to affect if affectType is RANK
if affectType == AFFECT_RANK:
# Iterate over all the possible Access Level integers and see if any match with the one provided
for level in list(OTPGlobals.AccessLevelName2Int.values()):
# It matches, woohoo!
if magicWordNoPrefix.startswith(str(level)):
# Sorry, I'm commenting this way after the fact, so not even I know why there is a try/except here
# My guess is that sometimes this doesn't work for whatever reason, but I'm not too sure
# It typically works fine for me but I will keep it here just in-case
try:
int(magicWordNoPrefix[len(str(level)):][:1])
self.generateResponse(responseType="BadTarget")
return
except:
pass
# Strip the Access Level integer from the Magic Word string
magicWordNoPrefix = magicWordNoPrefix[len(str(level)):]
# Store the Access Level integer here instead
affectExtra = level
break
# The invoker wanted to target an Access Level but provided an invalid integer, so let them know
if affectExtra == -1:
self.generateResponse(responseType="BadTarget")
return
# Finally, we can get the name of the Magic Word used
word = magicWordNoPrefix.split(' ', 1)[0].lower()
# The Magic Word the invoker used doesn't exist
if word not in MagicWordIndex:
# Iterate over all Magic Word names and see if the one provided is close to any of them
for magicWord in list(MagicWordIndex.keys()):
# If it is close, suggest to the invoker that they made a typo
if word in magicWord:
self.generateResponse(responseType="CloseWord", extraMessageData=magicWord)
return
# Couldn't find any Magic Word close to what was provided, so let them know the word doesn't exist
self.generateResponse(responseType="BadWord")
return
# Grab the Magic Word info based off of it's name
magicWordInfo = MagicWordIndex[word]
# The location of the Magic Word's execution was not specified, so raise an error
if magicWordInfo['execLocation'] == EXEC_LOC_INVALID:
raise ValueError("execLocation not set for magic word {}!".format(magicWordInfo['classname']))
# The execLocation is valid, so let's finally send data over to the server to execute our Magic Word
elif magicWordInfo['execLocation'] in (EXEC_LOC_SERVER, EXEC_LOC_CLIENT):
self.sendUpdate('requestExecuteMagicWord', [affectRange, affectType, affectExtra, self.lastClickedAvId,
magicWordNoPrefix])
def executeMagicWord(self, word, commandName, targetIds, args, affectRange, affectType, affectExtra, lastClickedAvId):
# We have have a target avId and the affectRange isn't ourself, we want to execute this Magic Word on the target
# This is alright, but we should only execute it on the target if they are visible on our client
if self.lastClickedAvId and affectRange != AFFECT_SELF:
toon = base.cr.doId2do.get(self.lastClickedAvId)
if not toon:
self.generateResponse(responseType="NoTarget")
return
# Get the Magic Word info based off of it's name
magicWord = commandName.lower()
magicWordInfo = MagicWordIndex[magicWord]
# Load the class tied to the Magic Word
command = magicWordInfo['class']
command.loadWord(None, self.cr, base.localAvatar.getDoId(), targetIds, args)
# Execute the Magic Word and store the return value
returnValue = command.executeWord()
# If we have a return value, route it through
if returnValue:
self.generateResponse(responseType="Success", returnValue=returnValue)
# If not just route a generic response through
else:
self.generateResponse(responseType="SuccessNoResp", magicWord=word, args=args, affectRange=affectRange,
affectType=affectType, affectExtra=affectExtra, lastClickedAvId=lastClickedAvId)

View file

@ -0,0 +1,298 @@
##################################################
# 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 list(self.air.tutorialManager.avId2fsm.keys()):
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:
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])

View file

View file

@ -186,6 +186,7 @@ class DistributedToon(DistributedPlayer.DistributedPlayer, Toon.Toon, Distribute
self.gmNameTagColor = 'whiteGM' self.gmNameTagColor = 'whiteGM'
self.gmNameTagString = '' self.gmNameTagString = ''
self._lastZombieContext = None self._lastZombieContext = None
self.transitioning = False
return return
def disable(self): def disable(self):
@ -2610,3 +2611,6 @@ class DistributedToon(DistributedPlayer.DistributedPlayer, Toon.Toon, Distribute
if not present: if not present:
self.notify.warning('hiding av %s because they are not on the district!' % self.doId) self.notify.warning('hiding av %s because they are not on the district!' % self.doId)
self.setParent(OTPGlobals.SPHidden) self.setParent(OTPGlobals.SPHidden)
def getTransitioning(self):
return self.transitioning