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 ]