Compare commits
3 Commits
4944292483
...
7936dd0d10
Author | SHA1 | Date |
---|---|---|
Nicholas Hope | 7936dd0d10 | |
Nicholas Hope | c3b816c532 | |
Nicholas Hope | 9ba82add22 |
|
@ -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'
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
"""
|
||||
'''
|
||||
Command line module for making Team Fortress 2 macro scripts from
|
||||
YAML source code.
|
||||
"""
|
||||
'''
|
||||
|
||||
__all__ = ['parseFile']
|
||||
__author__ = "Nicholas Hope <tfscript@nickhope.world"
|
||||
__date__ = "26 August 2022"
|
||||
__version__ = "1.0"
|
||||
__copyright__ = "Copyright © 2022 Nicholas Hope. See LICENSE for details."
|
||||
__author__ = 'Nicholas Hope <tfscript@nickhope.world'
|
||||
__date__ = '26 August 2022'
|
||||
__version__ = '1.0'
|
||||
__copyright__ = 'Copyright © 2022 Nicholas Hope. See LICENSE for details.'
|
||||
|
||||
# Standard libraries
|
||||
from sys import stderr
|
||||
|
@ -31,38 +31,38 @@ from tfscript import verify
|
|||
from tfscript import writing
|
||||
|
||||
args = {}
|
||||
targetDir = ""
|
||||
targetDir = ''
|
||||
|
||||
def parseFile(inputFile) -> (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())
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue