You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
640 lines
21 KiB
Python
640 lines
21 KiB
Python
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 ScriptBind(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 ScriptBind:
|
|
# 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):
|
|
self.alias = self.optional('alias', default=False)
|
|
if not isinstance(self.alias, bool):
|
|
self.err(
|
|
f'`alias` should be "yes" or "no", not "{self.alias}"'
|
|
)
|
|
|
|
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 optional(self, name, /,*, default=None):
|
|
try:
|
|
return self.fields.pop(name)
|
|
except popErrors:
|
|
return default
|
|
|
|
def cmdListFrom(self, name, /,*, default=None):
|
|
result = self.fields.pop(name)
|
|
if isinstance(result, str):
|
|
return result.split(';')
|
|
elif isinstance(result, list):
|
|
return result
|
|
else:
|
|
return default
|
|
|
|
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(ScriptBind):
|
|
def verify(self):
|
|
self.command: list = None
|
|
if not isinstance(self.fields, dict):
|
|
self.fields = {'command': self.fields}
|
|
|
|
try:
|
|
self.command = self.cmdListFrom(
|
|
'command',
|
|
default=self.fields
|
|
)
|
|
if self.command is None:
|
|
self.err('`command` field must be string or list')
|
|
except popErrors:
|
|
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'
|
|
}
|
|
# if is shortcut, change
|
|
cmd = simpleSCs.get(cmd, cmd)
|
|
|
|
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:
|
|
loadoutNum = ['a','b','c','d'].index(restOfCmd.lower())
|
|
restOfCmd = str(loadoutNum)
|
|
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'
|
|
}
|
|
return buildingNums.get(building, building)
|
|
|
|
|
|
class Hold(ScriptBind):
|
|
def verify(self):
|
|
self.press: list = None
|
|
self.release: list = None
|
|
if not isinstance(self.fields, dict):
|
|
self.fields = {'press': self.fields}
|
|
|
|
# verify press
|
|
try:
|
|
self.press = self.cmdListFrom('press')
|
|
if self.press is None:
|
|
self.err('`press` field must be string or list')
|
|
except popErrors:
|
|
self.err('requires `press` field')
|
|
|
|
# verify release
|
|
try:
|
|
self.release = self.cmdListFrom('release')
|
|
if self.release is None:
|
|
self.err('`release` field must be string or list')
|
|
except popErrors:
|
|
if self.press is None:
|
|
return
|
|
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('+' + holdStr, self.press)
|
|
pressObj.alias = True
|
|
pressStr = pressObj.toTF2()
|
|
|
|
releaseObj = Impulse('-' + holdStr, self.release)
|
|
releaseObj.alias = True
|
|
releaseStr = releaseObj.toTF2()
|
|
|
|
if self.alias:
|
|
# if alias, do this to avoid activating
|
|
# and never deactivating
|
|
minuskey = '-' + self.key
|
|
self.key = '+' + self.key
|
|
|
|
code = (
|
|
pressStr + releaseStr
|
|
+ f'{bindOrAlias} {self.key} "+{holdStr}"\n'
|
|
)
|
|
if self.alias:
|
|
code += f'alias {minuskey} "-{holdStr}"\n'
|
|
return code
|
|
|
|
|
|
class Toggle(ScriptBind):
|
|
def verify(self):
|
|
self.on : list = None
|
|
self.off: list = None
|
|
if not isinstance(self.fields, dict):
|
|
self.fields = {'on': self.fields}
|
|
|
|
# verify on
|
|
try:
|
|
self.on = self.cmdListFrom('on')
|
|
if self.on is None:
|
|
self.err(f'`on` field must be string or list')
|
|
except popErrors:
|
|
self.err('requires `on` field')
|
|
|
|
# verify off
|
|
try:
|
|
self.off = self.cmdListFrom('off')
|
|
if self.off is None:
|
|
self.err(f'`off` field must be string or list')
|
|
except popErrors:
|
|
# no off specified, do -action for each item in on
|
|
self.off = []
|
|
if self.on is None:
|
|
return
|
|
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(ScriptBind):
|
|
defaultDict = {}
|
|
condDict = {}
|
|
bindNames = []
|
|
|
|
def verify(self):
|
|
self.primStr = f'{self.key}_primary'
|
|
self.secondStr = f'{self.key}_secondary'
|
|
self.isToggle = False
|
|
self.cancelBoth = False
|
|
|
|
self.primary: ScriptBind = None
|
|
self.secondary: ScriptBind = None
|
|
self.condition: str = None
|
|
self.type: str = None
|
|
|
|
# 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')
|
|
|
|
self.isToggle = self.optional('toggle', default=False)
|
|
if not isinstance(self.isToggle, bool):
|
|
self.err(
|
|
'`toggle` field should be "yes" or "no",'
|
|
+ f' not "{self.isToggle}"'
|
|
)
|
|
|
|
# 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
|
|
cancel = self.optional('cancel', default='released')
|
|
|
|
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.cancelBoth = False
|
|
else:
|
|
self.err(
|
|
'`cancel` field must be "released"'
|
|
+ f' or "both", not "{cancel}"'
|
|
)
|
|
|
|
try:
|
|
self.primary = self.getSection('primary', self.primStr)
|
|
except popErrors:
|
|
self.primary = None
|
|
|
|
try:
|
|
self.secondary = self.getSection('secondary', self.secondStr)
|
|
except popErrors:
|
|
self.secondary = None
|
|
|
|
if self.primary is self.secondary is None:
|
|
self.err('has neither primary nor secondary')
|
|
|
|
def getSection(self, popName, key, /) -> ScriptBind:
|
|
section = self.fields.pop(popName)
|
|
bind = ScriptBind(f'{self.type} {key}', section)
|
|
bind = bind.toTargetType()
|
|
|
|
bind.errors.remove(f'invalid key name: "{key}"')
|
|
self.prettifyList(bind.errors, key)
|
|
self.errors.extend(bind.errors)
|
|
self.prettifyList(bind.warnings, key)
|
|
self.warnings.extend(bind.warnings)
|
|
|
|
return bind
|
|
|
|
def prettifyList(self, strList, origStr):
|
|
repStr = ' '.join(origStr.split('_', 1))
|
|
for i, cmd in enumerate(strList):
|
|
strList[i] = cmd.replace(origStr, repStr)
|
|
|
|
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 = '+hold_' + self.primStr
|
|
self.secondStr = '+hold_' + 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]
|
|
mainMinusName = mainMinusLine.split(' ')[1]
|
|
# second arg, without first or last quote
|
|
mainMinusStr = mainMinusLine.split(' ', 2)[2][1:-1]
|
|
|
|
altLines = altCode.splitlines()
|
|
altMinusLine = altLines[1]
|
|
altMinusName = altMinusLine.split(' ')[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)
|
|
|
|
altMinusLineStart = ' '.join(altMinusLine.split(' ')[:2])
|
|
altLines[1] = altMinusLineStart + f' "{allCancelStr}"'
|
|
altLines.insert(3, f'alias -{self.secondStr} "{altMinusName}"')
|
|
mainMinusLineStart = ' '.join(mainMinusLine.split(' ')[:2])
|
|
mainLines[1] = mainMinusLineStart + f' "{allCancelStr}"'
|
|
mainLines.insert(3, f'alias -{self.primStr} "{mainMinusName}"')
|
|
|
|
return (
|
|
'\n'.join(mainLines) + '\n',
|
|
'\n'.join(altLines) + '\n'
|
|
)
|
|
|
|
|
|
class Repeat(ScriptBind):
|
|
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(ScriptBind):
|
|
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'
|
|
|
|
|
|
class Bind(ScriptBind):
|
|
def verify(self):
|
|
# just set self.text, and if it's not a string, make an error
|
|
self.text = self.fields
|
|
if isinstance(self.text, str):
|
|
# clear self.fields to assure no "extra field" warnings
|
|
self.fields = {}
|
|
else:
|
|
# not passed string ==> error
|
|
self.err('argument must be a string')
|
|
|
|
def toTF2(self) -> str:
|
|
return f'bind {self.key} {self.text}\n'
|
|
|
|
# This is at the bottom because it has to happen after
|
|
# all inheritances have been completed
|
|
|
|
ScriptBind.bindTypes = ScriptBind.__subclasses__()
|
|
Double.bindNames = [ bind.__name__.lower() for bind in ScriptBind.bindTypes ]
|