tfscript/src/tfscript/tftypes.py

499 lines
16 KiB
Python

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'
]
class bind(object):
'''
Parent class for all bind types.
Verifies key, creates local variables
'''
bindTypes = []
instances = {}
def __init__(self, key, fields):
self.alias = False
self.key = key
self.fields = fields
self.errors = []
self.targetType = None
# 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.errors.append(f'extra fields in "{self.key}":')
if isinstance(self.fields, str):
# iterating over a str returns each character,
# making meaningless error messages
self.errors.append(f' "{self.fields}"')
else:
for field in self.fields:
self.errors.append(f' "{field}"')
elif 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:
typeName, self.key = self.key.split(' ', 1)
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
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 (KeyError, AttributeError, TypeError):
self.alias = False
if (not self.alias) and (self.key not in validKeyList):
self.errors.append(f'invalid key name: "{self.key}"')
for type_ in self.bindTypes:
if typeName == type_.__name__:
self.targetType = type_
break
if self.targetType is None:
self.errors.append(f'could not find type in "{self.key}"')
def toTargetType(self):
if self.targetType is None:
# do nothing
bind = self
else:
# cast to targetType, extend errors
bind = self.targetType(self.key, self.fields)
bind.errors.extend(self.errors)
bind.alias = self.alias
return bind
def err(self, message):
self.errors.append(f'{type(self).__name__} "{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')
except KeyError:
self.err('requires `command` field')
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
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 == 'load_itempreset' and restOfCmd.isalpha():
try:
restOfCmd = restOfCmd.lower()
restOfCmd = ['a','b','c','d'].index(restOfCmd)
except ValueError:
# not a load_itempreset shortcut
pass
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')
except KeyError:
self.err('requires `press` field')
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
# verify release
try:
self.release = self.fields.pop('release')
except KeyError:
# 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:])
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
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')
except KeyError:
self.err('requires `on` field')
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
# verify off
try:
self.off = self.fields.pop('off')
except KeyError:
# 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:])
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 = None
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 trailing " and \n
:-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 = {}
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.cancel = 'released'
# toggler
try:
self.condition = self.fields.pop('condition')
if self.condition not in validKeyList:
self.err(f'has invalid `condition` field: "{self.condition}"')
except KeyError:
self.err('requires `condition` field')
if 'toggle' in self.fields:
self.isToggle = self.fields.pop('toggle')
if not isinstance(self.isToggle, bool):
self.err(f'`toggle` field should be "yes" or "no", not "{self.isToggle}"')
# type
try:
self.type = self.fields.pop('type')
if self.type not in [ type_.__name__ for type_ in self.bindTypes ]:
# catastrophic: invalid type
self.err(f'has invalid type: "{self.type}"')
return
except KeyError:
# catastrophic: no type given
self.err('requires `type` field')
return
# cancel mode
if 'cancel' in self.fields:
self.cancel = self.fields.pop('cancel')
if self.cancel in ('released', 'both'):
if self.cancel == 'both' and self.type != 'hold':
self.err(f'`cancel` field only affects "hold", not "{self.type}"')
elif isinstance(self.cancel, str):
self.err(f'`cancel` field must be "released" or "both", not "{self.cancel}"')
else:
self.err(f'`cancel` field must be argument of "released" or "both"')
# primary action
try:
mainSection = self.fields.pop('primary')
mainAction = bind(f'{self.type} {self.primStr}', mainSection)
self.primary = mainAction.toTargetType()
except KeyError:
self.err('requires `primary` field')
# secondary action
try:
altSection = self.fields.pop('secondary')
altBind = bind(f'{self.type} {self.secondStr}', altSection)
self.secondary = altBind.toTargetType()
except KeyError:
self.err('requires `secondary` field')
def toTF2(self) -> str:
if self.alias:
bindOrAlias = 'alias'
else:
bindOrAlias = 'bind'
# Get code for primary and secondary actions
self.primary.alias = True
mainCode = self.primary.toTF2()
self.secondary.alias = True
altCode = self.secondary.toTF2()
# Make code to toggle between the two actions
pShiftStr = f'+shift_{self.key}'
mShiftStr = f'-shift_{self.key}'
if self.cancel == 'both':
# 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 quote
mainMinusStr = mainMinusLine.split(' ', 2)[2][1:]
altLines = altCode.splitlines()
altMinusLine = altLines[1]
# same as above
altMinusStr = altMinusLine.split(' ', 2)[2][1:]
mainLines[1] = mainLines[1][:-1] + f';{altMinusStr}'
altLines[1] = altLines[1][:-1] + f';{mainMinusStr}'
mainCode = '\n'.join(mainLines) + '\n'
altCode = '\n'.join(altLines) + '\n'
if self.type == 'hold':
self.primStr = '+' + self.primStr
self.secondStr = '+' + self.secondStr
result = mainCode + altCode +\
f'alias {pShiftStr} "{bindOrAlias} {self.key} {self.primStr}"\n' +\
f'alias {mShiftStr} "{bindOrAlias} {self.key} {self.secondStr}"\n'+\
f'{bindOrAlias} {self.key} "{self.primStr}"\n'
try:
# If the condition key (like 'mouse4') already has toggles,
# just append another toggle string
changes = self.condDict[self.condition]['change_keys']
restores = self.condDict[self.condition]['restore_keys']
if pShiftStr not in changes:
# not already in changes
changes.append(pShiftStr)
restores.append(mShiftStr)
except KeyError:
# If the condition key doesn't already exist, make it
self.condDict.update( {
self.condition: {
'change_keys': [ pShiftStr ],
'restore_keys': [ mShiftStr ],
'alias': self.alias
}
} )
return result
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:
self.err('requires interval')
except ValueError:
self.err(f'has invalid number of ticks: "{self.interval}"')
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 KeyError:
self.err('requires command')
def toTF2(self) -> str:
# commented-out placeholder
return f'// repeat {self.key}\n'
# This is at the bottom because it has to happen after
# all inheritances have been completed
bind.bindTypes = bind.__subclasses__()