diff --git a/src/tfscript/tftypes.py b/src/tfscript/tftypes.py index 2404817..c5ace2f 100644 --- a/src/tfscript/tftypes.py +++ b/src/tfscript/tftypes.py @@ -18,19 +18,13 @@ validKeyList = [ 'leftarrow', 'rightarrow' ] -bindTypes = [ - 'impulse', - 'hold', - 'toggle', - 'double', - 'repeat' -] - -class bind: +class bind(object): ''' Parent class for all bind types. Verifies key, creates local variables ''' + bindTypes = [] + instances = {} def __init__(self, key, fields): self.alias = False @@ -43,19 +37,29 @@ class bind: # and some other universal fields like alias and finds targetType self.verify() - if type(self) == bind or len(self.fields) == 0: + if type(self) is bind: + # not using isinstance(), because all subclasses are also instances + # of bind. return - # verify function should remove all fields relavent to the bind. - # Any extras are errors - self.errors.append(f'extra fields in "{self.key}":') - if isinstance(self.fields, str): - # iterating over a str returns each character, - # making meaningless error messages - self.errors.append(f' "{self.fields}"') - else: - for field in self.fields: - self.errors.append(f' "{field}"') + if len(self.fields) > 0: + # verify function should remove all fields relavent to the bind. + # Any extras are errors + + self.errors.append(f'extra fields in "{self.key}":') + if isinstance(self.fields, str): + # iterating over a str returns each character, + # making meaningless error messages + self.errors.append(f' "{self.fields}"') + else: + for field in self.fields: + self.errors.append(f' "{field}"') + elif 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): @@ -63,14 +67,14 @@ class bind: typeName, self.key = self.key.split(' ', 1) self.key = self.key.lower() except ValueError: - # catastrophic error: no type + # catastrophic error: either no type or no key, assume no type self.errors.append(f'could not find type in "{self.key}"') return 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.errors.append(f'alias should be "yes" or "no", not "{self.alias}"') self.alias = False except (KeyError, AttributeError, TypeError): self.alias = False @@ -78,16 +82,9 @@ class bind: if (not self.alias) and (self.key not in validKeyList): self.errors.append(f'invalid key name: "{self.key}"') - types = { - 'impulse': impulse, - 'hold': hold, - 'toggle': toggle, - 'double': double, - 'repeat': repeat - } - for loopName, typeClass in types.items(): - if loopName == typeName: - self.targetType = typeClass + for type_ in self.bindTypes: + if typeName == type_.__name__: + self.targetType = type_ break if self.targetType is None: @@ -101,6 +98,7 @@ class bind: # cast to targetType, extend errors bind = self.targetType(self.key, self.fields) bind.errors.extend(self.errors) + bind.alias = self.alias return bind def err(self, message): @@ -110,18 +108,19 @@ class bind: 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') - except (KeyError, AttributeError, TypeError): - self.fields = {'command': self.fields} - self.command = self.fields.pop('command') + except KeyError: + self.err('requires `command` field') if isinstance(self.command, str): self.command = self.command.split(';') elif not isinstance(self.command, list): - self.err('must be command or argument of string or list') + self.err('`command` field must be argument of string or list') self.command = None def toTF2(self) -> str: @@ -144,9 +143,9 @@ class impulse(bind): cmd, restOfCmd = instruction, '' simpleSCs = { - "primary": "slot1", - "secondary": "slot2", - "melee": "slot3" + 'primary': 'slot1', + 'secondary': 'slot2', + 'melee': 'slot3' } try: cmd = simpleSCs[cmd] @@ -154,14 +153,14 @@ class impulse(bind): # not a shortcut pass - if cmd == "voice": - cmd = "voicemenu" + if cmd == 'voice': + cmd = 'voicemenu' restOfCmd = self.expandVoice(restOfCmd) - elif cmd == "build" or cmd == "destroy": + elif cmd == 'build' or cmd == 'destroy': restOfCmd = self.expandBuildings(restOfCmd) - elif cmd == "load_itempreset" and restOfCmd.isalpha(): + elif cmd == 'load_itempreset' and restOfCmd.isalpha(): try: restOfCmd = restOfCmd.lower() restOfCmd = ['a','b','c','d'].index(restOfCmd) @@ -169,7 +168,7 @@ class impulse(bind): # not a load_itempreset shortcut pass - if restOfCmd != "": + if restOfCmd != '': cmd += ' ' + restOfCmd instList[i] = cmd @@ -179,9 +178,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): @@ -191,10 +190,10 @@ class impulse(bind): def expandBuildings(self, building): buildingNums = { - "dispenser": "0 0", - "entrance": "1 0", - "exit": "1 1", - "sentry": "2 0" + 'dispenser': '0 0', + 'entrance': '1 0', + 'exit': '1 1', + 'sentry': '2 0' } for shortBuild, num in buildingNums.items(): if building == shortBuild: @@ -205,60 +204,122 @@ class hold(bind): def verify(self): self.press = None self.release = None - if isinstance(self.fields, dict): - # verify press - try: - self.press = self.fields.pop('press') - if not isinstance(self.press, (str, list)): - self.err('press must be string or list') - self.press = None - except KeyError: - self.err('requires press field') - - # verify release - try: - self.release = self.fields.pop('release') - if not isinstance(self.release, (str, list)): - self.err('release must be string or list') - self.release = None - except KeyError: - self.err('requires release field') - - elif isinstance(self.fields, str): + if not isinstance(self.fields, dict): self.fields = {'press': self.fields} + + # verify press + try: self.press = self.fields.pop('press') + except KeyError: + self.err('requires `press` field') + + 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 + + # verify release + try: + self.release = self.fields.pop('release') + except KeyError: + # 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:]) + + 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 + + def toTF2(self) -> str: + if self.alias: + bindOrAlias = 'alias' else: - self.err('must be press and release, or argument of string') + 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 isinstance(self.fields, dict): - # verify on - try: - self.on = self.fields.pop('on') - if not isinstance(self.on, (str, list)): - self.err('on must be string or list') - self.on = None - except KeyError: - self.err('requires on field') - - # verify off - try: - self.off = self.fields.pop('off') - if not isinstance(self.off, (str, list)): - self.err('off must be string or list') - self.off = None - except KeyError: - self.err('requires end field') - - elif isinstance(self.fields, str): + if not isinstance(self.fields, dict): self.fields = {'on': self.fields} + + # verify on + try: self.on = self.fields.pop('on') + except KeyError: + self.err('requires `on` field') + + 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 + + # verify off + try: + self.off = self.fields.pop('off') + except KeyError: + # 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:]) + + 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 = None + + def toTF2(self) -> str: + if self.alias: + bindOrAlias = 'alias' else: - self.err('must be on and end, or argument of string') + 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 trailing " and \n + :-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): @@ -267,69 +328,142 @@ class double(bind): 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" + # either 'released' (default) or 'both' self.cancel = 'released' # toggler try: self.condition = self.fields.pop('condition') if self.condition not in validKeyList: - self.err(f'has invalid condition to toggle: "{self.condition}"') + self.err(f'has invalid `condition` field: "{self.condition}"') except KeyError: - self.err('requires condition to toggle') + self.err('requires `condition` field') - # cancel mode - try: - self.cancel = self.fields.pop('cancel') - if self.cancel not in ['released', 'both']: - self.err(f'cancel must be either "released" or "both", not "{self.cancel}"') - except KeyError: - # maintain default - pass + if 'toggle' in self.fields: + self.isToggle = self.fields.pop('toggle') + if not isinstance(self.isToggle, bool): + self.err(f'`toggle` field should be "yes" or "no", not "{self.isToggle}"') # type try: self.type = self.fields.pop('type') - if self.type not in bindTypes: + if self.type not in [ type_.__name__ for type_ in self.bindTypes ]: # catastrophic: invalid type self.err(f'has invalid type: "{self.type}"') return except KeyError: # catastrophic: no type given - self.err('requires type') + self.err('requires `type` field') return + # cancel mode + if 'cancel' in self.fields: + self.cancel = self.fields.pop('cancel') + if self.cancel in ('released', 'both'): + if self.cancel == 'both' and self.type != 'hold': + self.err(f'`cancel` field only affects "hold", not "{self.type}"') + elif isinstance(self.cancel, str): + self.err(f'`cancel` field must be "released" or "both", not "{self.cancel}"') + else: + self.err(f'`cancel` field must be argument of "released" or "both"') + # primary action try: mainSection = self.fields.pop('primary') - # mainSection.update({'alias': True}) - mainAction = bind(f'{self.type} primary', mainSection) - if mainAction.targetType is not None: - mainAction = mainAction.targetType(mainAction.key, mainAction.fields) - - self.errors.extend(mainAction.errors) + mainAction = bind(f'{self.type} {self.primStr}', mainSection) + self.primary = mainAction.toTargetType() except KeyError: - self.err('requires primary action') + self.err('requires `primary` field') # secondary action try: altSection = self.fields.pop('secondary') - # altSection.update({'alias': True}) - altBind = bind(f'{self.type} secondary', altSection) - if altBind.targetType is not None: - altBind = altBind.targetType(altBind.key, altBind.fields) - - self.errors.extend(altBind.errors) + altBind = bind(f'{self.type} {self.secondStr}', altSection) + self.secondary = altBind.toTargetType() except KeyError: - self.err('requires secondary action') + self.err('requires `secondary` field') + + def toTF2(self) -> str: + if self.alias: + bindOrAlias = 'alias' + else: + bindOrAlias = 'bind' + + # Get code for primary and secondary actions + self.primary.alias = True + mainCode = self.primary.toTF2() + self.secondary.alias = True + altCode = self.secondary.toTF2() + + # Make code to toggle between the two actions + pShiftStr = f'+shift_{self.key}' + mShiftStr = f'-shift_{self.key}' + + if self.cancel == 'both': + # 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 quote + mainMinusStr = mainMinusLine.split(' ', 2)[2][1:] + + altLines = altCode.splitlines() + altMinusLine = altLines[1] + # same as above + altMinusStr = altMinusLine.split(' ', 2)[2][1:] + + mainLines[1] = mainLines[1][:-1] + f';{altMinusStr}' + altLines[1] = altLines[1][:-1] + f';{mainMinusStr}' + mainCode = '\n'.join(mainLines) + '\n' + altCode = '\n'.join(altLines) + '\n' + + if self.type == 'hold': + self.primStr = '+' + self.primStr + self.secondStr = '+' + self.secondStr + + result = mainCode + altCode +\ + f'alias {pShiftStr} "{bindOrAlias} {self.key} {self.primStr}"\n' +\ + f'alias {mShiftStr} "{bindOrAlias} {self.key} {self.secondStr}"\n'+\ + f'{bindOrAlias} {self.key} "{self.primStr}"\n' + + try: + # If the condition key (like 'mouse4') already has toggles, + # just append another toggle string + changes = self.condDict[self.condition]['change_keys'] + restores = self.condDict[self.condition]['restore_keys'] + + if pShiftStr not in changes: + # not already in changes + changes.append(pShiftStr) + restores.append(mShiftStr) + + except KeyError: + # If the condition key doesn't already exist, make it + self.condDict.update( { + self.condition: { + 'change_keys': [ pShiftStr ], + 'restore_keys': [ mShiftStr ], + 'alias': self.alias + } + } ) + + return result class repeat(bind): @@ -354,3 +488,12 @@ class repeat(bind): self.command = None except KeyError: self.err('requires command') + + def toTF2(self) -> str: + # commented-out placeholder + return f'// repeat {self.key}\n' + +# This is at the bottom because it has to happen after +# all inheritances have been completed + +bind.bindTypes = bind.__subclasses__() \ No newline at end of file