199 lines
6.7 KiB
Python
199 lines
6.7 KiB
Python
"""
|
|
Command line module for making Team Fortress 2 macro scripts from
|
|
YAML source code.
|
|
"""
|
|
|
|
__all__ = ['parseFile']
|
|
__author__ = "Nicholas Hope <tfscript@nickhope.world"
|
|
__date__ = "26 August 2022"
|
|
__version__ = "1.0"
|
|
__copyright__ = "Copyright © 2022 Nicholas Hope. See LICENSE for details."
|
|
|
|
# Standard libraries
|
|
import sys
|
|
import os
|
|
import argparse
|
|
from tempfile import NamedTemporaryFile
|
|
import yaml
|
|
from platform import system as OSName, OSRelease
|
|
|
|
try:
|
|
import winreg
|
|
except ModuleNotFoundError:
|
|
# Not running on windows
|
|
pass
|
|
|
|
# Local libraries
|
|
import tfscript
|
|
from tfscript import verify
|
|
|
|
args = {}
|
|
targetDir = ""
|
|
|
|
def parseFile(inputFile):
|
|
"""Parse, verify, and do the conversion."""
|
|
config = yaml.safe_load(inputFile)
|
|
|
|
# See verify.py
|
|
config = tfscript.verify.verifyConfig(config)
|
|
if "errors" in config:
|
|
for e in config["errors"]:
|
|
print(e,file=sys.stderr)
|
|
else:
|
|
parseConfig(config)
|
|
|
|
def writeOutput(data, className) -> dict:
|
|
"""
|
|
Write `data' to various files as needed, returning a dict of
|
|
the temporary file names and their target destination names,
|
|
not including the target directory
|
|
"""
|
|
global args
|
|
namesDict = {} # return dict
|
|
|
|
# Variables
|
|
lineList = [ l.encode('utf8') for l in data.splitlines() ]
|
|
fileNum = 1
|
|
bytesWritten = 0
|
|
|
|
# Constants
|
|
maxFileSize = 2 ** 20 # 1MiB maximum cfg file size
|
|
filesNeeded = 1 + int( len(data)/maxFileSize )
|
|
if args.debug:
|
|
print( f'DEBUG: need {filesNeeded} files for {className}', file=sys.stderr)
|
|
|
|
FilNedLen = len(str(filesNeeded))
|
|
reservedSpace = len(f'{className}_script_{filesNeeded}.cfg') + 4
|
|
|
|
# Initialize variables
|
|
outfile = NamedTemporaryFile(prefix=className, delete=False)
|
|
# I know % formatting is old-school and pylint hates it,
|
|
# but "%*d" is the easiest way to left-pad with zeros
|
|
# without hardcoding a number. The extra 4 bytes is just some leeway
|
|
namesDict.update({ outfile.name: '%s_script_%0*d.cfg' % (className, FilNedLen, fileNum) })
|
|
|
|
while (fileNum <= filesNeeded and len(lineList) > 0):
|
|
line = lineList.pop(0) + '\n'.encode('utf8')
|
|
lineLen = len(line) # nice
|
|
|
|
if bytesWritten + reservedSpace + lineLen > maxFileSize:
|
|
outfile.write( ('exec %s_script_%0*d' % (className, FilNedLen, fileNum+1)).encode('utf8') )
|
|
bytesWritten += reservedSpace
|
|
if args.debug:
|
|
print( f'DEBUG: Wrote {bytesWritten} bytes to {className} ({fileNum}/{filesNeeded})', file=sys.stderr)
|
|
|
|
outfile.close()
|
|
outfile = NamedTemporaryFile(prefix=className, delete=False)
|
|
|
|
fileNum += 1
|
|
namesDict.update({ outfile.name: '%s_script_%0*d.cfg' % (className, FilNedLen, fileNum) })
|
|
bytesWritten = 0
|
|
|
|
outfile.write(line)
|
|
bytesWritten += lineLen
|
|
|
|
outfile.close() # the most-recent tempfile will not have been closed
|
|
if args.debug:
|
|
print( f'DEBUG: Wrote {bytesWritten} bytes to {className} ({fileNum}/{filesNeeded})', file=sys.stderr)
|
|
|
|
return namesDict
|
|
|
|
def parseConfig(config):
|
|
"""With validated data structure, write out all the files."""
|
|
global args
|
|
global targetDir
|
|
|
|
if os.path.isdir(targetDir) == False:
|
|
os.mkdir(targetDir)
|
|
if args.debug:
|
|
print( f"DEBUG: Created directory {targetDir}", file=sys.stderr)
|
|
|
|
tempsAndReals = {}
|
|
|
|
for currentClass in config:
|
|
classDict = config[currentClass]
|
|
stringToWrite = tfscript.makeCFG(classDict)
|
|
replaceDict = writeOutput(stringToWrite, currentClass)
|
|
tempsAndReals.update(replaceDict)
|
|
|
|
for tmpName, realName in tempsAndReals.items():
|
|
if args.dry_run:
|
|
if args.debug:
|
|
print( f'DEBUG: {tmpName} would be {targetDir}/{realName}.cfg', file=sys.stderr)
|
|
else:
|
|
os.replace( tmpName, f'{targetDir}/{realName}' )
|
|
if args.debug:
|
|
print( f'DEBUG: Created {targetDir}/{realName}', file=sys.stderr)
|
|
|
|
def parseCLI():
|
|
# Handle command line
|
|
parser = argparse.ArgumentParser(
|
|
description="Parse YAML file and produce TF2 config script."
|
|
)
|
|
parser.add_argument( '-d', '--debug', action='store_true',
|
|
help="Enable debugging messages.")
|
|
parser.add_argument( '-n', '--dry-run', action='store_true',
|
|
help="Parse input file, but don't write anything.")
|
|
parser.add_argument( '-f', '--force', action='store_true',
|
|
help="Force tfscript to continue until catastrophic failure")
|
|
parser.add_argument( '-D', '--directory', action='store', type=str,
|
|
help="Change output directory")
|
|
# positional argument: first non-hyphenated argument is input file
|
|
parser.add_argument( 'infile', type=argparse.FileType('r'),
|
|
help='File containing YAML to convert.')
|
|
return parser
|
|
|
|
def main():
|
|
""" Command line interface. """
|
|
global args
|
|
global targetDir
|
|
parser = parseCLI()
|
|
|
|
args = parser.parse_args()
|
|
if args.directory is not None:
|
|
targetDir = args.directory
|
|
else:
|
|
systemName = OSName()
|
|
if systemName == "Darwin":
|
|
if float( '.'.join(OSRelease().split('.')[0:2]) ) >= 10.15:
|
|
if not args.force:
|
|
print(
|
|
"As of macOS Catalina (v10.15), 32-bit applications "
|
|
"such as tf2 do not work, so tfscript does not function",
|
|
file=sys.stderr
|
|
)
|
|
else:
|
|
targetDir = os.path.expanduser("~/Library/Application Support/Steam/")
|
|
|
|
elif systemName == "Windows":
|
|
# oh god why do we have to use the registry
|
|
accessReg = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
|
|
accessKey = winreg.OpenKey(accessReg, "SOFTWARE\\WOW6432Node\\Valve\\Steam\\")
|
|
targetDir = winreg.QueryValue(accessKey, "InstallPath")
|
|
|
|
elif systemName == "Linux":
|
|
targetDir = os.path.expanduser("~/.local/Steam")
|
|
|
|
elif systemName == "Java":
|
|
print("Java-based OSes are not supported yet by tfscript.", file=sys.stderr)
|
|
|
|
if targetDir != "":
|
|
# Supported OS: add steamapps path
|
|
if targetDir[-1] != '/':
|
|
targetDir += '/'
|
|
targetDir += "steamapps/common/Team Fortress 2/tf/cfg"
|
|
elif args.force:
|
|
# Unsupported OS but -f specified
|
|
if args.debug:
|
|
print("DEBUG: forced to continue, output set to current directory", file=sys.stderr)
|
|
targetDir = '.'
|
|
else:
|
|
# Unsupported OS and not forced to continue
|
|
return 2
|
|
|
|
parseFile(args.infile)
|
|
return 0
|
|
|
|
if __name__ == "__main__":
|
|
exit(main())
|