Created
December 1, 2017 21:20
-
-
Save pl77/e03d58d2b25a3c3a5034684c7006c81b to your computer and use it in GitHub Desktop.
A small script to automate Bodyslide mesh conversions from old Skyrim CBBE to new CBBE format.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from pywinauto.application import Application | |
from pywinauto.timings import TimeoutError | |
from pywinauto.findbestmatch import MatchError | |
from bs4 import BeautifulSoup | |
import os | |
import time | |
import json | |
def get_soup(filepath): | |
with open(filepath, 'r', encoding='utf8') as myfile: | |
rawfile = myfile.read() | |
try: | |
soup = BeautifulSoup(rawfile, "lxml") | |
except (OSError or ValueError): | |
print("File error.") | |
soup = None | |
pass | |
return soup | |
def startbodyslide(): | |
# Run a windows shortcut to directly open Outfit Studio | |
os.startfile(r"E:\Bodyslide\Data\CalienteTools\BodySlide\BodySlideShortcut.lnk") | |
# Connect the automation framework to the Bodyslide Process | |
app = Application().connect(path=r"E:\Bodyslide\Data\CalienteTools\BodySlide\BodySlidex64.exe") | |
# Find the Outfit Studio Window | |
return app | |
def processbodyslide(app, dlg, shapedatadir, projectname, pdict): | |
# Open a new project | |
dlg.menu_select("File->New Project") | |
# Connect to the new project window | |
np = app['New Project'] | |
# Select the Reference | |
np['From TemplateComboBox'].Select('Convert: SK CBBE To SSE CBBE') | |
np['&Next >'].click() | |
# Enter a new project name | |
np.Edit.SetText(projectname) | |
# Click on the From File radio button | |
np['From File3'].click() | |
# enter the bodyslide nif file path | |
nifdir = os.path.join(shapedatadir, pdict['setfolder'], pdict['sourcefile']) | |
np['npWorkFilename'].Edit.SetText(nifdir) | |
np['&Finish'].click() | |
# accept the conversion from old skyrim nifs to SSE | |
try: | |
app['Target Game']['&Yes'].click() | |
except (TimeoutError, MatchError): | |
pass | |
# window gets renamed with the project name, so find the new name for the Outfit Studio window | |
windowtitle = 'Outfit Studio - {pn}'.format(pn=projectname) | |
dlg2 = app[windowtitle] | |
# conform sliders | |
dlg2.menu_select("Slider->Conform All") | |
# find and deleting the old BaseShape from original Skyrim | |
meshtree = dlg2['TreeView'] | |
meshtreeroots = meshtree.roots() | |
meshtree_children = meshtreeroots[0].children() | |
for child in meshtree_children: | |
child.click() | |
if child.text() == 'BaseShape': | |
dlg2.type_keys('{DEL}') | |
try: | |
app['Confirm Delete']['&Yes'].click() | |
except (TimeoutError, MatchError): | |
pass | |
# set the reference slider to 100% | |
tb = dlg2['De-/Select SlidersEdit2'] | |
tb.click() | |
tb.SetText("100") | |
tb.click() | |
time.sleep(1) | |
# open up the Bones tab for next step | |
dlg2['BonesButton'].click() | |
# click somewhere else to make the slider figure out it's been reset | |
dlg2['mGLView_container'].click() | |
# find and delete unused bones from the old skeleton | |
bonestree = app[windowtitle]['TreeView'] | |
bonesroots = bonestree.roots() | |
for idx, root in enumerate(bonesroots): | |
try: | |
root.click() | |
except AttributeError: | |
dlg2.print_control_identifiers() | |
print("Attribute Error") | |
continue | |
print(root.text()) | |
if root.text() == "NPC Belly" or root.text() == "NPC L Breast01" or root.text() == "NPC R Breast01": | |
root.select() | |
# open up the context menu | |
root.click_input(button='right') | |
popup = app['PopupMenu'] | |
# the only way I could find to click through the context menu and delete unused bones from project | |
popup.MenuItem('Delete').ClickInput() | |
popup.MenuItem('From Project').ClickInput() | |
# set base shape and load reference | |
dlg2.menu_select("Slider->Set Base Shape") | |
dlg2.menu_select("File->Load Reference") | |
lr = app['Load Reference'] | |
lr['From TemplateComboBox'].Select('CBBE Body Physics') | |
lr['&OK'].click() | |
# conform the outfit to the new reference shape | |
dlg2.menu_select("Slider->Conform All") | |
dlg2.menu_select("File->Save As") | |
svas = app['Save Project As...'] | |
svas['Output File NameEdit'].SetText(pdict['outputfile']) | |
svas['Output Data PathEdit'].SetText(pdict['outputpath']) | |
projfolder = '000TEST' # temporary folder for putzing around; need to convert into a variable | |
sldset = '{pf}\\{proj}.osp'.format(pf=projfolder, proj=projectname) # hardcoded and needs to be made a variable | |
svas['Slider Set FileEdit'].SetText(sldset) | |
svas['Shape Data FolderEdit'].SetText(projfolder) | |
svas['&Save'].click() | |
# reset the tab to Meshes or the next file will not work. | |
dlg2['MeshesButton'].click() | |
return app, dlg2 | |
def parseslidersets(sourcedir, prefix): | |
setdict = dict() | |
# walk the dir, pull the data, dump it into a dict and send it back | |
for dirpath, dirnames, dirfiles in os.walk(sourcedir): | |
for filename in dirfiles: | |
if filename.startswith(prefix) and filename.endswith('.xml'): | |
print(filename) | |
full_path_name = os.path.join(dirpath, filename) | |
filesoup = get_soup(full_path_name) | |
settag = filesoup.html.body.slidersetinfo.sliderset | |
projectname = settag.attrs['name'] | |
setdict[projectname] = dict() | |
setdict[projectname]['setfolder'] = settag.setfolder.text | |
setdict[projectname]['sourcefile'] = settag.sourcefile.text | |
setdict[projectname]['outputpath'] = settag.outputpath.text | |
setdict[projectname]['outputfile'] = settag.outputfile.text | |
baseshapetaglist = list(settag.find_all('baseshapename')) | |
baseshapelist = list() | |
for baseshape in baseshapetaglist: | |
baseshapelist.append(baseshape.attrs) | |
setdict[projectname]['baseshapelist'] = baseshapelist | |
slidertaglist = list(settag.find_all('slider')) | |
sliderlist = list() | |
for slidertag in slidertaglist: | |
sliderlist.append(slidertag.attrs) | |
setdict[projectname]['sliderlist'] = sliderlist | |
return setdict | |
def main(): | |
basedir = 'E:\Bodyslide\Data' | |
slidersetdir = 'CalienteTools\BodySlide\SliderSets' | |
shapedatadir = 'CalienteTools\BodySlide\ShapeData' | |
prefix = 'IA - ' | |
sourcedir = os.path.join(basedir, slidersetdir) | |
slidersetdict = parseslidersets(sourcedir, prefix) | |
# dump XML dict into a JSON in case I want to use later. Maybe to pause and resume? | |
with open('slidersets.json', 'w') as jfile: | |
jfile.write(json.dumps(slidersetdict)) | |
shapedir = os.path.join(basedir, shapedatadir) | |
app = startbodyslide() | |
dlg = app['Outfit Studio'] | |
for project, pdict in slidersetdict.items(): | |
app, dlg = processbodyslide(app, dlg, shapedir, project, pdict) | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment