191 lines
6.9 KiB
Python
191 lines
6.9 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
|
|
import tempfile
|
|
import yaml
|
|
from platform import system as getOS, release
|
|
|
|
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(scriptString, className):
|
|
"""Given the string of stuff to write, write it out to the given handle."""
|
|
global args
|
|
global targetDir
|
|
chunksize = 2**20 # 1Mb maximum cfg file size
|
|
chunk = 1
|
|
# If the string is more than 1048576 bytes, we need divide it into files that each
|
|
# are less than 1048576 bytes
|
|
chunksneeded = int( 1 + len(scriptString) / chunksize )
|
|
if args.debug:
|
|
print( f'DEBUG: need {chunksneeded} files for {className}')
|
|
|
|
if( chunksneeded == 1):
|
|
# If it can be done in one chunk, do it in one chunk.
|
|
outfile = tempfile.NamedTemporaryFile( prefix=className, delete=False )
|
|
outfile.write(scriptString.encode("utf8"))
|
|
outfile.close()
|
|
if( args.dry_run != True ):
|
|
os.replace(outfile.name, f'{targetDir}/{className}_script_{chunk:02d}.cfg')
|
|
if args.debug:
|
|
print( f'DEBUG: Created {targetDir}/{className}_script_{chunk:02d}.cfg')
|
|
else:
|
|
if args.debug:
|
|
print( f'DEBUG: {outfile.name} would be {targetDir}/{className}_script_{chunk:02d}.cfg')
|
|
else:
|
|
# Gotta do it in multiple chunks
|
|
classLines = scriptString.splitlines()
|
|
execString = f'exec {className}_script_{chunk:02d}'.encode("utf8")
|
|
# extra 4 bytes is just a little buffer so we don't get exactly chunksize bytes
|
|
reservedSpace = len(execString) + 4
|
|
n = 0
|
|
pieces = {}
|
|
while( chunk <= chunksneeded ):
|
|
outfile = tempfile.NamedTemporaryFile( prefix=className, delete=False )
|
|
pieces[outfile.name] = f'{targetDir}/{className}_script_{chunk:02d}.cfg'
|
|
byteswritten = 0
|
|
while( n < len(classLines) and (byteswritten + len(classLines[n]) + reservedSpace) < chunksize ):
|
|
line = classLines[n].encode("utf8") + os.linesep.encode("utf8")
|
|
outfile.write(line)
|
|
byteswritten += len(line)
|
|
n+=1
|
|
if( chunk < chunksneeded ):
|
|
line = f'exec {className}_script_{chunk+1:02d}'.encode("utf8") + os.linesep.encode("utf8")
|
|
outfile.write(line)
|
|
byteswritten += len(line)
|
|
outfile.close()
|
|
|
|
if args.debug:
|
|
print( f'DEBUG: Wrote {byteswritten} bytes to {className} ({chunk}/{chunksneeded})' )
|
|
chunk += 1
|
|
for tmpname, realname in pieces.items():
|
|
if( args.dry_run ):
|
|
if( args.debug ):
|
|
print( f'DEBUG: {outfile.name} would be {targetDir}/{className}_script_{chunk:02d}.cfg')
|
|
else:
|
|
os.replace(tmpname, realname)
|
|
print( f'DEBUG: Created {targetDir}/{className}_script_{chunk:02d}.cfg')
|
|
|
|
def parseConfig(config):
|
|
"""With validated data structure, write out all the files."""
|
|
global targetDir
|
|
# Make sure the target exists before we try to use it
|
|
if os.path.isdir( targetDir ) == False:
|
|
try:
|
|
os.mkdir( targetDir )
|
|
if args.debug:
|
|
print( f'DEBUG: created {targetDir}')
|
|
except Exception as fileExcept:
|
|
print( f'WARN: Failed to create {targetDir}: {fileExcept.strerror}\nUsing current directory instead.' )
|
|
targetDir = '.'
|
|
for currentClass in config:
|
|
classDict = config[currentClass]
|
|
stringToWrite = tfscript.makeCFG(classDict)
|
|
writeOutput(stringToWrite, currentClass)
|
|
|
|
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 = getOS()
|
|
if systemName == "Darwin":
|
|
if float( '.'.join(release().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
|
|
targetDir = os.path.expanduser('.')
|
|
else:
|
|
# Unsupported OS and not forced to continue
|
|
return 2
|
|
|
|
parseFile(args.infile)
|
|
return 0
|
|
|
|
if __name__ == "__main__":
|
|
exit(main())
|