From 494429248343ff49f7469f6a5a569e7b72332060 Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Sun, 21 Aug 2022 21:12:44 -0400 Subject: [PATCH 01/21] Verification for everything --- src/tfscript/types.py | 268 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 268 insertions(+) create mode 100644 src/tfscript/types.py diff --git a/src/tfscript/types.py b/src/tfscript/types.py new file mode 100644 index 0000000..2d154f7 --- /dev/null +++ b/src/tfscript/types.py @@ -0,0 +1,268 @@ +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' +] + +bindTypes = [ + 'impulse', + 'hold', + 'toggle', + 'double', + 'repeat' +] + +class bind: + ''' + Parent class for all bind types. + Verifies key, creates local variables + ''' + + def __init__(self, key, fields): + self.key = key + self.fields = fields + self.errors = [] + self.targetType = None + + # overloaded for each unique type, default just verifies key + # and some other universal fields like alias and finds targetType + self.verify() + + # 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}"') + + def verify(self): + veirfyErrors = [] + + splitKey = self.key.split(' ', 1) + splitLen = len(splitKey) + if splitLen == 1: + # catastrophic error + self.errors.append(f'could not find type in "{self.key}"') + return + elif splitLen == 2: + # temporarily storing string + typeName = splitKey[0] + self.fields = {typeName: self.fields} + self.key = splitKey[1] + + try: + isalias = self.fields.pop('alias') + if not isinstance(isalias, bool): + self.errors.append(f'alias should be yes or no, not "{isalias}"') + isalias = False + except KeyError: + isalias = False + + if not isalias 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 + break + self.fields = self.fields.pop(typeName) + + if self.targetType is None: + self.errors.append(f'could not find type in "{self.key}"') + +class impulse(bind): + def verify(self): + self.command = None + + try: + self.command = self.fields.pop('command') + if not strOrList(self.command): + self.errors.append('impulse command must be string or list') + self.command = None + except (KeyError, AttributeError): + self.command = self.fields + if not strOrList(self.command): + self.errors.append('impulse must be command or argument of string or list') + self.command = None + + +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 strOrList(self.press): + self.errors.append('hold press must be string or list') + self.press = None + except KeyError: + self.errors.append('hold requires press field') + + # verify release + try: + self.release = self.fields.pop('release') + if not strOrList(self.release): + self.errors.append('hold release must be string or list') + self.release = None + except KeyError: + self.errors.append('hold requires release field') + + elif isinstance(self.fields, str): + self.press = self.fields + else: + self.errors.append('hold must be press and release, or argument of string') + + +class toggle(bind): + def verify(self): + self.start = None + self.end = None + if isinstance(self.fields, dict): + # verify start + try: + self.start = self.fields.pop('start') + if not strOrList(self.start): + self.errors.append('hold start must be string or list') + self.start = None + except KeyError: + self.errors.append('hold requires start field') + + # verify end + try: + self.end = self.fields.pop('end') + if not strOrList(self.end): + self.errors.append('hold end must be string or list') + self.end = None + except KeyError: + self.errors.append('hold requires end field') + + elif isinstance(self.fields, str): + self.start = self.fields + else: + self.errors.append('hold must be start and end, or argument of string') + + +class double(bind): + defaultDict = {} + condDict = {} + + def verify(self): + self.primary = None + self.secondary = None + self.condition = None + + # name of a bind type + self.type = None + + # either "released" (default) or "both" + self.cancel = 'released' + self. + + # toggler + try: + self.condition = self.fields.pop('condition') + if self.condition not in validKeyList: + self.errors.append(f'double has invalid condition to toggle: "{self.condition}"') + except KeyError: + self.errors.append('double requires condition to toggle') + + # cancel mode + try: + self.cancel = self.fields.pop('cancel') + if self.cancel not in ['released', 'both']: + self.errors.append(f'double cancel must be either "released" or "both", not "{self.cancel}"') + except KeyError: + # maintain default + pass + + # type + try: + self.type = self.fields.pop('type') + if self.type not in bindTypes: + # catastrophic: invalid type + self.errors.append(f'double has invalid type: "{self.type}"') + return + except KeyError: + # catastrophic: no type given + self.errors.append('double requires type') + return + + # 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) + except KeyError: + self.errors.append('double requires primary action') + + # 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) + except KeyError: + self.errors.append('double requires secondary action') + +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.errors.append('repeat interval must be greater than 0') + except KeyError: + self.errors.append('repeat requires interval') + except ValueError: + self.errors.append(f'repeat has invalid number of ticks: "{self.interval}"') + + try: + self.command = self.fields.pop('command') + if not strOrList(self.command): + self.errors.append('repeat command must be string or list') + self.command = None + except KeyError: + self.errors.append('repeat requires command') + +def strOrList(self, thing): + return (isinstance(thing, str) or isinstance(thing, list)) \ No newline at end of file -- 2.40.1 From 9ba82add220829ac6d5dbe723c05e39cc675bcc5 Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Sun, 21 Aug 2022 21:20:52 -0400 Subject: [PATCH 02/21] Half of new double option --- src/tfscript/__init__.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/tfscript/__init__.py b/src/tfscript/__init__.py index 6627375..e255836 100644 --- a/src/tfscript/__init__.py +++ b/src/tfscript/__init__.py @@ -177,12 +177,12 @@ def listHold(key, options): oldBindOrAlias = bindOrAlias bindOrAlias = 'alias' - ret = impulse(f'+{key}_press', options["press"]) +\ - impulse(f'-{key}_press', options["release"]) + plus = impulse(f'+{key}_press', options["press"]) + minus = impulse(f'-{key}_press', options["release"]) bindOrAlias = oldBindOrAlias - ret += f'{bindOrAlias} {key} "+{key}_press"\n' + ret = plus + minus + f'{bindOrAlias} {key} "+{key}_press"\n' return ret @@ -201,9 +201,10 @@ def toggle(key, instruction): return ret def double(key, options): - primaryAction = options["primary"] + typeName = options["type"] + primaryAction = {typeName: options.pop("primary")} - secAction = options["secondary"] + secAction = {typeName: options.pop("secondary")} mainStr = f'{key}_main' altStr = f'{key}_alt' @@ -213,11 +214,24 @@ def double(key, options): global bindOrAlias oldBindOrAlias = bindOrAlias bindOrAlias = "alias" - recursiveCode = branch(mainStr, primaryAction) +\ - branch(altStr, secAction) + + mainCode = branch(mainStr, primaryAction) + altCode = branch(altStr, secAction) + cancelBoth = ("cancel" in options and options["cancel"] == "both") + if cancelBoth: + if isinstance(primaryAction["hold"], dict): + lines = mainCode.splitlines() + minusCmd = lines[1] + _, minusStr, previousMinus = minus.split(' ', 2) + newMinus = previousMinus[0:-2] + ';' + + '"\n' + lines[1] = f'alias {minusStr} "{newMinus}"\n' + else: + # simple + pass + bindOrAlias = oldBindOrAlias - ret = recursiveCode +\ + ret = mainCode + altCode +\ f'alias {pShiftStr} "{bindOrAlias} {key} {altStr}"\n' +\ f'alias {mShiftStr} "{bindOrAlias} {key} {mainStr}"\n'+\ f'{bindOrAlias} {key} "{mainStr}"\n' -- 2.40.1 From c3b816c53253a258676407d91c0da343f539a0a2 Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Sun, 21 Aug 2022 21:21:09 -0400 Subject: [PATCH 03/21] sort of works with OOP --- src/tfscript/cli.py | 76 ++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/src/tfscript/cli.py b/src/tfscript/cli.py index 506f93a..b5aac6f 100644 --- a/src/tfscript/cli.py +++ b/src/tfscript/cli.py @@ -1,13 +1,13 @@ -""" +''' Command line module for making Team Fortress 2 macro scripts from YAML source code. -""" +''' __all__ = ['parseFile'] -__author__ = "Nicholas Hope (dict, dict): - """Parse, verify, and do the conversion.""" + '''Parse, verify, and do the conversion.''' config = yaml.safe_load(inputFile) # See verify.py - config, aliases = verify.verifyConfig(config) - if "errors" in config: - for cclass, messages in config["errors"].items(): - print(f"Error in {cclass}:") + config, defaults = verify.verifyConfig(config) + if 'errors' in config: + for cclass, messages in config['errors'].items(): + print(f'Error in {cclass}:') for msg in messages: - print(f" {msg}") + print(f' {msg}') return None, None else: - return config, aliases + return config, defaults def parseConfig(config, defaults): - """With validated data structure, write out all the files.""" + '''With validated data structure, write out all the files.''' global args global targetDir if isdir(targetDir) == False: mkdir(targetDir) if args.debug: - print( f"DEBUG: Created directory {targetDir}", file=stderr) + print( f'DEBUG: Created directory {targetDir}', file=stderr) tempsAndReals = {} if defaults is not None: stringToWrite = tfscript.makeCFG(defaults, default=True) - replaceDict = writing.writeOutput(stringToWrite, "default", args) + replaceDict = writing.writeOutput(stringToWrite, 'default', args) tempsAndReals.update(replaceDict) for currentClass in config: @@ -76,56 +76,56 @@ def parseConfig(config, defaults): def parseCLI(): # Handle command line parser = argparse.ArgumentParser( - description="Parse YAML file and produce TF2 config script." + description='Parse YAML file and produce TF2 config script.' ) parser.add_argument( '-d', '--debug', action='store_true', - help="Enable debugging messages.") + help='Enable debugging messages.') parser.add_argument( '-n', '--dry-run', action='store_true', - help="Parse input file, but don't write anything.") + help='Parse input file, but don\'t write anything.') parser.add_argument( '-f', '--force', action='store_true', - help="Force tfscript to continue until catastrophic failure") + help='Force tfscript to continue until catastrophic failure') parser.add_argument( '-D', '--directory', action='store', type=str, - help="Change output directory") + help='Change output directory') # positional argument: first non-hyphenated argument is input file parser.add_argument( 'infile', type=argparse.FileType('r'), help='File containing YAML to convert.') return parser def getTargetDir(systemName): - if systemName == "Darwin": + if systemName == 'Darwin': if float( '.'.join( GetOSRelease().split('.')[0:2] ) ) >= 10.15: warn( - "As of macOS Catalina (v10.15), 32-bit applications " - "like TF2 do not run. tfscript will run, but you can't run TF2 " - "on this system", + 'As of macOS Catalina (v10.15), 32-bit applications ' + 'like TF2 do not run. tfscript will run, but you can\'t run TF2 ' + 'on this system', category=RuntimeWarning ) - return expanduser("~/Library/Application Support/Steam") + return expanduser('~/Library/Application Support/Steam') - elif systemName == "Windows": + elif systemName == 'Windows': # oh god why do we have to use the registry accessReg = ConnectRegistry(None, HKEY_LOCAL_MACHINE) - accessKey = OpenKey(accessReg, "SOFTWARE\\WOW6432Node\\Valve\\Steam") + accessKey = OpenKey(accessReg, 'SOFTWARE\\WOW6432Node\\Valve\\Steam') keyNum = 0 while True: try: accessSubkeyName, data, _ = EnumValue(accessKey, keyNum) - if accessSubkeyName == "InstallPath": + if accessSubkeyName == 'InstallPath': return data except EnvironmentError: break keyNum += 1 return None - elif systemName == "Linux": - return expanduser("~/.local/Steam") + elif systemName == 'Linux': + return expanduser('~/.local/Steam') - elif systemName == "Java": - warn("Java-based OSes are not supported yet by tfscript.", category=RuntimeWarning) + elif systemName == 'Java': + warn('Java-based OSes are not supported yet by tfscript.', category=RuntimeWarning) return None def main() -> int: - """ Command line interface. """ + ''' Command line interface. ''' global args global targetDir parser = parseCLI() @@ -139,11 +139,11 @@ def main() -> int: targetDir = getTargetDir(systemName) if targetDir is not None: # Supported OS: add steamapps path - targetDir += normpath("/steamapps/common/Team Fortress 2/tf/cfg") + dirsep + targetDir += normpath('/steamapps/common/Team Fortress 2/tf/cfg') + dirsep elif args.force: # Unsupported OS but -f specified if args.debug: - print("DEBUG: forced to continue, output set to current directory", file=stderr) + print('DEBUG: forced to continue, output set to current directory', file=stderr) targetDir = '.' else: # Unsupported OS and not forced to continue @@ -160,5 +160,5 @@ def main() -> int: return 0 -if __name__ == "__main__": +if __name__ == '__main__': exit(main()) -- 2.40.1 From 7936dd0d1009fa160a3a7fd86628f34a502f0014 Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Sun, 21 Aug 2022 21:21:41 -0400 Subject: [PATCH 04/21] refactored to work with OOP --- src/tfscript/verify.py | 249 +++++++---------------------------------- 1 file changed, 39 insertions(+), 210 deletions(-) diff --git a/src/tfscript/verify.py b/src/tfscript/verify.py index 973c8ff..1dc0d6f 100644 --- a/src/tfscript/verify.py +++ b/src/tfscript/verify.py @@ -1,5 +1,6 @@ """Verify all the things that could go wrong.""" from copy import deepcopy +from tfscript import types def verifyConfig(cfg: dict) -> (dict, dict): verifiedConfig = {} @@ -7,35 +8,37 @@ def verifyConfig(cfg: dict) -> (dict, dict): # Do defaults first errors = {} - defaults = None + defaults = [] - if "default" in cfg: - defaults = cfg.pop("default") + if 'default' in cfg: + defaultCFG = cfg.pop('default') errMessages = [] - for key, data in defaults.items(): - isAlias = False - if "alias" in data: - isAlias = data["alias"] - if not isinstance(isAlias, bool): - errMessages.append(f'"alias" field in "{key}" makes no sense: "{isAlias}"') - errMessages.extend(validBind(key, data, alias = isAlias) ) - if len(errMessages) > 0: - errors.update( {"default": errMessages} ) + for key, data in defaultCFG.items(): + bind = types.bind(key, data) + if bind.targetType is not None: + bind = bind.targetType(bind.key, bind.fields) + defaults.append(bind) + + errMessages.extend(bind.errors) + + if len(errList) > 0: + errors.update({'default': errMessages}) classList = [ - "scout", - "soldier", - "pyro", - ("demo","demoman"), - ("engi","engineer"), - ("heavy","heavyweapons"), - "medic", - "sniper", - "spy" + 'scout', + 'soldier', + 'pyro', + ('demo','demoman'), + ('engi','engineer'), + ('heavy','heavyweapons'), + 'medic', + 'sniper', + 'spy' ] for cclass in classList: classCFG = None + classBinds = [] className = cclass if isinstance(cclass, str) and cclass in cfg: classCFG = cfg.pop(cclass) @@ -52,21 +55,23 @@ def verifyConfig(cfg: dict) -> (dict, dict): continue errMessages = [] for key, data in classCFG.items(): - isAlias = False - if "alias" in data: - isAlias = data["alias"] - if not isinstance(isAlias, bool): - errMessages.append(f'"alias" field in "{key}" makes no sense: "{isAlias}"') - errMessages.extend( validBind(key, data, alias = isAlias) ) + bind = types.bind(key, data) + + if binds.targetType is not None: + bind = bind.targetType(bind.key, bind.fields) + classBinds.append(bind) + + errMessages.extend(bind.errors) + if len(errMessages) > 0: errors.update( {className: errMessages} ) - verifiedConfig.update({className: classCFG}) + verifiedConfig.update({className: classBinds}) # Turn list into only strings by expanding tuples - for i, clss in enumerate(classList): - if isinstance(clss, tuple): - classList.insert(i+1, clss[1]) - classList.insert(i+1, clss[0]) + for i, cclass in enumerate(classList): + if isinstance(cclass, tuple): + classList.insert(i+1, cclass[1]) + classList.insert(i+1, cclass[0]) classList.pop(i) globalErrors = [] @@ -78,189 +83,13 @@ def verifyConfig(cfg: dict) -> (dict, dict): globalErrors.append(f'Conflicting names for section: "{remainingClass}" and "{otherName}"') if len(globalErrors) > 0: - errors.update({"file": globalErrors}) + errors.update({'file': globalErrors}) if len(errors) > 0: - verifiedConfig.update({"errors": errors}) + verifiedConfig.update({'errors': errors}) return verifiedConfig, defaults -def validBind(key, data, alias = False) -> list: - """Check for valid key and valid binding""" - ret = [] - splitKey = key.split(' ') - if len(splitKey) > 1: - key = splitKey[1] - data = {splitKey[0]: data} - - if (not alias and not validKey(key)): - ret.append(f'Invalid key "{key}"') - - # The values passed to validBindType get mutilated, so a copy must be made - dataCopy, errMsgs = validBindType(key, data) - ret.extend(errMsgs) - - extras = dataCopy.keys() - if len(extras) > 0: - extrasString = "\n ".join(extras) - ret.append(f'Unused fields in "{key}":\n {extrasString}') - - return ret - -validKeyList = [ - '\'', '=', ',', '-', '[', '\\', ']', '`', '.', '/', - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', - 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', - 'u', 'v', 'w', 'x', 'y', 'z', - 'mouse1', 'mouse2', 'mouse3', 'mouse4', 'mouse5', - 'shift', 'capslock', 'ctrl', 'semicolon', 'space', 'enter', - 'backspace', - 'scrolllock', 'numlock', - 'ins', 'home', 'pgup', - 'del', 'end', 'pgdn' -] - -def validKey(key): - """determines if the key is a valid key""" - key = str(key).lower() - return key in validKeyList - -def validBindType(key, data: dict): - """ - Checks if `key` has a valid bind type, - like 'double' or 'hold' - """ - validType = False - types = [ - "impulse", - "hold", - "toggle", - "double", - "repeat" - ] - - errMsgs = [] - for potentialType in data.keys(): - if potentialType in types: - validType = True - break - - if validType: - expandSpaceFields(data) - dataCopy = deepcopy(data) - dataCopy, errMsgs = removeRelaventFields(key, dataCopy, potentialType) - else: - errMsgs.append(f'Key "{key}" has no known bind type') - - return dataCopy, errMsgs - -def expandSpaceFields(data): - keys = list(data.keys()) - for field in keys: - # if you change a key of a dict, while looping - # over the dict, it causes a RuntimeError. - # Looping as list avoids this - splitField = field.split(' ', 1) - if len(splitField) > 1: - newContent = {splitField[0]: data.pop(field)} - data.update({splitField[1]: newContent}) - field = splitField[1] - content = data[field] - if isinstance(content, dict): - expandSpaceFields(content) - -def removeRelaventFields(key, data, bindType): - errMsgs = [] - content = data.pop(bindType) - if "alias" in data: - # alias is universal - data.pop("alias") - - if bindType == "impulse": - if isinstance(content, dict): - if "command" not in content: - errMsgs.append('impulse requires `command` argument') - else: - content.pop("command") - else: - if not isinstance(content, str) and not isinstance(content, list): - errMsgs.append(f'impulse must be a single string or list') - - elif bindType == "toggle": - if isinstance(content, dict): - if "begin" not in content: - errMsgs.append("toggle requires `begin` argument") - if "end" not in content: - errMsgs.append("toggle requires `end` argument") - elif not isinstance(content, str): - errMsgs.append(f"toggle must be either single action or begin and end") - - elif bindType == "hold": - if isinstance(content, dict): - if "press" not in content: - errMsgs.append("hold requires `press` argument") - if "release" not in content: - errMsgs.append("hold requires `release` argument") - elif not isinstance(content, str): - errMsgs.append(f"Hold must be either single action or press and release") - - elif bindType == "double": - if "primary" not in content: - errMsgs.append("Double requires primary action") - else: - # Nasty bit of recursion to validate the action. - # It takes advantage of `alias = True` not verifying the key, - # but it isn't an alias, I'm just lazy. - errMessages = validBind("primary", content["primary"], alias = True) - errMsgs.extend(errMessages) - - if "secondary" not in content: - errMsgs.append("Double requires secondary action") - else: - # Same logic as above - errMessages = validBind("secondary", content["secondary"], alias = True) - errMsgs.extend(errMessages) - - if "condition" not in content: - errMsgs.append("Double requires condition to toggle") - else: - # Validate the toggler - condition = content["condition"] - if not validKey(condition): - errMsgs.append(f'Invalid condition to toggle "{condition}"') - - if "cancel" in content: - canType = content["cancel"] - if canType not in ["released", "both"]: - errMsgs.append(f'"cancel" must be either "released" or "both", not {canType}') - - elif bindType == "repeat": - unit = "s" - if "unit" in content: - # Set unit if provided - unitStr = str(content["unit"]).lower() - if unitStr in ["t", "ticks"]: - unit = "t" - elif unitStr not in ["s", "seconds"]: - # If not seconds or ticks, error - errMsgs.append(f"Invalid interval unit {unitStr}") - - if "interval" not in content: - errMsgs.append("Repeat requires interval") - else: - intervalStr = content["interval"] - if unit == "s": - interval = float(intervalStr) - else: - interval = int(intervalStr) - if interval <= 0: - errMsgs.append("Repeat interval must be positive") - elif interval <= 200/6: - errMsgs.append(f"Repeat interval must be greater than 1 tick (approx. {200/6:.3f}s)") - - return data, errMsgs - def findTwin(className): classDict = { "demo": "demoman", -- 2.40.1 From d49e79e4106ef4bfb9af91e91411dec2ff158d48 Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Thu, 25 Aug 2022 20:34:38 -0400 Subject: [PATCH 05/21] temp push --- src/tfscript/__init__.py | 3 +++ src/tfscript/cli.py | 12 ++++++------ src/tfscript/verify.py | 10 +++++----- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/tfscript/__init__.py b/src/tfscript/__init__.py index e255836..df98ed4 100644 --- a/src/tfscript/__init__.py +++ b/src/tfscript/__init__.py @@ -10,6 +10,9 @@ def makeCFG(cfg, default=False): global bindOrAlias global condDict global defaultDict + for bind in cfg: + print(bind.key) + return bindOrAlias = "bind" if default: diff --git a/src/tfscript/cli.py b/src/tfscript/cli.py index b5aac6f..a64f2d3 100644 --- a/src/tfscript/cli.py +++ b/src/tfscript/cli.py @@ -41,9 +41,9 @@ def parseFile(inputFile) -> (dict, dict): config, defaults = verify.verifyConfig(config) if 'errors' in config: for cclass, messages in config['errors'].items(): - print(f'Error in {cclass}:') + print(f'Error in {cclass}:', file=stderr) for msg in messages: - print(f' {msg}') + print(f' {msg}', file=stderr) return None, None else: return config, defaults @@ -62,14 +62,14 @@ def parseConfig(config, defaults): if defaults is not None: stringToWrite = tfscript.makeCFG(defaults, default=True) - replaceDict = writing.writeOutput(stringToWrite, 'default', args) - tempsAndReals.update(replaceDict) + # replaceDict = writing.writeOutput(stringToWrite, 'default', args) + # tempsAndReals.update(replaceDict) for currentClass in config: classDict = config[currentClass] stringToWrite = tfscript.makeCFG(classDict) - replaceDict = writing.writeOutput(stringToWrite, currentClass, args) - tempsAndReals.update(replaceDict) + # replaceDict = writing.writeOutput(stringToWrite, currentClass, args) + # tempsAndReals.update(replaceDict) return tempsAndReals diff --git a/src/tfscript/verify.py b/src/tfscript/verify.py index 1dc0d6f..6b1b033 100644 --- a/src/tfscript/verify.py +++ b/src/tfscript/verify.py @@ -1,6 +1,6 @@ """Verify all the things that could go wrong.""" from copy import deepcopy -from tfscript import types +from tfscript import tftypes def verifyConfig(cfg: dict) -> (dict, dict): verifiedConfig = {} @@ -14,14 +14,14 @@ def verifyConfig(cfg: dict) -> (dict, dict): defaultCFG = cfg.pop('default') errMessages = [] for key, data in defaultCFG.items(): - bind = types.bind(key, data) + bind = tftypes.bind(key, data) if bind.targetType is not None: bind = bind.targetType(bind.key, bind.fields) defaults.append(bind) errMessages.extend(bind.errors) - if len(errList) > 0: + if len(errMessages) > 0: errors.update({'default': errMessages}) classList = [ @@ -55,9 +55,9 @@ def verifyConfig(cfg: dict) -> (dict, dict): continue errMessages = [] for key, data in classCFG.items(): - bind = types.bind(key, data) + bind = tftypes.bind(key, data) - if binds.targetType is not None: + if bind.targetType is not None: bind = bind.targetType(bind.key, bind.fields) classBinds.append(bind) -- 2.40.1 From 70733b1aad0419139acc52f40e068919e50eb8c6 Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Thu, 25 Aug 2022 21:27:47 -0400 Subject: [PATCH 06/21] deleted --- src/tfscript/types.py | 268 ------------------------------------------ 1 file changed, 268 deletions(-) delete mode 100644 src/tfscript/types.py diff --git a/src/tfscript/types.py b/src/tfscript/types.py deleted file mode 100644 index 2d154f7..0000000 --- a/src/tfscript/types.py +++ /dev/null @@ -1,268 +0,0 @@ -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' -] - -bindTypes = [ - 'impulse', - 'hold', - 'toggle', - 'double', - 'repeat' -] - -class bind: - ''' - Parent class for all bind types. - Verifies key, creates local variables - ''' - - def __init__(self, key, fields): - self.key = key - self.fields = fields - self.errors = [] - self.targetType = None - - # overloaded for each unique type, default just verifies key - # and some other universal fields like alias and finds targetType - self.verify() - - # 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}"') - - def verify(self): - veirfyErrors = [] - - splitKey = self.key.split(' ', 1) - splitLen = len(splitKey) - if splitLen == 1: - # catastrophic error - self.errors.append(f'could not find type in "{self.key}"') - return - elif splitLen == 2: - # temporarily storing string - typeName = splitKey[0] - self.fields = {typeName: self.fields} - self.key = splitKey[1] - - try: - isalias = self.fields.pop('alias') - if not isinstance(isalias, bool): - self.errors.append(f'alias should be yes or no, not "{isalias}"') - isalias = False - except KeyError: - isalias = False - - if not isalias 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 - break - self.fields = self.fields.pop(typeName) - - if self.targetType is None: - self.errors.append(f'could not find type in "{self.key}"') - -class impulse(bind): - def verify(self): - self.command = None - - try: - self.command = self.fields.pop('command') - if not strOrList(self.command): - self.errors.append('impulse command must be string or list') - self.command = None - except (KeyError, AttributeError): - self.command = self.fields - if not strOrList(self.command): - self.errors.append('impulse must be command or argument of string or list') - self.command = None - - -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 strOrList(self.press): - self.errors.append('hold press must be string or list') - self.press = None - except KeyError: - self.errors.append('hold requires press field') - - # verify release - try: - self.release = self.fields.pop('release') - if not strOrList(self.release): - self.errors.append('hold release must be string or list') - self.release = None - except KeyError: - self.errors.append('hold requires release field') - - elif isinstance(self.fields, str): - self.press = self.fields - else: - self.errors.append('hold must be press and release, or argument of string') - - -class toggle(bind): - def verify(self): - self.start = None - self.end = None - if isinstance(self.fields, dict): - # verify start - try: - self.start = self.fields.pop('start') - if not strOrList(self.start): - self.errors.append('hold start must be string or list') - self.start = None - except KeyError: - self.errors.append('hold requires start field') - - # verify end - try: - self.end = self.fields.pop('end') - if not strOrList(self.end): - self.errors.append('hold end must be string or list') - self.end = None - except KeyError: - self.errors.append('hold requires end field') - - elif isinstance(self.fields, str): - self.start = self.fields - else: - self.errors.append('hold must be start and end, or argument of string') - - -class double(bind): - defaultDict = {} - condDict = {} - - def verify(self): - self.primary = None - self.secondary = None - self.condition = None - - # name of a bind type - self.type = None - - # either "released" (default) or "both" - self.cancel = 'released' - self. - - # toggler - try: - self.condition = self.fields.pop('condition') - if self.condition not in validKeyList: - self.errors.append(f'double has invalid condition to toggle: "{self.condition}"') - except KeyError: - self.errors.append('double requires condition to toggle') - - # cancel mode - try: - self.cancel = self.fields.pop('cancel') - if self.cancel not in ['released', 'both']: - self.errors.append(f'double cancel must be either "released" or "both", not "{self.cancel}"') - except KeyError: - # maintain default - pass - - # type - try: - self.type = self.fields.pop('type') - if self.type not in bindTypes: - # catastrophic: invalid type - self.errors.append(f'double has invalid type: "{self.type}"') - return - except KeyError: - # catastrophic: no type given - self.errors.append('double requires type') - return - - # 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) - except KeyError: - self.errors.append('double requires primary action') - - # 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) - except KeyError: - self.errors.append('double requires secondary action') - -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.errors.append('repeat interval must be greater than 0') - except KeyError: - self.errors.append('repeat requires interval') - except ValueError: - self.errors.append(f'repeat has invalid number of ticks: "{self.interval}"') - - try: - self.command = self.fields.pop('command') - if not strOrList(self.command): - self.errors.append('repeat command must be string or list') - self.command = None - except KeyError: - self.errors.append('repeat requires command') - -def strOrList(self, thing): - return (isinstance(thing, str) or isinstance(thing, list)) \ No newline at end of file -- 2.40.1 From 1fbe47943b4f9e0091751ac475291b802d98226d Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Thu, 25 Aug 2022 21:28:12 -0400 Subject: [PATCH 07/21] Replacement for types.py --- src/tfscript/tftypes.py | 278 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 src/tfscript/tftypes.py diff --git a/src/tfscript/tftypes.py b/src/tfscript/tftypes.py new file mode 100644 index 0000000..19f51e4 --- /dev/null +++ b/src/tfscript/tftypes.py @@ -0,0 +1,278 @@ +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' +] + +bindTypes = [ + 'impulse', + 'hold', + 'toggle', + 'double', + 'repeat' +] + +class bind: + ''' + Parent class for all bind types. + Verifies key, creates local variables + ''' + + def __init__(self, key, fields): + self.key = key + self.fields = fields + self.errors = [] + self.targetType = None + + # redefined for each unique type, default just verifies key + # and some other universal fields like alias and finds targetType + self.verify() + + if type(self) == bind or len(self.fields) == 0: + 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}"') + + def verify(self): + + try: + typeName, self.key = self.key.split(' ', 1) + self.key = self.key.lower() + except ValueError: + # catastrophic error: no type + self.errors.append(f'could not find type in "{self.key}"') + return + + try: + isalias = self.fields.pop('alias') + if not isinstance(isalias, bool): + self.errors.append(f'alias should be yes or no, not "{isalias}"') + isalias = False + except (KeyError, AttributeError, TypeError): + isalias = False + + if (not isalias) 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 + break + + if self.targetType is None: + self.errors.append(f'could not find type in "{self.key}"') + + def toTargetType(self): + if self.targetType is None: + # do nothing + bind = self + else: + # cast to targetType, extend errors + bind = self.targetType(self.key, self.fields) + bind.errors.extend(self.errors) + return bind + + def err(self, message): + self.errors.append(f'{type(self).__name__} "{self.key}" {message}') + + +class impulse(bind): + def verify(self): + self.command = None + + 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 (KeyError, AttributeError, TypeError): + self.fields = {'command': self.fields} + self.command = self.fields.pop('command') + if not isinstance(self.command, (str, list)): + self.err('must be command or argument of string or list') + self.command = None + + +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): + self.fields = {'press': self.fields} + self.press = self.fields.pop('press') + else: + self.err('must be press and release, or argument of string') + + +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): + self.fields = {'on': self.fields} + self.on = self.fields.pop('on') + else: + self.err('must be on and end, or argument of string') + + +class double(bind): + defaultDict = {} + condDict = {} + + def verify(self): + self.primary = None + self.secondary = None + self.condition = None + + # name of a bind type + self.type = None + + # 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}"') + except KeyError: + self.err('requires condition to toggle') + + # 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 + + # type + try: + self.type = self.fields.pop('type') + if self.type not in bindTypes: + # catastrophic: invalid type + self.err(f'has invalid type: "{self.type}"') + return + except KeyError: + # catastrophic: no type given + self.err('requires type') + return + + # 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) + except KeyError: + self.err('requires primary action') + + # 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) + except KeyError: + self.err('requires secondary action') + + +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: + self.err('requires interval') + except ValueError: + self.err(f'has invalid number of ticks: "{self.interval}"') + + 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 KeyError: + self.err('requires command') -- 2.40.1 From eef096777e2218587633052000fae08fb82e39a0 Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Thu, 25 Aug 2022 21:28:43 -0400 Subject: [PATCH 08/21] Redid 3 days of work after deleting it --- src/tfscript/verify.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/tfscript/verify.py b/src/tfscript/verify.py index 6b1b033..4e6a751 100644 --- a/src/tfscript/verify.py +++ b/src/tfscript/verify.py @@ -10,21 +10,8 @@ def verifyConfig(cfg: dict) -> (dict, dict): defaults = [] - if 'default' in cfg: - defaultCFG = cfg.pop('default') - errMessages = [] - for key, data in defaultCFG.items(): - bind = tftypes.bind(key, data) - if bind.targetType is not None: - bind = bind.targetType(bind.key, bind.fields) - defaults.append(bind) - - errMessages.extend(bind.errors) - - if len(errMessages) > 0: - errors.update({'default': errMessages}) - classList = [ + 'default', 'scout', 'soldier', 'pyro', @@ -36,10 +23,12 @@ def verifyConfig(cfg: dict) -> (dict, dict): 'spy' ] - for cclass in classList: + for isclass, cclass in enumerate(classList): + classCFG = None classBinds = [] className = cclass + if isinstance(cclass, str) and cclass in cfg: classCFG = cfg.pop(cclass) elif isinstance(cclass, tuple): @@ -53,18 +42,22 @@ def verifyConfig(cfg: dict) -> (dict, dict): # It may be less efficient this way, but # it makes for more descriptive error messages continue + errMessages = [] for key, data in classCFG.items(): bind = tftypes.bind(key, data) - if bind.targetType is not None: - bind = bind.targetType(bind.key, bind.fields) + bind = bind.toTargetType() + if isclass: classBinds.append(bind) + else: + defaults.append(bind) errMessages.extend(bind.errors) if len(errMessages) > 0: errors.update( {className: errMessages} ) + verifiedConfig.update({className: classBinds}) # Turn list into only strings by expanding tuples -- 2.40.1 From bacc85882684d05d2adad7510e58947ad71c741b Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Sun, 4 Sep 2022 09:25:33 -0400 Subject: [PATCH 09/21] removed repeated code --- src/tfscript/cli.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/tfscript/cli.py b/src/tfscript/cli.py index a64f2d3..801b4af 100644 --- a/src/tfscript/cli.py +++ b/src/tfscript/cli.py @@ -61,15 +61,15 @@ def parseConfig(config, defaults): tempsAndReals = {} if defaults is not None: - stringToWrite = tfscript.makeCFG(defaults, default=True) - # replaceDict = writing.writeOutput(stringToWrite, 'default', args) - # tempsAndReals.update(replaceDict) + config.update({'default': defaults}) for currentClass in config: - classDict = config[currentClass] - stringToWrite = tfscript.makeCFG(classDict) - # replaceDict = writing.writeOutput(stringToWrite, currentClass, args) - # tempsAndReals.update(replaceDict) + bindList = config[currentClass] + stringToWrite = '' + for bind in bindList: + stringToWrite += bind.toTF2() + replaceDict = writing.writeOutput(stringToWrite, currentClass, args) + tempsAndReals.update(replaceDict) return tempsAndReals -- 2.40.1 From 80a1379f49efe94e164d6b49778a2825c178b999 Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Sun, 4 Sep 2022 09:26:22 -0400 Subject: [PATCH 10/21] changed some variable names --- src/tfscript/verify.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/tfscript/verify.py b/src/tfscript/verify.py index 4e6a751..db6ec83 100644 --- a/src/tfscript/verify.py +++ b/src/tfscript/verify.py @@ -5,9 +5,9 @@ from tfscript import tftypes def verifyConfig(cfg: dict) -> (dict, dict): verifiedConfig = {} - # Do defaults first errors = {} + # Do defaults first defaults = [] classList = [ @@ -23,20 +23,22 @@ def verifyConfig(cfg: dict) -> (dict, dict): 'spy' ] - for isclass, cclass in enumerate(classList): + for isclass, class_ in enumerate(classList): classCFG = None classBinds = [] - className = cclass + className = class_ - if isinstance(cclass, str) and cclass in cfg: - classCFG = cfg.pop(cclass) - elif isinstance(cclass, tuple): - for tupClass in cclass: + if isinstance(class_, str) and class_ in cfg: + classCFG = cfg.pop(class_) + + elif isinstance(class_, tuple): + for tupClass in class_: if tupClass in cfg: classCFG = cfg.pop(tupClass) - className = cclass[0] + className = class_[0] break + if classCFG is None: # Invalid class, this gets caught later. # It may be less efficient this way, but @@ -61,10 +63,10 @@ def verifyConfig(cfg: dict) -> (dict, dict): verifiedConfig.update({className: classBinds}) # Turn list into only strings by expanding tuples - for i, cclass in enumerate(classList): - if isinstance(cclass, tuple): - classList.insert(i+1, cclass[1]) - classList.insert(i+1, cclass[0]) + for i, class_ in enumerate(classList): + if isinstance(class_, tuple): + classList.insert(i+1, class_[1]) + classList.insert(i+1, class_[0]) classList.pop(i) globalErrors = [] -- 2.40.1 From 8bf19e61536937e89aa5e8cae487794ba6c3dd30 Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Sun, 4 Sep 2022 09:26:57 -0400 Subject: [PATCH 11/21] All verification done, impulse.toTF2() done --- src/tfscript/tftypes.py | 102 +++++++++++++++++++++++++++++++++++----- 1 file changed, 90 insertions(+), 12 deletions(-) diff --git a/src/tfscript/tftypes.py b/src/tfscript/tftypes.py index 19f51e4..2404817 100644 --- a/src/tfscript/tftypes.py +++ b/src/tfscript/tftypes.py @@ -33,6 +33,7 @@ class bind: ''' def __init__(self, key, fields): + self.alias = False self.key = key self.fields = fields self.errors = [] @@ -67,14 +68,14 @@ class bind: return try: - isalias = self.fields.pop('alias') - if not isinstance(isalias, bool): - self.errors.append(f'alias should be yes or no, not "{isalias}"') - isalias = False + 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 (KeyError, AttributeError, TypeError): - isalias = False + self.alias = False - if (not isalias) and (self.key not in validKeyList): + if (not self.alias) and (self.key not in validKeyList): self.errors.append(f'invalid key name: "{self.key}"') types = { @@ -112,15 +113,92 @@ class impulse(bind): 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 (KeyError, AttributeError, TypeError): self.fields = {'command': self.fields} self.command = self.fields.pop('command') - if not isinstance(self.command, (str, list)): - self.err('must be command or argument of string or list') - self.command = None + + 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.command = None + + 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 == "load_itempreset" and restOfCmd.isalpha(): + try: + restOfCmd = restOfCmd.lower() + restOfCmd = ['a','b','c','d'].index(restOfCmd) + except ValueError: + # not a load_itempreset shortcut + pass + + 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): -- 2.40.1 From 1ac808ce1d12ff38b387824bd0088c4798b0e548 Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Sat, 17 Sep 2022 14:07:44 -0400 Subject: [PATCH 12/21] removed needless import --- src/tfscript/verify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tfscript/verify.py b/src/tfscript/verify.py index db6ec83..281c397 100644 --- a/src/tfscript/verify.py +++ b/src/tfscript/verify.py @@ -1,5 +1,5 @@ """Verify all the things that could go wrong.""" -from copy import deepcopy + from tfscript import tftypes def verifyConfig(cfg: dict) -> (dict, dict): -- 2.40.1 From 3f9f35cced467db9994a04cff002cae3cbe28541 Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Sat, 17 Sep 2022 14:08:25 -0400 Subject: [PATCH 13/21] reconfigured to work better together --- src/tfscript/__init__.py | 278 ++------------------------------------- src/tfscript/cli.py | 15 +-- 2 files changed, 18 insertions(+), 275 deletions(-) diff --git a/src/tfscript/__init__.py b/src/tfscript/__init__.py index df98ed4..fec9f67 100644 --- a/src/tfscript/__init__.py +++ b/src/tfscript/__init__.py @@ -1,287 +1,31 @@ -""" Makes the configs as a massive string """ +""" This has only one function. It mostly exists to allow imports of things like tftypes """ -# Used for the conditions in the type -condDict = {} -defaultDict = {} -bindOrAlias = "bind" from copy import deepcopy +from tfscript.tftypes import double -def makeCFG(cfg, default=False): - global bindOrAlias - global condDict - global defaultDict - for bind in cfg: - print(bind.key) - return +def makeCFG(bindList, default=False): - bindOrAlias = "bind" if default: # Write to defaultDict instead of condDict - condDict = defaultDict + double.condDict = double.defaultDict else: - condDict = deepcopy(defaultDict) + double.condDict = deepcopy(double.defaultDict) ret = '' - for key, data in cfg.items(): - isAlias = False - if "alias" in data: - isAlias = data.pop("alias") - if isAlias: - bindOrAlias = "alias" - else: - bindOrAlias = "bind" - ret += branch(key, data) + + for bind in bindList: + ret += bind.toTF2() # Doubles are weird. All of the toggles got put into a dictionary. # This takes all of the nested dictionaries and turns them into the right string - if default or condDict != defaultDict: + if default or double.condDict != double.defaultDict: # ==, and by extension !=, does in fact check # for dictionary equality in keys and values - for key, toggles in condDict.items(): + for key, toggles in double.condDict.items(): onCondPress = ';'.join(toggles["change_keys"]) onCondRelease = ';'.join(toggles["restore_keys"]) ret += f'alias +{key}_toggles "{onCondPress}"\n' +\ f'alias -{key}_toggles "{onCondRelease}"\n' +\ - f'{bindOrAlias} {key} "+{key}_toggles"\n' - - del condDict # free deep copy + f'bind {key} "+{key}_toggles"\n' return ret - -def typeOf(dictIn): - """ Find the first element common to both lists """ - types = [ - "impulse", - "hold", - "toggle", - "double", - "repeat" - ] - for t in types: - if t in dictIn.keys(): - return t - -def branch(keyName, bindContent): - """ Using the provided keyName and content, call the correct function """ - - """ - Terser syntax, ex. - impulse e: - - instead of - e: - impulse: - - """ - splitKey = keyName.split(' ', 1) - if len(splitKey) > 1: - keyName = splitKey[1] - bindContent = {splitKey[0]: bindContent} - - bindType = typeOf(bindContent) - bindContent = bindContent.pop(bindType) - - if bindType == "impulse": - return impulse(keyName, bindContent) - - elif bindType == "hold": - if isinstance(bindContent, str): - return simpleHold(keyName, bindContent) - else: - return listHold(keyName, bindContent) - - elif bindType == "toggle": - return toggle(keyName, bindContent) - - elif bindType == "double": - return double(keyName, bindContent) - - elif bindType == "repeat": - return repeat(keyName, bindContent) - -def impulse(key, instruction): - global bindOrAlias - if isinstance(instruction, dict): - instruction = instruction["command"] - - if not isinstance(instruction, list): - instruction = instruction.split(';') - - instuction = impulseShortcuts(instruction) - instruction = ';'.join(instruction) - - return f'{bindOrAlias} {key} "{instruction}"\n' - -def impulseShortcuts(instList): - for i, instruction in enumerate(instList): - splitCmd = instruction.split(' ') - cmd = splitCmd[0] - restOfCmd = ' '.join(splitCmd[1:]) - shortcuts = { - "primary": "slot1", - "secondary": "slot2", - "melee": "slot3" - } - if cmd in shortcuts: - cmd = shortcuts[cmd] - - if cmd == "voice": - cmd = "voicemenu" - restOfCmd = voice(restOfCmd) - - elif cmd == "build" or cmd == "destroy": - restOfCmd = expandBuildings(restOfCmd) - - elif cmd == "load_itempreset" and restOfCmd.isalpha(): - restOfCmd = restOfCmd.lower() - restOfCmd = ['a','b','c','d'].index(restOfCmd) - - if restOfCmd != "": - cmd += ' ' + restOfCmd - instList[i] = cmd - - return instList - -def voice(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(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 - -def simpleHold(key, instruction): - global bindOrAlias - # This isn't quite right, fix later! - if instruction[0] == '+' or instruction[0] == '-': - return f'{bindOrAlias} {key} "{instruction}"\n' - else: - return f'{bindOrAlias} {key} "+{instruction}"\n' - -def listHold(key, options): - global bindOrAlias - - oldBindOrAlias = bindOrAlias - bindOrAlias = 'alias' - plus = impulse(f'+{key}_press', options["press"]) - minus = impulse(f'-{key}_press', options["release"]) - - bindOrAlias = oldBindOrAlias - - ret = plus + minus + f'{bindOrAlias} {key} "+{key}_press"\n' - - return ret - -def toggle(key, instruction): - global bindOrAlias - onStr = f'turn_{key}_on' - offStr = f'turn_{key}_off' - togStr = f'toggle_{key}' - if instruction[0] == '+': - instruction = instruction[1:] - - ret = f'alias {onStr} "+{instruction}; alias {togStr} {offStr}"\n' +\ - f'alias {offStr} "-{instruction}; alias {togStr} {onStr}"\n' +\ - f'alias {togStr} "{onStr}"\n' +\ - f'{bindOrAlias} {key} "{togStr}"\n' - return ret - -def double(key, options): - typeName = options["type"] - primaryAction = {typeName: options.pop("primary")} - - secAction = {typeName: options.pop("secondary")} - - mainStr = f'{key}_main' - altStr = f'{key}_alt' - pShiftStr = f'+shift_{key}' - mShiftStr = f'-shift_{key}' - - global bindOrAlias - oldBindOrAlias = bindOrAlias - bindOrAlias = "alias" - - mainCode = branch(mainStr, primaryAction) - altCode = branch(altStr, secAction) - cancelBoth = ("cancel" in options and options["cancel"] == "both") - if cancelBoth: - if isinstance(primaryAction["hold"], dict): - lines = mainCode.splitlines() - minusCmd = lines[1] - _, minusStr, previousMinus = minus.split(' ', 2) - newMinus = previousMinus[0:-2] + ';' + + '"\n' - lines[1] = f'alias {minusStr} "{newMinus}"\n' - else: - # simple - pass - - bindOrAlias = oldBindOrAlias - - ret = mainCode + altCode +\ - f'alias {pShiftStr} "{bindOrAlias} {key} {altStr}"\n' +\ - f'alias {mShiftStr} "{bindOrAlias} {key} {mainStr}"\n'+\ - f'{bindOrAlias} {key} "{mainStr}"\n' - - isToggle = ("toggle" in options and options.pop("toggle") == True) - if isToggle: - toggleStr = toggle(key, pShiftStr) - - condName = options["condition"] - global condDict - if condName in condDict: - # If the condition key (like "mouse4") already has toggles, - # just append another toggle string - changes = condDict[condName]["change_keys"] - restores = condDict[condName]["restore_keys"] - - if isToggle: - # "toggle: true" specified, add the toggle string - if toggleStr not in changes: - changes.append(toggleStr) - if pShiftStr in changes: - # If key already has normal shift, remove it - changes.remove(pShiftStr) - changes.remove(mShiftStr) - - elif pShiftStr not in changes: - # not toggle, not already in changes - changes.append(pShiftStr) - restores.append(mShiftStr) - else: - # If the condition key doesn't already exist, make it - if isToggle: - condDict.update( { - condName: { - "change_keys": [ toggleStr ], - "restore_keys": [ ] - } - } ) - else: - condDict.update( { - condName: { - "change_keys": [ pShiftStr ], - "restore_keys": [ mShiftStr ] - } - } ) - - return ret - -def repeat(key, options): - return f'placeholder for {key} (repeat)\n' diff --git a/src/tfscript/cli.py b/src/tfscript/cli.py index 801b4af..dc6d7a9 100644 --- a/src/tfscript/cli.py +++ b/src/tfscript/cli.py @@ -27,8 +27,7 @@ except ModuleNotFoundError: # Local libraries import tfscript -from tfscript import verify -from tfscript import writing +from tfscript import verify, writing, makeCFG args = {} targetDir = '' @@ -63,12 +62,12 @@ def parseConfig(config, defaults): if defaults is not None: config.update({'default': defaults}) - for currentClass in config: - bindList = config[currentClass] - stringToWrite = '' - for bind in bindList: - stringToWrite += bind.toTF2() - replaceDict = writing.writeOutput(stringToWrite, currentClass, args) + for class_ in config: + stringToWrite = makeCFG( + config[class_], + default=(class_ == 'default') + ) + replaceDict = writing.writeOutput(stringToWrite, class_, args) tempsAndReals.update(replaceDict) return tempsAndReals -- 2.40.1 From 5014161b02df0f0b5d1488a473f86b45c4733de1 Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Sat, 17 Sep 2022 14:09:51 -0400 Subject: [PATCH 14/21] got to where i was at the start of the refactor --- src/tfscript/tftypes.py | 383 +++++++++++++++++++++++++++------------- 1 file changed, 263 insertions(+), 120 deletions(-) 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 -- 2.40.1 From 4babdedb8194026dd7f85c6dc9bc809856851697 Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Sat, 17 Sep 2022 22:17:34 -0400 Subject: [PATCH 15/21] cancel: both works, fixed swapped var names --- src/tfscript/tftypes.py | 86 ++++++++++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 32 deletions(-) diff --git a/src/tfscript/tftypes.py b/src/tfscript/tftypes.py index c5ace2f..fb40424 100644 --- a/src/tfscript/tftypes.py +++ b/src/tfscript/tftypes.py @@ -63,22 +63,27 @@ class bind(object): def verify(self): - try: - typeName, self.key = self.key.split(' ', 1) - 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 - 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 (KeyError, AttributeError, TypeError): + # KeyError, if dict but no alias field; + # AttributeError, if no pop method; + # TypeError, if pop method won't take str() as arg self.alias = False + try: + typeName, self.key = self.key.split(' ', 1) + 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 + if (not self.alias) and (self.key not in validKeyList): self.errors.append(f'invalid key name: "{self.key}"') @@ -163,7 +168,7 @@ class impulse(bind): elif cmd == 'load_itempreset' and restOfCmd.isalpha(): try: restOfCmd = restOfCmd.lower() - restOfCmd = ['a','b','c','d'].index(restOfCmd) + restOfCmd = str(['a','b','c','d'].index(restOfCmd)) except ValueError: # not a load_itempreset shortcut pass @@ -367,7 +372,7 @@ class double(bind): self.err('requires `type` field') return - # cancel mode + # cancel mode, must happend after type has been inferred if 'cancel' in self.fields: self.cancel = self.fields.pop('cancel') if self.cancel in ('released', 'both'): @@ -408,38 +413,20 @@ class double(bind): self.secondary.alias = True altCode = self.secondary.toTF2() - # Make code to toggle between the two actions + # Make code to switch 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' + mainCode, altCode = self.cancelBoth(mainCode, altCode) 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'alias {pShiftStr} "{bindOrAlias} {self.key} {self.secondStr}"\n' +\ + f'alias {mShiftStr} "{bindOrAlias} {self.key} {self.primStr}"\n'+\ f'{bindOrAlias} {self.key} "{self.primStr}"\n' try: @@ -465,6 +452,41 @@ class double(bind): return result + def cancelBoth(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 + mainMinusList = set(mainMinusStr.split(';')) + altMinusList = set(altMinusStr.split(';')) + uniqMain = mainMinusList.difference(altMinusList) + uniqAlt = altMinusList.difference(mainMinusList) + + mainMinusStr = ';'.join(uniqMain) + altMinusStr = ';'.join(uniqAlt) + if not uniqMain.issuperset(uniqAlt): + # main has things alt doesn't + mainLines[1] = mainLines[1][:-1] + f';{altMinusStr}"' + if not uniqAlt.issuperset(uniqMain): + altLines[1] = altLines[1][:-1] + f';{mainMinusStr}"' + + return ( + '\n'.join(mainLines) + '\n', + '\n'.join(altLines) + '\n' + ) + class repeat(bind): def verify(self): -- 2.40.1 From ace0af71019bb5b7f5cc1efdbda9706b3714c066 Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Sat, 17 Sep 2022 22:17:42 -0400 Subject: [PATCH 16/21] ignore --- examples/nicks_config.yaml | 73 ++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/examples/nicks_config.yaml b/examples/nicks_config.yaml index d1b0032..5d1e963 100644 --- a/examples/nicks_config.yaml +++ b/examples/nicks_config.yaml @@ -9,51 +9,50 @@ default: # voice-based doubles double e: - impulse primary: voice medic - impulse secondary: voice activate uber + type: impulse + primary: voice medic + secondary: voice activate uber condition: mouse4 double t: - impulse primary: voice thanks - impulse secondary: voice nice shot + type: impulse + primary: voice thanks + secondary: voice nice shot condition: mouse4 double v: - impulse primary: voice spy - impulse secondary: voice help + type: impulse + primary: voice spy + secondary: voice help condition: mouse4 # hold doubles double r: - primary: - hold: "class_action" - secondary: - hold: "reload" + type: hold + primary: +class_action + secondary: +reload condition: mouse4 - cancel both: yes + cancel: both # other - double =: - primary: - impulse: kill - secondary: - impulse: explode - condition: "-" + impulse =: kill + impulse -: explode + double q: - impulse primary: lastinv - impulse secondary: + type: impulse + primary: lastinv + secondary: - "slot2" - "wait 10" - "slot1" condition: mouse4 double ctrl: # I use shift to crouch - impulse primary: - voice yes - impulse secondary: - voice no + type: impulse + primary: voice yes + secondary: voice no condition: mouse4 # toggle - toggle capslock: voicerecord + toggle capslock: +voicerecord # hold: null-cancelled movement, so hitting a while holding d causes # me to go left instead of stopping, or vice-versa. @@ -83,7 +82,6 @@ default: impulse maybeMoveRight: alias: yes command: "" - # class action is something useful for each class, # like destroying and rebuilding a sentry for the engineer # this is just the default. I do a lot of hybridknight and @@ -99,16 +97,16 @@ default: impulse load0: alias: yes - command: "load_itempreset 0" + command: "load_itempreset a" impulse load1: alias: yes - command: "load_itempreset 1" + command: "load_itempreset b" impulse load2: alias: yes - command: "load_itempreset 2" + command: "load_itempreset c" impulse load3: alias: yes - command: "load_itempreset 3" + command: "load_itempreset d" impulse INS: - "load0" @@ -140,12 +138,21 @@ medic: alias: yes press: "hud_medicautocallersthreshold 150" release: "hud_medicautocallersthreshold 75" + double e: + type: impulse + condition: mouse4 + primary: voice medic + secondary: voice uber ready soldier: double mouse1: - hold primary: - attack - hold secondary: + type: hold + condition: mouse4 + cancel: both + # normal firing + primary: +attack + # rocket jump + secondary: press: - +attack - +duck @@ -154,5 +161,3 @@ soldier: - -attack - -duck - -jump - condition: mouse4 - \ No newline at end of file -- 2.40.1 From 0bbca21fea3f9f991c965ea7f58ab3450d8a2fd5 Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Sun, 2 Oct 2022 12:12:15 -0400 Subject: [PATCH 17/21] Added warnings --- src/tfscript/cli.py | 6 ++++++ src/tfscript/verify.py | 13 +++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/tfscript/cli.py b/src/tfscript/cli.py index dc6d7a9..1c4f0ba 100644 --- a/src/tfscript/cli.py +++ b/src/tfscript/cli.py @@ -38,6 +38,12 @@ def parseFile(inputFile) -> (dict, dict): # See verify.py config, defaults = verify.verifyConfig(config) + if 'warnings' in config: + for cclass, messages in config.pop('warnings').items(): + print(f'Warning in {cclass}:', file=stderr) + for msg in messages: + print(f' {msg}', file=stderr) + if 'errors' in config: for cclass, messages in config['errors'].items(): print(f'Error in {cclass}:', file=stderr) diff --git a/src/tfscript/verify.py b/src/tfscript/verify.py index 281c397..aabfc43 100644 --- a/src/tfscript/verify.py +++ b/src/tfscript/verify.py @@ -6,6 +6,7 @@ def verifyConfig(cfg: dict) -> (dict, dict): verifiedConfig = {} errors = {} + warnings = {} # Do defaults first defaults = [] @@ -26,28 +27,27 @@ def verifyConfig(cfg: dict) -> (dict, dict): for isclass, class_ in enumerate(classList): classCFG = None - classBinds = [] className = class_ if isinstance(class_, str) and class_ in cfg: classCFG = cfg.pop(class_) - elif isinstance(class_, tuple): for tupClass in class_: if tupClass in cfg: classCFG = cfg.pop(tupClass) className = class_[0] break - if classCFG is None: # Invalid class, this gets caught later. # It may be less efficient this way, but # it makes for more descriptive error messages continue + classBinds = [] errMessages = [] + warnMessages = [] for key, data in classCFG.items(): - bind = tftypes.bind(key, data) + bind = tftypes.Bind(key, data) bind = bind.toTargetType() if isclass: @@ -56,9 +56,12 @@ def verifyConfig(cfg: dict) -> (dict, dict): defaults.append(bind) errMessages.extend(bind.errors) + warnMessages.extend(bind.warnings) if len(errMessages) > 0: errors.update( {className: errMessages} ) + if len(warnMessages) > 0: + warnings.update( {className: warnMessages} ) verifiedConfig.update({className: classBinds}) @@ -82,6 +85,8 @@ def verifyConfig(cfg: dict) -> (dict, dict): if len(errors) > 0: verifiedConfig.update({'errors': errors}) + if len(warnings) > 0: + verifiedConfig.update({'warnings': warnings}) return verifiedConfig, defaults -- 2.40.1 From f0dac0d1a10f8d224fd2ab3a4aad792b15d9cb47 Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Sun, 2 Oct 2022 12:12:33 -0400 Subject: [PATCH 18/21] Just, a ton of work. Aded literals --- src/tfscript/tftypes.py | 422 +++++++++++++++++++++++++--------------- 1 file changed, 265 insertions(+), 157 deletions(-) diff --git a/src/tfscript/tftypes.py b/src/tfscript/tftypes.py index fb40424..ad7cac6 100644 --- a/src/tfscript/tftypes.py +++ b/src/tfscript/tftypes.py @@ -1,3 +1,5 @@ +import re + validKeyList = [ # top row 'escape', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12', @@ -18,7 +20,9 @@ validKeyList = [ 'leftarrow', 'rightarrow' ] -class bind(object): +popErrors = (AttributeError, KeyError, TypeError) + +class Bind(object): ''' Parent class for all bind types. Verifies key, creates local variables @@ -26,18 +30,26 @@ class bind(object): bindTypes = [] instances = {} - def __init__(self, key, fields): - self.alias = False - self.key = key - self.fields = fields - self.errors = [] - self.targetType = 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.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: + if type(self) is Bind: # not using isinstance(), because all subclasses are also instances # of bind. return @@ -46,15 +58,15 @@ class bind(object): # verify function should remove all fields relavent to the bind. # Any extras are errors - self.errors.append(f'extra fields in "{self.key}":') + 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.errors.append(f' "{self.fields}"') + self.warnings.append(f' "{self.fields}"') else: for field in self.fields: - self.errors.append(f' "{field}"') - elif len(self.errors) == 0: + 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) @@ -62,20 +74,20 @@ 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.errors.append( + f'alias should be "yes" or "no", not "{self.alias}"' + ) self.alias = False - except (KeyError, AttributeError, TypeError): - # KeyError, if dict but no alias field; - # AttributeError, if no pop method; - # TypeError, if pop method won't take str() as arg + 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() @@ -84,33 +96,39 @@ class bind(object): 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}"') - for type_ in self.bindTypes: - if typeName == type_.__name__: - self.targetType = type_ - break - - if self.targetType is None: - self.errors.append(f'could not find type in "{self.key}"') - def toTargetType(self): - if self.targetType is None: + if self.TargetType is None: # do nothing - bind = self - else: - # cast to targetType, extend errors - bind = self.targetType(self.key, self.fields) - bind.errors.extend(self.errors) - bind.alias = self.alias + 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__} "{self.key}" {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): +class Impulse(Bind): def verify(self): self.command = None if not isinstance(self.fields, dict): @@ -118,15 +136,14 @@ class impulse(bind): 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') - 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 def toTF2(self) -> str: if self.alias: @@ -165,7 +182,8 @@ class impulse(bind): elif cmd == 'build' or cmd == 'destroy': restOfCmd = self.expandBuildings(restOfCmd) - elif cmd == 'load_itempreset' and restOfCmd.isalpha(): + elif cmd == 'loadout' and restOfCmd.isalpha(): + cmd = 'load_itempreset' try: restOfCmd = restOfCmd.lower() restOfCmd = str(['a','b','c','d'].index(restOfCmd)) @@ -173,6 +191,11 @@ class impulse(bind): # 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 @@ -205,7 +228,7 @@ class impulse(bind): return num -class hold(bind): +class Hold(Bind): def verify(self): self.press = None self.release = None @@ -215,31 +238,32 @@ class hold(bind): # 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 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 + if self.press is None: + return # verify release try: self.release = self.fields.pop('release') - except KeyError: + 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:]) - 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' @@ -249,11 +273,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(f'+{holdStr}', self.press) pressObj.alias = True pressStr = pressObj.toTF2() - releaseObj = impulse(f'-{holdStr}', self.release) + releaseObj = Impulse(f'-{holdStr}', self.release) releaseObj.alias = True releaseStr = releaseObj.toTF2() @@ -262,10 +286,13 @@ class hold(bind): # and never deactivating self.key = '+' + self.key - return pressStr + releaseStr + f'{bindOrAlias} {self.key} "+{holdStr}"\n' + return ( + pressStr + releaseStr + + f'{bindOrAlias} {self.key} "+{holdStr}"\n' + ) -class toggle(bind): +class Toggle(Bind): def verify(self): self.on = None self.off = None @@ -275,31 +302,30 @@ class toggle(bind): # 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 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 + if self.on is None: + return # verify off try: self.off = self.fields.pop('off') - except KeyError: + 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:]) - 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' @@ -309,27 +335,28 @@ class toggle(bind): onStr = f'{toggleStr}_on' offStr = f'{toggleStr}_off' - onObj = impulse(onStr, self.on) + onObj = Impulse(onStr, self.on) onObj.alias = True - toggleOn = onObj.toTF2()[ - # remove trailing " and \n - :-2 - ] + toggleOn = onObj.toTF2() + # remove starting/trailing " and \n + toggleOn = toggleOn[:-2] - offObj = impulse(offStr, self.off) + 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' + 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): +class Double(Bind): defaultDict = {} condDict = {} + bindNames = [] def verify(self): self.primary = None @@ -345,114 +372,152 @@ class double(bind): self.type = None # either 'released' (default) or 'both' - self.cancel = 'released' + 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 KeyError: + except popErrors: self.err('requires `condition` field') - if 'toggle' in self.fields: + try: 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}"') + 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') - if self.type not in [ type_.__name__ for type_ in self.bindTypes ]: + 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 KeyError: + except popErrors: # catastrophic: no type given self.err('requires `type` field') return # cancel mode, must happend after type has been inferred - 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}"') + try: + cancel = self.fields.pop('cancel') + + if not isinstance(cancel, str): + self.err(f'`cancel` field must be "released" or "both"') + else: - self.err(f'`cancel` field must be argument of "released" or "both"') + 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() - mainAction = bind(f'{self.type} {self.primStr}', mainSection) - self.primary = mainAction.toTargetType() - except KeyError: + 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() - altBind = bind(f'{self.type} {self.secondStr}', altSection) - self.secondary = altBind.toTargetType() - except KeyError: + 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: - if self.alias: - bindOrAlias = 'alias' - else: - bindOrAlias = 'bind' - # Get code for primary and secondary actions + 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 - pShiftStr = f'+shift_{self.key}' - mShiftStr = f'-shift_{self.key}' - - if self.cancel == 'both': - mainCode, altCode = self.cancelBoth(mainCode, altCode) + if self.cancelBoth: + mainCode, altCode = self.getCancelCode(mainCode, altCode) if self.type == 'hold': self.primStr = '+' + self.primStr self.secondStr = '+' + self.secondStr - result = mainCode + altCode +\ - f'alias {pShiftStr} "{bindOrAlias} {self.key} {self.secondStr}"\n' +\ - f'alias {mShiftStr} "{bindOrAlias} {self.key} {self.primStr}"\n'+\ - f'{bindOrAlias} {self.key} "{self.primStr}"\n' + shiftStr = f'shift_{self.key}' + shiftCode = self.getChangeCode(shiftStr) + self.addToCondDict(shiftStr) - 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'] + return mainCode + altCode + shiftCode - if pShiftStr not in changes: - # not already in changes - changes.append(pShiftStr) - restores.append(mShiftStr) + 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' + ) - except KeyError: - # If the condition key doesn't already exist, make it + 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': [ pShiftStr ], - 'restore_keys': [ mShiftStr ], - 'alias': self.alias + 'change_keys': [], + 'restore_keys': [] } } ) - return result + self.condDict[self.condition]['change_keys'].append(changeStr) + if self.isToggle == False: + self.condDict[self.condition]['restore_keys'].append(restoreStr) - def cancelBoth(self, mainCode, altCode) -> (str, str): + 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, @@ -469,18 +534,13 @@ class double(bind): altMinusStr = altMinusLine.split(' ', 2)[2][1:-1] # remove duplicate - actions - mainMinusList = set(mainMinusStr.split(';')) - altMinusList = set(altMinusStr.split(';')) - uniqMain = mainMinusList.difference(altMinusList) - uniqAlt = altMinusList.difference(mainMinusList) + mainMinusSet = set(mainMinusStr.split(';')) + altMinusSet = set(altMinusStr.split(';')) + allCancels = mainMinusSet | altMinusSet + allCancelStr = ';'.join(allCancels) - mainMinusStr = ';'.join(uniqMain) - altMinusStr = ';'.join(uniqAlt) - if not uniqMain.issuperset(uniqAlt): - # main has things alt doesn't - mainLines[1] = mainLines[1][:-1] + f';{altMinusStr}"' - if not uniqAlt.issuperset(uniqMain): - altLines[1] = altLines[1][:-1] + f';{mainMinusStr}"' + altLines[1] = altLines[1][:-1] + f';{allCancelStr}"' + mainLines[1] = mainLines[1][:-1] + f';{allCancelStr}"' return ( '\n'.join(mainLines) + '\n', @@ -488,7 +548,7 @@ class double(bind): ) -class repeat(bind): +class Repeat(Bind): def verify(self): self.interval = None self.command = None @@ -497,25 +557,73 @@ class repeat(bind): intervalStr = str(self.fields.pop('interval')) self.interval = int(intervalStr) if self.interval <= 0: - self.err('interval must be greater than 0') - except KeyError: - self.err('requires interval') + self.err('`interval` must be greater than 0') + except (KeyError, TypeError): + self.err('requires `interval` field') except ValueError: - self.err(f'has invalid number of ticks: "{self.interval}"') + 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.err('`command` must be string or list') self.command = None - except KeyError: - self.err('requires command') + 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__() \ No newline at end of file +Bind.bindTypes = Bind.__subclasses__() +Double.bindNames = [ bind.__name__.lower() for bind in Bind.bindTypes ] \ No newline at end of file -- 2.40.1 From f483beb0bd8f4cd9b8201aee7227c608f12660e9 Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Sun, 2 Oct 2022 12:12:55 -0400 Subject: [PATCH 19/21] Made it prettier --- src/tfscript/__init__.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/tfscript/__init__.py b/src/tfscript/__init__.py index fec9f67..b679ab8 100644 --- a/src/tfscript/__init__.py +++ b/src/tfscript/__init__.py @@ -1,15 +1,15 @@ """ This has only one function. It mostly exists to allow imports of things like tftypes """ from copy import deepcopy -from tfscript.tftypes import double +from tfscript.tftypes import Double def makeCFG(bindList, default=False): if default: # Write to defaultDict instead of condDict - double.condDict = double.defaultDict + Double.condDict = Double.defaultDict else: - double.condDict = deepcopy(double.defaultDict) + Double.condDict = deepcopy(Double.defaultDict) ret = '' @@ -18,14 +18,16 @@ def makeCFG(bindList, default=False): # Doubles are weird. All of the toggles got put into a dictionary. # This takes all of the nested dictionaries and turns them into the right string - if default or double.condDict != double.defaultDict: + if default or Double.condDict != Double.defaultDict: # ==, and by extension !=, does in fact check # for dictionary equality in keys and values - for key, toggles in double.condDict.items(): + for key, toggles in Double.condDict.items(): onCondPress = ';'.join(toggles["change_keys"]) onCondRelease = ';'.join(toggles["restore_keys"]) - ret += f'alias +{key}_toggles "{onCondPress}"\n' +\ - f'alias -{key}_toggles "{onCondRelease}"\n' +\ - f'bind {key} "+{key}_toggles"\n' + ret += ( + f'alias +{key}_toggles "{onCondPress}"\n' + + f'alias -{key}_toggles "{onCondRelease}"\n' + + f'bind {key} "+{key}_toggles"\n' + ) return ret -- 2.40.1 From a7552fad96240dc2d8b73d9823be70414b3810bf Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Sun, 2 Oct 2022 12:13:04 -0400 Subject: [PATCH 20/21] A few more changes --- examples/nicks_config.yaml | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/examples/nicks_config.yaml b/examples/nicks_config.yaml index 5d1e963..93c8ea6 100644 --- a/examples/nicks_config.yaml +++ b/examples/nicks_config.yaml @@ -26,11 +26,11 @@ default: # hold doubles double r: + condition: mouse4 + cancel: both type: hold primary: +class_action secondary: +reload - condition: mouse4 - cancel: both # other impulse =: kill @@ -60,26 +60,26 @@ default: press: - "-moveright" - "+moveleft" - - "alias maybeMoveLeft +moveleft" + - "alias maybe_move_left +moveleft" release: - "-moveleft" - - "maybeMoveRight" - - "alias maybeMoveLeft " + - "maybe_move_right" + - "unalias maybe_move_left" hold d: press: - "-moveleft" - "+moveright" - - "alias maybeMoveRight +moveright" + - "alias maybe_move_right +moveright" release: - "-moveright" - - "maybeMoveLeft" - - "alias maybeMoveRight " + - "maybe_move_left" + - "unalias maybe_move_right" # This just stops an error message the first time you release # either of 'a' or 'd' - impulse maybeMoveLeft: + impulse maybe_move_left: alias: yes command: "" - impulse maybeMoveRight: + impulse maybe_move_right: alias: yes command: "" # class action is something useful for each class, @@ -97,16 +97,16 @@ default: impulse load0: alias: yes - command: "load_itempreset a" + command: "loadout a" impulse load1: alias: yes - command: "load_itempreset b" + command: "loadout b" impulse load2: alias: yes - command: "load_itempreset c" + command: "loadout c" impulse load3: alias: yes - command: "load_itempreset d" + command: "loadout d" impulse INS: - "load0" @@ -120,8 +120,10 @@ default: impulse END: - "load3" - "alias reload_presets load3" + literal set_default_loadout: + alias reload_presets load0 impulse backspace: - "reload_presets" + reload_presets engi: hold class_action: @@ -129,7 +131,6 @@ engi: press: - destroy sentry - build sentry - release: "" medic: # "Radar" feature: causes all teammates to autocall for medic, allowing -- 2.40.1 From ab882fcf55000f5d511199fa94229e3e41f5daf7 Mon Sep 17 00:00:00 2001 From: Nicholas Hope Date: Sun, 2 Oct 2022 12:13:37 -0400 Subject: [PATCH 21/21] Added new type test --- tests/types.yaml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/types.yaml diff --git a/tests/types.yaml b/tests/types.yaml new file mode 100644 index 0000000..1e69eca --- /dev/null +++ b/tests/types.yaml @@ -0,0 +1,25 @@ +default: + impulse a: 'alias' + impulse b: ['alias'] + impulse c: + key: value + + hold d: 'alias' + hold e: ['alias'] + hold f: + key: value + + toggle g: 'alias' + toggle h: ['alias'] + toggle i: + key: value + + double j: 'alias' + double k: ['alias'] + double l: + key: value + + repeat m: 'alias' + repeat n: ['alias'] + repeat o: + key: value -- 2.40.1