Jump to content

BodySlide to BodyGen Conversion (CBBE)


shoka

Recommended Posts

Intended Audience: This topic only concerns patient people who have both BodySlide and RaceMenu installed, use CBBE, and really want a variety of body types in the game. You also have beginner (Can edit text files) to intermediate (Can use TES5Edit) level modding skills. You also need basic maths skills: You can work with decimals, do the four operations and take averages.

 

How to Convert BodySlide Presets to BodyGenData Strings?

 

There are two short answers:

1. You cannot, because BodySlide sets the body shape, and BodyGen morphs a body shape set by BodySlide. They are two different things.

2. I have a Python script in the appendix that does the conversion. However, it is currently only for Python programmers and you need to read the rest of this document to make sense of it.

 

Second answer seems to conflict the first. Read on if you are interested.

 

If there is enough interest, I will compile my conversion tool to an executable command line utility. If there is even more interest, I might be able to create a graphic user interface.

 

How Do BodySlide and BodyGen Work?

 

Here is how:

1. BodySlide presets set what the body of weight=0 (small) and weight=100 (big) should be.

(Presets are in the xml file Data\CalienteTools\BodySlide\SliderPresets\CBBEPresets.xml)

2. Right before you see an NPC in the game, the game engine reads the weight from the NPC base. Then the engine takes an average of the two presets (small and big) and draws that shape. (So, small + (small-big) * weight/100)

3. After all these happen, BodyGen's job starts. Nearly instantaneously, BodyGen reads the morphs.ini and templates.ini files, and adds the value in the slider to the previously drawn body shape. With this addition, the body is morphed.

 

So in a sense, there is no point in comparing BodySlide and BodyGen. They are two different things. However, they are also similar in the sense that they share the sliders, and morphing the body is essentially the same thing as setting the body shape if you can start with default values.

 

The Problem is the if you convert BodySlide xml files into BodyGen strings and apply them in-game, the game adds the bodygen values to bodyslide xml values. This creates a body shape that was intended by neither the BodySlide preset, nor the BodyGen string, but their addition.

 

Solutions, of which there are two, follow. I tested (very briefly) the two solutions.

 

Solution 1 - Perfect yet Incorrect Solution

 

This is a solution in two steps:

1. Set the BodySlide sliders to zero

2. Use either my Python script, or Kitsune's manual method using notepad++ to convert.

 

Step 1:

1.1 Open the BodySlide program (lives in Data\CalienteTools\BodySlide)

1.2 Set the small sliders (ie sliders on the right) to zero

1.3 Set the big sliders (ie sliders on the left) to zero

1.4 Tick the "RaceMenu Morphs". Either generate your body. Or "batch build" all your favourite outfits altogether. (You can also save this preset as "ZeroedSliders" for ease-of-use in the future.)

 

With this, you set your body type to a default starting point. Now BodyGen will behave exactly (I think) as BodySlide, because it will morph these zeroed sliders. (As you noticed, this solution completely obliterates the variation in bodies created by the game engine. This is why I call this solution "incorrect".)

 

Step 2:

2.1 Get the BodySlide preset or presets that you want to convert to BodyGen. These would be your favourite body mods, such as "ASLS" or "Natural Curves" or "CBBE Variant Bodies", all found on Nexus. It could also be something you created. Or it could be something you picked at the LoversLab forums, such as the diligently-named "yoga teacher". In all cases, you are trying to find

2.2 Choose one of the two following options:

Option A: If you can run a Python script in your computer, run my script from the appendix. Here is the example to get you working on it.

bodyslide_to_bodygen_converter.py "CBBE VB.xml" --weight=50

Option B:

Watch Kitsune's ( https://www.loverslab.com/user/111984-zerodomai-kitsune/ ) video in the following link to learn how to do the conversion. However, I have a different set of conversions than Kitsune's:

Small:
"Breasts":0, "BreastsSmall":1, "BreastGravity":0.4, "NippleDistance":1, "NippleSize":0.6, "Arms":1, "ShoulderWidth":0.75, "Waist":0.25, "ButtCrack":1, "Butt":0, "ButtSmall":1, "Thighs":0.1, "Legs":0.25, "Ankles":1, "Belly":0.4
Big:
"Breasts":0.8, "BreastsSmall":1, "BreastGravity":0.7, "NippleDistance":1, "NippleSize":0.8, "Arms":0.25, "ShoulderWidth":0.65, "Waist":1, "ButtCrack":1, "Butt":1, "ButtSmall":1, "Thighs":0.2, "Legs":1, "Ankles":1, "Belly":0.3

Use these as the "base changes", because these seem to be the default CBBE values. It seems that the BodySlide program takes these values as default, so BodySlide presets do not always include these values. This is why you need base values.

 

 

2.3 Add the scripts to your morphs.ini and templates.ini file. Details on how to do this are in the following forum:

 

https://www.loverslab.com/topic/53531-unofficial-bodygen-docs/

 

2.4 You can now assign body types randomly to all NPCs by using either the format "All|NordFemale" (Did not try myself) or the TES5Script  ExportNPCBaseIDsForBodyGen.pas (I use this, but I do not have the link)

 

The point of all this is to, of course, have multiple body types and distribute them all across the NPCs in all your mods. smile.png

 

You are done: You essentially offset the effect of BodySlide and put BodyGen in its place. (Which is why I call this solution "perfect") However, you have also offset the effect of weights in the game - which is why I call this imperfect. In the words of zollom ( https://www.loverslab.com/user/150251-zollom/ ):

Using zeroed sliders is not a benefit with CBBE, since you don't have "presets sliders" like in UUNP, and I find [CBBE] better [than UNP], because this way, the default NPC's weight are taken into account. [This benefit disappears] when using zeroed sliders, since both low and high weight are the same, all your females using the same template will look the same, Aela (weight 20), Mjoll (weight 80) and Lydia (weight 100) willl have the same body, the same ammount of breast and the same [buttocks], even when they are [supposed] to look different.

So, solution 2 is an imperfect solution to this issue. It is slightly harder to implement.

 

Solution 2 - Imperfect yet Correct Solution

 

If you did not read solution 1, please do so now: Solution 2 is difficult to understand if you do not understand Solution 1 (and the problem it creates). I will only have general instructions. Also watch Kitsune's video.

 

The idea behind this solution is to keep your BodySlide preset as default, but subtract its averages (ie weight 50) from the files. You will then get an approximation of the body type you want implemented, but it will be effected by the in-game weight.

 

In other words, you need to take into account your "starting point" when you want to implement BodyGen morphs. So your BodyGen morphs' values depend on your choice of BodySlide preset. (CBBE, CBBE Curvy, CBBE Slim, etc...)

 

So here is what you need to do. Make sure you understand, because this is more involved than Solution 1.

 

Step 1: Repeat Step 2 from Solution 1. However, since you do not have "zeroed sliders" anymore, you need to subtract an offset from your preset. This offset is the average of big and small in your preset. (Your preset is in Data\CalienteTools\BodySlide\SliderPresets\CBBEPresets.xml) However, with all sliders in the CBBE preset, you should be adding, not subtracting, because all the non-zero sliders in the CBBE are inverted. (In fact, this is probably why they were inverted in the first place.)

 

So for example, if your preset is CBBE (like mine) you need to add 0.4 to the slider Breast. This is because weight=0 in CBBE has 0% on the BodySlide program and weight=100 has 80%. The maths: ( 0% + 80% ) / 2 = 40% = 0.40.

 

You can now do this by watching applying Kitsune's technique from the video. However, you are ignoring the base values, and instead adding these offsets that depend on your BodySlide preset.

 

Alternatively, if you use the body type CBBE, and you can run my Python script, you can actually use my script. Here is your example:

bodyslide_to_bodygen_converter.py "CBBE VB.xml" --weight=50 --offset=CBBE

Step 2: Put the resulting string into your templates.ini. (Same as Step 2.3 from Solution 1.) Use one of the methods in Solution 1 Step 2.3 to assign random body types to your game.

 

That's it. You now have random body types in your game. These body types effectively use your converted BodyGen string as a base, but use your default body type to change the shape according to the weight. Therefore, the bodies in the game are not perfectly fitting the BodySlide preset that the BodyGen string is converted from - which is why I call this solution imperfect. But they are also making the best use of CBBE BodySlide and BodyGen capabilities in conjunction with the in-game weight, and you have much more variance - which is why I think that this is the correct solution.

 

Conclusion & Future

 

So here is the conclusion: If there is interest, and if all I wrote seems correct, I can compile my script as an executable, and possibly even wrap it in a GUI so that this turns into a nifty utility to generate (or edit) a templates.ini file. In fact, it could be added to the Body Slide utility itself if Caliente wants to do so. (Although I think that they are involved in FO4 now.)

 

If anybody wants to do alpha testing of the code, or give this guide a try doing all of this manually, please do so and write the results in this forum. I just tried it once and it seems to work.

 

Appendix I: Python Converter Code

#! python2

import argparse
import os
from bs4 import BeautifulSoup
from Tkinter import Tk
import re

folderSkyrimRoot = ''

parser = argparse.ArgumentParser(description='Output the BodySlide information in an XML file in BodyGenData format.')
parser.add_argument("filename", type=str, default="", help="The BodySlide xml file to convert to BodyGen")
parser.add_argument("--weight", type=str, default="range", help="What weight do you want to convert your --weight=0 --weight=50 --weight=100 --weight=range")
parser.add_argument("--base", type=str, help="Your default BodySlide preset -- This is the preset you chose when generating the bodies through BodySlide. These values are subtracted from the BodyGenData string to offset the effect of the original body")
parser.add_argument("--baseweight", type=str, help="How much of the base preset to subtract. Very low values will may result in extra (weirdly) thin bodies, and higher may result in misshapen bodies. Higher values may result in overly large body parts.")
parser.add_argument("--presetsfolder", type=str, default='Data\\CalienteTools\\BodySlide\\SliderPresets\\', help="Your presets file (You do not need to enter this if you are running from the Skyrim folder.)")
args = parser.parse_args()

dictDefaultSliderValues = {}
dictDefaultSliderValues['small'] = {"Ankles":1, "Breasts":0.2, "BreastsSmall":1, "ButtCrack":1, "ButtSmall":1, "NippleDistance":1, "NippleSize":1, "ShoulderWidth":1}
dictDefaultSliderValues['big'] = {"Ankles":1, "Arms":0.2, "Breasts":1, "BreastsSmall":1, "Butt":1, "ButtCrack":1, "ButtSmall":1, "Legs":1, "NippleDistance":1, "NippleSize":1, "ShoulderWidth":1, "Waist":1}
dictDefaultSliderValues['small'] = {"Breasts":0, "BreastsSmall":1, "BreastGravity":0.4, "NippleDistance":1, "NippleSize":0.6, "Arms":1, "ShoulderWidth":0.75, "Waist":0.25, "ButtCrack":1, "Butt":0, "ButtSmall":1, "Thighs":0.1, "Legs":0.25, "Ankles":1, "Belly":0.4}
dictDefaultSliderValues['big'] = {"Breasts":0.8, "BreastsSmall":1, "BreastGravity":0.7, "NippleDistance":1, "NippleSize":0.8, "Arms":0.25, "ShoulderWidth":0.65, "Waist":1, "ButtCrack":1, "Butt":1, "ButtSmall":1, "Thighs":0.2, "Legs":1, "Ankles":1, "Belly":0.3}
# dictDefaultSliderValues['small'] = {}
# dictDefaultSliderValues['big'] = {}
arrInvertedSliders = [ "Ankles", "Arms", "Breasts", "BreastsSmall", "Belly", "BigButt", "Butt", "ButtCrack", "ButtSmall", "DoubleMelon", "Legs", "NippleDistance", "NippleSize", "ShoulderWidth" ]
scaleDefault = {0:0, 100:1}
offset = {}
offset[None] = {}
offset['CBBE'] = {'Breasts':0.4, 'BreastsSmall':1, 'BreastGravity':0.65, 'NippleDistance':1, 'Arms':0.62, 'ShoulderWidth':0.7, 'Waist':0.62, 'ButtCrack':1, 'ButtSmall':1, 'Thighs':0.2, 'Legs':0.62, 'Ankles':1, 'Belly':0.6}
# offset['Physique - Athletic'] = {'Arms':0.75, 'Ankles':0.5, 'Belly':0.3, 'BreastsSmall':0.1, 'Butt':0.6, 'ButtCrack':0.5, 'Legs':0.3, 'NippleSize':0.5, 'NipplePerkiness':0.2, 'NippleDistance':1, 'ShoulderWidth':1.2, 'Thighs':0.35, 'Waist':0.6}
# offset['UpperBodyVariance'] = {'Breasts':0.25, 'BreastsSmall':0.25, 'Arms':0.25, 'ChubbyArms':0.3, 'NippleSize':0.5, 'NippleAreola':0.3}

class Preset:
    scale = {0:0, 100:1}
    fltSlope = float(scale[0]-scale[100])/float(0-100)
    fltIntercept = float(float(scale[100])-fltSlope*100)
    def __init__(self, preset):
        self.name = preset.get('name')
        self.sliders = {}
        for slider in preset.find_all('SetSlider'):
            fltSliderValue = self.fltSlope*float(slider.get('value'))+self.fltIntercept
            if slider.get('name') in self.sliders.keys():
                self.sliders[slider.get('name')][slider.get('size')] = fltSliderValue
            else:
                self.sliders[slider.get('name')] = { slider.get('size'): fltSliderValue }
    def valuesat(self, weight):
        values = {}
        for slider in sorted(self.sliders):
            if "small" in self.sliders[slider].keys() and "big" in self.sliders[slider].keys():
                values[slider] = self.sliders[slider]['small']+(self.sliders[slider]['big']-self.sliders[slider]['small'])*(float(weight)/100)
            else:
                values[slider] = self.sliders[slider].itervalues().next()
        return values

class Presets:
    def __init__(self, filename):
        file = open(filename, "r")
        soup = BeautifulSoup(file, "lxml-xml")
        self.dictPresets = {}
        for preset in soup.find_all('Preset'):
            self.dictPresets[preset.get('name')] = Preset(preset)

if args.base is not None:
    # if not args.base in offset.keys():
    if os.path.isfile(folderSkyrimRoot+args.presetsfolder+args.base+".xml"):
        presets = Presets(folderSkyrimRoot+args.presetsfolder+args.base+".xml")
    try:
        presets.dictPresets[args.base]
    except:
        if os.path.isfile(folderSkyrimRoot+args.presetsfolder+"CBBEPresets.xml"):
            presets = Presets(folderSkyrimRoot+args.presetsfolder+"CBBEPresets.xml")
    try:
        presets.dictPresets[args.base]
    except:
        for filename in os.listdir(folderSkyrimRoot+args.presetsfolder):
            if filename.endswith(".xml"):
                presets = Presets(folderSkyrimRoot+args.presetsfolder+filename)
                if args.base in presets.dictPresets.keys():
                    break
    presetBase = presets.dictPresets[args.base]
    offset[args.base] = presetBase.valuesat(args.weight)
if os.path.isfile(args.filename):
    file = open(args.filename, "r")
    soup = BeautifulSoup(file, "lxml-xml")
    strText = ""
    for set in soup.find_all('Preset'):
        rx = re.compile('([\s.])')
        if args.weight=="range":
            strLine = rx.sub(r"", set.get('name')) + "="
        elif args.weight=="big":
            strLine = rx.sub(r"", set.get('name')) + "_100" + "="
        elif args.weight=="small":
            strLine = rx.sub(r"", set.get('name')) + "_00" + "="
        else:
            strLine = rx.sub(r"", set.get('name')) + "_" + args.weight.zfill(2) + "="
        dictSlider = {}
        # First loop: Where the magic happens
        for slider in set.find_all('SetSlider'):
            scale = scaleDefault
            fltSlope = float(scale[0]-scale[100])/float(0-100)
            fltIntercept = float(float(scale[100])-fltSlope*100)
            fltSliderValue = fltSlope*float(slider.get('value'))+fltIntercept
            if slider.get('name') in dictSlider.keys():
                dictSlider[slider.get('name')][slider.get('size')] = fltSliderValue
            else:
                dictSlider[slider.get('name')] = { slider.get('size'): fltSliderValue }
        # Second loop: Add in default CBBE values
        for size in ['small', 'big']:
            for sliderBase in dictDefaultSliderValues[size]:
                if not sliderBase in dictSlider.keys():
                    dictSlider[sliderBase] = { size: dictDefaultSliderValues[size][sliderBase] }
                elif not size in dictSlider[sliderBase].keys():
                    dictSlider[sliderBase][size] = dictDefaultSliderValues[size][sliderBase]
        # Third loop: Some sliders are reversed, reverse them
        for sliderInvert in arrInvertedSliders:
            if sliderInvert in dictSlider.keys():
                for size in ['small', 'big', '']:
                        try:
                            if dictSlider[sliderInvert][size] > 0:
                                dictSlider[sliderInvert][size] = -dictSlider[sliderInvert][size]
                        except:
                            pass
        # Fourth loop: Apply the command line weight and subtract the offset
        # Bodies with 0 and 100 will be distorted.
        for slider in sorted(dictSlider):
            if slider in offset[args.base]:
                if slider in arrInvertedSliders:
                    fltOffset = offset[args.base][slider]
                else:
                    fltOffset = -offset[args.base][slider]
            else:
                fltOffset = 0
            if len( dictSlider[slider] )==2 and dictSlider[slider]['small']!=dictSlider[slider]['big']:
                if args.weight=="range":
                    strLine += slider + "@" + str(dictSlider[slider]['small']+fltOffset) + ":" + str(dictSlider[slider]['big']+fltOffset) + ", "
                elif args.weight=="small" or args.weight=="big":
                    strLine += slider + "@" + str(dictSlider[slider][args.weight]+fltOffset) + ", "
                else:
                    strLine += slider + "@" + str(round(dictSlider[slider]['small']+(dictSlider[slider]['big']-dictSlider[slider]['small'])*(float(args.weight)/100)+fltOffset, 2)) + ", "
            else:
                strLine += slider + "@" + str(dictSlider[slider].itervalues().next()+fltOffset) + ", "
        strLine = strLine[:-2]
        if strText=="":
            strText = strLine
        else:
            strText = strText + "\n" + strLine
    print strText
    try:
        r = Tk()
        r.withdraw()
        r.clipboard_clear()
        r.clipboard_append(strText)
    except:
        pass

Appendix II: Kitsune's Tutorial for Manual Conversion

 

 

Note: I have different set of base values, based on the default values of the CBBE body preset. Consider substituting these values.

Link to comment
Nice idea in solution 2, I ended up using a similar solution, built everything in curvy and then used racemenu to make the changes (note: racemenu sliders are correct, there are no inverted values), after that, just copy the racemenu values in the bodygen file and done (thanks OneTweak and his alt-tabbing function).

 

Fortunately for me, I didn't need too much body variation (vanilla has only 1 to begin with) I just made 1 body for each race plus 1 body for elders and 1 fat body.

 

PS.: Take into account that some bodyslide presets set sliders to all negative values (in CBBE Curvy: doubleMelon, bigButt and belly among others) so the offset for them should be negative too (since you have to substract the 50% of those sliders too)

Link to comment

A few points i need to make

1. BreastPerkiness and ButtShape2 are not inverted as far as I can see, you can test this by loading Skyrim and moving the Racemenu body sliders, they function exactly as Bodygen does (it's how i found all the inverted sliders)

 

2. if you try to use a non zeroed slider body, you can risk having weird morphs like breast that cave inside the body, or impossibly thin hips (i tried it before settling on the zeroed slider method)

 

3. if possible someone should message the bodygen creator and ask if he can add a filter for weight so the system checks the npc's weight, it does have race filtering (I can't get it to work though T_T) so weight filtering should be possible right?
ex. at 0-30 weight use A,B,C preset (thin and petite ones).

at 31-60 use D,E,F (normal cbbe body.

and at 61-100 use G,H,I (anime tig ol bitties)

 

4. instead of "All|NordFemale" use all|female (idk why race filtering doesn't work but if its ever fixed "All|NordFemale" will only morph nords)

 

5. the one weakness of bodygen is it seems to only apply itself to female actors, if a male character has its sex changed via console or mod then that actor will not be edited" (not a big deal but should be noted)

 

love all the work you are putting into this man and appreciate the video links ^_^

Link to comment

A few points i need to make

1. BreastPerkiness and ButtShape2 are not inverted as far as I can see, you can test this by loading Skyrim and moving the Racemenu body sliders, they function exactly as Bodygen does (it's how i found all the inverted sliders)

 

2. if you try to use a non zeroed slider body, you can risk having weird morphs like breast that cave inside the body, or impossibly thin hips (i tried it before settling on the zeroed slider method)

 

3. if possible someone should message the bodygen creator and ask if he can add a filter for weight so the system checks the npc's weight, it does have race filtering (I can't get it to work though T_T) so weight filtering should be possible right?

ex. at 0-30 weight use A,B,C preset (thin and petite ones).

at 31-60 use D,E,F (normal cbbe body.

and at 61-100 use G,H,I (anime tig ol bitties)

 

4. instead of "All|NordFemale" use all|female (idk why race filtering doesn't work but if its ever fixed "All|NordFemale" will only morph nords)

 

5. the one weakness of bodygen is it seems to only apply itself to female actors, if a male character has its sex changed via console or mod then that actor will not be edited" (not a big deal but should be noted)

 

love all the work you are putting into this man and appreciate the video links happy.png

 

1. Gotcha. I will give it a try and edit my post with other corrections.

 

The inverted sliders do not change with BodySlide preset, do they?

 

2. Yes, you absolutely do.

 

3. Well, I looked at the TED5Edit script to see if we can randomize using stuff like that - weight, level, etc... Would make sense... I am not sure, that could be another. Who is the bodygendata creator anyway?

 

I will post more after some experimenting.

 

And no worries, I am just trying to understand. It could be such a great tool if understood properly.

 

Link to comment

 

3. if possible someone should message the bodygen creator and ask if he can add a filter for weight so the system checks the npc's weight, it does have race filtering (I can't get it to work though T_T) so weight filtering should be possible right?

ex. at 0-30 weight use A,B,C preset (thin and petite ones).

at 31-60 use D,E,F (normal cbbe body.

and at 61-100 use G,H,I (anime tig ol bitties)

 

 

I experimented with the zeroed sliders (Solution I) for a bit. I seem to get really good results with ALSL body, Natural Curves body types and CBBE Variant Bodies. I will test a bit more, and then perhaps testing Solution II.

 

However, combined with a good filter, Solution I could be the better way to go. There is a TES5Edit script that randomizes by race. I was thinking that it could be advanced to take race as base, and then consider weight when adding the BodyGen. (So for example, I could automatically add ALSL_00, ALSL_10, ALSL_20, ... ALSL_100 to templates.ini, and the script could add whichever is closest depending on NPC weight.)

 

This could also include options for different body types for uniques and for auto-level or higher-level characters. Just a thought.

 

I am just wondering if there is any demand for this or if it is just me. If it is just the few people on this forum, then I could just convert the body types you want depending on your base and we could be done.

Link to comment
  • 2 weeks later...

I have a GUI and an executable.

 

For me, this works. But keep in mind that this is only meaningful if:

1) You can already use BodyGen with custom bodies. (Keep in mind that there is a TES5Edit script that randomizes based on race.)

2) You are willing to set your body shape to a static shape: It need to be the same shape for all weights. (Otherwise you get some weird body shapes with this: All sliders will be a sum of the change your body shape and the morph.ini file)

 

Here is the Python code for all who want to see and test. Here is the link to the executable:

https://www.loverslab.com/files/file/3223-body-slide-to-bodygen-converter/

#!/usr/bin/env python2

import argparse
import os
from bs4 import BeautifulSoup
import pyperclip
import re
from decimal import Decimal
import PySide
from PySide.QtCore import *
from PySide.QtGui import *
import sys
import json

matchVersion = re.search('\_v([0-9.]+)', os.path.splitext(sys.argv[0])[0])
if matchVersion:
    version = matchVersion.group(1)

class Preset:
    scale = {0:0, 100:1}
    fltSlope = float(scale[0]-scale[100])/float(0-100)
    fltIntercept = float(float(scale[100])-fltSlope*100)
    dictDefaultSliderValues = {}
    dictDefaultSliderValues['small'] = {"Breasts":0, "BreastsSmall":1, "BreastGravity":0.4, "NippleDistance":1, "NippleSize":0.6, "Arms":1, "ShoulderWidth":0.75, "Waist":0.25, "ButtCrack":1, "Butt":0, "ButtSmall":1, "Thighs":0.1, "Legs":0.25, "Ankles":1, "Belly":0.4}
    dictDefaultSliderValues['big'] = {"Breasts":0.8, "BreastsSmall":1, "BreastGravity":0.7, "NippleDistance":1, "NippleSize":0.8, "Arms":0.25, "ShoulderWidth":0.65, "Waist":1, "ButtCrack":1, "Butt":1, "ButtSmall":1, "Thighs":0.2, "Legs":1, "Ankles":1, "Belly":0.3}
    arrInvertedSliders = [ "Ankles", "Arms", "Breasts", "BreastsSmall", "Belly", "BigButt", "Butt", "ButtCrack", "ButtSmall", "DoubleMelon", "Legs", "NippleDistance", "NippleSize", "ShoulderWidth" ]

    def __init__(self, preset, file):
        self.name = preset.get('name')
        self.path, self.filename = os.path.split(file.name)
        self.sliders = {}
        self.i = 0
        for slider in preset.find_all('SetSlider'):
            fltSliderValue = self.fltSlope*float(slider.get('value'))+self.fltIntercept
            if slider.get('name') in self.sliders.keys():
                self.sliders[slider.get('name')][slider.get('size')] = fltSliderValue
            else:
                self.sliders[slider.get('name')] = { slider.get('size'): fltSliderValue }
        # Add the default CBBE values where missing
        for size in ['small', 'big']:
            for sliderDefault in self.dictDefaultSliderValues[size]:
                if not sliderDefault in self.sliders.keys():
                    self.sliders[sliderDefault] = { size: self.dictDefaultSliderValues[size][sliderDefault] }
                elif not size in self.sliders[sliderDefault].keys():
                    self.sliders[sliderDefault][size] = self.dictDefaultSliderValues[size][sliderDefault]
        # If only one weight exists, add the same other weight.
        for slider in self.sliders:
            if len(self.sliders[slider])<2:
                self.sliders[slider]['small'] = self.sliders[slider].itervalues().next()
                self.sliders[slider]['big'] = self.sliders[slider]['small']
        # Invert the inverted sliders
        for sliderInvert in Preset.arrInvertedSliders:
            if sliderInvert in self.sliders.keys():
                for size in ['small', 'big']:
                    if self.sliders[sliderInvert][size] > 0:
                        self.sliders[sliderInvert][size] = -self.sliders[sliderInvert][size]

    def __len__(self):
        return len(self.sliders)
    
    def __getitem__(self, key):
        if isinstance(key, (list, tuple)):
            key=list(key)
            if len(key)>2 or len(key)<0:
                raise KeyError
        elif isinstance(key, (basestring,int)):
            key = (key,)
        else:
            raise KeyError
        if len(key)==2:
            slider = str(key[0])
            try:
                valueBig = Decimal(self.sliders[slider]['big'])
            except:
                valueBig = 0
            try:
                valueSmall = Decimal(self.sliders[slider]['small'])
            except:
                valueSmall = 0
            if key[1]=='small':
                key[1] = 0
            elif key[1]=='big':
                key[1] = 100
            else:
                key[1] = Decimal(key[1])
            weight = key[1]/100
            return round(valueSmall + (valueBig - valueSmall) * weight, 2)
        elif len(key)==1:
            if int(key[0])>=0 and int(key[0])<=100:
                dict = {}
                for slider in self.sliders.keys():
                    dict.update(self[(slider,key[0])])
                return dict
            else:
                slider = str(key[0])
                try:
                    valueBig = Decimal(self.sliders[slider]['big'])
                except:
                    valueBig = 0
                try:
                    valueSmall = Decimal(self.sliders[slider]['small'])
                except:
                    valueSmall = 0
                return [round(valueSmall, 2), round(valueBig, 2)]
        else:
            raise KeyError

    def __iter__(self):
        for slider in self.sliders:
            yield slider

    def __contains__(self, slider):
        return (slider in self.sliders)

    def __add__(self, other):
        if other.weight is not None:
            weight = other.weight
        else:
            weight = 50
        for slider in self.sliders:
            for size in ['small', 'big']:
                self.sliders[slider][size] += other[(slider,weight)]
        return self
    
    def __sub__(self, other):
        if other.weight is not None:
            weight = other.weight
        else:
            weight = 50
        for slider in self.sliders:
            for size in ['small', 'big']:
                self.sliders[slider][size] -= other[(slider,weight)]
        return self

    def __sizeof__(self):
        return sizeof(self.sliders)

    def __repr__(self):
        return repr(self.sliders)

    def __str__(self):
        try:
            weight = self.weight
        except:
            raise InputError("weight attribute needs to be set before the preset can be converted to string.")
        rx = re.compile('([\s.])')
        try:
            strPreset = rx.sub(r"", self.name) + "_" + str(weight).zfill(2) + "="
        except:
            strPreset = rx.sub(r"", self.name) + "="
        for slider in sorted(self):
            try:
                strPreset += slider + "@" + str(self[(slider, weight)]) + ", "
            except:
                strPreset += slider + "@" + str(self[(slider, 0)]) + ":" + str(self[(slider, 100)]) + ", "
        return strPreset[0:-2]

class Presets:
    
    def __init__(self, folderRoot, folderPresets):
        self.dict = {}
        self.folderRoot = folderRoot
        self.folderPresets = folderPresets
    
    def load(self, filename):
        file = open(filename, "r")
        soup = BeautifulSoup(file, "lxml-xml")
        for preset in soup.find_all('Preset'):
            self.dict[preset.get('name')] = Preset(preset, file)
            
    def load_all(self):
        for filename in os.listdir(self.folderRoot+self.folderPresets):
            if filename.endswith(".xml"):
                self.load(self.folderRoot+self.folderPresets+filename)
            
    def __getitem__(self, key):
        try:
            return self.dict[key]
        except KeyError:
            if os.path.isfile(self.folderRoot+self.folderPresets+key+".xml"):
                self.load(self.folderRoot+self.folderPresets+key+".xml")
            try:
                self.dict[key]
            except:
                if os.path.isfile(self.folderRoot+self.folderPresets+"CBBEPresets.xml"):
                    self.load(self.folderRoot+self.folderPresets+"CBBEPresets.xml")
            try:
                self.dict[key]
            except:
                self.load_all()
            return self.dict[key]

    def __iter__(self):
        for preset in sorted(self.dict):
            yield self.dict[preset]

    def __contains__(self, preset):
        if type(preset) is Preset:
            return (preset.name in self.dict.keys())
        else:
            return (preset in self.dict.keys())
            
class Form(QDialog):
 
    def __init__(self, parent=None):
        super(Form, self).__init__(parent)
        self.memory = Memory(self)
        self.memory.listOfThingsToRemember = ['folderSkyrimRoot', 'folderPresets', 'strBasePreset', 'intWeight', 'listPresetsToBeConverted', 'list_of_geometryAtClose']
        self.setWindowTitle('BodySlide to BodyGen Converter Version: '+version)
        self.folderSkyrimRoot = ''
        self.folderPresets = 'Data\\CalienteTools\\BodySlide\\SliderPresets\\'
        self.presetsAll = Presets(self.folderSkyrimRoot, self.folderPresets)
        self.presetsAll.load_all()
        # CSS
        self.setStyleSheet("\
                QLabel { font-weight: bold } \
                QTextArea { font-family: Courier } \
                QLineEdit[readOnly='true'] { background-color: rgb(%s); border: none; }\
            " % self.rgbColorBackground())
        # Create widgets
        self.widgetBasePreset = QComboBox()
        self.widgetPresetsToBeConverted = QListWidget()
        self.widgetPresetsToBeConverted.setSelectionMode(QAbstractItemView.ExtendedSelection)
        for preset in self.presetsAll:
            self.widgetBasePreset.addItem(preset.name+" ("+preset.filename+")")
            self.widgetPresetsToBeConverted.addItem(preset.name+" ("+preset.filename+")")
        if self.memory.can_recall('strBasePreset'):
            self.widgetBasePreset.setCurrentIndex(self.widgetBasePreset.findText(self.strBasePreset))
        if self.memory.can_recall('listPresetsToBeConverted'):
            for index in range(self.widgetPresetsToBeConverted.count()):
                if str(self.widgetPresetsToBeConverted.item(index).text()) in self.listPresetsToBeConverted:
                    self.widgetPresetsToBeConverted.item(index).setSelected(True)
        self.label_of_widgetBasePreset = QLabel("Base Preset")
        self.label_of_widgetBasePreset.setBuddy(self.widgetBasePreset)
        self.labelExplanation = QGraphicsTextItem("\
                Base preset is the preset that you used on BodySlide to generate all your body meshes in the game.\
                Choose the same preset here. Remember that you need to do your BodyGen conversions again <em>every time</em> you generate \
                new meshes in the game.\
                The more variation there is between the min and max weight bodies in your base preset, the more distorted the conversions will be.\
                For the ideal result, go with a <em>static preset</em>: Set the minimum and maximum of you BodySlide sliders to the same value.\
                (This way, the game weight will be completely ignored for body generation purposes.)\
            ")
        self.label_of_widgetPresetsToBeConverted = QLabel("Presets To Be Converted", parent=self.widgetPresetsToBeConverted)
        self.sliderWeight = QSlider(Qt.Horizontal)
        self.sliderWeight.setRange(0, 100)
        self.sliderWeight.setTickInterval(1)
        self.sliderWeight.valueChanged.connect(self.display_weight)
        if self.memory.can_recall('intWeight'):
            self.sliderWeight.setValue(self.intWeight)
        self.label_of_sliderWeight = QLabel("Weight")
        self.label_of_sliderWeight.setBuddy(self.sliderWeight)
        self.lineWeight = QLineEdit()
        self.lineWeight.setReadOnly(True)
        self.textBodyGen = QTextEdit()
        self.textBodyGen.setReadOnly(True)
        self.textBodyGen.setLineWrapMode(QTextEdit.WidgetWidth)
        self.buttonConvert = QPushButton("Convert")
        self.buttonConvert.setDefault(True)
        self.buttonConvert.clicked.connect(self.convert)
        self.buttonClear = QPushButton("Clear")
        self.buttonClear.clicked.connect(self.clear)
        # Create layout and add widgets
        hboxButtons = QHBoxLayout()
        hboxButtons.addWidget(self.buttonClear)
        hboxButtons.addWidget(self.buttonConvert)
        hboxButtons.addStretch(1)
        hboxSlider = QHBoxLayout()
        hboxSlider.addWidget(self.label_of_sliderWeight)
        hboxSlider.addWidget(self.sliderWeight)
        hboxSlider.addWidget(self.lineWeight)
        hboxSlider.addStretch(1)
        hboxBasePreset = QHBoxLayout()
        hboxBasePreset.addWidget(self.label_of_widgetBasePreset)
        hboxBasePreset.addWidget(self.widgetBasePreset)
        vboxBottom = QVBoxLayout()
        vboxBottom.addLayout(hboxSlider)
        vboxBottom.addLayout(hboxButtons)
        hboxBottom = QHBoxLayout()
        hboxBottom.addLayout(vboxBottom)
        vboxLeft = QVBoxLayout()
        vboxLeft.addLayout(hboxBasePreset)
        vboxLeft.addWidget(self.label_of_widgetPresetsToBeConverted)
        vboxLeft.addWidget(self.widgetPresetsToBeConverted)
        vboxLeft.addLayout(hboxBottom)
        vboxRight = QVBoxLayout()
        vboxRight.addWidget(self.textBodyGen)
        grid = QGridLayout()
        grid.addLayout(vboxLeft, 1, 0)
        grid.addLayout(vboxRight, 1, 1)
        # Set dialog layout
        self.setLayout(grid)

    def __setattr__(self, name, value):
        self.__dict__[name] = value
        try:
            if name in self.memory.listOfThingsToRemember:
                self.memory.remember(name)
        except AttributeError:
            pass
            
    def closeEvent(self, event):
        self.list_of_geometryAtClose = [ self.frameGeometry().x(), self.frameGeometry().y(), self.frameGeometry().width(), self.frameGeometry().height() ]
        event.accept()
 
    def convert(self):
        self.strBasePreset = self.widgetBasePreset.currentText()
        self.intWeight = self.sliderWeight.value()
        presetBase = self.presetsAll[self.strBasePreset.split(" (")[0]]
        if self.memory.can_recall('intWeightBasePreset'):
            presetBase.weight = self.intWeightBasePreset
        else:
            presetBase.weight = 50
        self.listPresetsToBeConverted = [x.data() for x in self.widgetPresetsToBeConverted.selectedIndexes()]
        presetsToBeConverted = []
        for strPreset in self.listPresetsToBeConverted:
            presetsToBeConverted.append(self.presetsAll[strPreset.split(" (")[0]])
        strText = ""
        for preset in presetsToBeConverted:
            preset -= presetBase
            preset.weight = self.intWeight
            strLine = str(preset)
            if strText=="":
                strText = strLine
            else:
                strText = strText + "\n" + strLine
        self.textBodyGen.insertPlainText(strText+"\n")
        try:
            pyperclip.copy(strText)
        except:
            pass

    def clear(self):
        self.textBodyGen.clear()
    
    def display_weight(self):
        self.lineWeight.setText(str(self.sliderWeight.value()))
        
    def rgbColorBackground(self):
        color = self.palette().color(QPalette.Background)
        return "%d, %d, %d " % (color.red(), color.green(), color.blue())

class Memory:

    def __init__(self, parent):
        self.parent = parent
        self.filename = os.path.splitext(sys.argv[0])[0]+".json"
        try:
            with open(self.filename, 'r') as f:
                self.parameters = json.load(f)
                self.existsFile = True
        except:
            self.existsFile = False
            self.parameters = {}
            # Built-in default values
            self.parameters['intWeight'] = 50
            
    def remember(self, param):
        self.parameters[param] = getattr(self.parent, param)
        self.save()
        
    def can_recall(self, param):
        try:
            self.recall(param)
            return True
        except KeyError:
            return False

    def recall(self, param):
        setattr(self.parent, param, self.parameters[param])
        
    def forget(self, param):
        del self.parameters[param]
        self.save()
            
    def save(self):
        with open(self.filename, 'w') as f:
            json.dump(self.parameters, f)

            
# Create the Qt Application
app = QApplication(sys.argv)
# Create and show the form
formConverter = Form()
formConverter.show()
if formConverter.memory.can_recall('list_of_geometryAtClose'):
    formConverter.setGeometry(*formConverter.list_of_geometryAtClose)
formConverter.display_weight()
# print formConverter.frameGeometry().width()
# Run the main Qt loop
sys.exit(app.exec_())

Link to comment

Archived

This topic is now archived and is closed to further replies.

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...