moved to new locations
parent
782692cf41
commit
3ddf60d8ab
|
@ -10,7 +10,7 @@
|
|||
"request": "launch",
|
||||
"program": "${file}",
|
||||
"console": "integratedTerminal",
|
||||
"args": ["-d", "demo.yaml"],
|
||||
"args": ["-d", "./tests/mega.yaml"],
|
||||
"justMyCode": true
|
||||
}
|
||||
]
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
"""CLI module for converting YAML to tfscript"""
|
||||
# https://www.w3schools.io/file/yaml-arrays/
|
||||
|
||||
# Standard libraries
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
import tempfile
|
||||
import yaml
|
||||
|
||||
# Local libraries
|
||||
import tfscript
|
||||
import verify
|
||||
|
||||
def parseFile(inputFile):
|
||||
"""Parse, verify, and do the conversion."""
|
||||
config = yaml.safe_load(inputFile)
|
||||
|
||||
# See verify.py
|
||||
config = verify.verifyConfig(config)
|
||||
if "errors" in config:
|
||||
for e in config["errors"]:
|
||||
print(e,file=sys.stderr)
|
||||
else:
|
||||
parseConfig(config)
|
||||
|
||||
def writeOutput(scriptString, className):
|
||||
"""Given the string of stuff to write, write it out to the given handle."""
|
||||
global args
|
||||
prefix = './cfg'
|
||||
chunksize = 2**20 # 1Mb maximum cfg file size
|
||||
chunk = 1
|
||||
# Make sure ./cfg exists before we try to use it
|
||||
if os.path.isdir( prefix ) == False:
|
||||
try:
|
||||
os.mkdir( prefix )
|
||||
if args.debug:
|
||||
print( f'DEBUG: created {prefix}')
|
||||
except Exception as fileExcept:
|
||||
print( f'WARN: Failed to create {prefix}: {fileExcept.strerror}\nUsing current directory instead.' )
|
||||
prefix = '.'
|
||||
# If the string is more than 1048576 bytes, we need divide it into files that each
|
||||
# are less than 1048576 bytes
|
||||
chunksneeded = int( 1 + len(scriptString) / chunksize )
|
||||
if args.debug:
|
||||
print( f'DEBUG: need {chunksneeded} files for {className}')
|
||||
|
||||
if( chunksneeded == 1):
|
||||
# If it can be done in one chunk, do it in one chunk.
|
||||
outfile = tempfile.NamedTemporaryFile( prefix=className, delete=False )
|
||||
if args.debug:
|
||||
print( f'DEBUG: created temporary {outfile.name} ')
|
||||
outfile.write(scriptString.encode("utf8"))
|
||||
outfile.close()
|
||||
os.replace(outfile.name, f'{prefix}/{className}_script_{chunk:02d}.cfg')
|
||||
if args.debug:
|
||||
print( f'DEBUG: Created {prefix}/{className}_script_{chunk:02d}.cfg')
|
||||
else:
|
||||
# Gotta do it in multiple chunks
|
||||
classLines = scriptString.splitlines()
|
||||
execString = f'exec {className}_script_{chunk:02d}'.encode("utf8")
|
||||
# extra 4 bytes is just a little buffer so we don't get exactly chunksize bytes
|
||||
reservedSpace = len(execString) + 4
|
||||
n = 0
|
||||
while( chunk <= chunksneeded ):
|
||||
outfile = tempfile.NamedTemporaryFile( prefix=className, delete=False )
|
||||
byteswritten = 0
|
||||
if args.debug:
|
||||
print( f'DEBUG: created temporary {outfile.name} ')
|
||||
while( n < len(classLines) and (byteswritten + len(classLines[n]) + reservedSpace) < chunksize ):
|
||||
line = classLines[n].encode("utf8") + os.linesep.encode("utf8")
|
||||
outfile.write(line)
|
||||
byteswritten += len(line)
|
||||
n+=1
|
||||
if( chunk < chunksneeded ):
|
||||
line = f'exec {className}_script_{chunk+1:02d}'.encode("utf8") + os.linesep.encode("utf8")
|
||||
outfile.write(line)
|
||||
byteswritten += len(line)
|
||||
outfile.close()
|
||||
os.replace(outfile.name, f'{prefix}/{className}_script_{chunk:02d}.cfg')
|
||||
if args.debug:
|
||||
print( f'DEBUG: Wrote {byteswritten} bytes to {prefix}/{className}_script_{chunk:02d}.cfg')
|
||||
chunk += 1
|
||||
|
||||
def parseConfig(config):
|
||||
"""With validated data structure, write out all the files."""
|
||||
for currentClass in config:
|
||||
classDict = config[currentClass]
|
||||
stringToWrite = tfscript.makeCFG(classDict)
|
||||
writeOutput(stringToWrite, currentClass)
|
||||
|
||||
# Main function
|
||||
if __name__ == "__main__":
|
||||
# Handle command line
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Parse YAML file and produce TF2 config script."
|
||||
)
|
||||
parser.add_argument( '-d', '--debug', action='store_true',
|
||||
help="Enable debugging messages.")
|
||||
parser.add_argument( '-n', '--dry-run', action='store_true',
|
||||
help="Parse input file, but don't write anything.")
|
||||
# positional argument: first non-hyphenated argument is input file
|
||||
parser.add_argument( 'infile', type=argparse.FileType('r'),
|
||||
help='File containing YAML to convert.')
|
||||
args = parser.parse_args()
|
||||
parseFile(args.infile)
|
|
@ -0,0 +1,206 @@
|
|||
""" Makes the configs as a massive string """
|
||||
|
||||
# Used for the conditions in the <double> type
|
||||
condDict = {}
|
||||
|
||||
def makeCFG(cfg):
|
||||
condDict.clear()
|
||||
ret = ''
|
||||
for key, data in cfg.items():
|
||||
# I know all of these fields exist because it was verified in verify.py
|
||||
bindType = firstTypeIn(data.keys())
|
||||
bindContent = data[bindType]
|
||||
ret += branch(key, bindContent, bindType)
|
||||
|
||||
# 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
|
||||
for key, toggles in condDict.items():
|
||||
ret += f'alias +{key}_toggles "{toggles["plus_toggles"]}"\n' +\
|
||||
f'alias -{key}_toggles "{toggles["minus_toggles"]}"\n' +\
|
||||
f'bind {key} "+{key}_toggles"\n'
|
||||
|
||||
return ret
|
||||
|
||||
def firstTypeIn(inputList):
|
||||
""" Find the first element common to both lists """
|
||||
types = [
|
||||
"impulse",
|
||||
"hold",
|
||||
"toggle",
|
||||
"double",
|
||||
"repeat"
|
||||
]
|
||||
for t in types:
|
||||
if t in inputList:
|
||||
return t
|
||||
|
||||
def branch(keyName, bindContent, 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):
|
||||
if isinstance(instruction, list):
|
||||
instruction = ';'.join(instruction)
|
||||
|
||||
allInstructions = []
|
||||
|
||||
for indivCmd in instruction.split(';'):
|
||||
allInstructions.append(impulseShortcuts(indivCmd))
|
||||
|
||||
instruction = ';'.join(allInstructions)
|
||||
|
||||
return f'bind {key} "{instruction}"\n'
|
||||
|
||||
def impulseShortcuts(instruction):
|
||||
splitCommand = instruction.split(' ')
|
||||
cmd = splitCommand[0]
|
||||
shortcuts = {
|
||||
"primary": "slot1",
|
||||
"secondary": "slot2",
|
||||
"melee": "slot3"
|
||||
}
|
||||
if cmd in shortcuts:
|
||||
for sc, expansion in shortcuts.items():
|
||||
if cmd == shortcut:
|
||||
splitCommand[0] = expansion
|
||||
break
|
||||
instruction = ' '.join(splitCommand)
|
||||
|
||||
restOfCmd = ' '.join(splitCommand[1:])
|
||||
if cmd == "voice":
|
||||
instruction = voice(restOfCmd)
|
||||
|
||||
elif cmd == "build" or cmd == "destroy":
|
||||
instruction = f"{cmd} " + expandBuildings(restOfCmd)
|
||||
|
||||
return instruction
|
||||
|
||||
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'voicemenu {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):
|
||||
# This isn't quite right, fix later!
|
||||
if instruction[0] != '+':
|
||||
return f'bind {key} "+{instruction}"\n'
|
||||
else:
|
||||
return f'bind {key} "{instruction}"\n'
|
||||
|
||||
def listHold(key, options):
|
||||
press_str = options["press"]
|
||||
if isinstance(press_str, list):
|
||||
press_str = ';'.join(press_str)
|
||||
|
||||
release_str = options["release"]
|
||||
if isinstance(release_str, list):
|
||||
release_str = ';'.join(release_str)
|
||||
|
||||
ret = f'alias +{key}_bind "{press_str}"\n' +\
|
||||
f'alias -{key}_bind "{release_str}"\n' +\
|
||||
f'bind {key} "+{key}_bind"\n'
|
||||
return ret
|
||||
|
||||
def toggle(key, instruction):
|
||||
onStr = f'turn_{key}_on'
|
||||
offStr = f'turn_{key}_off'
|
||||
togStr = f'toggle_{key}'
|
||||
|
||||
ret = f'alias {onStr} "+{instruction}; alias {togStr} {offStr}"\n' +\
|
||||
f'alias {offStr} "-{instruction}; alias {togStr} {onStr}"\n' +\
|
||||
f'alias {togStr} "{onStr}"\n' +\
|
||||
f'bind {key} "{togStr}"\n'
|
||||
return ret
|
||||
|
||||
def double(key, options):
|
||||
prim_action = options["primary"]
|
||||
pBindType = firstTypeIn(prim_action.keys())
|
||||
pBindContent = prim_action[pBindType]
|
||||
|
||||
sec_action = options["secondary"]
|
||||
sBindType = firstTypeIn(sec_action.keys())
|
||||
sBindContent = sec_action[sBindType]
|
||||
|
||||
main_str = f'{key}_main'
|
||||
alt_str = f'{key}_alt'
|
||||
tog_str = f'toggle_{key}'
|
||||
|
||||
recursive_code = branch(main_str, pBindContent, pBindType) +\
|
||||
branch(alt_str, sBindContent, sBindType)
|
||||
|
||||
newcode = []
|
||||
for line in recursive_code.split('\n'):
|
||||
# For every line gotten by the recursive call, change all "bind"s to "alias",
|
||||
# since main_str and alt_str aren't valid bind targes
|
||||
llist = line.split(' ')
|
||||
for i in range(len(llist)):
|
||||
alphanum_chars = ''.join(c for c in llist[i] if c.isalnum())
|
||||
if alphanum_chars == 'bind':
|
||||
if llist[i][0].isalnum():
|
||||
llist[i] = 'alias'
|
||||
else:
|
||||
# If the first character isn't a normal character.
|
||||
# Almost always because it is a double quote
|
||||
llist[i] = llist[i][0] + 'alias'
|
||||
newcode.append(' '.join(llist))
|
||||
|
||||
ret = '\n'.join(newcode) +\
|
||||
f'alias +{tog_str} "bind {key} {alt_str}"\n' +\
|
||||
f'alias -{tog_str} "bind {key} {main_str}"\n'+\
|
||||
f'bind {key} "{main_str}"\n'
|
||||
|
||||
cond_name = options["condition"]
|
||||
|
||||
if cond_name in condDict:
|
||||
# If the condition key (like "mouse4") already has toggles,
|
||||
# just append another toggle string (it gets encased in quotes later)
|
||||
condDict[cond_name]["plus_toggles"] += f"; +{tog_str}"
|
||||
condDict[cond_name]["minus_toggles"] += f"; -{tog_str}"
|
||||
else:
|
||||
# If the condition key doesn't already exist, make it with the correct values
|
||||
condDict.update( {
|
||||
cond_name: {
|
||||
"plus_toggles": f"+{tog_str}",
|
||||
"minus_toggles": f"-{tog_str}"
|
||||
}
|
||||
} )
|
||||
|
||||
return ret
|
||||
|
||||
def repeat(key, options):
|
||||
return f'placeholder for {key} (repeat)\n'
|
|
@ -0,0 +1,161 @@
|
|||
"""Verify all the things that could go wrong."""
|
||||
def verifyConfig(cfg: dict):
|
||||
|
||||
# Do aliases first
|
||||
aliasErrors = []
|
||||
|
||||
aliases = None
|
||||
|
||||
if "aliases" in cfg:
|
||||
aliases = cfg.pop("aliases")
|
||||
for key, data in aliases.items():
|
||||
errMessages = validBind(key, data, alias = True)
|
||||
if len(errMessages) > 0:
|
||||
for msg in errMessages:
|
||||
aliasErrors.append(f"Error in aliases: {msg}")
|
||||
|
||||
errors = []
|
||||
|
||||
for cclass in cfg:
|
||||
for key, data in cfg[cclass].items():
|
||||
errMessages = validBind(key, data)
|
||||
if len(errMessages) > 0:
|
||||
for msg in errMessages:
|
||||
errors.append(f"Error in {cclass}: {msg}")
|
||||
|
||||
errors += aliasErrors
|
||||
|
||||
if len(errors) > 0:
|
||||
cfg.update({"errors": errors})
|
||||
|
||||
return cfg
|
||||
|
||||
def validBind(key, data, alias = False) -> list:
|
||||
"""Check for valid key and valid binding"""
|
||||
ret = []
|
||||
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 = data.copy()
|
||||
dataCopy, errMsgs = validBindType(key, dataCopy)
|
||||
if len(errMsgs) > 0:
|
||||
for msg in errMsgs:
|
||||
ret.append(msg)
|
||||
|
||||
extras = dataCopy.keys()
|
||||
if len(extras) > 0:
|
||||
extrasString = "\n\t".join(extras)
|
||||
ret.append(f'Unused fields in "{key}":\n\t{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'
|
||||
]
|
||||
|
||||
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
|
||||
data, errMsgs = removeRelaventFields(data, potentialType)
|
||||
break
|
||||
|
||||
if not validType:
|
||||
errMsgs.append(f'Key "{key}" has no known bind type')
|
||||
|
||||
return data, errMsgs
|
||||
|
||||
def removeRelaventFields(data, bindType):
|
||||
errMsgs = []
|
||||
# These types are simple, just the bind type and argument
|
||||
if bindType in ["impulse", "toggle"]:
|
||||
data.pop(bindType)
|
||||
|
||||
elif bindType == "hold":
|
||||
content = data.pop("hold")
|
||||
if isinstance(content, dict):
|
||||
if "press" not in content:
|
||||
errMsgs.append("If hold is not a single argument, it requires a `press` argument")
|
||||
elif "release" not in content:
|
||||
errMsgs.append("If hold is not a single argument, it requires a `release` argument")
|
||||
elif not isinstance(content, str):
|
||||
errMsgs.append(f"Hold must be either single action or press and release")
|
||||
|
||||
elif bindType == "double":
|
||||
content = data.pop("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)
|
||||
if len(errMessages) > 0:
|
||||
errMsgs += errMessages
|
||||
|
||||
if "secondary" not in content:
|
||||
errMsgs.append("Double requires secondary action")
|
||||
else:
|
||||
# Same logic as above
|
||||
errMessages = validBind("secondary", content["secondary"], alias = True)
|
||||
if len(errMessages) > 0:
|
||||
errMsgs += errMessages
|
||||
|
||||
if "condition" not in content:
|
||||
errMsgs.append("Double requires condition to toggle")
|
||||
else:
|
||||
# Validate the toggler
|
||||
key = content["condition"]
|
||||
if not validKey(key):
|
||||
errMsgs.append(f'Invalid condition to toggle "{key}"')
|
||||
|
||||
elif bindType == "repeat":
|
||||
content = data.pop("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:
|
||||
interval = content["interval"]
|
||||
if interval < 0:
|
||||
errMsgs.append("Repeat interval cannot be negative")
|
||||
if (unit == "t" and not isinstance(interval, int)):
|
||||
errMsgs.append("Repeat interval must be integer if unit is ticks")
|
||||
|
||||
return data, errMsgs
|
|
@ -0,0 +1,53 @@
|
|||
# Example tfscript
|
||||
# https://www.w3schools.io/file/yaml-arrays/
|
||||
|
||||
medic:
|
||||
e:
|
||||
impulse: "voicemenu 0 0"
|
||||
condition: "ctrl"
|
||||
foobar: "baz"
|
||||
1:
|
||||
toggle: "forward"
|
||||
|
||||
spy:
|
||||
mouse2:
|
||||
action: "sapperSpam"
|
||||
condition: "mouse4"
|
||||
default: "secondary fire"
|
||||
|
||||
pyro:
|
||||
mouse1:
|
||||
hold: pyrocombo
|
||||
|
||||
engi:
|
||||
q:
|
||||
impulse: con_pda
|
||||
|
||||
aliases:
|
||||
sapperSpam:
|
||||
repeat: sap
|
||||
interval: 0.1
|
||||
unit: "seconds"
|
||||
|
||||
pyrocombo:
|
||||
hold: primary fire
|
||||
press:
|
||||
- "slot1"
|
||||
- "+attack"
|
||||
- "wait 32"
|
||||
- "slot2"
|
||||
release:
|
||||
- "-attack"
|
||||
- "slot1"
|
||||
|
||||
default:
|
||||
w:
|
||||
hold: forward
|
||||
a:
|
||||
hold: left
|
||||
s:
|
||||
hold: backward
|
||||
d:
|
||||
hold: right
|
||||
mouse1:
|
||||
hold: primary_fire
|
Loading…
Reference in New Issue