Skip to content

Commit

Permalink
patched all current bugs and streamlined features
Browse files Browse the repository at this point in the history
  • Loading branch information
snake-biscuits committed Aug 30, 2021
1 parent d0aaab5 commit 990f161
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 87 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ batch .vmt generator (WIP)

Uses [VTFLibWrapper by Ganonmaster](https://github.com/Ganonmaster/VTFLibWrapper),
which provides python bindings for [Nem's VTFLib](https://web.archive.org/web/20191229074421/http://nemesis.thewavelength.net/index.php?p=40)
(also used by the [SourceIO](https://github.com/REDxEYE/SourceIO/tree/master/source1/vtf/VTFWrapper) blender addon)
(also used by the [SourceIO](https://github.com/REDxEYE/SourceIO/tree/master/source1/vtf/VTFWrapper) blender addon)
See also: [VTFCmd](https://github.com/TitusStudiosMediaGroup/VTFcmd-Resources)
<!-- reVaMpT; community tool or proprietary? -->

## Installation
Clone this repo:
Expand Down
156 changes: 70 additions & 86 deletions batch_vmt.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# batch_vmt (c) by Bikkie / snake-biscuits [b!scuit#3659]
#
# batch_vmt is licensed under a
Expand All @@ -8,14 +7,12 @@
# work. If not, see <http://creativecommons.org/licenses/by-sa/4.0/>.
"""generate .vmt files from a folder of .vtf files"""
from __future__ import annotations
import argparse
import fnmatch
import os
import re
from typing import Dict, List, Tuple
from typing import Dict, List

from colour import Color
# from gooey import Gooey
from gooey import Gooey, GooeyParser

# import VTFLibWrapper

Expand All @@ -31,16 +28,18 @@ def from_template(vtf_filename: str, template: str, **substitutions: Dict[str, s
for keyword, replacement in substitutions.items():
template = template.replace(f"<{keyword}>", replacement)
# e.g. replacements = {"texture2": "<filename>a"}
# '$basetexture2 <texture2>' --> '"$blendmodulatetexture" "<filename>_bm"'
# then at the file level: '<filename>_bm' -> 'texture_bm' for "texture.vmt"
# '$basetexture2 <blend_texture>' --> '"$basetexture2" "<filename>_a"'
# then at the file level: '<filename>_a' -> 'texture_a' for "texture.vmt"
# NOTE: never put filename in replacements unless you want to replace all textures with one texture!
filename = os.path.splitext(vtf_filename)[0] # remove .vtf extension
with open(f"{filename}.vmt", "w") as vmt_file:
vmt_file.write(template.replace("<filename>", filename))


# TODO: maybe separate file filtering from .vmt writing?
def from_metadata(vtf_filename: str, shader: str = "LightmappedGeneric", **flags: Dict[str, Tuple[str, str]]):
def from_metadata(vtf_filename: str, shader: str = "LightmappedGeneric",
colour=Color("White"), hue_range: float = 0,
defaults={"colour": ("%keywords", "white")}):
"""generate an appropriate .vmt from .vmt flags"""
raise NotImplementedError()
# * EXPECTED FLAGS *
Expand All @@ -52,22 +51,17 @@ def from_metadata(vtf_filename: str, shader: str = "LightmappedGeneric", **flags

vtf = ... # TODO: load f"{filename}.vtf" with VTFLibWrapper
# check flags
if "color" in flags:
flags["colour"] = flags.pop("color")
if "transparent" in flags:
flags["has_alpha"] = flags.pop("transparent")
checks: Dict[str, bool]
checks = {"colour": fuzzy_colour_match(vtf.reflectivity, flags["colour"], flags.get("hue_range", 0)),
"has_alpha": has_alpha(vtf),
None: None}
check: Dict[str, bool]
check = {"colour": fuzzy_colour_match(vtf.reflectivity, colour, hue_range),
"has_alpha": has_alpha(vtf)}
# ^ {"flag": True or False}
metadata = {f: checks.get(f, None) for f in flags}

# compose the .vmt text
lines = [shader, "{"]
for flag in flags:
parameter, value = flags[flag]
if metadata[flag] is True:
for condition in check:
parameter, value = defaults[condition]
if check[condition] is True:
value = value.replace("<filename>", filename)
lines.append('\t"{paramater}" "{value}"')
lines.append("}")
# write to file
Expand All @@ -77,17 +71,18 @@ def from_metadata(vtf_filename: str, shader: str = "LightmappedGeneric", **flags

# check functions for from_metadata
def fuzzy_colour_match(a: Color, b: Color, hue_range: float) -> bool:
# TODO: accept a hue_range or rgb_range 3-ple
return abs(a.hsl[0] - b.hsl[0]) <= hue_range


def has_alpha(vtf) -> bool:
raise NotImplementedError()
return vtf.image_format in (...)
return "A" in vtf.image_format.name # NOTE: untested


def parse_folder(method: str, folders: List[str], template=None, ignore=[], recursive=False, verbose=False, **kwargs):
# TODO: split for both modes? or is it still easier to process folder recursion here?
def parse_folder(method: str, folders: List[str], template=None, **kwargs):
# pre-processing
ignore_patterns = [re.compile(p) for p in ignore]
if method == "template":
# kwargs["substutions"]: Dict[str, str] = {"keyword": "replacement"}
template = open(template).read()
Expand All @@ -96,86 +91,75 @@ def parse_folder(method: str, folders: List[str], template=None, ignore=[], recu
for keyword, replacement in kwargs.pop("substitutions", dict()).items():
template = template.replace(f"<{keyword}>", replacement)
elif method == "metadata":
# kwargs = {"flags": Dict[str, Tuple[str, str]], shader: str}
# flags = {"flag": ("$parameter", "value")}
from_metadata(method)
# TODO: figure out what can be pre-processes for metadata batches
from_metadata(method) # NotImplemented!
else:
raise RuntimeError("Invalid method: '{method}', only 'template' & 'metadata' are accepted")

# parse all folders
for folder in folders:
folder_contents = [os.path.join(f) for f in os.listdir(folder)]
if recursive:
folders.extend([d for d in folder_contents if os.path.isdir(d)])
folders.extend([d for d in folder_contents if os.path.isdir(d)])
# NOTE: this isn't recursing folders like I thought it would...
for vtf_filename in fnmatch.filter(folder_contents, "*.vtf"):
filename = os.path.join(folder, os.path.splitext(vtf_filename)[0])
if any([pattern.match(filename) for pattern in ignore_patterns]):
if verbose:
print("Skipping {filename}.vmt")
continue
# process file
if verbose:
print(f"Writing {filename}.vmt... ", end="")
print(f"Writing {filename}.vmt... ", end="")
if method == "template":
from_template(filename, template)
elif method == "metadata":
# TODO: do a keyword substitution pass on flags.values
from_metadata(filename, **kwargs)
if verbose:
print("Done!")


# @Gooey
def main(with_args: List[str] = None):
notes = ["You can drag any folder over %(prog)s and just use the defaults",
"--template (default: base.vmt next to %(prog)s) must have a <filename> keyword!",
"--replace KEYWORD:REPLACEMENT will replace any keyword",
"However, if no replacement is given, lines with <keyword> will remain and break .vmt",
" \nYou should find a displacement_base.vmt included with %(prog)s",
"--ignore .*_a .*_bump .*_bm .*_editor should be used when generating displacements"]

parser = argparse.ArgumentParser(description=__doc__, epilog="\n".join(notes),
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("folders", nargs="*",
help="folder to generate .vmts for (one for each .vtf)")
mode = parser.add_mutually_exclusive_group()
mode.add_argument("-t", "--template", default="base.vmt",
help="generate vmts from the supplied template\ndefault: base.vmt")
mode.add_argument("-m", "--metadata", action="store_true",
help="generate each .vmt based on flags set in the .vtf\nNOT IMPLEMENTED YET")
parser.add_argument("-f", "--flags",
help="colon separated metadata flags\n(e.g. has_alpha:$translucent:1)\nNOT IMPLEMENTED YET")
# TODO: set vmt shader (default: LightmappedGeneric)
# TODO: list all available flags
parser.add_argument("-s", "--substitute", action="append", metavar="keyword:replacement", default=[],
print("Done!")


@Gooey
def main():
# NOTES:
# --template (default: base.vmt next to batch_vmt.py must have a <filename> keyword!
# --replace KEYWORD:REPLACEMENT will replace any keyword
# However, if no replacement is given, lines with <keyword> will remain and break .vmt
# Also, You should find a displacement_base.vmt included with batch_vmt.py
# --ignore .*_a .*_bump .*_bm .*_editor should be used when generating displacements

parser = GooeyParser(description=__doc__)
parser.add_argument("filenames", nargs="*", metavar="FILE", widget="MultiFileChooser", default="tests/materials",
help="filename(s) / folder(s) to generate .vmts for\nFolders must be typed in manually")
parser.add_argument("-t", "--template", default="base.vmt", widget="FileChooser",
help="generate vmts from the supplied template\ndefault: base.vmt")
# subparsers = parser.add_subparsers(title="mode", description="mode to run in")
# template_mode = subparsers.add_parser("template")
parser.add_argument("-s", "--substitute", nargs="*", default="tags:generated",
help="substitute <keyword> in template with replacement\n(e.g. `bumpmap:<filename>_bump`)")
parser.add_argument("-r", "--recurse", action="store_true",
help="generate .vmts for all folders within folder")
parser.add_argument("-i", "--ignore", action="append", metavar="patterns", nargs="*", default=[],
help="skip <filename> if it matches any of the given patterns")
# TODO: --generate (metadata based .vmt & adding / removing relevant key-value pairs [no template])
# ...
# generate_mode = subparsers.add_parser("generate")
# TODO: --surfaceprop choice to add a surfaceprop
# TODO: match replacements to filename from a .csv
parser.add_argument("-v", "--verbose", action="store_true",
help="print each filename as it is processed")
parser.add_argument("-V", "--version", action="version", version=f"%(prog)s v{__version__}")

if with_args is not None:
args = parser.parse_args(with_args)
else:
args = parser.parse_args()

# if not args.metadata: # template mode
replacements = {k.strip("<>"): v for a in args.substitute for k, v in a.split(":")}
# TODO: --hsv_range H:S:V
# TODO: --rgb_range R:G:B
# TODO: --colour "White":"$parameter":"<keyword>"
# ...
args = parser.parse_args()

folders = list(filter(os.path.isdir, args.filenames))
filenames = set(args.filenames).difference(set(folders))
# TODO: handle any wildcards in filenames

# TODO: handle both modes here
# if args.generate:
# process_folder("metadata", folders, ...)
# for filename in filenames:
# from_metadata(filename, ...)
# else: # template mode
replacements = dict()
for kr in args.substitute:
k, r = kr.split(":")
replacements[k] = r
# replacements are colon separated; < & > around the keyword are optional
# setting filename will give all make all .vmts generated identical! even the basetexture!
parse_folder("template", args.folders, template=args.template, substitutions=replacements,
ignore=args.ignore, verbose=args.verbose)
# else: # metadata mode
# flags = {f: (p, v) for m in args.metadata for f, p, v in m.split(":")}
# process_folder("metadata", args.folders, flags=flags,
# ignore=args.ignore, verbose=args.verbose)
# setting <filename> will give all make all .vmts generated identical! even the basetexture!
parse_folder("template", folders, template=args.template, substitutions=replacements)
for filename in filenames:
from_template(filename, args.template, substitutions=replacements)


if __name__ == "__main__":
main()
main() # run the Gooey UI

0 comments on commit 990f161

Please sign in to comment.