class based refactor pull req #14
|
@ -9,51 +9,50 @@
|
|||
default:
|
||||
# voice-based doubles
|
||||
double e:
|
||||
impulse primary: voice medic
|
||||
impulse secondary: voice activate uber
|
||||
type: impulse
|
||||
primary: voice medic
|
||||
secondary: voice activate uber
|
||||
condition: mouse4
|
||||
double t:
|
||||
impulse primary: voice thanks
|
||||
impulse secondary: voice nice shot
|
||||
type: impulse
|
||||
primary: voice thanks
|
||||
secondary: voice nice shot
|
||||
condition: mouse4
|
||||
double v:
|
||||
impulse primary: voice spy
|
||||
impulse secondary: voice help
|
||||
type: impulse
|
||||
primary: voice spy
|
||||
secondary: voice help
|
||||
condition: mouse4
|
||||
|
||||
# hold doubles
|
||||
double r:
|
||||
primary:
|
||||
hold: "class_action"
|
||||
secondary:
|
||||
hold: "reload"
|
||||
condition: mouse4
|
||||
cancel both: yes
|
||||
cancel: both
|
||||
type: hold
|
||||
primary: +class_action
|
||||
secondary: +reload
|
||||
|
||||
# other
|
||||
double =:
|
||||
primary:
|
||||
impulse: kill
|
||||
secondary:
|
||||
impulse: explode
|
||||
condition: "-"
|
||||
impulse =: kill
|
||||
impulse -: explode
|
||||
|
||||
double q:
|
||||
impulse primary: lastinv
|
||||
impulse secondary:
|
||||
type: impulse
|
||||
primary: lastinv
|
||||
secondary:
|
||||
- "slot2"
|
||||
- "wait 10"
|
||||
- "slot1"
|
||||
condition: mouse4
|
||||
double ctrl:
|
||||
# I use shift to crouch
|
||||
impulse primary:
|
||||
voice yes
|
||||
impulse secondary:
|
||||
voice no
|
||||
type: impulse
|
||||
primary: voice yes
|
||||
secondary: voice no
|
||||
condition: mouse4
|
||||
|
||||
# toggle
|
||||
toggle capslock: voicerecord
|
||||
toggle capslock: +voicerecord
|
||||
|
||||
# hold: null-cancelled movement, so hitting a while holding d causes
|
||||
# me to go left instead of stopping, or vice-versa.
|
||||
|
@ -61,29 +60,28 @@ default:
|
|||
press:
|
||||
- "-moveright"
|
||||
- "+moveleft"
|
||||
- "alias maybeMoveLeft +moveleft"
|
||||
- "alias maybe_move_left +moveleft"
|
||||
release:
|
||||
- "-moveleft"
|
||||
- "maybeMoveRight"
|
||||
- "alias maybeMoveLeft "
|
||||
- "maybe_move_right"
|
||||
- "unalias maybe_move_left"
|
||||
hold d:
|
||||
press:
|
||||
- "-moveleft"
|
||||
- "+moveright"
|
||||
- "alias maybeMoveRight +moveright"
|
||||
- "alias maybe_move_right +moveright"
|
||||
release:
|
||||
- "-moveright"
|
||||
- "maybeMoveLeft"
|
||||
- "alias maybeMoveRight "
|
||||
- "maybe_move_left"
|
||||
- "unalias maybe_move_right"
|
||||
# This just stops an error message the first time you release
|
||||
# either of 'a' or 'd'
|
||||
impulse maybeMoveLeft:
|
||||
impulse maybe_move_left:
|
||||
alias: yes
|
||||
command: ""
|
||||
impulse maybeMoveRight:
|
||||
impulse maybe_move_right:
|
||||
alias: yes
|
||||
command: ""
|
||||
|
||||
# class action is something useful for each class,
|
||||
# like destroying and rebuilding a sentry for the engineer
|
||||
# this is just the default. I do a lot of hybridknight and
|
||||
|
@ -99,16 +97,16 @@ default:
|
|||
|
||||
impulse load0:
|
||||
alias: yes
|
||||
command: "load_itempreset 0"
|
||||
command: "loadout a"
|
||||
impulse load1:
|
||||
alias: yes
|
||||
command: "load_itempreset 1"
|
||||
command: "loadout b"
|
||||
impulse load2:
|
||||
alias: yes
|
||||
command: "load_itempreset 2"
|
||||
command: "loadout c"
|
||||
impulse load3:
|
||||
alias: yes
|
||||
command: "load_itempreset 3"
|
||||
command: "loadout d"
|
||||
|
||||
impulse INS:
|
||||
- "load0"
|
||||
|
@ -122,8 +120,10 @@ default:
|
|||
impulse END:
|
||||
- "load3"
|
||||
- "alias reload_presets load3"
|
||||
literal set_default_loadout:
|
||||
alias reload_presets load0
|
||||
impulse backspace:
|
||||
"reload_presets"
|
||||
reload_presets
|
||||
|
||||
engi:
|
||||
hold class_action:
|
||||
|
@ -131,7 +131,6 @@ engi:
|
|||
press:
|
||||
- destroy sentry
|
||||
- build sentry
|
||||
release: ""
|
||||
|
||||
medic:
|
||||
# "Radar" feature: causes all teammates to autocall for medic, allowing
|
||||
|
@ -140,12 +139,21 @@ medic:
|
|||
alias: yes
|
||||
press: "hud_medicautocallersthreshold 150"
|
||||
release: "hud_medicautocallersthreshold 75"
|
||||
double e:
|
||||
type: impulse
|
||||
condition: mouse4
|
||||
primary: voice medic
|
||||
secondary: voice uber ready
|
||||
|
||||
soldier:
|
||||
double mouse1:
|
||||
hold primary:
|
||||
attack
|
||||
hold secondary:
|
||||
type: hold
|
||||
condition: mouse4
|
||||
cancel: both
|
||||
# normal firing
|
||||
primary: +attack
|
||||
# rocket jump
|
||||
secondary:
|
||||
press:
|
||||
- +attack
|
||||
- +duck
|
||||
|
@ -154,5 +162,3 @@ soldier:
|
|||
- -attack
|
||||
- -duck
|
||||
- -jump
|
||||
condition: mouse4
|
||||
|
|
@ -1,270 +1,33 @@
|
|||
""" Makes the configs as a massive string """
|
||||
""" This has only one function. It mostly exists to allow imports of things like tftypes """
|
||||
|
||||
# Used for the conditions in the <double> type
|
||||
condDict = {}
|
||||
defaultDict = {}
|
||||
bindOrAlias = "bind"
|
||||
from copy import deepcopy
|
||||
from tfscript.tftypes import Double
|
||||
|
||||
def makeCFG(cfg, default=False):
|
||||
global bindOrAlias
|
||||
global condDict
|
||||
global defaultDict
|
||||
def makeCFG(bindList, default=False):
|
||||
|
||||
bindOrAlias = "bind"
|
||||
if default:
|
||||
# Write to defaultDict instead of condDict
|
||||
condDict = defaultDict
|
||||
Double.condDict = Double.defaultDict
|
||||
else:
|
||||
condDict = deepcopy(defaultDict)
|
||||
Double.condDict = deepcopy(Double.defaultDict)
|
||||
|
||||
ret = ''
|
||||
for key, data in cfg.items():
|
||||
isAlias = False
|
||||
if "alias" in data:
|
||||
isAlias = data.pop("alias")
|
||||
if isAlias:
|
||||
bindOrAlias = "alias"
|
||||
else:
|
||||
bindOrAlias = "bind"
|
||||
ret += branch(key, data)
|
||||
|
||||
for bind in bindList:
|
||||
ret += bind.toTF2()
|
||||
|
||||
# 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
|
||||
if default or condDict != defaultDict:
|
||||
if default or Double.condDict != Double.defaultDict:
|
||||
# ==, and by extension !=, does in fact check
|
||||
# for dictionary equality in keys and values
|
||||
for key, toggles in condDict.items():
|
||||
for key, toggles in Double.condDict.items():
|
||||
onCondPress = ';'.join(toggles["change_keys"])
|
||||
onCondRelease = ';'.join(toggles["restore_keys"])
|
||||
ret += f'alias +{key}_toggles "{onCondPress}"\n' +\
|
||||
f'alias -{key}_toggles "{onCondRelease}"\n' +\
|
||||
f'{bindOrAlias} {key} "+{key}_toggles"\n'
|
||||
|
||||
del condDict # free deep copy
|
||||
ret += (
|
||||
f'alias +{key}_toggles "{onCondPress}"\n'
|
||||
+ f'alias -{key}_toggles "{onCondRelease}"\n'
|
||||
+ f'bind {key} "+{key}_toggles"\n'
|
||||
)
|
||||
|
||||
return ret
|
||||
|
||||
def typeOf(dictIn):
|
||||
""" Find the first element common to both lists """
|
||||
types = [
|
||||
"impulse",
|
||||
"hold",
|
||||
"toggle",
|
||||
"double",
|
||||
"repeat"
|
||||
]
|
||||
for t in types:
|
||||
if t in dictIn.keys():
|
||||
return t
|
||||
|
||||
def branch(keyName, bindContent):
|
||||
""" Using the provided keyName and content, call the correct function """
|
||||
|
||||
"""
|
||||
Terser syntax, ex.
|
||||
impulse e:
|
||||
<content>
|
||||
instead of
|
||||
e:
|
||||
impulse:
|
||||
<content>
|
||||
"""
|
||||
splitKey = keyName.split(' ', 1)
|
||||
if len(splitKey) > 1:
|
||||
keyName = splitKey[1]
|
||||
bindContent = {splitKey[0]: bindContent}
|
||||
|
||||
bindType = typeOf(bindContent)
|
||||
bindContent = bindContent.pop(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):
|
||||
global bindOrAlias
|
||||
if isinstance(instruction, dict):
|
||||
instruction = instruction["command"]
|
||||
|
||||
if not isinstance(instruction, list):
|
||||
instruction = instruction.split(';')
|
||||
|
||||
instuction = impulseShortcuts(instruction)
|
||||
instruction = ';'.join(instruction)
|
||||
|
||||
return f'{bindOrAlias} {key} "{instruction}"\n'
|
||||
|
||||
def impulseShortcuts(instList):
|
||||
for i, instruction in enumerate(instList):
|
||||
splitCmd = instruction.split(' ')
|
||||
cmd = splitCmd[0]
|
||||
restOfCmd = ' '.join(splitCmd[1:])
|
||||
shortcuts = {
|
||||
"primary": "slot1",
|
||||
"secondary": "slot2",
|
||||
"melee": "slot3"
|
||||
}
|
||||
if cmd in shortcuts:
|
||||
cmd = shortcuts[cmd]
|
||||
|
||||
if cmd == "voice":
|
||||
cmd = "voicemenu"
|
||||
restOfCmd = voice(restOfCmd)
|
||||
|
||||
elif cmd == "build" or cmd == "destroy":
|
||||
restOfCmd = expandBuildings(restOfCmd)
|
||||
|
||||
elif cmd == "load_itempreset" and restOfCmd.isalpha():
|
||||
restOfCmd = restOfCmd.lower()
|
||||
restOfCmd = ['a','b','c','d'].index(restOfCmd)
|
||||
|
||||
if restOfCmd != "":
|
||||
cmd += ' ' + restOfCmd
|
||||
instList[i] = cmd
|
||||
|
||||
return instList
|
||||
|
||||
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'{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):
|
||||
global bindOrAlias
|
||||
# This isn't quite right, fix later!
|
||||
if instruction[0] == '+' or instruction[0] == '-':
|
||||
return f'{bindOrAlias} {key} "{instruction}"\n'
|
||||
else:
|
||||
return f'{bindOrAlias} {key} "+{instruction}"\n'
|
||||
|
||||
def listHold(key, options):
|
||||
global bindOrAlias
|
||||
|
||||
oldBindOrAlias = bindOrAlias
|
||||
bindOrAlias = 'alias'
|
||||
ret = impulse(f'+{key}_press', options["press"]) +\
|
||||
impulse(f'-{key}_press', options["release"])
|
||||
|
||||
bindOrAlias = oldBindOrAlias
|
||||
|
||||
ret += f'{bindOrAlias} {key} "+{key}_press"\n'
|
||||
|
||||
return ret
|
||||
|
||||
def toggle(key, instruction):
|
||||
global bindOrAlias
|
||||
onStr = f'turn_{key}_on'
|
||||
offStr = f'turn_{key}_off'
|
||||
togStr = f'toggle_{key}'
|
||||
if instruction[0] == '+':
|
||||
instruction = instruction[1:]
|
||||
|
||||
ret = f'alias {onStr} "+{instruction}; alias {togStr} {offStr}"\n' +\
|
||||
f'alias {offStr} "-{instruction}; alias {togStr} {onStr}"\n' +\
|
||||
f'alias {togStr} "{onStr}"\n' +\
|
||||
f'{bindOrAlias} {key} "{togStr}"\n'
|
||||
return ret
|
||||
|
||||
def double(key, options):
|
||||
primaryAction = options["primary"]
|
||||
|
||||
secAction = options["secondary"]
|
||||
|
||||
mainStr = f'{key}_main'
|
||||
altStr = f'{key}_alt'
|
||||
pShiftStr = f'+shift_{key}'
|
||||
mShiftStr = f'-shift_{key}'
|
||||
|
||||
global bindOrAlias
|
||||
oldBindOrAlias = bindOrAlias
|
||||
bindOrAlias = "alias"
|
||||
recursiveCode = branch(mainStr, primaryAction) +\
|
||||
branch(altStr, secAction)
|
||||
bindOrAlias = oldBindOrAlias
|
||||
|
||||
ret = recursiveCode +\
|
||||
f'alias {pShiftStr} "{bindOrAlias} {key} {altStr}"\n' +\
|
||||
f'alias {mShiftStr} "{bindOrAlias} {key} {mainStr}"\n'+\
|
||||
f'{bindOrAlias} {key} "{mainStr}"\n'
|
||||
|
||||
isToggle = ("toggle" in options and options.pop("toggle") == True)
|
||||
if isToggle:
|
||||
toggleStr = toggle(key, pShiftStr)
|
||||
|
||||
condName = options["condition"]
|
||||
global condDict
|
||||
if condName in condDict:
|
||||
# If the condition key (like "mouse4") already has toggles,
|
||||
# just append another toggle string
|
||||
changes = condDict[condName]["change_keys"]
|
||||
restores = condDict[condName]["restore_keys"]
|
||||
|
||||
if isToggle:
|
||||
# "toggle: true" specified, add the toggle string
|
||||
if toggleStr not in changes:
|
||||
changes.append(toggleStr)
|
||||
if pShiftStr in changes:
|
||||
# If key already has normal shift, remove it
|
||||
changes.remove(pShiftStr)
|
||||
changes.remove(mShiftStr)
|
||||
|
||||
elif pShiftStr not in changes:
|
||||
# not toggle, not already in changes
|
||||
changes.append(pShiftStr)
|
||||
restores.append(mShiftStr)
|
||||
else:
|
||||
# If the condition key doesn't already exist, make it
|
||||
if isToggle:
|
||||
condDict.update( {
|
||||
condName: {
|
||||
"change_keys": [ toggleStr ],
|
||||
"restore_keys": [ ]
|
||||
}
|
||||
} )
|
||||
else:
|
||||
condDict.update( {
|
||||
condName: {
|
||||
"change_keys": [ pShiftStr ],
|
||||
"restore_keys": [ mShiftStr ]
|
||||
}
|
||||
} )
|
||||
|
||||
return ret
|
||||
|
||||
def repeat(key, options):
|
||||
return f'placeholder for {key} (repeat)\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
|
||||
|
@ -27,48 +27,53 @@ except ModuleNotFoundError:
|
|||
|
||||
# Local libraries
|
||||
import tfscript
|
||||
from tfscript import verify
|
||||
from tfscript import writing
|
||||
from tfscript import verify, writing, makeCFG
|
||||
|
||||
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 'warnings' in config:
|
||||
for cclass, messages in config.pop('warnings').items():
|
||||
print(f'Warning in {cclass}:', file=stderr)
|
||||
for msg in messages:
|
||||
print(f" {msg}")
|
||||
print(f' {msg}', file=stderr)
|
||||
|
||||
if 'errors' in config:
|
||||
for cclass, messages in config['errors'].items():
|
||||
print(f'Error in {cclass}:', file=stderr)
|
||||
for msg in messages:
|
||||
print(f' {msg}', file=stderr)
|
||||
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)
|
||||
tempsAndReals.update(replaceDict)
|
||||
config.update({'default': defaults})
|
||||
|
||||
for currentClass in config:
|
||||
classDict = config[currentClass]
|
||||
stringToWrite = tfscript.makeCFG(classDict)
|
||||
replaceDict = writing.writeOutput(stringToWrite, currentClass, args)
|
||||
for class_ in config:
|
||||
stringToWrite = makeCFG(
|
||||
config[class_],
|
||||
default=(class_ == 'default')
|
||||
)
|
||||
replaceDict = writing.writeOutput(stringToWrite, class_, args)
|
||||
tempsAndReals.update(replaceDict)
|
||||
|
||||
return tempsAndReals
|
||||
|
@ -76,56 +81,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 +144,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 +165,5 @@ def main() -> int:
|
|||
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == '__main__':
|
||||
exit(main())
|
||||
|
|
|
@ -0,0 +1,629 @@
|
|||
import re
|
||||
|
||||
validKeyList = [
|
||||
# top row
|
||||
'escape', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12',
|
||||
# keyboard
|
||||
'`', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '=', 'backspace',
|
||||
'tab', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\\',
|
||||
'capslock', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'semicolon', '\'', 'enter',
|
||||
'shift', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', 'rshift',
|
||||
'ctrl', 'lwin', 'alt', 'space', 'rwin', 'ralt', 'rctrl',
|
||||
# mouse
|
||||
'mouse1', 'mouse2', 'mouse3', 'mouse4', 'mouse5', 'mwheelup', 'mwheeldown',
|
||||
# 8 of the 9 keys to the upper-right (PrtScn can't be bound)
|
||||
'scrolllock', 'numlock',
|
||||
'ins', 'home', 'pgup',
|
||||
'del', 'end', 'pgdn',
|
||||
# arrows
|
||||
'uparrow', 'downarrow',
|
||||
'leftarrow', 'rightarrow'
|
||||
]
|
||||
|
||||
popErrors = (AttributeError, KeyError, TypeError)
|
||||
|
||||
class Bind(object):
|
||||
'''
|
||||
Parent class for all bind types.
|
||||
Verifies key, creates local variables
|
||||
'''
|
||||
bindTypes = []
|
||||
instances = {}
|
||||
|
||||
def __init__(self, key='', fields={}, *, parent=None):
|
||||
if parent is None:
|
||||
self.alias = False
|
||||
self.key = str(key)
|
||||
self.fields = fields
|
||||
self.errors = []
|
||||
self.warnings = []
|
||||
self.TargetType = None
|
||||
else:
|
||||
self.alias = parent.alias
|
||||
self.key = parent.key
|
||||
self.fields = parent.fields
|
||||
self.errors = parent.errors
|
||||
self.warnings = parent.warnings
|
||||
|
||||
# redefined for each unique type, default just verifies key
|
||||
# and some other universal fields like alias and finds targetType
|
||||
self.verify()
|
||||
|
||||
if type(self) is Bind:
|
||||
# not using isinstance(), because all subclasses are also instances
|
||||
# of bind.
|
||||
return
|
||||
|
||||
if len(self.fields) > 0:
|
||||
# verify function should remove all fields relavent to the bind.
|
||||
# Any extras are errors
|
||||
|
||||
self.warnings.append(f'extra fields in "{self.key}":')
|
||||
if isinstance(self.fields, str):
|
||||
# iterating over a str returns each character,
|
||||
# making meaningless error messages
|
||||
self.warnings.append(f' "{self.fields}"')
|
||||
else:
|
||||
for field in self.fields:
|
||||
self.warnings.append(f' "{field}"')
|
||||
if len(self.errors) == 0:
|
||||
# no errors, add new instance to the list of instances
|
||||
try:
|
||||
self.instances[type(self)].append(self)
|
||||
except KeyError:
|
||||
self.instances[type(self)] = [self]
|
||||
|
||||
def verify(self):
|
||||
try:
|
||||
self.alias = self.fields.pop('alias')
|
||||
if not isinstance(self.alias, bool):
|
||||
self.errors.append(
|
||||
f'alias should be "yes" or "no", not "{self.alias}"'
|
||||
)
|
||||
self.alias = False
|
||||
except popErrors:
|
||||
self.alias = False
|
||||
|
||||
try:
|
||||
typeName, self.key = self.key.split(' ', 1)
|
||||
# all types start with a capital
|
||||
typeName = typeName.lower().capitalize()
|
||||
if not self.alias:
|
||||
# don't mess with alias names
|
||||
self.key = self.key.lower()
|
||||
except ValueError:
|
||||
# catastrophic error: either no type or no key, assume no type
|
||||
self.errors.append(f'could not find type in "{self.key}"')
|
||||
return
|
||||
|
||||
for type_ in self.bindTypes:
|
||||
if typeName == type_.__name__:
|
||||
self.TargetType = type_
|
||||
break
|
||||
|
||||
if self.TargetType is None:
|
||||
self.errors.append(
|
||||
f'"{typeName}" is not a valid type for "{self.key}"'
|
||||
)
|
||||
|
||||
if (not self.alias) and (self.key not in validKeyList):
|
||||
self.errors.append(f'invalid key name: "{self.key}"')
|
||||
|
||||
def toTargetType(self):
|
||||
if self.TargetType is None:
|
||||
# do nothing
|
||||
return self
|
||||
# cast to targetType, "inheriting" stuff from self
|
||||
bind = self.TargetType(parent=self)
|
||||
return bind
|
||||
|
||||
def err(self, message):
|
||||
self.errors.append(
|
||||
f'{type(self).__name__.lower()} "{self.key}" {message}'
|
||||
)
|
||||
|
||||
def warn(self, message):
|
||||
self.warnings.append(
|
||||
f'{type(self).__name__.lower()} "{self.key}" {message}'
|
||||
)
|
||||
|
||||
|
||||
class Impulse(Bind):
|
||||
def verify(self):
|
||||
self.command = None
|
||||
if not isinstance(self.fields, dict):
|
||||
self.fields = {'command': self.fields}
|
||||
|
||||
try:
|
||||
self.command = self.fields.pop('command')
|
||||
if isinstance(self.command, str):
|
||||
self.command = self.command.split(';')
|
||||
elif not isinstance(self.command, list):
|
||||
self.err('`command` field must be argument of string or list')
|
||||
self.command = None
|
||||
except KeyError:
|
||||
self.err('requires `command` field')
|
||||
|
||||
|
||||
def toTF2(self) -> str:
|
||||
if self.alias:
|
||||
bindOrAlias = 'alias'
|
||||
else:
|
||||
bindOrAlias = 'bind'
|
||||
|
||||
allInstructions = self.shortcut(self.command)
|
||||
instruction = ';'.join(allInstructions)
|
||||
|
||||
return f'{bindOrAlias} {self.key} "{instruction}"\n'
|
||||
|
||||
def shortcut(self, instList):
|
||||
for i, instruction in enumerate(instList):
|
||||
try:
|
||||
cmd, restOfCmd = instruction.split(' ', 1)
|
||||
except ValueError:
|
||||
# no spaces in cmd
|
||||
cmd, restOfCmd = instruction, ''
|
||||
|
||||
simpleSCs = {
|
||||
'primary': 'slot1',
|
||||
'secondary': 'slot2',
|
||||
'melee': 'slot3'
|
||||
}
|
||||
try:
|
||||
cmd = simpleSCs[cmd]
|
||||
except KeyError:
|
||||
# not a shortcut
|
||||
pass
|
||||
|
||||
if cmd == 'voice':
|
||||
cmd = 'voicemenu'
|
||||
restOfCmd = self.expandVoice(restOfCmd)
|
||||
|
||||
elif cmd == 'build' or cmd == 'destroy':
|
||||
restOfCmd = self.expandBuildings(restOfCmd)
|
||||
|
||||
elif cmd == 'loadout' and restOfCmd.isalpha():
|
||||
cmd = 'load_itempreset'
|
||||
try:
|
||||
restOfCmd = restOfCmd.lower()
|
||||
restOfCmd = str(['a','b','c','d'].index(restOfCmd))
|
||||
except ValueError:
|
||||
# not a load_itempreset shortcut
|
||||
pass
|
||||
|
||||
elif cmd == 'unalias':
|
||||
cmd = 'alias'
|
||||
# adding empty arg to indicate unaliasing
|
||||
restOfCmd += ' '
|
||||
|
||||
if restOfCmd != '':
|
||||
cmd += ' ' + restOfCmd
|
||||
instList[i] = cmd
|
||||
|
||||
return instList
|
||||
|
||||
def expandVoice(self, 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'{menu} {selection}'
|
||||
|
||||
def expandBuildings(self, 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
|
||||
|
||||
|
||||
class Hold(Bind):
|
||||
def verify(self):
|
||||
self.press = None
|
||||
self.release = None
|
||||
if not isinstance(self.fields, dict):
|
||||
self.fields = {'press': self.fields}
|
||||
|
||||
# verify press
|
||||
try:
|
||||
self.press = self.fields.pop('press')
|
||||
if isinstance(self.press, str):
|
||||
self.press = self.press.split(';')
|
||||
elif not isinstance(self.press, list):
|
||||
self.err('`press` field must be string or list')
|
||||
self.press = None
|
||||
except KeyError:
|
||||
self.err('requires `press` field')
|
||||
if self.press is None:
|
||||
return
|
||||
|
||||
# verify release
|
||||
try:
|
||||
self.release = self.fields.pop('release')
|
||||
if isinstance(self.release, str):
|
||||
self.release = self.release.split(';')
|
||||
elif not isinstance(self.release, list):
|
||||
self.err('`release` field must be string or list')
|
||||
self.release = None
|
||||
except popErrors:
|
||||
self.warn('has no `release`, creating one')
|
||||
# no release specified, do -action for each item in press
|
||||
self.release = []
|
||||
for cmd in self.press:
|
||||
if cmd[0] == '+':
|
||||
self.release.append('-' + cmd[1:])
|
||||
|
||||
def toTF2(self) -> str:
|
||||
if self.alias:
|
||||
bindOrAlias = 'alias'
|
||||
else:
|
||||
bindOrAlias = 'bind'
|
||||
holdStr = f'hold_{self.key}'
|
||||
|
||||
# Making impulse instances from self.press and .release
|
||||
# allows them to share the shortcuts
|
||||
pressObj = Impulse(f'+{holdStr}', self.press)
|
||||
pressObj.alias = True
|
||||
pressStr = pressObj.toTF2()
|
||||
|
||||
releaseObj = Impulse(f'-{holdStr}', self.release)
|
||||
releaseObj.alias = True
|
||||
releaseStr = releaseObj.toTF2()
|
||||
|
||||
if self.alias:
|
||||
# if alias, do this to avoid activating
|
||||
# and never deactivating
|
||||
self.key = '+' + self.key
|
||||
|
||||
return (
|
||||
pressStr + releaseStr
|
||||
+ f'{bindOrAlias} {self.key} "+{holdStr}"\n'
|
||||
)
|
||||
|
||||
|
||||
class Toggle(Bind):
|
||||
def verify(self):
|
||||
self.on = None
|
||||
self.off = None
|
||||
if not isinstance(self.fields, dict):
|
||||
self.fields = {'on': self.fields}
|
||||
|
||||
# verify on
|
||||
try:
|
||||
self.on = self.fields.pop('on')
|
||||
if isinstance(self.on, str):
|
||||
self.on = self.on.split(';')
|
||||
elif not isinstance(self.on, list):
|
||||
self.err('`on` field must be string or list')
|
||||
self.on = None
|
||||
except KeyError:
|
||||
self.err('requires `on` field')
|
||||
if self.on is None:
|
||||
return
|
||||
|
||||
# verify off
|
||||
try:
|
||||
self.off = self.fields.pop('off')
|
||||
if isinstance(self.off, str):
|
||||
self.off = self.off.split(';')
|
||||
elif not isinstance(self.off, list):
|
||||
self.err('`off` field must be string or list')
|
||||
except popErrors:
|
||||
# no off specified, do -action for each item in on
|
||||
self.off = []
|
||||
for cmd in self.on:
|
||||
if cmd[0] == '+':
|
||||
self.off.append('-' + cmd[1:])
|
||||
|
||||
def toTF2(self) -> str:
|
||||
if self.alias:
|
||||
bindOrAlias = 'alias'
|
||||
else:
|
||||
bindOrAlias = 'bind'
|
||||
toggleStr = f'toggle_{self.key}'
|
||||
onStr = f'{toggleStr}_on'
|
||||
offStr = f'{toggleStr}_off'
|
||||
|
||||
onObj = Impulse(onStr, self.on)
|
||||
onObj.alias = True
|
||||
toggleOn = onObj.toTF2()
|
||||
# remove starting/trailing " and \n
|
||||
toggleOn = toggleOn[:-2]
|
||||
|
||||
offObj = Impulse(offStr, self.off)
|
||||
offObj.alias = True
|
||||
toggleOff = offObj.toTF2()[:-2]
|
||||
|
||||
return (
|
||||
f'{toggleOn}; alias {toggleStr} {offStr}"\n'
|
||||
+ f'{toggleOff}; alias {toggleStr} {onStr}"\n'
|
||||
+ f'alias {toggleStr} "{onStr}"\n'
|
||||
+ f'{bindOrAlias} {self.key} "{toggleStr}"\n'
|
||||
)
|
||||
|
||||
|
||||
class Double(Bind):
|
||||
defaultDict = {}
|
||||
condDict = {}
|
||||
bindNames = []
|
||||
|
||||
def verify(self):
|
||||
self.primary = None
|
||||
self.primStr = f'{self.key}_primary'
|
||||
|
||||
self.secondary = None
|
||||
self.secondStr = f'{self.key}_secondary'
|
||||
|
||||
self.condition = None
|
||||
self.isToggle = False
|
||||
|
||||
# name of a bind type
|
||||
self.type = None
|
||||
|
||||
# either 'released' (default) or 'both'
|
||||
self.cancelBoth = False
|
||||
|
||||
# toggler
|
||||
try:
|
||||
self.condition = self.fields.pop('condition')
|
||||
if self.condition not in validKeyList:
|
||||
self.err(f'has invalid `condition` field: "{self.condition}"')
|
||||
except popErrors:
|
||||
self.err('requires `condition` field')
|
||||
|
||||
try:
|
||||
self.isToggle = self.fields.pop('toggle')
|
||||
if not isinstance(self.isToggle, bool):
|
||||
self.err(
|
||||
'`toggle` field should be "yes" or "no", '
|
||||
+ f'not "{self.isToggle}"'
|
||||
)
|
||||
except popErrors:
|
||||
self.isToggle = False
|
||||
|
||||
# type
|
||||
try:
|
||||
self.type = self.fields.pop('type').lower()
|
||||
if self.type not in self.bindNames:
|
||||
# catastrophic: invalid type
|
||||
self.err(f'has invalid type: "{self.type}"')
|
||||
return
|
||||
except popErrors:
|
||||
# catastrophic: no type given
|
||||
self.err('requires `type` field')
|
||||
return
|
||||
|
||||
# cancel mode, must happend after type has been inferred
|
||||
try:
|
||||
cancel = self.fields.pop('cancel')
|
||||
|
||||
if not isinstance(cancel, str):
|
||||
self.err(f'`cancel` field must be "released" or "both"')
|
||||
|
||||
else:
|
||||
if cancel == 'both':
|
||||
if self.type == 'hold':
|
||||
self.cancelBoth = True
|
||||
else:
|
||||
self.err(
|
||||
'`cancel` field only affects "hold", '
|
||||
+ f'not "{self.type}"'
|
||||
)
|
||||
|
||||
elif cancel != 'released':
|
||||
self.err(
|
||||
'`cancel` field must be "released" '
|
||||
+ f'or "both", not "{cancel}"'
|
||||
)
|
||||
except popErrors:
|
||||
cancel = 'released'
|
||||
|
||||
# primary action
|
||||
try:
|
||||
mainSection = self.fields.pop('primary')
|
||||
mainBind = Bind(f'{self.type} {self.primStr}', mainSection)
|
||||
mainBind = mainBind.toTargetType()
|
||||
|
||||
self.errors.extend(mainBind.errors)
|
||||
self.warnings.extend(mainBind.warnings)
|
||||
self.errors.remove(f'invalid key name: "{self.primStr}"')
|
||||
self.primary = mainBind
|
||||
except popErrors:
|
||||
self.err('requires `primary` field')
|
||||
|
||||
# secondary action
|
||||
try:
|
||||
altSection = self.fields.pop('secondary')
|
||||
altBind = Bind(f'{self.type} {self.secondStr}', altSection)
|
||||
altBind = altBind.toTargetType()
|
||||
|
||||
self.errors.extend(altBind.errors)
|
||||
self.warnings.extend(altBind.warnings)
|
||||
self.errors.remove(f'invalid key name: "{self.secondStr}"')
|
||||
self.secondary = altBind
|
||||
except popErrors:
|
||||
self.err('requires `secondary` field')
|
||||
|
||||
|
||||
def toTF2(self) -> str:
|
||||
# Get code for primary and secondary actions.
|
||||
# alias=true so the toTF2() method aliases
|
||||
# them instead of binding them
|
||||
self.primary.alias = True
|
||||
mainCode = self.primary.toTF2()
|
||||
self.secondary.alias = True
|
||||
altCode = self.secondary.toTF2()
|
||||
|
||||
# Make code to switch between the two actions
|
||||
if self.cancelBoth:
|
||||
mainCode, altCode = self.getCancelCode(mainCode, altCode)
|
||||
|
||||
if self.type == 'hold':
|
||||
self.primStr = '+' + self.primStr
|
||||
self.secondStr = '+' + self.secondStr
|
||||
|
||||
shiftStr = f'shift_{self.key}'
|
||||
shiftCode = self.getChangeCode(shiftStr)
|
||||
self.addToCondDict(shiftStr)
|
||||
|
||||
return mainCode + altCode + shiftCode
|
||||
|
||||
def getChangeCode(self, shift):
|
||||
if self.alias:
|
||||
bindOrAlias = 'alias'
|
||||
else:
|
||||
bindOrAlias = 'bind'
|
||||
code = (
|
||||
f'alias +{shift} "{bindOrAlias} {self.key} {self.secondStr}"\n'
|
||||
+ f'alias -{shift} "{bindOrAlias} {self.key} {self.primStr}"\n'
|
||||
)
|
||||
|
||||
if self.isToggle:
|
||||
toggleObj = Toggle(shift, f'+{shift}')
|
||||
toggleObj.alias = True # so it aliases instead of binding
|
||||
code += toggleObj.toTF2()
|
||||
else:
|
||||
code += f'{bindOrAlias} {self.key} "{self.primStr}"\n'
|
||||
return code
|
||||
|
||||
def addToCondDict(self, shiftStr):
|
||||
if self.isToggle:
|
||||
changeStr = shiftStr
|
||||
else:
|
||||
changeStr = '+' + shiftStr
|
||||
restoreStr = '-' + shiftStr
|
||||
|
||||
if self.condition not in self.condDict:
|
||||
# if not already present, make dict for key
|
||||
self.condDict.update( {
|
||||
self.condition: {
|
||||
'change_keys': [],
|
||||
'restore_keys': []
|
||||
}
|
||||
} )
|
||||
|
||||
self.condDict[self.condition]['change_keys'].append(changeStr)
|
||||
if self.isToggle == False:
|
||||
self.condDict[self.condition]['restore_keys'].append(restoreStr)
|
||||
|
||||
def getCancelCode(self, mainCode, altCode) -> (str, str):
|
||||
# code to cancel both if either is released
|
||||
# it copies the - statement from each to both.
|
||||
# if it just extracted the name of the - statement,
|
||||
# you'd end up with each recursively calling the other
|
||||
|
||||
mainLines = mainCode.splitlines()
|
||||
mainMinusLine = mainLines[1]
|
||||
# second arg, without first or last quote
|
||||
mainMinusStr = mainMinusLine.split(' ', 2)[2][1:-1]
|
||||
|
||||
altLines = altCode.splitlines()
|
||||
altMinusLine = altLines[1]
|
||||
# same as above
|
||||
altMinusStr = altMinusLine.split(' ', 2)[2][1:-1]
|
||||
|
||||
# remove duplicate - actions
|
||||
mainMinusSet = set(mainMinusStr.split(';'))
|
||||
altMinusSet = set(altMinusStr.split(';'))
|
||||
allCancels = mainMinusSet | altMinusSet
|
||||
allCancelStr = ';'.join(allCancels)
|
||||
|
||||
altLines[1] = altLines[1][:-1] + f';{allCancelStr}"'
|
||||
mainLines[1] = mainLines[1][:-1] + f';{allCancelStr}"'
|
||||
|
||||
return (
|
||||
'\n'.join(mainLines) + '\n',
|
||||
'\n'.join(altLines) + '\n'
|
||||
)
|
||||
|
||||
|
||||
class Repeat(Bind):
|
||||
def verify(self):
|
||||
self.interval = None
|
||||
self.command = None
|
||||
|
||||
try:
|
||||
intervalStr = str(self.fields.pop('interval'))
|
||||
self.interval = int(intervalStr)
|
||||
if self.interval <= 0:
|
||||
self.err('`interval` must be greater than 0')
|
||||
except (KeyError, TypeError):
|
||||
self.err('requires `interval` field')
|
||||
except ValueError:
|
||||
self.err(f'has invalid `interval`: "{self.interval}"')
|
||||
except AttributeError:
|
||||
self.err(f'requires `interval` field')
|
||||
|
||||
try:
|
||||
self.command = self.fields.pop('command')
|
||||
if not isinstance(self.command, (str, list)):
|
||||
self.err('`command` must be string or list')
|
||||
self.command = None
|
||||
except popErrors:
|
||||
self.err('requires `command` field')
|
||||
|
||||
def toTF2(self) -> str:
|
||||
# commented-out placeholder
|
||||
return f'// repeat {self.key}\n'
|
||||
|
||||
|
||||
class Literal(Bind):
|
||||
def verify(self):
|
||||
self.text = ''
|
||||
self.run = False
|
||||
if not isinstance(self.fields, dict):
|
||||
self.fields = {'text': self.fields}
|
||||
|
||||
if not self.alias:
|
||||
try:
|
||||
# keyname should be invalid, remove the error
|
||||
self.errors.remove(
|
||||
f'invalid key name: "{self.key}"'
|
||||
)
|
||||
except ValueError:
|
||||
# if not invalid key, indicate as such
|
||||
self.warn('should not use a key as a label')
|
||||
|
||||
if 'run' in self.fields:
|
||||
self.run = self.fields.pop('run')
|
||||
if not isinstance(self.run, bool):
|
||||
self.errors.append(
|
||||
f'`run` should be "yes" or "no", not "{self.run}"'
|
||||
)
|
||||
if not self.alias:
|
||||
self.warn('`run` specified without alias')
|
||||
|
||||
try:
|
||||
self.text = self.fields.pop('text')
|
||||
except KeyError:
|
||||
self.err('requires `text` field')
|
||||
|
||||
if isinstance(self.text, str):
|
||||
self.text = self.text.split(';')
|
||||
elif not isinstance(self.text, list):
|
||||
self.err('argument must be of string or list')
|
||||
|
||||
def toTF2(self) -> str:
|
||||
result = ';'.join(self.text)
|
||||
if self.alias:
|
||||
result = f'alias {self.key} "{result}"'
|
||||
if self.run:
|
||||
result += f'\n{self.key}'
|
||||
return result + '\n'
|
||||
|
||||
# This is at the bottom because it has to happen after
|
||||
# all inheritances have been completed
|
||||
|
||||
Bind.bindTypes = Bind.__subclasses__()
|
||||
Double.bindNames = [ bind.__name__.lower() for bind in Bind.bindTypes ]
|
|
@ -1,72 +1,75 @@
|
|||
"""Verify all the things that could go wrong."""
|
||||
from copy import deepcopy
|
||||
|
||||
from tfscript import tftypes
|
||||
|
||||
def verifyConfig(cfg: dict) -> (dict, dict):
|
||||
verifiedConfig = {}
|
||||
|
||||
# Do defaults first
|
||||
errors = {}
|
||||
warnings = {}
|
||||
|
||||
defaults = None
|
||||
|
||||
if "default" in cfg:
|
||||
defaults = 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} )
|
||||
# Do defaults first
|
||||
defaults = []
|
||||
|
||||
classList = [
|
||||
"scout",
|
||||
"soldier",
|
||||
"pyro",
|
||||
("demo","demoman"),
|
||||
("engi","engineer"),
|
||||
("heavy","heavyweapons"),
|
||||
"medic",
|
||||
"sniper",
|
||||
"spy"
|
||||
'default',
|
||||
'scout',
|
||||
'soldier',
|
||||
'pyro',
|
||||
('demo','demoman'),
|
||||
('engi','engineer'),
|
||||
('heavy','heavyweapons'),
|
||||
'medic',
|
||||
'sniper',
|
||||
'spy'
|
||||
]
|
||||
|
||||
for cclass in classList:
|
||||
for isclass, class_ in enumerate(classList):
|
||||
|
||||
classCFG = None
|
||||
className = cclass
|
||||
if isinstance(cclass, str) and cclass in cfg:
|
||||
classCFG = cfg.pop(cclass)
|
||||
elif isinstance(cclass, tuple):
|
||||
for tupClass in cclass:
|
||||
className = class_
|
||||
|
||||
if isinstance(class_, str) and class_ in cfg:
|
||||
classCFG = cfg.pop(class_)
|
||||
elif isinstance(class_, tuple):
|
||||
for tupClass in class_:
|
||||
if tupClass in cfg:
|
||||
classCFG = cfg.pop(tupClass)
|
||||
className = cclass[0]
|
||||
className = class_[0]
|
||||
break
|
||||
if classCFG is None:
|
||||
# Invalid class, this gets caught later.
|
||||
# It may be less efficient this way, but
|
||||
# it makes for more descriptive error messages
|
||||
continue
|
||||
|
||||
classBinds = []
|
||||
errMessages = []
|
||||
warnMessages = []
|
||||
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 = tftypes.Bind(key, data)
|
||||
|
||||
bind = bind.toTargetType()
|
||||
if isclass:
|
||||
classBinds.append(bind)
|
||||
else:
|
||||
defaults.append(bind)
|
||||
|
||||
errMessages.extend(bind.errors)
|
||||
warnMessages.extend(bind.warnings)
|
||||
|
||||
if len(errMessages) > 0:
|
||||
errors.update( {className: errMessages} )
|
||||
verifiedConfig.update({className: classCFG})
|
||||
if len(warnMessages) > 0:
|
||||
warnings.update( {className: warnMessages} )
|
||||
|
||||
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, class_ in enumerate(classList):
|
||||
if isinstance(class_, tuple):
|
||||
classList.insert(i+1, class_[1])
|
||||
classList.insert(i+1, class_[0])
|
||||
classList.pop(i)
|
||||
|
||||
globalErrors = []
|
||||
|
@ -78,189 +81,15 @@ 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})
|
||||
if len(warnings) > 0:
|
||||
verifiedConfig.update({'warnings': warnings})
|
||||
|
||||
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",
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
default:
|
||||
impulse a: 'alias'
|
||||
impulse b: ['alias']
|
||||
impulse c:
|
||||
key: value
|
||||
|
||||
hold d: 'alias'
|
||||
hold e: ['alias']
|
||||
hold f:
|
||||
key: value
|
||||
|
||||
toggle g: 'alias'
|
||||
toggle h: ['alias']
|
||||
toggle i:
|
||||
key: value
|
||||
|
||||
double j: 'alias'
|
||||
double k: ['alias']
|
||||
double l:
|
||||
key: value
|
||||
|
||||
repeat m: 'alias'
|
||||
repeat n: ['alias']
|
||||
repeat o:
|
||||
key: value
|
Loading…
Reference in New Issue