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",