moved to new locations

Paco Hope 2022-08-06 21:45:36 -04:00
parent 782692cf41
commit 3ddf60d8ab
5 changed files with 527 additions and 1 deletions

.vscode/launch.json vendored
View File

@ -10,7 +10,7 @@
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"args": ["-d", "demo.yaml"],
"args": ["-d", "./tests/mega.yaml"],
"justMyCode": true

src/tfscript/ Normal file
View File

@ -0,0 +1,106 @@
"""CLI module for converting YAML to tfscript"""
# 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
config = verify.verifyConfig(config)
if "errors" in config:
for e in config["errors"]:
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:
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 {} ')
os.replace(, f'{prefix}/{className}_script_{chunk:02d}.cfg')
if args.debug:
print( f'DEBUG: Created {prefix}/{className}_script_{chunk:02d}.cfg')
# 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 {} ')
while( n < len(classLines) and (byteswritten + len(classLines[n]) + reservedSpace) < chunksize ):
line = classLines[n].encode("utf8") + os.linesep.encode("utf8")
byteswritten += len(line)
if( chunk < chunksneeded ):
line = f'exec {className}_script_{chunk+1:02d}'.encode("utf8") + os.linesep.encode("utf8")
byteswritten += len(line)
os.replace(, 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()

src/tfscript/ Normal file
View File

@ -0,0 +1,206 @@
""" Makes the configs as a massive string """
# Used for the conditions in the <double> type
condDict = {}
def makeCFG(cfg):
ret = ''
for key, data in cfg.items():
# I know all of these fields exist because it was verified in
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 = [
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)
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(';'):
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
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'
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'
# 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}"
# 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'

src/tfscript/ Normal file
View File

@ -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:
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',
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 = [
errMsgs = []
for potentialType in data.keys():
if potentialType in types:
validType = True
data, errMsgs = removeRelaventFields(data, potentialType)
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"]:
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")
# 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")
# 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")
# 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")
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

tests/testScript.yaml Normal file
View File

@ -0,0 +1,53 @@
# Example tfscript
impulse: "voicemenu 0 0"
condition: "ctrl"
foobar: "baz"
toggle: "forward"
action: "sapperSpam"
condition: "mouse4"
default: "secondary fire"
hold: pyrocombo
impulse: con_pda
repeat: sap
interval: 0.1
unit: "seconds"
hold: primary fire
- "slot1"
- "+attack"
- "wait 32"
- "slot2"
- "-attack"
- "slot1"
hold: forward
hold: left
hold: backward
hold: right
hold: primary_fire