tfscript/src/tfscript/tftypes.py

629 lines
20 KiB
Python

import re
validKeyList = [
# top row
'escape', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12',
# keyboard
'`', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '=', 'backspace',
'tab', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\\',
'capslock', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'semicolon', '\'', 'enter',
'shift', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', 'rshift',
'ctrl', 'lwin', 'alt', 'space', 'rwin', 'ralt', 'rctrl',
# mouse
'mouse1', 'mouse2', 'mouse3', 'mouse4', 'mouse5', 'mwheelup', 'mwheeldown',
# 8 of the 9 keys to the upper-right (PrtScn can't be bound)
'scrolllock', 'numlock',
'ins', 'home', 'pgup',
'del', 'end', 'pgdn',
# arrows
'uparrow', 'downarrow',
'leftarrow', 'rightarrow'
]
popErrors = (AttributeError, KeyError, TypeError)
class Bind(object):
'''
Parent class for all bind types.
Verifies key, creates local variables
'''
bindTypes = []
instances = {}
def __init__(self, key='', fields={}, *, parent=None):
if parent is None:
self.alias = False
self.key = str(key)
self.fields = fields
self.errors = []
self.warnings = []
self.TargetType = None
else:
self.alias = parent.alias
self.key = parent.key
self.fields = parent.fields
self.errors = parent.errors
self.warnings = parent.warnings
# redefined for each unique type, default just verifies key
# and some other universal fields like alias and finds targetType
self.verify()
if type(self) is Bind:
# not using isinstance(), because all subclasses are also instances
# of bind.
return
if len(self.fields) > 0:
# verify function should remove all fields relavent to the bind.
# Any extras are errors
self.warnings.append(f'extra fields in "{self.key}":')
if isinstance(self.fields, str):
# iterating over a str returns each character,
# making meaningless error messages
self.warnings.append(f' "{self.fields}"')
else:
for field in self.fields:
self.warnings.append(f' "{field}"')
if len(self.errors) == 0:
# no errors, add new instance to the list of instances
try:
self.instances[type(self)].append(self)
except KeyError:
self.instances[type(self)] = [self]
def verify(self):
try:
self.alias = self.fields.pop('alias')
if not isinstance(self.alias, bool):
self.errors.append(
f'alias should be "yes" or "no", not "{self.alias}"'
)
self.alias = False
except popErrors:
self.alias = False
try:
typeName, self.key = self.key.split(' ', 1)
# all types start with a capital
typeName = typeName.lower().capitalize()
if not self.alias:
# don't mess with alias names
self.key = self.key.lower()
except ValueError:
# catastrophic error: either no type or no key, assume no type
self.errors.append(f'could not find type in "{self.key}"')
return
for type_ in self.bindTypes:
if typeName == type_.__name__:
self.TargetType = type_
break
if self.TargetType is None:
self.errors.append(
f'"{typeName}" is not a valid type for "{self.key}"'
)
if (not self.alias) and (self.key not in validKeyList):
self.errors.append(f'invalid key name: "{self.key}"')
def toTargetType(self):
if self.TargetType is None:
# do nothing
return self
# cast to targetType, "inheriting" stuff from self
bind = self.TargetType(parent=self)
return bind
def err(self, message):
self.errors.append(
f'{type(self).__name__.lower()} "{self.key}" {message}'
)
def warn(self, message):
self.warnings.append(
f'{type(self).__name__.lower()} "{self.key}" {message}'
)
class Impulse(Bind):
def verify(self):
self.command = None
if not isinstance(self.fields, dict):
self.fields = {'command': self.fields}
try:
self.command = self.fields.pop('command')
if isinstance(self.command, str):
self.command = self.command.split(';')
elif not isinstance(self.command, list):
self.err('`command` field must be argument of string or list')
self.command = None
except KeyError:
self.err('requires `command` field')
def toTF2(self) -> str:
if self.alias:
bindOrAlias = 'alias'
else:
bindOrAlias = 'bind'
allInstructions = self.shortcut(self.command)
instruction = ';'.join(allInstructions)
return f'{bindOrAlias} {self.key} "{instruction}"\n'
def shortcut(self, instList):
for i, instruction in enumerate(instList):
try:
cmd, restOfCmd = instruction.split(' ', 1)
except ValueError:
# no spaces in cmd
cmd, restOfCmd = instruction, ''
simpleSCs = {
'primary': 'slot1',
'secondary': 'slot2',
'melee': 'slot3'
}
try:
cmd = simpleSCs[cmd]
except KeyError:
# not a shortcut
pass
if cmd == 'voice':
cmd = 'voicemenu'
restOfCmd = self.expandVoice(restOfCmd)
elif cmd == 'build' or cmd == 'destroy':
restOfCmd = self.expandBuildings(restOfCmd)
elif cmd == 'loadout' and restOfCmd.isalpha():
cmd = 'load_itempreset'
try:
restOfCmd = restOfCmd.lower()
restOfCmd = str(['a','b','c','d'].index(restOfCmd))
except ValueError:
# not a load_itempreset shortcut
pass
elif cmd == 'unalias':
cmd = 'alias'
# adding empty arg to indicate unaliasing
restOfCmd += ' '
if restOfCmd != '':
cmd += ' ' + restOfCmd
instList[i] = cmd
return instList
def expandVoice(self, keyword):
keyword = keyword.lower()
allLists = (
('medic', 'thanks', 'go', 'move up', 'go left', 'go right', 'yes', 'no', 'pass to me'),
('incoming', 'spy', 'sentry ahead', 'teleporter here', 'dispenser here', 'sentry here', 'activate uber', 'uber ready'),
('help', 'battle cry', 'cheers', 'jeers', 'positive', 'negative', 'nice shot', 'good job'),
)
for menu, voiceList in enumerate(allLists):
for selection, shortcut in enumerate(voiceList):
if keyword == shortcut:
return f'{menu} {selection}'
def expandBuildings(self, building):
buildingNums = {
'dispenser': '0 0',
'entrance': '1 0',
'exit': '1 1',
'sentry': '2 0'
}
for shortBuild, num in buildingNums.items():
if building == shortBuild:
return num
class Hold(Bind):
def verify(self):
self.press = None
self.release = None
if not isinstance(self.fields, dict):
self.fields = {'press': self.fields}
# verify press
try:
self.press = self.fields.pop('press')
if isinstance(self.press, str):
self.press = self.press.split(';')
elif not isinstance(self.press, list):
self.err('`press` field must be string or list')
self.press = None
except KeyError:
self.err('requires `press` field')
if self.press is None:
return
# verify release
try:
self.release = self.fields.pop('release')
if isinstance(self.release, str):
self.release = self.release.split(';')
elif not isinstance(self.release, list):
self.err('`release` field must be string or list')
self.release = None
except popErrors:
self.warn('has no `release`, creating one')
# no release specified, do -action for each item in press
self.release = []
for cmd in self.press:
if cmd[0] == '+':
self.release.append('-' + cmd[1:])
def toTF2(self) -> str:
if self.alias:
bindOrAlias = 'alias'
else:
bindOrAlias = 'bind'
holdStr = f'hold_{self.key}'
# Making impulse instances from self.press and .release
# allows them to share the shortcuts
pressObj = Impulse(f'+{holdStr}', self.press)
pressObj.alias = True
pressStr = pressObj.toTF2()
releaseObj = Impulse(f'-{holdStr}', self.release)
releaseObj.alias = True
releaseStr = releaseObj.toTF2()
if self.alias:
# if alias, do this to avoid activating
# and never deactivating
self.key = '+' + self.key
return (
pressStr + releaseStr
+ f'{bindOrAlias} {self.key} "+{holdStr}"\n'
)
class Toggle(Bind):
def verify(self):
self.on = None
self.off = None
if not isinstance(self.fields, dict):
self.fields = {'on': self.fields}
# verify on
try:
self.on = self.fields.pop('on')
if isinstance(self.on, str):
self.on = self.on.split(';')
elif not isinstance(self.on, list):
self.err('`on` field must be string or list')
self.on = None
except KeyError:
self.err('requires `on` field')
if self.on is None:
return
# verify off
try:
self.off = self.fields.pop('off')
if isinstance(self.off, str):
self.off = self.off.split(';')
elif not isinstance(self.off, list):
self.err('`off` field must be string or list')
except popErrors:
# no off specified, do -action for each item in on
self.off = []
for cmd in self.on:
if cmd[0] == '+':
self.off.append('-' + cmd[1:])
def toTF2(self) -> str:
if self.alias:
bindOrAlias = 'alias'
else:
bindOrAlias = 'bind'
toggleStr = f'toggle_{self.key}'
onStr = f'{toggleStr}_on'
offStr = f'{toggleStr}_off'
onObj = Impulse(onStr, self.on)
onObj.alias = True
toggleOn = onObj.toTF2()
# remove starting/trailing " and \n
toggleOn = toggleOn[:-2]
offObj = Impulse(offStr, self.off)
offObj.alias = True
toggleOff = offObj.toTF2()[:-2]
return (
f'{toggleOn}; alias {toggleStr} {offStr}"\n'
+ f'{toggleOff}; alias {toggleStr} {onStr}"\n'
+ f'alias {toggleStr} "{onStr}"\n'
+ f'{bindOrAlias} {self.key} "{toggleStr}"\n'
)
class Double(Bind):
defaultDict = {}
condDict = {}
bindNames = []
def verify(self):
self.primary = None
self.primStr = f'{self.key}_primary'
self.secondary = None
self.secondStr = f'{self.key}_secondary'
self.condition = None
self.isToggle = False
# name of a bind type
self.type = None
# either 'released' (default) or 'both'
self.cancelBoth = False
# toggler
try:
self.condition = self.fields.pop('condition')
if self.condition not in validKeyList:
self.err(f'has invalid `condition` field: "{self.condition}"')
except popErrors:
self.err('requires `condition` field')
try:
self.isToggle = self.fields.pop('toggle')
if not isinstance(self.isToggle, bool):
self.err(
'`toggle` field should be "yes" or "no", '
+ f'not "{self.isToggle}"'
)
except popErrors:
self.isToggle = False
# type
try:
self.type = self.fields.pop('type').lower()
if self.type not in self.bindNames:
# catastrophic: invalid type
self.err(f'has invalid type: "{self.type}"')
return
except popErrors:
# catastrophic: no type given
self.err('requires `type` field')
return
# cancel mode, must happend after type has been inferred
try:
cancel = self.fields.pop('cancel')
if not isinstance(cancel, str):
self.err(f'`cancel` field must be "released" or "both"')
else:
if cancel == 'both':
if self.type == 'hold':
self.cancelBoth = True
else:
self.err(
'`cancel` field only affects "hold", '
+ f'not "{self.type}"'
)
elif cancel != 'released':
self.err(
'`cancel` field must be "released" '
+ f'or "both", not "{cancel}"'
)
except popErrors:
cancel = 'released'
# primary action
try:
mainSection = self.fields.pop('primary')
mainBind = Bind(f'{self.type} {self.primStr}', mainSection)
mainBind = mainBind.toTargetType()
self.errors.extend(mainBind.errors)
self.warnings.extend(mainBind.warnings)
self.errors.remove(f'invalid key name: "{self.primStr}"')
self.primary = mainBind
except popErrors:
self.err('requires `primary` field')
# secondary action
try:
altSection = self.fields.pop('secondary')
altBind = Bind(f'{self.type} {self.secondStr}', altSection)
altBind = altBind.toTargetType()
self.errors.extend(altBind.errors)
self.warnings.extend(altBind.warnings)
self.errors.remove(f'invalid key name: "{self.secondStr}"')
self.secondary = altBind
except popErrors:
self.err('requires `secondary` field')
def toTF2(self) -> str:
# Get code for primary and secondary actions.
# alias=true so the toTF2() method aliases
# them instead of binding them
self.primary.alias = True
mainCode = self.primary.toTF2()
self.secondary.alias = True
altCode = self.secondary.toTF2()
# Make code to switch between the two actions
if self.cancelBoth:
mainCode, altCode = self.getCancelCode(mainCode, altCode)
if self.type == 'hold':
self.primStr = '+' + self.primStr
self.secondStr = '+' + self.secondStr
shiftStr = f'shift_{self.key}'
shiftCode = self.getChangeCode(shiftStr)
self.addToCondDict(shiftStr)
return mainCode + altCode + shiftCode
def getChangeCode(self, shift):
if self.alias:
bindOrAlias = 'alias'
else:
bindOrAlias = 'bind'
code = (
f'alias +{shift} "{bindOrAlias} {self.key} {self.secondStr}"\n'
+ f'alias -{shift} "{bindOrAlias} {self.key} {self.primStr}"\n'
)
if self.isToggle:
toggleObj = Toggle(shift, f'+{shift}')
toggleObj.alias = True # so it aliases instead of binding
code += toggleObj.toTF2()
else:
code += f'{bindOrAlias} {self.key} "{self.primStr}"\n'
return code
def addToCondDict(self, shiftStr):
if self.isToggle:
changeStr = shiftStr
else:
changeStr = '+' + shiftStr
restoreStr = '-' + shiftStr
if self.condition not in self.condDict:
# if not already present, make dict for key
self.condDict.update( {
self.condition: {
'change_keys': [],
'restore_keys': []
}
} )
self.condDict[self.condition]['change_keys'].append(changeStr)
if self.isToggle == False:
self.condDict[self.condition]['restore_keys'].append(restoreStr)
def getCancelCode(self, mainCode, altCode) -> (str, str):
# code to cancel both if either is released
# it copies the - statement from each to both.
# if it just extracted the name of the - statement,
# you'd end up with each recursively calling the other
mainLines = mainCode.splitlines()
mainMinusLine = mainLines[1]
# second arg, without first or last quote
mainMinusStr = mainMinusLine.split(' ', 2)[2][1:-1]
altLines = altCode.splitlines()
altMinusLine = altLines[1]
# same as above
altMinusStr = altMinusLine.split(' ', 2)[2][1:-1]
# remove duplicate - actions
mainMinusSet = set(mainMinusStr.split(';'))
altMinusSet = set(altMinusStr.split(';'))
allCancels = mainMinusSet | altMinusSet
allCancelStr = ';'.join(allCancels)
altLines[1] = altLines[1][:-1] + f';{allCancelStr}"'
mainLines[1] = mainLines[1][:-1] + f';{allCancelStr}"'
return (
'\n'.join(mainLines) + '\n',
'\n'.join(altLines) + '\n'
)
class Repeat(Bind):
def verify(self):
self.interval = None
self.command = None
try:
intervalStr = str(self.fields.pop('interval'))
self.interval = int(intervalStr)
if self.interval <= 0:
self.err('`interval` must be greater than 0')
except (KeyError, TypeError):
self.err('requires `interval` field')
except ValueError:
self.err(f'has invalid `interval`: "{self.interval}"')
except AttributeError:
self.err(f'requires `interval` field')
try:
self.command = self.fields.pop('command')
if not isinstance(self.command, (str, list)):
self.err('`command` must be string or list')
self.command = None
except popErrors:
self.err('requires `command` field')
def toTF2(self) -> str:
# commented-out placeholder
return f'// repeat {self.key}\n'
class Literal(Bind):
def verify(self):
self.text = ''
self.run = False
if not isinstance(self.fields, dict):
self.fields = {'text': self.fields}
if not self.alias:
try:
# keyname should be invalid, remove the error
self.errors.remove(
f'invalid key name: "{self.key}"'
)
except ValueError:
# if not invalid key, indicate as such
self.warn('should not use a key as a label')
if 'run' in self.fields:
self.run = self.fields.pop('run')
if not isinstance(self.run, bool):
self.errors.append(
f'`run` should be "yes" or "no", not "{self.run}"'
)
if not self.alias:
self.warn('`run` specified without alias')
try:
self.text = self.fields.pop('text')
except KeyError:
self.err('requires `text` field')
if isinstance(self.text, str):
self.text = self.text.split(';')
elif not isinstance(self.text, list):
self.err('argument must be of string or list')
def toTF2(self) -> str:
result = ';'.join(self.text)
if self.alias:
result = f'alias {self.key} "{result}"'
if self.run:
result += f'\n{self.key}'
return result + '\n'
# This is at the bottom because it has to happen after
# all inheritances have been completed
Bind.bindTypes = Bind.__subclasses__()
Double.bindNames = [ bind.__name__.lower() for bind in Bind.bindTypes ]