moved to new locations
parent
782692cf41
commit
3ddf60d8ab
|
@ -10,7 +10,7 @@
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "${file}",
|
"program": "${file}",
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"args": ["-d", "demo.yaml"],
|
"args": ["-d", "./tests/mega.yaml"],
|
||||||
"justMyCode": true
|
"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