Jump to content

AAF_MainQuestScript.psc:1049 copyMorphs(Actor a, Actor b) bugfix


Recommended Posts

Hi @dagobaking,

 

(hope it's OK to make a bug report here?)

 

the copyMorphs function has the following bug:

 

if multiple mods change a morphvalue, then the last mod in the keywords[] array will overwrite the value of all other mods (including the values set in looksmenu).

Obviously there are multiple solutions to this issue, but (because i don't know how bodygen blends multiple morph values: could be a simple addition, could be more complex)
here's a solution which just copies every single morph+keyword from one actor to another (usually player to doppelganger):

 

1049 Function copyMorphs(Actor a, Actor b)
   1
   2 |   If looksMenu_installed == 1 && disable_morph_copying == false
   3
   4 |   |   Bool isFemale = a.GetLeveledActorBase().GetSex() == 1
   5
   6 |   |   String[] morphs = BodyGen.GetMorphs(a, isFemale)
   7
   8 |   |   BodyGen.RemoveAllMorphs(b, isFemale)
   9
  10 |   |   int i = 0
  11 |   |   While i < morphs.Length
  12 |   |   |   Keyword[] keywords = BodyGen.GetKeywords(a, isFemale, morphs[i])
  13 |   |   |   int t = 0
  14 |   |   |   While t < keywords.Length
  15 |   |   |   |   float morphValue = BodyGen.GetMorph(a, isFemale, morphs[i], keywords[t])
  16 |   |   |   |   BodyGen.SetMorph(b, isFemale, morphs[i], keywords[t], morphValue)
  17 |   |   |   |   t = t + 1
  18 |   |   |   EndWhile
  19 |   |       i = i + 1
  20 |   |   EndWhile
  21
  22 |   |   BodyGen.UpdateMorphs(b)
  23 |   EndIf
  24 EndFunction

This obviously has the drawback that the preexisting morphs on actor b are being deleted.
Let me know if this is an issue and i'll change the code s.t. every morph from every mod is being added together if you prefer that.
 

Link to comment
16 hours ago, yeahhowaboutnooo said:

This obviously has the drawback that the preexisting morphs on actor b are being deleted.
Let me know if this is an issue and i'll change the code s.t. every morph from every mod is being added together if you prefer that.

Thank you for pointing this out.

 

Deleting morphs on actor b shouldn't be an issue because this is only used to re-create the morphs on the PC onto a temporary body-double that gets created on-the-fly as needed anyway.

 

This might resolve something related to keywords. But, in terms of which morph gets applied, I think it will still result in only the last one referenced applying?

 

Have you tried compiling and testing in game to see what happens?

Link to comment
41 minutes ago, dagobaking said:

Have you tried compiling and testing in game to see what happens?

I've been using the code i posted for the past 2 days or so and all morphs (Looksmenu + BDH + FPE) have been applied to the doppelganger.
There was one AAF scene once, where no morphs were applied at all to the doppelganger, not sure if this had to do with when the freecam gets activated.

 

45 minutes ago, dagobaking said:

But, in terms of which morph gets applied, I think it will still result in only the last one referenced applying?

No, because afaik morphs in bodygen are always a morph-keyword pair:
the original code always sets all morphs to the "AAF_MorphKeyword", which obviously gets overwritten if there are multiple keywords on actor a setting the same morph.

 

the code i posted circumvents this by copying the keyword as well -> it's now up to bodygen to blend these morphs (currently it seems to be a simple addition, but iirc in skyrim's version it's configurable. maybe that'll come later as well for FO4?) -> in any case: this way the blending of morphs is the same for the actual player character and the doppelganger

FYI: the updateMorphs function is the only other usage of the "AAF_MorphKeyword", but that function seems to be never used:

 

Function updateMorphs(Var[] actorFormID, Var[] isFemale, Var[] morph, Var[] value, Bool updateArmor = False)
AAF Beta 154-31304-154b-1603335756/Scripts/Source$ grep -rn . -ie updateMorphs
./User/AAF/AAF_MainQuestScript.psc:1071:                BodyGen.UpdateMorphs(b)
./User/AAF/AAF_MainQuestScript.psc:2522:Function updateMorphs(Var[] actorFormID, Var[] isFemale, Var[] morph, Var[] value, Bool updateArmor = False)
./User/AAF/AAF_MainQuestScript.psc:2535:                        BodyGen.UpdateMorphs(targetActor[i])

 

Link to comment
3 hours ago, yeahhowaboutnooo said:

it's now up to bodygen to blend these morphs (currently it seems to be a simple addition

Doesn't seem to be true based on F4EE sources from expired6978's github [ https://github.com/expired6978/F4SEPlugins ]. To be honest, making it sum over all values seems trivial, if there's no need for configuration. There might be more to this under the surface as Expired hasn't done that..

...
MorphApplicator morpher(geometry, newBlock, newBlock, [&](std::vector<Morpher::Vector3> & verts)
		{
			SimpleLocker locker(&m_morphLock);
			for(auto & actorMorph : *actorMorphs)
			{
				float effectiveValue = actorMorph.second->GetEffectiveValue();
				if(effectiveValue == 0.0f)
					continue;

				auto morph = morphMap->GetVertexData(*actorMorph.first);
				if(!morph)
					continue;

				bool outOfBounds = morph->ApplyMorph(geometry->numVertices, (NiPoint3*)&verts.at(0), effectiveValue);
				if(outOfBounds) {
					_WARNING("%s - Shape: %s Morph: %s contained out of bounds vertices\t[%s]", __FUNCTION__, morphableShape->shapeName.c_str(), actorMorph.first->c_str(), morphableShape->morphPath.c_str());
				}
			}
		});
...
  
float UserValues::GetEffectiveValue()
{
	SimpleLocker locker(&m_morphLock);

	auto maxIt = std::max_element(begin(), end());
	if(maxIt != end()) {
		return maxIt->second;
	}

	return 0.0f;
}

 

Link to comment
19 hours ago, yeahhowaboutnooo said:

I've been using the code i posted for the past 2 days or so and all morphs (Looksmenu + BDH + FPE) have been applied to the doppelganger.
There was one AAF scene once, where no morphs were applied at all to the doppelganger, not sure if this had to do with when the freecam gets activated.

 

No, because afaik morphs in bodygen are always a morph-keyword pair:
the original code always sets all morphs to the "AAF_MorphKeyword", which obviously gets overwritten if there are multiple keywords on actor a setting the same morph.

 

the code i posted circumvents this by copying the keyword as well -> it's now up to bodygen to blend these morphs (currently it seems to be a simple addition, but iirc in skyrim's version it's configurable. maybe that'll come later as well for FO4?) -> in any case: this way the blending of morphs is the same for the actual player character and the doppelganger

FYI: the updateMorphs function is the only other usage of the "AAF_MorphKeyword", but that function seems to be never used:

 


Function updateMorphs(Var[] actorFormID, Var[] isFemale, Var[] morph, Var[] value, Bool updateArmor = False)

AAF Beta 154-31304-154b-1603335756/Scripts/Source$ grep -rn . -ie updateMorphs
./User/AAF/AAF_MainQuestScript.psc:1071:                BodyGen.UpdateMorphs(b)
./User/AAF/AAF_MainQuestScript.psc:2522:Function updateMorphs(Var[] actorFormID, Var[] isFemale, Var[] morph, Var[] value, Bool updateArmor = False)
./User/AAF/AAF_MainQuestScript.psc:2535:                        BodyGen.UpdateMorphs(targetActor[i])

 

I would be interested in seeing a series of tests with screenshots verifying all of this.

 

My understanding of the reason for these keywords is to disable the morphs if/when the mod that created them gets uninstalled.

 

Also, I could be wrong. But, I don't think that morph commands on the same morph "blend". They just over-write the last one anyway. Otherwise, the schlong angle morphs that were used for a long time never would have worked as implemented.

Link to comment

From the SetMorph source:

 

; Sets the specified actors morph and key to value (LooksMenu internally uses None for the keyword)
; This keyword should be a Keyword in a plugin so that when the plugin is uninstalled the morph is removed automatically
Function SetMorph(Actor akActor, bool isFemale, string morph, keyword akKeyword, float value) global native

 

This seems to suggest that the keyword is for morph removal after the mod is uninstalled. "Setting to a value", to me, suggests no blending. Though it isn't 100% clear.

Link to comment
Spoiler

ingame PC:

01-ingame.thumb.png.39f77cca16e1b736591445bbec814877.png

 

aaf vanilla-doppelganger:

169392451_02-aafvanilla.thumb.png.7ad9f9ced5ec3604659a6fb093b4d96f.png

 

aaf patched-doppelganger:

815925047_03-aafpatched.thumb.png.7fdd6a784892e28fe8578cc4067bcda4.png

 

 

I wouldn't be surprised if the behavior of F4EE has changed in the past 3 years since the last github-commit.

 

In any case: The code i posted leaves it up to F4EE how to display different values for the same morph from different keywords.

 

Let me know if you have more questions and/or want more tests :)

 

One last thing (which i'm not sure is a problem but wanted to mention nevertheless?

; Removes all morphs on the specified actor (Actor becomes applicable for BodyGen when they are no active morphs, use a dummy morph to prevent)
Function RemoveAllMorphs(Actor akActor, bool isFemale) global native

i interpret this as: if you call removeallmorphs, immediately follow it with a setmorph call s.t. bodygen does not run

^ i don't think the above is an issue because:

a) setmorph is being called (almost) immediately (for copying the morphs from actor a)

b) in case actor a does not have any morphs applied: it's safe to assume the user doesn't use the bodygen feature (because if the user were using bodygen, there would be morphs applied to the PC)

 

i use bodygen myself and haven't seen any random morphs being applied (yet)

Link to comment
Quote

I wouldn't be surprised if the behavior of F4EE has changed in the past 3 years since the last github-commit.

Sure but I don't see any mention of such in nexus changelog, a setting determing used reduce function in f4ee.ini or strings exported from f4ee.dll.

If there's something to control this, I'd be very intrested to know.

 

For reference this is from nioverride.ini for Skyrim LE

 

; Determines scaling mode

; 0 - Multiplicative

; 1 - Averaged

; 2 - Additive

; 3 - Largest

iScaleMode=3

 

; Determines combination mode for BodyMorph

; 0 - Additive

; 1 - Averaged

; 2 - Largest

iBodyMorphMode=0

 

and this is from f4ee.ini 1.6.20

 

[BodyMorph]

; Enables loading of tri files and morphing meshes

bEnable=1

; Enables loading bodygen files to randomize actor sliders

; Disabled by default due to bugs with invis bodies on actors starting dead

bEnableBodyGen=0

uMaxCache=2147483648

bParallelShapes=1

 

I'll do a quick test to see how it behaves when multiple mos set same morhp under different keywords.

Link to comment

Test setup

Function Preg1() Global
    Actor a = Game.GetPlayer()
    ; AAF_MorphKeyword [KYWD:09000F9E] AAF.esm
    Keyword m = Game.GetFormFromFile(0x09000F9E, "AAF.esm") as Keyword
    BodyGen.SetMorph(a, true, "Belly Pregnant", m, 0.5)
    BodyGen.UpdateMorphs(a)
EndFunction

Function Preg2() Global
    Actor a = Game.GetPlayer()
    ; FPFP_Keyword [KYWD:36001743] FP_FamilyPlanningEnhanced.esp
    Keyword m = Game.GetFormFromFile(0x36001743, "FP_FamilyPlanningEnhanced.esp") as Keyword
    BodyGen.SetMorph(a, true, "Belly Pregnant", m, 0.5)
    BodyGen.UpdateMorphs(a)
EndFunction

They are called from console in order > Preg1 > Preg2 with CallGlobalFunction "QA.PregN".

Keyword hosting mods are loaded during these tests.

 

Test 1:

After calling both functions from console, behavior matches either taking max from morph values or latest value. There's no addition or multiplication or averaging.

 

Test 2:

Change Preg2 to apply morph at strength 0.25. Hypothesis: After calling Preg1 then Preg2, morph matches that of Preg1.

Results: hypothesis holds, morphed at values equal to 0.5.

 

Test 3:

Same setup as in test 2 but swap function call order. Result: morphs on actor are changed if new morph has larger value then current max.

Adding morphs in order 0.25 -> 0.5 results in morphs applied that matches visually value of max([0.25, 0.5]).

So it looks like looksmenu doesn't blend but applies morph with largest value found for each morph key.

Therefore, it should be fine to let looksmenu handle this.

However this might be a slight problem if user wants to apply morphs with negative values.

Link to comment

Thank you both.

 

I asked the author directly. He said that the purpose of the keywords is to automatically disable the morphs when the mod is uninstalled. But, he did also say that there is an averaging mechanism that can be changed in an ini (and he didn't remember what the default was).

 

I've asked some follow up questions because I'm still not sure how it gets applied. For example, if you use the same keyword on the same morph does it replace? Or, is every single call averaged regardless of keyword? I don't see how it could be the latter given that we've had morphSets that go up and down already that worked like a replacement.

 

requiredname65's test appears to show that the default "averaging" method is to use the largest value. If that is the case, the only explanations for yeahhowaboutnoo's visual test is that:

A) Some other plugin we don't know about in that build impacts the PC. There are many unique player type mods that can cause issues.

B) Some smaller value (from where though?) replaces a larger value because it uses the same keyword and is the last setmorph applied.

 

Ultimately, I think I will make this adjustment even though I'm not 100% sure about it. Mainly because I can't see any harm. If a morph change originated in some other mod, AAF doesn't really need the morph to be disabled if someone strangely happens to uninstall AAF on a save that occurred during a PC animation (which I believe is blocked anyway).

Link to comment
3 hours ago, dagobaking said:

But, he did also say that there is an averaging mechanism that can be changed in an ini (and he didn't remember what the default was).

Any chance he was talking about Skyrim's Racemenu instead?

3 hours ago, dagobaking said:

.. the only explanations for yeahhowaboutnoo's .. Some smaller value (from where though?) replaces a larger value because it uses the same keyword and is the last setmorph applied.

Based on old sources, for every actor there is a map of "morphnames" to UserValues, where UserValues is map of UInt32 to floats.

UInt32 is keyword's formId. When setMorph is called and passed in keyword is in UserValues map, it's assoc float is replaced. Otherwise a new

one is added. In reverse when keyword is missing, this entry is removed. When UpdateMorphs is called, at one point for each morphs,

UserValues is reduced to single value and then applied on mesh.

 

What seems to happen here is that UserValues map contains two or more elements in such order

(native implementation seems to use unordered_map, 'ordering' is based on hash value of key) where larger value

is retrieved first then smaller etc when map is iterated over. BodyGen.GetKeywords returns those UserValues

keys in that order. So when you do this

		While i < morphs.Length
			Keyword[] keywords = BodyGen.GetKeywords(a, isFemale, morphs[i])
			int t = 0
			While t < keywords.Length
				float morphValue = BodyGen.GetMorph(a, isFemale, morphs[i], keywords[t])
				BodyGen.SetMorph(b, isFemale, morphs[i], AAF_MorphKeyword, morphValue)
				t = t + 1
			EndWhile
		    i = i + 1
		EndWhile

it' looks to be same as BodyGen.SetMorph(b, isFemale, morphs, AAF_MorphKeyword, BodyGen.GetMorph(..., keywords[keywords.length - 1])).

So +1 for the change. But anything having to do with native implementation relies on older sources staying accurate, so let reader beware

and do their own tests. But so far ingame behavior is aligned with assumed implementation.

 

Such a bummer there is no papyrus vm binding for this

void BodyMorphInterface::CloneMorphs(Actor * source, Actor * target)
{
	if(!source || !target)
		return;

	SimpleLocker locker(&m_morphLock);	
	bool isFemale = false;
	TESNPC * npc = DYNAMIC_CAST(source->baseForm, TESForm, TESNPC);
	if(npc)
		isFemale = CALL_MEMBER_FN(npc, GetSex)() == 1 ? true : false;

	auto it = m_morphMap[isFemale ? 1 : 0].find(source->formID);
	if(it != m_morphMap[isFemale ? 1 : 0].end()) {
		m_morphMap[isFemale ? 1 : 0][target->formID] = it->second;
	}
}

 

 

Link to comment
On 12/13/2020 at 3:20 AM, requiredname65 said:

Any chance he was talking about Skyrim's Racemenu instead?

It's possible. Not sure.

On 12/13/2020 at 3:20 AM, requiredname65 said:

Such a bummer there is no papyrus vm binding for this


void BodyMorphInterface::CloneMorphs(Actor * source, Actor * target)
{
	if(!source || !target)
		return;

	SimpleLocker locker(&m_morphLock);	
	bool isFemale = false;
	TESNPC * npc = DYNAMIC_CAST(source->baseForm, TESForm, TESNPC);
	if(npc)
		isFemale = CALL_MEMBER_FN(npc, GetSex)() == 1 ? true : false;

	auto it = m_morphMap[isFemale ? 1 : 0].find(source->formID);
	if(it != m_morphMap[isFemale ? 1 : 0].end()) {
		m_morphMap[isFemale ? 1 : 0][target->formID] = it->second;
	}
}

 

 

I think he mentioned that he would work on a function like this a very long time ago. I guess never completed?

Link to comment

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

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

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue. For more information, see our Privacy Policy & Terms of Use