Compare commits

...

14 Commits

8 changed files with 245 additions and 203 deletions

View File

@ -29,8 +29,19 @@ default:
condition: mouse4
cancel: both
type: hold
primary: +class_action
secondary: +reload
primary: +reload
secondary: +class_action
# 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
# mantread soldier so having a melee/main weapon switch bind
# is useful
hold class_action:
alias: yes
press:
- "slot3"
- "wait 10"
- "slot1"
# other
impulse =: kill
@ -54,6 +65,10 @@ default:
# toggle
toggle capslock: +voicerecord
hold space:
- "+duck"
- "+jump"
# hold: null-cancelled movement, so hitting a while holding d causes
# me to go left instead of stopping, or vice-versa.
hold a:
@ -74,6 +89,11 @@ default:
- "-moveright"
- "maybe_move_left"
- "unalias maybe_move_right"
# both do something different for engi
bind mouse5: +jump
bind 4: slot4
# This just stops an error message the first time you release
# either of 'a' or 'd'
impulse maybe_move_left:
@ -82,18 +102,6 @@ default:
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
# mantread soldier so having a melee/main weapon switch bind
# is useful
hold class_action:
alias: yes
press:
- "slot3"
- "wait 10"
- "slot1"
release: ""
impulse load0:
alias: yes
@ -122,7 +130,7 @@ default:
- "alias reload_presets load3"
literal set_default_loadout:
alias reload_presets load0
impulse backspace:
impulse alt:
reload_presets
engi:
@ -131,6 +139,32 @@ engi:
press:
- destroy sentry
- build sentry
double 4:
type: double
condition: mouse4
primary:
type: impulse
condition: mouse5
primary:
- destroy dispenser
- build dispenser
secondary:
- destroy exit
- build exit
secondary:
type: impulse
condition: mouse5
primary:
- destroy entrance
- build entrance
secondary:
- destroy entrance
- build entrance
soldier:
# I want control over how high I jump
bind space: +jump
medic:
# "Radar" feature: causes all teammates to autocall for medic, allowing
@ -144,21 +178,3 @@ medic:
condition: mouse4
primary: voice medic
secondary: voice uber ready
soldier:
double mouse1:
type: hold
condition: mouse4
cancel: both
# normal firing
primary: +attack
# rocket jump
secondary:
press:
- +attack
- +duck
- +jump
release:
- -attack
- -duck
- -jump

View File

@ -3,4 +3,4 @@ name = tfscript
version = 1.0
[options]
packages = src/tfscript
packages = tfscript

View File

@ -91,6 +91,16 @@ def parseCLI():
help='Force tfscript to continue until catastrophic failure')
parser.add_argument( '-D', '--directory', action='store', type=str,
help='Change output directory')
# warnings
parseWarnNames = [
'implicit-release', 'implicit-off',
'implicit-primary', 'implicit-secondary',
'implicit'
]
for warnName in parseWarnNames:
splitWarnName = ' '.join(warnName.split('-'))
parser.add_argument( '-W' + warnName, action='store_true',
help=f'Generate warning on {splitWarnName} creation')
# positional argument: first non-hyphenated argument is input file
parser.add_argument( 'infile', type=argparse.FileType('r'),
help='File containing YAML to convert.')
@ -100,13 +110,13 @@ def getTargetDir(systemName):
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':
if 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')
@ -114,17 +124,23 @@ def getTargetDir(systemName):
while True:
try:
accessSubkeyName, data, _ = EnumValue(accessKey, keyNum)
if accessSubkeyName == 'InstallPath':
return data
except EnvironmentError:
break
keyNum += 1
else:
if accessSubkeyName == 'InstallPath':
return data
keyNum += 1
return None
elif systemName == 'Linux':
return expanduser('~/.local/Steam')
if systemName == 'Linux':
homedir = expanduser('~')
for potentialdir in ['.steam/steam', '.local/share/Steam']:
fullTargetPath = normpath(f'{homedir}/{potentialdir}')
if isdir(fullTargetPath):
return fullTargetPath
return None
elif systemName == 'Java':
if systemName == 'Java':
warn('Java-based OSes are not supported yet by tfscript.', category=RuntimeWarning)
return None

View File

@ -1,6 +1,4 @@
import re
validKeyList = [
validKeyList = {
# top row
'escape', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12',
# keyboard
@ -18,11 +16,11 @@ validKeyList = [
# arrows
'uparrow', 'downarrow',
'leftarrow', 'rightarrow'
]
}
popErrors = (AttributeError, KeyError, TypeError)
class Bind(object):
class ScriptBind(object):
'''
Parent class for all bind types.
Verifies key, creates local variables
@ -30,26 +28,26 @@ class Bind(object):
bindTypes = []
instances = {}
def __init__(self, key='', fields={}, *, parent=None):
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.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.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:
if type(self) is ScriptBind:
# not using isinstance(), because all subclasses are also instances
# of bind.
return
@ -74,15 +72,11 @@ class Bind(object):
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
self.alias = self.optional('alias', default=False)
if not isinstance(self.alias, bool):
self.err(
f'`alias` should be "yes" or "no", not "{self.alias}"'
)
try:
typeName, self.key = self.key.split(' ', 1)
@ -109,6 +103,21 @@ class Bind(object):
if (not self.alias) and (self.key not in validKeyList):
self.errors.append(f'invalid key name: "{self.key}"')
def optional(self, name, /,*, default=None):
try:
return self.fields.pop(name)
except popErrors:
return default
def cmdListFrom(self, name, /,*, default=None):
result = self.fields.pop(name)
if isinstance(result, str):
return result.split(';')
elif isinstance(result, list):
return result
else:
return default
def toTargetType(self):
if self.TargetType is None:
# do nothing
@ -128,23 +137,22 @@ class Bind(object):
)
class Impulse(Bind):
class Impulse(ScriptBind):
def verify(self):
self.command = None
self.command: list = 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.command = self.cmdListFrom(
'command',
default=self.fields
)
if self.command is None:
self.err('`command` field must be string or list')
except popErrors:
self.err('requires `command` field')
def toTF2(self) -> str:
if self.alias:
bindOrAlias = 'alias'
@ -169,11 +177,8 @@ class Impulse(Bind):
'secondary': 'slot2',
'melee': 'slot3'
}
try:
cmd = simpleSCs[cmd]
except KeyError:
# not a shortcut
pass
# if is shortcut, change
cmd = simpleSCs.get(cmd, cmd)
if cmd == 'voice':
cmd = 'voicemenu'
@ -185,8 +190,8 @@ class Impulse(Bind):
elif cmd == 'loadout' and restOfCmd.isalpha():
cmd = 'load_itempreset'
try:
restOfCmd = restOfCmd.lower()
restOfCmd = str(['a','b','c','d'].index(restOfCmd))
loadoutNum = ['a','b','c','d'].index(restOfCmd.lower())
restOfCmd = str(loadoutNum)
except ValueError:
# not a load_itempreset shortcut
pass
@ -206,9 +211,9 @@ class Impulse(Bind):
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'),
('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):
@ -223,40 +228,32 @@ class Impulse(Bind):
'exit': '1 1',
'sentry': '2 0'
}
for shortBuild, num in buildingNums.items():
if building == shortBuild:
return num
return buildingNums.get(building, building)
class Hold(Bind):
class Hold(ScriptBind):
def verify(self):
self.press = None
self.release = None
self.press: list = None
self.release: list = 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.press = self.cmdListFrom('press')
if self.press is None:
self.err('`press` field must be string or list')
self.press = None
except KeyError:
except popErrors:
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.release = self.cmdListFrom('release')
if self.release is None:
self.err('`release` field must be string or list')
self.release = None
except popErrors:
if self.press is None:
return
self.warn('has no `release`, creating one')
# no release specified, do -action for each item in press
self.release = []
@ -273,55 +270,54 @@ class Hold(Bind):
# Making impulse instances from self.press and .release
# allows them to share the shortcuts
pressObj = Impulse(f'+{holdStr}', self.press)
pressObj = Impulse('+' + holdStr, self.press)
pressObj.alias = True
pressStr = pressObj.toTF2()
releaseObj = Impulse(f'-{holdStr}', self.release)
releaseObj = Impulse('-' + holdStr, self.release)
releaseObj.alias = True
releaseStr = releaseObj.toTF2()
if self.alias:
# if alias, do this to avoid activating
# and never deactivating
minuskey = '-' + self.key
self.key = '+' + self.key
return (
code = (
pressStr + releaseStr
+ f'{bindOrAlias} {self.key} "+{holdStr}"\n'
)
if self.alias:
code += f'alias {minuskey} "-{holdStr}"\n'
return code
class Toggle(Bind):
class Toggle(ScriptBind):
def verify(self):
self.on = None
self.off = None
self.on : list = None
self.off: list = 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.on = self.cmdListFrom('on')
if self.on is None:
self.err(f'`on` field must be string or list')
except popErrors:
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')
self.off = self.cmdListFrom('off')
if self.off is None:
self.err(f'`off` field must be string or list')
except popErrors:
# no off specified, do -action for each item in on
self.off = []
if self.on is None:
return
for cmd in self.on:
if cmd[0] == '+':
self.off.append('-' + cmd[1:])
@ -353,27 +349,22 @@ class Toggle(Bind):
)
class Double(Bind):
class Double(ScriptBind):
defaultDict = {}
condDict = {}
bindNames = []
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.primStr = f'{self.key}_primary'
self.secondStr = f'{self.key}_secondary'
self.isToggle = False
self.cancelBoth = False
self.primary: ScriptBind = None
self.secondary: ScriptBind = None
self.condition: str = None
self.type: str = None
# toggler
try:
self.condition = self.fields.pop('condition')
@ -382,15 +373,12 @@ class Double(Bind):
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
self.isToggle = self.optional('toggle', default=False)
if not isinstance(self.isToggle, bool):
self.err(
'`toggle` field should be "yes" or "no",'
+ f' not "{self.isToggle}"'
)
# type
try:
@ -405,56 +393,57 @@ class Double(Bind):
return
# cancel mode, must happend after type has been inferred
try:
cancel = self.fields.pop('cancel')
cancel = self.optional('cancel', default='released')
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':
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 must be "released" '
+ f'or "both", not "{cancel}"'
'`cancel` field only affects "hold",'
+ f' not "{self.type}"'
)
except popErrors:
cancel = 'released'
elif cancel == 'released':
self.cancelBoth = False
else:
self.err(
'`cancel` field must be "released"'
+ f' or "both", not "{cancel}"'
)
# 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
self.primary = self.getSection('primary', self.primStr)
except popErrors:
self.err('requires `primary` field')
self.primary = None
# 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
self.secondary = self.getSection('secondary', self.secondStr)
except popErrors:
self.err('requires `secondary` field')
self.secondary = None
if self.primary is self.secondary is None:
self.err('has neither primary nor secondary')
def getSection(self, popName, key, /) -> ScriptBind:
section = self.fields.pop(popName)
bind = ScriptBind(f'{self.type} {key}', section)
bind = bind.toTargetType()
bind.errors.remove(f'invalid key name: "{key}"')
self.prettifyList(bind.errors, key)
self.errors.extend(bind.errors)
self.prettifyList(bind.warnings, key)
self.warnings.extend(bind.warnings)
return bind
def prettifyList(self, strList, origStr):
repStr = ' '.join(origStr.split('_', 1))
for i, cmd in enumerate(strList):
strList[i] = cmd.replace(origStr, repStr)
def toTF2(self) -> str:
# Get code for primary and secondary actions.
@ -470,8 +459,8 @@ class Double(Bind):
mainCode, altCode = self.getCancelCode(mainCode, altCode)
if self.type == 'hold':
self.primStr = '+' + self.primStr
self.secondStr = '+' + self.secondStr
self.primStr = '+hold_' + self.primStr
self.secondStr = '+hold_' + self.secondStr
shiftStr = f'shift_{self.key}'
shiftCode = self.getChangeCode(shiftStr)
@ -525,11 +514,13 @@ class Double(Bind):
mainLines = mainCode.splitlines()
mainMinusLine = mainLines[1]
mainMinusName = mainMinusLine.split(' ')[1]
# second arg, without first or last quote
mainMinusStr = mainMinusLine.split(' ', 2)[2][1:-1]
altLines = altCode.splitlines()
altMinusLine = altLines[1]
altMinusName = altMinusLine.split(' ')[1]
# same as above
altMinusStr = altMinusLine.split(' ', 2)[2][1:-1]
@ -539,8 +530,12 @@ class Double(Bind):
allCancels = mainMinusSet | altMinusSet
allCancelStr = ';'.join(allCancels)
altLines[1] = altLines[1][:-1] + f';{allCancelStr}"'
mainLines[1] = mainLines[1][:-1] + f';{allCancelStr}"'
altMinusLineStart = ' '.join(altMinusLine.split(' ')[:2])
altLines[1] = altMinusLineStart + f' "{allCancelStr}"'
altLines.insert(3, f'alias -{self.secondStr} "{altMinusName}"')
mainMinusLineStart = ' '.join(mainMinusLine.split(' ')[:2])
mainLines[1] = mainMinusLineStart + f' "{allCancelStr}"'
mainLines.insert(3, f'alias -{self.primStr} "{mainMinusName}"')
return (
'\n'.join(mainLines) + '\n',
@ -548,7 +543,7 @@ class Double(Bind):
)
class Repeat(Bind):
class Repeat(ScriptBind):
def verify(self):
self.interval = None
self.command = None
@ -578,7 +573,7 @@ class Repeat(Bind):
return f'// repeat {self.key}\n'
class Literal(Bind):
class Literal(ScriptBind):
def verify(self):
self.text = ''
self.run = False
@ -622,8 +617,23 @@ class Literal(Bind):
result += f'\n{self.key}'
return result + '\n'
class Bind(ScriptBind):
def verify(self):
# just set self.text, and if it's not a string, make an error
self.text = self.fields
if isinstance(self.text, str):
# clear self.fields to assure no "extra field" warnings
self.fields = {}
else:
# not passed string ==> error
self.err('argument must be a string')
def toTF2(self) -> str:
return f'bind {self.key} {self.text}\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 ]
ScriptBind.bindTypes = ScriptBind.__subclasses__()
Double.bindNames = [ bind.__name__.lower() for bind in ScriptBind.bindTypes ]

View File

@ -47,7 +47,7 @@ def verifyConfig(cfg: dict) -> (dict, dict):
errMessages = []
warnMessages = []
for key, data in classCFG.items():
bind = tftypes.Bind(key, data)
bind = tftypes.ScriptBind(key, data)
bind = bind.toTargetType()
if isclass: