Compare commits

..

No commits in common. "main" and "class_based_refactor" have entirely different histories.

8 changed files with 203 additions and 245 deletions

View File

@ -29,19 +29,8 @@ default:
condition: mouse4
cancel: both
type: hold
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"
primary: +class_action
secondary: +reload
# other
impulse =: kill
@ -65,10 +54,6 @@ 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:
@ -89,11 +74,6 @@ 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:
@ -102,6 +82,18 @@ 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
@ -130,7 +122,7 @@ default:
- "alias reload_presets load3"
literal set_default_loadout:
alias reload_presets load0
impulse alt:
impulse backspace:
reload_presets
engi:
@ -139,32 +131,6 @@ 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
@ -178,3 +144,21 @@ 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 = tfscript
packages = src/tfscript

View File

@ -91,16 +91,6 @@ 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.')
@ -110,13 +100,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')
if 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')
@ -124,23 +114,17 @@ def getTargetDir(systemName):
while True:
try:
accessSubkeyName, data, _ = EnumValue(accessKey, keyNum)
except EnvironmentError:
break
else:
if accessSubkeyName == 'InstallPath':
return data
keyNum += 1
except EnvironmentError:
break
keyNum += 1
return None
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 == 'Linux':
return expanduser('~/.local/Steam')
if systemName == 'Java':
elif systemName == 'Java':
warn('Java-based OSes are not supported yet by tfscript.', category=RuntimeWarning)
return None

View File

@ -1,4 +1,6 @@
validKeyList = {
import re
validKeyList = [
# top row
'escape', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12',
# keyboard
@ -16,11 +18,11 @@ validKeyList = {
# arrows
'uparrow', 'downarrow',
'leftarrow', 'rightarrow'
}
]
popErrors = (AttributeError, KeyError, TypeError)
class ScriptBind(object):
class Bind(object):
'''
Parent class for all bind types.
Verifies key, creates local variables
@ -28,26 +30,26 @@ class ScriptBind(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 ScriptBind:
if type(self) is Bind:
# not using isinstance(), because all subclasses are also instances
# of bind.
return
@ -72,11 +74,15 @@ class ScriptBind(object):
self.instances[type(self)] = [self]
def verify(self):
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:
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)
@ -103,21 +109,6 @@ class ScriptBind(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
@ -137,22 +128,23 @@ class ScriptBind(object):
)
class Impulse(ScriptBind):
class Impulse(Bind):
def verify(self):
self.command: list = None
self.command = None
if not isinstance(self.fields, dict):
self.fields = {'command': self.fields}
try:
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.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'
@ -177,8 +169,11 @@ class Impulse(ScriptBind):
'secondary': 'slot2',
'melee': 'slot3'
}
# if is shortcut, change
cmd = simpleSCs.get(cmd, cmd)
try:
cmd = simpleSCs[cmd]
except KeyError:
# not a shortcut
pass
if cmd == 'voice':
cmd = 'voicemenu'
@ -190,8 +185,8 @@ class Impulse(ScriptBind):
elif cmd == 'loadout' and restOfCmd.isalpha():
cmd = 'load_itempreset'
try:
loadoutNum = ['a','b','c','d'].index(restOfCmd.lower())
restOfCmd = str(loadoutNum)
restOfCmd = restOfCmd.lower()
restOfCmd = str(['a','b','c','d'].index(restOfCmd))
except ValueError:
# not a load_itempreset shortcut
pass
@ -211,9 +206,9 @@ class Impulse(ScriptBind):
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):
@ -228,32 +223,40 @@ class Impulse(ScriptBind):
'exit': '1 1',
'sentry': '2 0'
}
return buildingNums.get(building, building)
for shortBuild, num in buildingNums.items():
if building == shortBuild:
return num
class Hold(ScriptBind):
class Hold(Bind):
def verify(self):
self.press: list = None
self.release: list = None
self.press = None
self.release = None
if not isinstance(self.fields, dict):
self.fields = {'press': self.fields}
# verify press
try:
self.press = self.cmdListFrom('press')
if self.press is None:
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')
except popErrors:
self.press = None
except KeyError:
self.err('requires `press` field')
if self.press is None:
return
# verify release
try:
self.release = self.cmdListFrom('release')
if self.release is None:
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:
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 = []
@ -270,54 +273,55 @@ class Hold(ScriptBind):
# Making impulse instances from self.press and .release
# allows them to share the shortcuts
pressObj = Impulse('+' + holdStr, self.press)
pressObj = Impulse(f'+{holdStr}', self.press)
pressObj.alias = True
pressStr = pressObj.toTF2()
releaseObj = Impulse('-' + holdStr, self.release)
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
minuskey = '-' + self.key
self.key = '+' + self.key
code = (
return (
pressStr + releaseStr
+ f'{bindOrAlias} {self.key} "+{holdStr}"\n'
)
if self.alias:
code += f'alias {minuskey} "-{holdStr}"\n'
return code
class Toggle(ScriptBind):
class Toggle(Bind):
def verify(self):
self.on : list = None
self.off: list = None
self.on = None
self.off = None
if not isinstance(self.fields, dict):
self.fields = {'on': self.fields}
# verify on
try:
self.on = self.cmdListFrom('on')
if self.on is None:
self.err(f'`on` field must be string or list')
except popErrors:
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.cmdListFrom('off')
if self.off is None:
self.err(f'`off` field must be string or list')
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 = []
if self.on is None:
return
for cmd in self.on:
if cmd[0] == '+':
self.off.append('-' + cmd[1:])
@ -349,21 +353,26 @@ class Toggle(ScriptBind):
)
class Double(ScriptBind):
class Double(Bind):
defaultDict = {}
condDict = {}
bindNames = []
condDict = {}
bindNames = []
def verify(self):
self.primStr = f'{self.key}_primary'
self.secondStr = f'{self.key}_secondary'
self.isToggle = False
self.cancelBoth = False
self.primary = None
self.primStr = f'{self.key}_primary'
self.primary: ScriptBind = None
self.secondary: ScriptBind = None
self.condition: str = None
self.type: str = None
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:
@ -373,12 +382,15 @@ class Double(ScriptBind):
except popErrors:
self.err('requires `condition` field')
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}"'
)
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:
@ -393,57 +405,56 @@ class Double(ScriptBind):
return
# cancel mode, must happend after type has been inferred
cancel = self.optional('cancel', default='released')
try:
cancel = self.fields.pop('cancel')
if not isinstance(cancel, str):
self.err(f'`cancel` field must be "released" or "both"')
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.cancelBoth = False
else:
self.err(
'`cancel` field must be "released"'
+ f' or "both", not "{cancel}"'
)
if cancel == 'both':
if self.type == 'hold':
self.cancelBoth = True
else:
self.err(
'`cancel` field only affects "hold", '
+ f'not "{self.type}"'
)
try:
self.primary = self.getSection('primary', self.primStr)
elif cancel != 'released':
self.err(
'`cancel` field must be "released" '
+ f'or "both", not "{cancel}"'
)
except popErrors:
self.primary = None
cancel = 'released'
# primary action
try:
self.secondary = self.getSection('secondary', self.secondStr)
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.secondary = None
self.err('requires `primary` field')
if self.primary is self.secondary is None:
self.err('has neither primary nor secondary')
# secondary action
try:
altSection = self.fields.pop('secondary')
altBind = Bind(f'{self.type} {self.secondStr}', altSection)
altBind = altBind.toTargetType()
def getSection(self, popName, key, /) -> ScriptBind:
section = self.fields.pop(popName)
bind = ScriptBind(f'{self.type} {key}', section)
bind = bind.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')
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.
@ -459,8 +470,8 @@ class Double(ScriptBind):
mainCode, altCode = self.getCancelCode(mainCode, altCode)
if self.type == 'hold':
self.primStr = '+hold_' + self.primStr
self.secondStr = '+hold_' + self.secondStr
self.primStr = '+' + self.primStr
self.secondStr = '+' + self.secondStr
shiftStr = f'shift_{self.key}'
shiftCode = self.getChangeCode(shiftStr)
@ -514,13 +525,11 @@ class Double(ScriptBind):
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]
@ -530,12 +539,8 @@ class Double(ScriptBind):
allCancels = mainMinusSet | altMinusSet
allCancelStr = ';'.join(allCancels)
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}"')
altLines[1] = altLines[1][:-1] + f';{allCancelStr}"'
mainLines[1] = mainLines[1][:-1] + f';{allCancelStr}"'
return (
'\n'.join(mainLines) + '\n',
@ -543,7 +548,7 @@ class Double(ScriptBind):
)
class Repeat(ScriptBind):
class Repeat(Bind):
def verify(self):
self.interval = None
self.command = None
@ -573,7 +578,7 @@ class Repeat(ScriptBind):
return f'// repeat {self.key}\n'
class Literal(ScriptBind):
class Literal(Bind):
def verify(self):
self.text = ''
self.run = False
@ -617,23 +622,8 @@ class Literal(ScriptBind):
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
ScriptBind.bindTypes = ScriptBind.__subclasses__()
Double.bindNames = [ bind.__name__.lower() for bind in ScriptBind.bindTypes ]
Bind.bindTypes = Bind.__subclasses__()
Double.bindNames = [ bind.__name__.lower() for bind in Bind.bindTypes ]

View File

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