class based refactor pull req #14
|
@ -9,51 +9,50 @@
|
||||||
default:
|
default:
|
||||||
# voice-based doubles
|
# voice-based doubles
|
||||||
double e:
|
double e:
|
||||||
impulse primary: voice medic
|
type: impulse
|
||||||
impulse secondary: voice activate uber
|
primary: voice medic
|
||||||
|
secondary: voice activate uber
|
||||||
condition: mouse4
|
condition: mouse4
|
||||||
double t:
|
double t:
|
||||||
impulse primary: voice thanks
|
type: impulse
|
||||||
impulse secondary: voice nice shot
|
primary: voice thanks
|
||||||
|
secondary: voice nice shot
|
||||||
condition: mouse4
|
condition: mouse4
|
||||||
double v:
|
double v:
|
||||||
impulse primary: voice spy
|
type: impulse
|
||||||
impulse secondary: voice help
|
primary: voice spy
|
||||||
|
secondary: voice help
|
||||||
condition: mouse4
|
condition: mouse4
|
||||||
|
|
||||||
# hold doubles
|
# hold doubles
|
||||||
double r:
|
double r:
|
||||||
primary:
|
|
||||||
hold: "class_action"
|
|
||||||
secondary:
|
|
||||||
hold: "reload"
|
|
||||||
condition: mouse4
|
condition: mouse4
|
||||||
cancel both: yes
|
cancel: both
|
||||||
|
type: hold
|
||||||
|
primary: +class_action
|
||||||
|
secondary: +reload
|
||||||
|
|
||||||
# other
|
# other
|
||||||
double =:
|
impulse =: kill
|
||||||
primary:
|
impulse -: explode
|
||||||
impulse: kill
|
|
||||||
secondary:
|
|
||||||
impulse: explode
|
|
||||||
condition: "-"
|
|
||||||
double q:
|
double q:
|
||||||
impulse primary: lastinv
|
type: impulse
|
||||||
impulse secondary:
|
primary: lastinv
|
||||||
|
secondary:
|
||||||
- "slot2"
|
- "slot2"
|
||||||
- "wait 10"
|
- "wait 10"
|
||||||
- "slot1"
|
- "slot1"
|
||||||
condition: mouse4
|
condition: mouse4
|
||||||
double ctrl:
|
double ctrl:
|
||||||
# I use shift to crouch
|
# I use shift to crouch
|
||||||
impulse primary:
|
type: impulse
|
||||||
voice yes
|
primary: voice yes
|
||||||
impulse secondary:
|
secondary: voice no
|
||||||
voice no
|
|
||||||
condition: mouse4
|
condition: mouse4
|
||||||
|
|
||||||
# toggle
|
# toggle
|
||||||
toggle capslock: voicerecord
|
toggle capslock: +voicerecord
|
||||||
|
|
||||||
# hold: null-cancelled movement, so hitting a while holding d causes
|
# hold: null-cancelled movement, so hitting a while holding d causes
|
||||||
# me to go left instead of stopping, or vice-versa.
|
# me to go left instead of stopping, or vice-versa.
|
||||||
|
@ -61,29 +60,28 @@ default:
|
||||||
press:
|
press:
|
||||||
- "-moveright"
|
- "-moveright"
|
||||||
- "+moveleft"
|
- "+moveleft"
|
||||||
- "alias maybeMoveLeft +moveleft"
|
- "alias maybe_move_left +moveleft"
|
||||||
release:
|
release:
|
||||||
- "-moveleft"
|
- "-moveleft"
|
||||||
- "maybeMoveRight"
|
- "maybe_move_right"
|
||||||
- "alias maybeMoveLeft "
|
- "unalias maybe_move_left"
|
||||||
hold d:
|
hold d:
|
||||||
press:
|
press:
|
||||||
- "-moveleft"
|
- "-moveleft"
|
||||||
- "+moveright"
|
- "+moveright"
|
||||||
- "alias maybeMoveRight +moveright"
|
- "alias maybe_move_right +moveright"
|
||||||
release:
|
release:
|
||||||
- "-moveright"
|
- "-moveright"
|
||||||
- "maybeMoveLeft"
|
- "maybe_move_left"
|
||||||
- "alias maybeMoveRight "
|
- "unalias maybe_move_right"
|
||||||
# This just stops an error message the first time you release
|
# This just stops an error message the first time you release
|
||||||
# either of 'a' or 'd'
|
# either of 'a' or 'd'
|
||||||
impulse maybeMoveLeft:
|
impulse maybe_move_left:
|
||||||
alias: yes
|
alias: yes
|
||||||
command: ""
|
command: ""
|
||||||
impulse maybeMoveRight:
|
impulse maybe_move_right:
|
||||||
alias: yes
|
alias: yes
|
||||||
command: ""
|
command: ""
|
||||||
|
|
||||||
# class action is something useful for each class,
|
# class action is something useful for each class,
|
||||||
# like destroying and rebuilding a sentry for the engineer
|
# like destroying and rebuilding a sentry for the engineer
|
||||||
# this is just the default. I do a lot of hybridknight and
|
# this is just the default. I do a lot of hybridknight and
|
||||||
|
@ -99,16 +97,16 @@ default:
|
||||||
|
|
||||||
impulse load0:
|
impulse load0:
|
||||||
alias: yes
|
alias: yes
|
||||||
command: "load_itempreset 0"
|
command: "loadout a"
|
||||||
impulse load1:
|
impulse load1:
|
||||||
alias: yes
|
alias: yes
|
||||||
command: "load_itempreset 1"
|
command: "loadout b"
|
||||||
impulse load2:
|
impulse load2:
|
||||||
alias: yes
|
alias: yes
|
||||||
command: "load_itempreset 2"
|
command: "loadout c"
|
||||||
impulse load3:
|
impulse load3:
|
||||||
alias: yes
|
alias: yes
|
||||||
command: "load_itempreset 3"
|
command: "loadout d"
|
||||||
|
|
||||||
impulse INS:
|
impulse INS:
|
||||||
- "load0"
|
- "load0"
|
||||||
|
@ -122,8 +120,10 @@ default:
|
||||||
impulse END:
|
impulse END:
|
||||||
- "load3"
|
- "load3"
|
||||||
- "alias reload_presets load3"
|
- "alias reload_presets load3"
|
||||||
|
literal set_default_loadout:
|
||||||
|
alias reload_presets load0
|
||||||
impulse backspace:
|
impulse backspace:
|
||||||
"reload_presets"
|
reload_presets
|
||||||
|
|
||||||
engi:
|
engi:
|
||||||
hold class_action:
|
hold class_action:
|
||||||
|
@ -131,7 +131,6 @@ engi:
|
||||||
press:
|
press:
|
||||||
- destroy sentry
|
- destroy sentry
|
||||||
- build sentry
|
- build sentry
|
||||||
release: ""
|
|
||||||
|
|
||||||
medic:
|
medic:
|
||||||
# "Radar" feature: causes all teammates to autocall for medic, allowing
|
# "Radar" feature: causes all teammates to autocall for medic, allowing
|
||||||
|
@ -140,12 +139,21 @@ medic:
|
||||||
alias: yes
|
alias: yes
|
||||||
press: "hud_medicautocallersthreshold 150"
|
press: "hud_medicautocallersthreshold 150"
|
||||||
release: "hud_medicautocallersthreshold 75"
|
release: "hud_medicautocallersthreshold 75"
|
||||||
|
double e:
|
||||||
|
type: impulse
|
||||||
|
condition: mouse4
|
||||||
|
primary: voice medic
|
||||||
|
secondary: voice uber ready
|
||||||
|
|
||||||
soldier:
|
soldier:
|
||||||
double mouse1:
|
double mouse1:
|
||||||
hold primary:
|
type: hold
|
||||||
attack
|
condition: mouse4
|
||||||
hold secondary:
|
cancel: both
|
||||||
|
# normal firing
|
||||||
|
primary: +attack
|
||||||
|
# rocket jump
|
||||||
|
secondary:
|
||||||
press:
|
press:
|
||||||
- +attack
|
- +attack
|
||||||
- +duck
|
- +duck
|
||||||
|
@ -154,5 +162,3 @@ soldier:
|
||||||
- -attack
|
- -attack
|
||||||
- -duck
|
- -duck
|
||||||
- -jump
|
- -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 copy import deepcopy
|
||||||
|
from tfscript.tftypes import Double
|
||||||
|
|
||||||
def makeCFG(cfg, default=False):
|
def makeCFG(bindList, default=False):
|
||||||
global bindOrAlias
|
|
||||||
global condDict
|
|
||||||
global defaultDict
|
|
||||||
|
|
||||||
bindOrAlias = "bind"
|
|
||||||
if default:
|
if default:
|
||||||
# Write to defaultDict instead of condDict
|
# Write to defaultDict instead of condDict
|
||||||
condDict = defaultDict
|
Double.condDict = Double.defaultDict
|
||||||
else:
|
else:
|
||||||
condDict = deepcopy(defaultDict)
|
Double.condDict = deepcopy(Double.defaultDict)
|
||||||
|
|
||||||
ret = ''
|
ret = ''
|
||||||
for key, data in cfg.items():
|
|
||||||
isAlias = False
|
for bind in bindList:
|
||||||
if "alias" in data:
|
ret += bind.toTF2()
|
||||||
isAlias = data.pop("alias")
|
|
||||||
if isAlias:
|
|
||||||
bindOrAlias = "alias"
|
|
||||||
else:
|
|
||||||
bindOrAlias = "bind"
|
|
||||||
ret += branch(key, data)
|
|
||||||
|
|
||||||
# Doubles are weird. All of the toggles got put into a dictionary.
|
# 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
|
# 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
|
# ==, and by extension !=, does in fact check
|
||||||
# for dictionary equality in keys and values
|
# 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"])
|
onCondPress = ';'.join(toggles["change_keys"])
|
||||||
onCondRelease = ';'.join(toggles["restore_keys"])
|
onCondRelease = ';'.join(toggles["restore_keys"])
|
||||||
ret += f'alias +{key}_toggles "{onCondPress}"\n' +\
|
ret += (
|
||||||
f'alias -{key}_toggles "{onCondRelease}"\n' +\
|
f'alias +{key}_toggles "{onCondPress}"\n'
|
||||||
f'{bindOrAlias} {key} "+{key}_toggles"\n'
|
+ f'alias -{key}_toggles "{onCondRelease}"\n'
|
||||||
|
+ f'bind {key} "+{key}_toggles"\n'
|
||||||
del condDict # free deep copy
|
)
|
||||||
|
|
||||||
return ret
|
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
|
Command line module for making Team Fortress 2 macro scripts from
|
||||||
YAML source code.
|
YAML source code.
|
||||||
"""
|
'''
|
||||||
|
|
||||||
__all__ = ['parseFile']
|
__all__ = ['parseFile']
|
||||||
__author__ = "Nicholas Hope <tfscript@nickhope.world"
|
__author__ = 'Nicholas Hope <tfscript@nickhope.world'
|
||||||
__date__ = "26 August 2022"
|
__date__ = '26 August 2022'
|
||||||
__version__ = "1.0"
|
__version__ = '1.0'
|
||||||
__copyright__ = "Copyright © 2022 Nicholas Hope. See LICENSE for details."
|
__copyright__ = 'Copyright © 2022 Nicholas Hope. See LICENSE for details.'
|
||||||
|
|
||||||
# Standard libraries
|
# Standard libraries
|
||||||
from sys import stderr
|
from sys import stderr
|
||||||
|
@ -27,48 +27,53 @@ except ModuleNotFoundError:
|
||||||
|
|
||||||
# Local libraries
|
# Local libraries
|
||||||
import tfscript
|
import tfscript
|
||||||
from tfscript import verify
|
from tfscript import verify, writing, makeCFG
|
||||||
from tfscript import writing
|
|
||||||
|
|
||||||
args = {}
|
args = {}
|
||||||
targetDir = ""
|
targetDir = ''
|
||||||
|
|
||||||
def parseFile(inputFile) -> (dict, dict):
|
def parseFile(inputFile) -> (dict, dict):
|
||||||
"""Parse, verify, and do the conversion."""
|
'''Parse, verify, and do the conversion.'''
|
||||||
config = yaml.safe_load(inputFile)
|
config = yaml.safe_load(inputFile)
|
||||||
|
|
||||||
# See verify.py
|
# See verify.py
|
||||||
config, aliases = verify.verifyConfig(config)
|
config, defaults = verify.verifyConfig(config)
|
||||||
if "errors" in config:
|
if 'warnings' in config:
|
||||||
for cclass, messages in config["errors"].items():
|
for cclass, messages in config.pop('warnings').items():
|
||||||
print(f"Error in {cclass}:")
|
print(f'Warning in {cclass}:', file=stderr)
|
||||||
for msg in messages:
|
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
|
return None, None
|
||||||
else:
|
else:
|
||||||
return config, aliases
|
return config, defaults
|
||||||
|
|
||||||
def parseConfig(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 args
|
||||||
global targetDir
|
global targetDir
|
||||||
|
|
||||||
if isdir(targetDir) == False:
|
if isdir(targetDir) == False:
|
||||||
mkdir(targetDir)
|
mkdir(targetDir)
|
||||||
if args.debug:
|
if args.debug:
|
||||||
print( f"DEBUG: Created directory {targetDir}", file=stderr)
|
print( f'DEBUG: Created directory {targetDir}', file=stderr)
|
||||||
|
|
||||||
tempsAndReals = {}
|
tempsAndReals = {}
|
||||||
|
|
||||||
if defaults is not None:
|
if defaults is not None:
|
||||||
stringToWrite = tfscript.makeCFG(defaults, default=True)
|
config.update({'default': defaults})
|
||||||
replaceDict = writing.writeOutput(stringToWrite, "default", args)
|
|
||||||
tempsAndReals.update(replaceDict)
|
|
||||||
|
|
||||||
for currentClass in config:
|
for class_ in config:
|
||||||
classDict = config[currentClass]
|
stringToWrite = makeCFG(
|
||||||
stringToWrite = tfscript.makeCFG(classDict)
|
config[class_],
|
||||||
replaceDict = writing.writeOutput(stringToWrite, currentClass, args)
|
default=(class_ == 'default')
|
||||||
|
)
|
||||||
|
replaceDict = writing.writeOutput(stringToWrite, class_, args)
|
||||||
tempsAndReals.update(replaceDict)
|
tempsAndReals.update(replaceDict)
|
||||||
|
|
||||||
return tempsAndReals
|
return tempsAndReals
|
||||||
|
@ -76,56 +81,56 @@ def parseConfig(config, defaults):
|
||||||
def parseCLI():
|
def parseCLI():
|
||||||
# Handle command line
|
# Handle command line
|
||||||
parser = argparse.ArgumentParser(
|
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',
|
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',
|
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',
|
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,
|
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
|
# positional argument: first non-hyphenated argument is input file
|
||||||
parser.add_argument( 'infile', type=argparse.FileType('r'),
|
parser.add_argument( 'infile', type=argparse.FileType('r'),
|
||||||
help='File containing YAML to convert.')
|
help='File containing YAML to convert.')
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def getTargetDir(systemName):
|
def getTargetDir(systemName):
|
||||||
if systemName == "Darwin":
|
if systemName == 'Darwin':
|
||||||
if float( '.'.join( GetOSRelease().split('.')[0:2] ) ) >= 10.15:
|
if float( '.'.join( GetOSRelease().split('.')[0:2] ) ) >= 10.15:
|
||||||
warn(
|
warn(
|
||||||
"As of macOS Catalina (v10.15), 32-bit applications "
|
'As of macOS Catalina (v10.15), 32-bit applications '
|
||||||
"like TF2 do not run. tfscript will run, but you can't run TF2 "
|
'like TF2 do not run. tfscript will run, but you can\'t run TF2 '
|
||||||
"on this system",
|
'on this system',
|
||||||
category=RuntimeWarning )
|
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
|
# oh god why do we have to use the registry
|
||||||
accessReg = ConnectRegistry(None, HKEY_LOCAL_MACHINE)
|
accessReg = ConnectRegistry(None, HKEY_LOCAL_MACHINE)
|
||||||
accessKey = OpenKey(accessReg, "SOFTWARE\\WOW6432Node\\Valve\\Steam")
|
accessKey = OpenKey(accessReg, 'SOFTWARE\\WOW6432Node\\Valve\\Steam')
|
||||||
keyNum = 0
|
keyNum = 0
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
accessSubkeyName, data, _ = EnumValue(accessKey, keyNum)
|
accessSubkeyName, data, _ = EnumValue(accessKey, keyNum)
|
||||||
if accessSubkeyName == "InstallPath":
|
if accessSubkeyName == 'InstallPath':
|
||||||
return data
|
return data
|
||||||
except EnvironmentError:
|
except EnvironmentError:
|
||||||
break
|
break
|
||||||
keyNum += 1
|
keyNum += 1
|
||||||
return None
|
return None
|
||||||
|
|
||||||
elif systemName == "Linux":
|
elif systemName == 'Linux':
|
||||||
return expanduser("~/.local/Steam")
|
return expanduser('~/.local/Steam')
|
||||||
|
|
||||||
elif systemName == "Java":
|
elif systemName == 'Java':
|
||||||
warn("Java-based OSes are not supported yet by tfscript.", category=RuntimeWarning)
|
warn('Java-based OSes are not supported yet by tfscript.', category=RuntimeWarning)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
""" Command line interface. """
|
''' Command line interface. '''
|
||||||
global args
|
global args
|
||||||
global targetDir
|
global targetDir
|
||||||
parser = parseCLI()
|
parser = parseCLI()
|
||||||
|
@ -139,11 +144,11 @@ def main() -> int:
|
||||||
targetDir = getTargetDir(systemName)
|
targetDir = getTargetDir(systemName)
|
||||||
if targetDir is not None:
|
if targetDir is not None:
|
||||||
# Supported OS: add steamapps path
|
# 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:
|
elif args.force:
|
||||||
# Unsupported OS but -f specified
|
# Unsupported OS but -f specified
|
||||||
if args.debug:
|
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 = '.'
|
targetDir = '.'
|
||||||
else:
|
else:
|
||||||
# Unsupported OS and not forced to continue
|
# Unsupported OS and not forced to continue
|
||||||
|
@ -160,5 +165,5 @@ def main() -> int:
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
exit(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."""
|
"""Verify all the things that could go wrong."""
|
||||||
from copy import deepcopy
|
|
||||||
|
from tfscript import tftypes
|
||||||
|
|
||||||
def verifyConfig(cfg: dict) -> (dict, dict):
|
def verifyConfig(cfg: dict) -> (dict, dict):
|
||||||
verifiedConfig = {}
|
verifiedConfig = {}
|
||||||
|
|
||||||
# Do defaults first
|
|
||||||
errors = {}
|
errors = {}
|
||||||
|
warnings = {}
|
||||||
|
|
||||||
defaults = None
|
# Do defaults first
|
||||||
|
defaults = []
|
||||||
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} )
|
|
||||||
|
|
||||||
classList = [
|
classList = [
|
||||||
"scout",
|
'default',
|
||||||
"soldier",
|
'scout',
|
||||||
"pyro",
|
'soldier',
|
||||||
("demo","demoman"),
|
'pyro',
|
||||||
("engi","engineer"),
|
('demo','demoman'),
|
||||||
("heavy","heavyweapons"),
|
('engi','engineer'),
|
||||||
"medic",
|
('heavy','heavyweapons'),
|
||||||
"sniper",
|
'medic',
|
||||||
"spy"
|
'sniper',
|
||||||
|
'spy'
|
||||||
]
|
]
|
||||||
|
|
||||||
for cclass in classList:
|
for isclass, class_ in enumerate(classList):
|
||||||
|
|
||||||
classCFG = None
|
classCFG = None
|
||||||
className = cclass
|
className = class_
|
||||||
if isinstance(cclass, str) and cclass in cfg:
|
|
||||||
classCFG = cfg.pop(cclass)
|
if isinstance(class_, str) and class_ in cfg:
|
||||||
elif isinstance(cclass, tuple):
|
classCFG = cfg.pop(class_)
|
||||||
for tupClass in cclass:
|
elif isinstance(class_, tuple):
|
||||||
|
for tupClass in class_:
|
||||||
if tupClass in cfg:
|
if tupClass in cfg:
|
||||||
classCFG = cfg.pop(tupClass)
|
classCFG = cfg.pop(tupClass)
|
||||||
className = cclass[0]
|
className = class_[0]
|
||||||
break
|
break
|
||||||
if classCFG is None:
|
if classCFG is None:
|
||||||
# Invalid class, this gets caught later.
|
# Invalid class, this gets caught later.
|
||||||
# It may be less efficient this way, but
|
# It may be less efficient this way, but
|
||||||
# it makes for more descriptive error messages
|
# it makes for more descriptive error messages
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
classBinds = []
|
||||||
errMessages = []
|
errMessages = []
|
||||||
|
warnMessages = []
|
||||||
for key, data in classCFG.items():
|
for key, data in classCFG.items():
|
||||||
isAlias = False
|
bind = tftypes.Bind(key, data)
|
||||||
if "alias" in data:
|
|
||||||
isAlias = data["alias"]
|
bind = bind.toTargetType()
|
||||||
if not isinstance(isAlias, bool):
|
if isclass:
|
||||||
errMessages.append(f'"alias" field in "{key}" makes no sense: "{isAlias}"')
|
classBinds.append(bind)
|
||||||
errMessages.extend( validBind(key, data, alias = isAlias) )
|
else:
|
||||||
|
defaults.append(bind)
|
||||||
|
|
||||||
|
errMessages.extend(bind.errors)
|
||||||
|
warnMessages.extend(bind.warnings)
|
||||||
|
|
||||||
if len(errMessages) > 0:
|
if len(errMessages) > 0:
|
||||||
errors.update( {className: errMessages} )
|
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
|
# Turn list into only strings by expanding tuples
|
||||||
for i, clss in enumerate(classList):
|
for i, class_ in enumerate(classList):
|
||||||
if isinstance(clss, tuple):
|
if isinstance(class_, tuple):
|
||||||
classList.insert(i+1, clss[1])
|
classList.insert(i+1, class_[1])
|
||||||
classList.insert(i+1, clss[0])
|
classList.insert(i+1, class_[0])
|
||||||
classList.pop(i)
|
classList.pop(i)
|
||||||
|
|
||||||
globalErrors = []
|
globalErrors = []
|
||||||
|
@ -78,189 +81,15 @@ def verifyConfig(cfg: dict) -> (dict, dict):
|
||||||
globalErrors.append(f'Conflicting names for section: "{remainingClass}" and "{otherName}"')
|
globalErrors.append(f'Conflicting names for section: "{remainingClass}" and "{otherName}"')
|
||||||
|
|
||||||
if len(globalErrors) > 0:
|
if len(globalErrors) > 0:
|
||||||
errors.update({"file": globalErrors})
|
errors.update({'file': globalErrors})
|
||||||
|
|
||||||
if len(errors) > 0:
|
if len(errors) > 0:
|
||||||
verifiedConfig.update({"errors": errors})
|
verifiedConfig.update({'errors': errors})
|
||||||
|
if len(warnings) > 0:
|
||||||
|
verifiedConfig.update({'warnings': warnings})
|
||||||
|
|
||||||
return verifiedConfig, defaults
|
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):
|
def findTwin(className):
|
||||||
classDict = {
|
classDict = {
|
||||||
"demo": "demoman",
|
"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