diff --git a/src/tfscript/tftypes.py b/src/tfscript/tftypes.py index b39beb0..e4b8fd4 100644 --- a/src/tfscript/tftypes.py +++ b/src/tfscript/tftypes.py @@ -28,19 +28,19 @@ class Bind(object): bindTypes = [] instances = {} - def __init__(self, key='', fields={}, *, parent=None): + 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.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.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 @@ -72,15 +72,11 @@ class Bind(object): 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 + 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) @@ -107,6 +103,21 @@ class Bind(object): 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 @@ -128,21 +139,20 @@ class Bind(object): class Impulse(Bind): def verify(self): - self.command = None + self.command: list = 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.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' @@ -167,11 +177,8 @@ class Impulse(Bind): 'secondary': 'slot2', 'melee': 'slot3' } - try: - cmd = simpleSCs[cmd] - except KeyError: - # not a shortcut - pass + # if is shortcut, change + cmd = simpleSCs.get(cmd, cmd) if cmd == 'voice': cmd = 'voicemenu' @@ -183,8 +190,8 @@ class Impulse(Bind): elif cmd == 'loadout' and restOfCmd.isalpha(): cmd = 'load_itempreset' try: - restOfCmd = restOfCmd.lower() - restOfCmd = str(['a','b','c','d'].index(restOfCmd)) + loadoutNum = ['a','b','c','d'].index(restOfCmd.lower()) + restOfCmd = str(loadoutNum) except ValueError: # not a load_itempreset shortcut pass @@ -204,9 +211,9 @@ class Impulse(Bind): 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'), + ('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): @@ -221,40 +228,32 @@ class Impulse(Bind): 'exit': '1 1', 'sentry': '2 0' } - for shortBuild, num in buildingNums.items(): - if building == shortBuild: - return num + return buildingNums.get(building, building) class Hold(Bind): def verify(self): - self.press = None - self.release = None + 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.fields.pop('press') - if isinstance(self.press, str): - self.press = self.press.split(';') - elif not isinstance(self.press, list): + self.press = self.cmdListFrom('press') + if self.press is None: self.err('`press` field must be string or list') - self.press = None - except KeyError: + except popErrors: 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.release = self.cmdListFrom('release') + if self.release is None: self.err('`release` field must be string or list') - self.release = None 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 = [] @@ -271,11 +270,11 @@ class Hold(Bind): # Making impulse instances from self.press and .release # allows them to share the shortcuts - pressObj = Impulse(f'+{holdStr}', self.press) + pressObj = Impulse('+' + holdStr, self.press) pressObj.alias = True pressStr = pressObj.toTF2() - releaseObj = Impulse(f'-{holdStr}', self.release) + releaseObj = Impulse('-' + holdStr, self.release) releaseObj.alias = True releaseStr = releaseObj.toTF2() @@ -296,34 +295,29 @@ class Hold(Bind): class Toggle(Bind): def verify(self): - self.on = None - self.off = None + 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.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.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') - 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') + 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:]) @@ -357,25 +351,20 @@ class Toggle(Bind): class Double(Bind): defaultDict = {} - condDict = {} - bindNames = [] + 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.primStr = f'{self.key}_primary' + self.secondStr = f'{self.key}_secondary' + self.isToggle = False self.cancelBoth = False + self.primary: Bind = None + self.secondary: Bind = None + self.condition: str = None + self.type: str = None + # toggler try: self.condition = self.fields.pop('condition') @@ -384,15 +373,12 @@ class Double(Bind): 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 + 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: @@ -407,56 +393,57 @@ class Double(Bind): return # cancel mode, must happend after type has been inferred - try: - cancel = self.fields.pop('cancel') + 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': + 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 must be "released" ' - + f'or "both", not "{cancel}"' + '`cancel` field only affects "hold",' + + f' not "{self.type}"' ) - except popErrors: - cancel = 'released' + elif cancel == 'released': + self.cancelBoth = False + else: + self.err( + '`cancel` field must be "released"' + + f' or "both", not "{cancel}"' + ) - # 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 + self.primary = self.getSection('primary', self.primStr) except popErrors: - self.err('requires `primary` field') + self.primary = None - # 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 + self.secondary = self.getSection('secondary', self.secondStr) except popErrors: - self.err('requires `secondary` field') + self.secondary = None + if self.primary is self.secondary is None: + self.err('has neither primary nor secondary') + + def getSection(self, popName, key, /) -> Bind: + section = self.fields.pop(popName) + bind = Bind(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.