Files
RobustToolbox/Resources/buildResourcePack.py

281 lines
9.6 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
# Potential improvements:
# * Asynchronous file I/O.
# * Stream files in from MSpriteRenderer instead of waiting for it to complete.
# * Don't wait on all atlasses to finish before starting non-texture files.
# TODO:
# * Better handling of errors.
# * Better logging.
import argparse
import asyncio
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import typing # autism.
import zipfile
try:
from colorama import init, Fore, Back, Style
init()
except ImportError:
# Just give an empty string for everything, no colored logging.
class ColorDummy(object):
def __getattr__(self, name):
return ""
Fore = ColorDummy()
Style = ColorDummy()
Back = ColorDummy()
IS_WINDOWS = sys.platform in ("win32", "cygwin")
async def main():
parser = argparse.ArgumentParser(description="Builds the SS14 resource pack zip file.")
parser.add_argument("--no-atlas",
dest="atlas",
help=("Disable texture atlas generation. "
"This severely ruins rendering performance in game. Beware."),
action="store_false")
parser.add_argument("--no-animations",
dest="animations",
help="Skip generating animations",
action="store_false")
parser.add_argument("--sprite-renderer",
dest="renderer",
type=Path,
default=Path("SpriteRenderer/MSpriteRenderer.exe"),
help="Provide a custom path to the animations sprite renderer's directory.")
parser.add_argument("--atlas-tool",
dest="atlas_tool",
type=str,
default="../Tools/AtlasTool.exe",
help="Provide a custom path to the atlas tool used.")
parser.add_argument("--out",
dest="out",
type=Path,
default=Path("ResourcePack.zip"),
help="Output file.")
parser.add_argument("--no-temp-out-file",
dest="temp_out",
action="store_false",
help=("Do not write to a temporarily file during operation, "
"instead write to the output file directly. "
"Note that this will cause a current zip to be corrupted "
"if the script gets killed midway through."))
parser.add_argument("--resources-dir",
dest="source",
type=Path,
default=Path("."),
help="Set the directory to pull resources from.")
# For debugging on non-Windows systems.
# You're probably gonna want to pass --sprite-renderer SpriteDummy/DummySpriteRenderer.py too.
parser.add_argument("--force-animations-build",
dest="force_animations",
action="store_true",
help=argparse.SUPPRESS)
parser.add_argument("--to-stderr",
dest="stderr",
action="store_true",
help="Redirect all output to stderr.")
args = parser.parse_args()
if args.stderr:
sys.stdout = sys.stderr
animations = args.animations
atlas = args.atlas
atlas_command = [args.atlas_tool]
atlas_dir = None
textures_path = args.source.joinpath("Textures")
if not IS_WINDOWS:
if animations and not args.force_animations:
print(Fore.YELLOW + "WARNING: animations generation will be disabled because you are not on Windows.")
print(" Pass --no-animations to surpress this warning." + Style.RESET_ALL)
animations = False
# Maybe softcode this to allow using .NET core?
atlas_command.insert(0, "mono")
outfile = args.out
if args.temp_out:
outfile = outfile.with_suffix(".tmp")
zip_target = zipfile.ZipFile(str(outfile), "w", zipfile.ZIP_DEFLATED, allowZip64=False)
if atlas:
atlas_dir = tempfile.TemporaryDirectory()
atlas_dir.path = Path(atlas_dir.name)
# Separare so we can easily hand them to build_animations()
# Which can then easily hand them to handle_texture_directory.
# Also bit of a misnomer, sorry about that.
atlas_args = {
"zip_target": zip_target,
"atlas": atlas,
"atlas_command": atlas_command,
"atlas_out_dir": atlas_dir.name if atlas_dir else Path("")
}
# Animations atlas building gets done after build_animations.
# If animations are done.
tasks = [
handle_texture_directory(
input_dir=textures_path.joinpath(name),
atlas_name=name,
**atlas_args
) for name in [
"Decals",
"Effects",
"Items",
"Objects",
"Tiles",
"UserInterface"
]
]
tasks.append(handle_texture_directory(textures_path.joinpath("Unatlased"), zip_target, False))
if animations:
tasks.append(build_animations(args.renderer, atlas_args))
await asyncio.gather(*tasks)
if atlas:
# Gotta copy over the files from the atlas output dir.
for filepath in atlas_dir.path.iterdir():
zip_path = Path("TAI" if filepath.suffix == ".TAI" else "Textures")
zip_target.write(str(filepath), str(zip_path.joinpath(filepath.name)))
write_zip_dir_node(zip_target, "TAI/")
write_zip_dir_node(zip_target, "Textures/")
# Copy over non-image files.
for otherassets in ["Fonts", "ParticleSystems", "Shaders"]:
for (dirpath, dirnames, filenames) in os.walk(str(args.source.joinpath(otherassets))):
dirpath = Path(dirpath)
write_zip_dir_node(zip_target, str(dirpath.relative_to(args.source)))
for filename in filenames:
filepath = dirpath.joinpath(filename)
targetpath = filepath.relative_to(args.source)
print(Fore.CYAN + "Wrote {0} -> {1}".format(filepath, targetpath) + Style.RESET_ALL)
zip_target.write(str(filepath), str(targetpath))
# Clean up.
zip_target.close()
if args.temp_out:
outfile.replace(args.out)
if atlas_dir is not None:
atlas_dir.cleanup()
async def build_animations(path: Path, atlas_args: typing.Dict[str, typing.Any]):
dirpath = path.parent
outdir = dirpath.joinpath("output")
if outdir.exists():
print(Fore.CYAN + "Deleting previous output from the animation renderer..." + Style.RESET_ALL)
def handle_errors(function, path, excinfo):
print(Fore.RED + " ERROR: failed to remove {0}: {1}".format(path, excinfo[1].strerror) + Style.RESET_ALL)
shutil.rmtree(str(outdir), onerror=handle_errors)
outdir.mkdir()
print(Fore.GREEN + "Running the animation renderer, this will take a while..." + Style.RESET_ALL)
exec_path = path.resolve()
process = await asyncio.create_subprocess_exec(str(exec_path), cwd=str(dirpath))
await process.wait()
await handle_texture_directory(input_dir=outdir, atlas_name="Animations", **atlas_args)
zip_target = atlas_args["zip_target"]
write_zip_dir_node(zip_target, "Animations/")
# Write animation XML files to ZIP/Animations/
for xmlfile in outdir.glob("*.xml"):
targetpath = Path("Animations").joinpath(xmlfile.name)
print(Fore.CYAN + "Wrote {0} -> {1}".format(xmlfile, targetpath) + Style.RESET_ALL)
zip_target.write(str(xmlfile), str(targetpath))
async def handle_texture_directory(input_dir: Path,
zip_target: zipfile.ZipFile,
atlas: bool,
atlas_name: str = None,
atlas_command: typing.List[str] = None,
atlas_out_dir: Path = None):
if not atlas:
# NOTE: Hardcoding for .png is probably bad.
# But it prevents the animation XML files from being copied so...
for pngfile in input_dir.glob("**/*.png"):
texturepath = Path("Textures").joinpath(pngfile.name)
zip_target.write(str(pngfile), str(texturepath))
print(Fore.CYAN + "Wrote {0} -> {1}".format(pngfile, texturepath) + Style.RESET_ALL)
return
print(Fore.GREEN + "Generating atlas for {0}...".format(atlas_name) + Style.RESET_ALL)
process = await asyncio.create_subprocess_exec(
*atlas_command,
"-n", atlas_name,
"-o", str(atlas_out_dir),
"-i", str(input_dir)
)
print(Fore.GREEN + "Done generating atlas for {0}.".format(atlas_name) + Style.RESET_ALL)
await process.wait()
# If it's stupid and it works, is it still stupid?
def write_zip_dir_node(file: zipfile.ZipFile, path: str):
"""
Attempt to write a dummy directory node into the zip file.
Does nothing if the node already exists.
"""
if not path.endswith("/"):
path += "/"
try:
info = file.getinfo(path)
except:
info = zipfile.ZipInfo()
info.date_time = (2001, 9, 11, 8, 46, 0)
info.filename = path
file.writestr(info, "")
print(Fore.CYAN + Style.DIM + "Wrote " + path + " zip directory." + Style.RESET_ALL)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
if IS_WINDOWS:
loop = asyncio.ProactorEventLoop()
asyncio.set_event_loop(loop)
loop.run_until_complete(main())