#!/bin/python3
SCRIPT_VERSION = '1.1'
import json
import sys
import os
import zipfile
import re
import glob
import traceback
import logging
try:
from tqdm import tqdm as tqdmfunc
except ImportError:
print("tqdm not installed, not showing progress bar")
def tqdmfunc(iterator):
yield from iterator
out_dir = None # Set in main()
dupes = set()
# logging.getLogger().setLevel(logging.INFO)
def getCleanedPersonAtoms(scenejson):
for atom in scenejson['atoms']:
if atom['type'] != "Person":
continue
storables = atom['storables']
for storable in [*storables]:
storable.pop("position", None)
storable.pop("rotation", None)
# Remove all keys containing these substrings
for key in [*storable.keys()]:
if any(ss in key.lower() for ss in ['position', 'rotation']):
storable.pop(key)
# Remove all storables whose ids contain these substrings
if any(ss in storable['id'].lower() for ss in ["control", "trigger", "plugin", "preset", "animation"]):
# print("pop", storable['id'])
storables.remove(storable)
continue
# Remove transient morphs
if storable['id'] == "geometry" and 'morphs' in storable:
for morph in [*storable['morphs']]:
if any(re.match(ss, morph.get('uid', '')) for ss in [
r'Breast Impact',
r'^Eyelids (Top|Bottom) (Down|Up) (Left|Right)$', r'Brow .*(Up|Down)', r'^(Left|Right) Fingers'
r'^Mouth Open', r'Tongue In-Out', 'Shock', 'Surprise', 'Fear', 'Pain', 'Concentrate', 'Eyes Closed'
]):
# print(morph)
storable['morphs'].remove(morph)
# Remove storables with just an id and no properties
if len(storable.keys()) == 1:
# print("pop", storable['id'])
storables.remove(storable)
continue
yield (atom, storables)
def saveAppearance(appearance, outname, readthumb, self_name):
appearance_hash = json.dumps(appearance, sort_keys=True)
appearance = appearance_hash.replace("SELF:", self_name + ":")
if appearance_hash not in dupes:
outpath = os.path.join(out_dir, outname + '.vap')
with open(outpath, 'w') as fp:
logging.info("-> %s", outpath)
fp.write(appearance)
try:
with open(os.path.join(out_dir, outname + '.jpg'), 'wb') as fp:
with readthumb() as fp2:
fp.write(fp2.read())
except (KeyError, FileNotFoundError):
# No image in archive
pass
dupes.add(appearance_hash)
else:
# logging.warn("Skip duplicate %s", outname)
pass
def extractFromVar(var):
__, filename = os.path.split(var)
self_name, __ = os.path.splitext(filename)
author = self_name.split('.')[0]
try:
with zipfile.ZipFile(var) as varzip:
infodict = {
zi.filename: zi
for zi in varzip.infolist()
}
zip_scenes = {
k: v for k, v in infodict.items()
if k.startswith("Saves/scene") and k.endswith(".json")
}
for scenejsonpath in zip_scenes.keys():
logging.debug("%s:%s", var, scenejsonpath)
__, sjp_filename = os.path.split(scenejsonpath)
sjp_plain, sjp_ext = os.path.splitext(sjp_filename)
with varzip.open(scenejsonpath, 'r') as fp:
scenejson = json.load(fp)
def readthumb():
return varzip.open(scenejsonpath.replace('.json', '.jpg'), 'r')
def outnameFn(atom):
return f"Preset_{self_name}.{sjp_plain}{atom['id'].replace('Person', '')}".replace('#', '').replace('/', '-')
extractFromSceneJson(scenejson, outnameFn, readthumb, self_name)
except json.JSONDecodeError:
print(var)
traceback.print_exc()
os.rename(var, var + '.invalid')
except zipfile.BadZipFile:
print(var)
traceback.print_exc()
os.rename(var, var + '.invalid')
def extractFromSceneJsonPath(scenejsonpath):
__, sjp_filename = os.path.split(scenejsonpath)
sjp_plain, sjp_ext = os.path.splitext(sjp_filename)
def readthumb():
return open(scenejsonpath.replace('.json', '.jpg'), 'rb')
def outnameFn(atom):
return f"Preset_!local.{sjp_plain}{atom['id'].replace('Person', '')}".replace('#', '').replace('/', '-')
with open(scenejsonpath, 'r') as fp:
extractFromSceneJson(json.load(fp), outnameFn, readthumb)
def extractFromSceneJson(scenejson, outnameFn, readthumb, self_name="SELF:"):
# print([atom['id'] for atom in scenejson['atoms'] if atom['type'] == 'Person'])
for atom, storables in getCleanedPersonAtoms(scenejson):
# print(atom['id'])
appearance = {
"setUnlistedParamsToDefault": "true",
'storables': storables
}
saveAppearance(appearance, outnameFn(atom), readthumb, self_name)
def main():
if not sys.argv[1:]:
print('Appearance Preset Extractor', SCRIPT_VERSION, 'by cartitolaire')
print('')
print('This tool automatically export presets from existing scenes and collects them in an "extracted" folder under presets. The presets will use the thumbnail for the source scene.')
print('')
print("Example usages:")
print(' ', sys.argv[0], 'AddonPackages/Anon.SceneName.1.var')
print(' ', sys.argv[0], '"AddonPackages/Anon*.var"')
print(' ', sys.argv[0], '"Saves/scene/*.json" "AddonPackages/*.var"')
return
global out_dir
if not os.path.isdir("./Custom/Atom/Person/"):
logging.error("Script is not being run from the VaM root directory!")
logging.warn("Can't cleanly merge into Person/Appearance presets; making ExtractedAppearance folder instead.")
out_dir = "./ExtractedAppearance/"
else:
out_dir = "./Custom/Atom/Person/Appearance/extracted/"
os.makedirs(out_dir, exist_ok=True)
try:
iterable = sum((glob.glob(a, recursive=True) for a in sys.argv[1:]), [])
for filepath in tqdmfunc(iterable):
__, filename = os.path.split(filepath)
self_name, ext = os.path.splitext(filename)
if ext == ".var":
extractFromVar(filepath)
elif ext == ".json":
extractFromSceneJsonPath(filepath)
else:
raise NotImplementedError(ext)
except KeyboardInterrupt:
return
if __name__ == "__main__":
main()