Jump to content

Soft dependencies without pain


Lupine00

3,505 views

A lot of mods that have soft-dependencies have all kinds of nasty cross-mod issues that cause a CTD at game start, or simply break other mods that are involved in the mess.

 

Often, the problems only manifest when three or more mods are involved.

 

The way to avoid these problems completely is quite simple. The problem is created by a handful of bad patterns, and there is nothing mysterious or magical about it.

There is a solid reason for why it happens, and why it can sometimes be quite complex to reproduce, or for a player to fix when setting up a new LO.

 

In most cases, the cause of problems is a mod that has code to call some foreign mod in a quest script that auto-starts, and it doesn't check whether the ESP is present, it simply calls Game.GetFormFromFile and if it gets a None, it assumes the mod is missing. USLEEP does this FFS! It is the root cause of all that stupid spam it spews on every single game start.

 

This pattern is toxic. Do not ever do this:

Form foo = Game.GetFormFromFile(0x00123456, "Foreign Mod.esp")

If foo != None

    TypeFromSomeOtherMod realFoo = foo As TypeFromSomeOtherMod

    ... do something with realFoo

EndIf

 

 

So what should you do?

 

Follow the five rules to avoid ever referencing types that don't necessarily exist, and your mod should be much more stable with respect to soft-dependencies, even if you rely on a mod that is a confluence of trouble, such as SLIF.

 

 

The Rules

 

1) Put all the mod interactions for a single foreign mod into a single global function. Do not (ever) mix mod interactions between different mods in the same file!

2) Do not interact with a type from a foreign mod in any function other than a global function.

3) Do not access a global function that performs an interaction with a foreign mod without first confirming that its ESP is present.

4) Do not access foreign mods during first start-up of the game.

5) Avoid access to foreign mods during OnLoad and equivalent events.

 

You can see this pattern followed properly in Deviously Cursed Loot.

SLD doesn't follow these patterns properly; there are several errors that need to be fixed.

It seems to get away with it anyway, but it's better to be safe than sorry with Skyrim.

 

 

 

But why?

 

Quite simply, if you reference a type from a script file that doesn't exist, Skyrim doesn't know how to handle it. The type is garbage. It will either CTD, or - if you are lucky - the type will be incompatible with all objects and never take a value apart from None.

 

To be sure we don't reference types that don't exist, we must make sure those types are not present anywhere in a script we might execute when that type's defining script doesn't exist.

And if that defining type is bound to an ESP, it will also need that ESP, so we should check that exists first.

 

The type only properly exists when its script files are present, and the best rule we have for determining the presence of the script files, is the presence of the ESP.

This is important for other reasons too, because we need the ESP to populate pre-defined properties on foreign types.

Clearly both ESP and script need to exist, but checking both creates an absurdly tight binding, and is busywork that requires special Papyrus extensions anyway.

It's good enough to check for the ESP. It's not good enough to skip that check, because an object script without its ESP may be parsed but will still break your game because its an orphan: a class that has no possible instances.

Only global scripts can operate properly without their parent ESP or ESM, because a global script cannot be instanced.

 

By putting the interactions in a global function in a script that contains only global functions, we can be sure that our mod will never try to reference those types unless we explicitly call the global.

The same is not the case for a script attached to a quest. If you attach a script to a quest, its papyrus instance will be created when the quest is started.

If the quest is auto-start, that will be on first load of the game.

 

So, if you reference a foreign type - even if due to control flow, you do not execute that reference - it is still processed by Skyrim when the potential calling script is loaded.

Skyrim must know the type's signature, and to do that it instantiates the type, and to do that, requires an ESP.

Using global scripts stops this, because they are not pre-instantiated, and they are only instantiated when executed, and executed when explicitly called.

 

But we must be sure not to call those globals at sensitive times, of if the scripts (and types) they rely on do not exist, or cannot be instantiated.

 

Thus, we check the ESP of the foreign mod exists before trying to call the globals, and we only call the globals from "safe" functions that will not run on first game startup.

 

I'm simplifying a bit. It's really only an issue with scripts that are bound to objects, but those are the most common soft-dependency. Any other kind of script would be a global, and would have no persistent internal state and no properties. For such scripts the mere existence of the script object is enough. If it's missing, your game will not explode. Though the code that tries to call it won't work and will crash, merely having a reference to it in your own quest won't cause an issue at all, beyond the crashing of your script that is.

 

 

Why must we avoid first game startup?

 

This is important because the foreign types we want to reference may not even be loaded, even if the ESP exists.

Skyrim loads the types when the ESP loads, and normally that happens in strict load order.

But if we reference a type in our script, it will cause the script that defines that type to be loaded before the ESP that instances it.

 

This is not well supported in Skyrim. 

Spoiler

 

It seems like there is a bug in the on-demand loading of types.

The first mod to reference a foreign type will succeed - if the type properly exists that type will be loaded (out of order) at that time.

However, the second mod to reference the type will crash in flames.

My belief is that there is a flag that indicates the script is loaded, and that is set after the first load, but the ESP hasn't been loaded, just the script.

There is a special path that handles the unloaded case, and prevents it all blowing up, but that path is skipped when the flag indicates the script is already loaded.

The result is that the first consumer succeeds, the second consumer (and subsequent) fails, until after the parent mod for the referenced script has been loaded via the normal process.

I suspect also that this can lead to an improperly formed VMAD for the script. The script is loaded, but has an empty VMAD, rather than the proper loading process, which sets up the VMAD and loads the script into it. With a bad VMAD, the script never has anything but None in its object properties. I suspect that trying to reference this broken VMAD prior to loading the parent mod results in a CTD.

This explanation is somewhat speculative, but fits the observed behavior.

There seems to be some indication that there was an attempt to solve the dependency problem by loading on demand, and that code is broken. When exercised there are numerous ways it can leave the game in a corrupt state. On the other hand, if it's used only once, and the load was satisfied, you may get away with it apart from a mod that complains about mismatched types for no obvious reasons, and fails to load its quest properties.

 

 

Either way, we don't know the load order, and whether the other mods have loaded yet. This is why changing load order can sometimes fix issues.

If your mod is not following the pattern cleanly, but loads really late, it might get away with it anyway.

 

OnLoad is an edge case.  OnLoad doesn't run on first load (strange, I know, but I believe this is on purpose so OnLoad can rely on initialization).

So, mods that reference foreign mods in their OnLoad should be safe? They are. Mostly.

In some cases, a player adds a mod during play. That mod may then get picked up by a "naughty" soft-depending mod that checks for soft-deps OnLoad, but the mod just added by the player might not have properly loaded. This is why checking in OnLoad is hazardous. If you don't add mods mid-play, it will be fine. And if you do, you may get away with it if the mod gets properly loaded before the OnLoad for the depending mod runs. SLD seems to get away with it in practice, but you shouldn't push your luck; I see the SLD behavior as a bug that needs to be fixed.

 

 

Practical Examples

 

So, where should we check for foreign mods? I believe the correct answer is in an OnUpdate handler.

 

When OnLoad runs, it should set an int "checkSoftDeps" variable to 1.

It should also set the presence of ALL soft-dep mods to false - keep a boolean flag for each one.

When OnUpdate runs, it has a branch that runs if checkSoftDeps > 0

Inside that branch it increments checkSoftDeps

If checkSoftDeps > 2, then run the handling code to check for soft deps, then reset checkSoftDeps to 0.

This makes it impossible to run the checks prematurely, but only requires one variable.

 

Inside the soft-dep check handler, we need to see if the ESPs are present.

If the ESP index is not 255, then it is, and we can set the presence flag to true.

 

e.g.

 

In OnLoad:

    checkSoftDeps = 1

    slifPresent = False

    aproposPresent = False

 

 

In OnUpdate:

    If checkSoftDeps > 0

        checkSoftDeps += 1

        If checkSoftDeps > 2

            slifPresent = 255 != Game.GetModByName("Sexlab Inflation Framework.esp")

            aproposPresent = 255 != Game.GetModByName("Apropos2.esp")

            checkSoftDeps = 0

        EndIf

    EndIf

 

In code that uses apropos:

 

    If aproposPresent && 255 != Game.GetModByName("Apropos2.esp")

        Actor aproposActor = MyModAproposShim.GetAproposActor()

        ...

    EndIf

 

 

In the Global function file MyModAproposShim:

 

Actor Function GetAproposActor() Global

    Apropos2Actors actorLibrary = Game.GetFormFromFile(0x0002902C, "Apropos2.esp")

    ....

    Return foundActor

EndFunction

 

 

Note how at the point of use, the mod ESP is checked again, even though we have the boolean? That's because the boolean may not get reset fast enough via OnLoad, but it saves us checking GetModByName over and over if the mod is never present.

 

Note how only the global function, safe in its own file contains an actual TYPE from Apropos2: Apropos2Actors.

 

 

When I wrote SLD, I was under the misapprehension that simply not using a type was enough. That is wrong.

If the type is not loaded, and you load a script that references it, you can break the game. Not necessarily though, and that is where the complex results that Monoman wrote about originate. See my comments in the spoiler on first and second consumers.

This topic has actually been written about by engine experts, but the posts are hard to find.

Essentially, if you reference the type, and the mod isn't loaded, you will cause the type to be loaded if it can be. If that goes ok, then your mod may not cause a problem, but you may have now caused a type to load outside of the normal order of load ordering ... consequences of this can result in the type itself breaking, but if it ends up trying to load references to ANOTHER type that is itself later in the LO, and that's where things can go bad. If you start this chain, and any type is missing, Skyrim fails to instantiate the type, and code - in some other mod - may then try to use it anyway, and CTD.

 

Thus if you follow the pattern, you stop other mods that aren't well-behaved, from breaking the game.

 

Some things to think about...

Spoiler

 

If we have a type Foo that is defined in a script file, and that script file is used on two different quests that exist in two different ESPs, how does Skyrim know which one you mean if you use the type?

For example a quest type that is a common base used to create derivative quests.

It doesn't matter. The type is the same in either case, but when you deal with a variable of that type, it will come from one or the other ESP, resolving the ambiguity.

If the type was already loaded by a mod, prior in the LO, that will be the source of the type, until another mod loads the same type and overwrites it.

Why then does it matter that an owning ESP exists when we reference a type in a script? Can Skyrim even figure out how to identify the owner?

 

It probably doesn't, and can't for types that aren't objects: global scripts have no owner. However, non-object types are used in soft-dependencies rather infrequently.

Objects on the other hand, are special. They are easily located, and more importantly, they usually have properties, so Skyrim looks for the owning ESP so it can fill those properties, even if none actually exist. The difference is clear in the ESP in Tes5Edit. 

 

Quests within the ESP have a VMAD (virtual machine adapter) which binds all the scripts, script fragments, and aliases, for the quest and their properties together. The quest is the object, and the types that are on that quest all have that same object instance.

 

Imagine linking to a DD object... Though it's an armor object, it has scripts on it, and thus it has a VMAD. It becomes clear from inspection than any script bound to an object requires a VMAD - that is what makes the script from functional code into object-oriented-code. The only way for a script to have persistent internal state is to be bound to an object. Global scripts can only store persistent state via files, or trickery like StorageUtil, which are, strictly, external.

 

 

19 Comments


Recommended Comments

I'm trying to sum it up for myself:

Basically, we need to be sure that the game has settled itself and all background tasks of the engine have been finished. That means we delay artificially.

 

That implies

  • checking for soft dependencies at any game start is bad
  • checking at onLoad is bad

OnUpdate requires me to register for update, I only need it once, so I registerForSingleUpdate and need to come up with a proper afInterval .  => do we have a common idea for a good afInterval? ?

 

OR:

I could nudge the player somehow to trigger these checks manually and in the best case in an immersive way. (E.g. push the dwemer button, listen for some sound, while in the background, the mod is doing it's thing)

 

Spoiler

Disclaimer: I'm a bloody amateur when it comes to programming :classic_blush: I'm sorry, if I lack a bunch of professional knowledge and come up with stupid questions.

 

Link to comment

Auto start mods create the game start overload problem, and they amplify problems with soft-deps.

SexLab doesn't auto start because it does a lot of work when it starts up.

 

ME's "clever" idea of force-starting SexLab on game start is probably related to various stability issues with ME.

 

The immersive start, or start via story manager is great, as long as you don't overload the story manager with hundreds of quest triggers.

Link to comment
On 1/28/2020 at 4:07 PM, Lupine00 said:

The immersive start, or start via story manager is great, as long as you don't overload the story manager with hundreds of quest triggers.

:classic_happy: It's decided then.

Back to the drawing board ...

*disappears in the do-not-disturb-room*

*receives facepalms from close family members*

Link to comment

I wanted to see if I fully understand this. Take this example of a soft dependency of Devious Devices, done through three files:

 

The first file is a quest script solely for Devious Devices compatibility:

Scriptname DDiCompat Extends Quest

zadLibs DDiLibs = none

Function SetUp()
	DDiLibs = (Game.GetFormFromFile(0x00F624, "Devious Devices - Integration.esm") as Quest) as zadLibs
endFunction

Function Restrain(Actor victim)
	...
	DDiLibs.ManipulateGenericDevice(Victim, Device, true)
endFunction

The second script is a sort of 'master' compatibility script, that would contain links to the other compatibility scripts, and would start and stop individual compatibility quests based on if the requirement is installed.

Scriptname CompatController Extends Quest

DDiCompat Property DDiController Auto

The third script is an actual gameplay script. For example, say I wanted to equip a device onto an actor when a magic effect is applied. The magic effect would be doing other things besides, but wants to add a restraint if DDi is installed. I could do this two ways:

Scriptname RestrainEffect Extends ActiveMagicEffect

CompatController Property Compat Auto

Event OnEffectStart(Actor akTarget, Actor akCaster)
	if(Compat.DDiController.IsRunning())
		Compat.DDiController.Restrain(akTarget)
	endIf
endEvent

Or

Scriptname RestrainEffect extends activemagiceffect

CompatController Property Compat Auto

Event OnEffectStart(Actor akTarget, Actor akCaster)
	if(Compat.DDiController.IsRunning())
		...
		Compat.DDiController.DDiLibs.ManipulateGenericDevice(akTarget, Device, true)
	endIf
endEvent

As I understand it. The second effect script would cause the mod to fail in some circumstances, as I'm accessing the Devious Devices script 'directly' from a gameplay script. So, I should access soft dependencies only through 'liason' functions in the individual compatibility scripts, like in the first effect script. Am I right or, am I horribly confused?

Link to comment
On 2/23/2020 at 9:52 AM, Code Serpent said:

The first file is a quest script solely for Devious Devices compatibility:

Firstly, don't call GetFormFromFile unless you KNOW the file (ESP/M) exists. It spams the log, even if it does no other harm.

Call the ESP check first. USLEEP and some other mods create hundreds of lines of spam in my log every startup because they don't make the checks.

Sure, it allows merges, but mostly it's a pain.

 

Secondly, in your example above, the assignment of DDiLibs occurs in a standard quest file, and references a DD type (zadLibs).

That's created a type dependence and risk.

 

If your mod is not HARD dependent on DD, then you should really isolate all that inside a file full of globals.

 

I see what you're aiming for, which is to not start, or reference, DDCompat unless DD is present.

The first example you give superficially seems safe, but it isn't.

 

 

The second example where you indirect through the DDiLibs type puts the type dependence straight in the using script, and will not be safe at all, ever - which you appreciate.

 

 

I wasn't fooling around when I said you *must* use globals to do this.

Why is using globals necessary you may ask?

 

It's more bother for you, because they can't store any state, and you have to design the state storage to be separate (actually you can have state, but it has to be in StorageUtil, not in script variables).

 

Yes, globals are more bother, but safer ... because a global is only ever loaded if its executed. Merely referencing a global is safe.

 

Quests however, can be loaded even if they aren't running, if you reference the quest TYPE anywhere else.

 

And that's exactly what you have done with:

CompatController Property Compat Auto

The reference to Compat is right there, in your consumer script. That will cause Compat to be loaded, and that will cause DDi to be loaded and badoom, now you have a binding to DDi at load time, to types that may not even exist.

 

That you can't compile this code without DDi installed may be a hint there's a problem. 

You may have similar compile problems with globals, but they aren't the same.

A global cannot infect the caller with a type binding... Well, it can, but only if it has alien types in its prototype.

If your globals only use vanilla types, or types that come from your own mod, it's safe.

 

e.g.

function MyGlobal(Int count, Form target) ... harmless, no alien types

function MyGlobal(DDiEffect fx, _SL_Thread thread) ... totally not harmless, binds you to two different scripts.

 

 

Now, even if you had been tricky and made Compat a vanilla Quest, and used some bizarre SetStage trick to call it, it still wouldn't be safe because of the IsRunning check.

That quest could be 'running' but DD could be removed. You don't know at runtime.

DD might have been there once, but that doesn't stop a player removing it and loading to try and make a clean save.

Their next step is to clean scripts, but first they want to save without DD present.

That takes your mod from simply losing functionality cleanly after an uninstall to a likely CTD scenario.

Yes, mod removal is an outlier case, but it's just one more reason to do it exactly as I describe and not change the pattern without fully understanding the pattern.

Link to comment

Ok, I think I was confused by what you meant by a 'global' script.

 

I originally thought you meant a global script as some master 'controller' script, which are usually attached to quests. But, based on your last post, a global script is a script that is not attached to any game object, contains no properties, and is only referenced during runtime. Tell me if I'm right or wrong here.

 

Taking the last example again, what I should have is a loose script for DDi integration. And I merely call that function from any gameplay file. Also, I shouldn't call anything from this script unless I'm sure the esp is loaded. So, I can't do this:

Scriptname DDiCompat hidden

bool CheckInstalled()
	return Game.GetModByName("Devious Devices - Integration.esm") != 255
endFunction

Function Restrain(Actor victim)
	if(CheckInstalled)
		zadLibs DDiLibs = (Game.GetFormFromFile(0x00F624, "Devious Devices - Integration.esm") as Quest) as zadLibs
		...
		DDiLibs.ManipulateGenericDevice(Victim, Device, true)
	endIf
endFunction
Scriptname RestrainEffect extends activemagiceffect

Event OnEffectStart(Actor akTarget, Actor akCaster)
	DDiCombat.Restrain(akTarget)
endEvent

But should instead do this:

Scriptname DDiCompat hidden

Function Restrain(Actor victim)
	zadLibs DDiLibs = (Game.GetFormFromFile(0x00F624, "Devious Devices - Integration.esm") as Quest) as zadLibs
	...
	DDiLibs.ManipulateGenericDevice(Victim, Device, true)
endFunction
Scriptname RestrainEffect extends activemagiceffect

Event OnEffectStart(Actor akTarget, Actor akCaster)
	if(Game.GetModByName("Devious Devices - Integration.esm"))
		DDiCombat.Restrain(akTarget)
	endIf
endEvent

 

Link to comment

? I have to come back here with another  question to see if I understood it right.

 

I wonder if using states could help here ? :classic_huh:

 

E.g.

If I have one script, that uses states.

It would check at the OnInit event if the mod in question is loaded with GetModByName().

If "yes" it sets the state to "ModIsLoaded" state.

If "no" it sets the state to "ModNotLoaded" state

 

All my getFormFromFile() commands would happen in the "ModIsLoaded" state. So they are always valid, when executed.

The "ModNotLoaded" state would contain no references to the Mod in question at all

 

Would this help?

Or must it literally be seperated into two different script files as in @Code Serpent's example above?

Link to comment
On 6/22/2020 at 4:16 AM, worik said:

If I have one script, that uses states.

It would check at the OnInit event if the mod in question is loaded with GetModByName().

If "yes" it sets the state to "ModIsLoaded" state.

If "no" it sets the state to "ModNotLoaded" state

This is old now but I'll answer anyway ... for reference ...

 

States are useful in the consumer mod for handling presence and absence of a dependency, but I wouldn't use them for it.

 

They aren't useful in protecting your code from type dependencies at all. If you reference the type anywhere in the script, it's referenced, states or not states.

 

The reason not to use them for handling a case like MME present/absent, is that they do not scale. A script can be in one state and one state only.

So if you have to soft-deps, now the state handling is not enough to handle the four possible combinations of presence and absence.

 

It's perfectly fine to track mod presence/absence in a boolean per-mod. What matters is that you do not reference any of the mod's types in that consumer script.

Link to comment
11 hours ago, Lupine00 said:

If you reference the type anywhere in the script, it's referenced,

I think, that is the key (to understand it) for me  ?

 

Just to get it right for me:

Currently, I have a function that getFromFile's a bunch of things from soft depending mods.

E.g. I pick a potion and add that to a list of leveled items if BF is present.

myPotion = Game.getFormFromFile(0x000038C9, "BeeingFemale.esm") as Potion
my_Litem_FemalePotion.addForm(myPotion,1,2)

From now on, I don't touch the BF object anymore, I just use my list. So I thought I would be safe.

 

BUT: From what I understood, this IS in fact the dangerous part, that should be encapsulated into a seperate script? 

Link to comment

In that case, Potion is a vanilla type, so there is no risk from the type... in your script.

 

But, I don't see a guard there (though it could be outside the snippet).

If BF is not present, trying to cast a None to a Potion will fail and spew spam into your log.

 

And assuming it was not None and you could cast it, and add it to your list, there's a good chance that BF is installed.

Of course, if its scripts are missing, and just the ESP is there, you may into problems if you ever try and use that potion.

It would depend whether the Form really is a Potion, or whether it's a derivative declared in BF, in which case ... trouble.

But if BF is present, and properly installed, it should all be benign because BF is actually there.

 

 

However, if BF has some type in it like DB_Fertility_Spell, which is derived from Spell, then casting to that type references it in your script, regardless of whether that code is in a state that is enabled or not.

 

For Skyrim to load your script requires that it resolve that type, which requires that it resolve the associated code file ... which may, or may not exist.

 

 

BF itself is not a stable mod, so I wouldn't want to depend on it, but that's another matter. Even in games with almost nothing but BF installed, it was not stable for me.

Link to comment

i've seen issues with special edition where, in oldrim, your undefined types would still work fine if you wrapped them with the proper checks, but in sse it seems to do an additional pass during loading to make sure all the referenced types exist and won't load a script if one is missing even if it was in an unreachable path because of checks.

wasn't even mad since it is a compiled language i was sort of surprised it ever worked. like how in php you can do if(TRUE || FuncThatDoesNotExist()) and it is fine but not in c.

Link to comment
15 hours ago, Lupine00 said:

But, I don't see a guard there (though it could be outside the snippet).

:classic_shy:

FYI the complete script ... the current testing version. I do use states in here, that's why I'm asking so much about those.

Spoiler

Scriptname WMAE_ModIf_BF extends Quest  
{BeeingFemale soft-dependency interface script}

GlobalVariable	Property WMAE_GV_ModBF auto
FormList		Property WMAE_VendorItemsApothecary auto
LeveledItem		Property WMAE_Litem_FemalePotion auto
LeveledItem		Property WMAE_Litem_FemaleItem auto
Keyword			myKeyword
Armor			myItem
Potion			myPotion

Event OnInit()
	if WMAE_GV_ModBF.GetValue() != -2
		RegisterForModEvent("WMAE_Ev_DetectMods", "On_WMAE_DetectMods")
		;Debug.Notification("WMAE BF interface")
	EndIf
	RegisterForModEvent("WMAE_Ev_ReportMods", "On_WMAE_ReportMods")
EndEvent


Event On_WMAE_DetectMods(string eventName, string strArg, float numArg, Form sender)
	checkInstalled()
	populate()
EndEvent

Event On_WMAE_ReportMods(string eventName, string strArg, float numArg, Form sender)
	getStatus()
EndEvent

Function checkInstalled() 
	If Game.GetModByName("BeeingFemale.esm") != 255
		WMAE_GV_ModBF.SetValue(1)
		If GetState() != "Installed"
			GoToState("Installed")
		EndIf
		Debug.Notification("WMAE detected BeeingFemale")
	Else
		WMAE_GV_ModBF.SetValue(0)
		If GetState() != "NotInstalled"
			GoToState("NotInstalled")
		EndIf
		;Debug.Notification("WMAE did not detect BeeingFemale")
	EndIf
EndFunction

Function getStatus()
	If WMAE_GV_ModBF.getValue() == 1
		Debug.Notification("WMAE BeeingFemale active")
	ElseIf WMAE_GV_ModBF.getValue() == 0
		Debug.Notification("WMAE BeeingFemale is inactive")
	ElseIf WMAE_GV_ModBF.getValue() == -1
		Debug.Notification("WMAE BeeingFemale interface is not initialized, yet")
	Else
		Debug.Notification("WMAE BeeingFemale is switched off")
	EndIf
EndFunction

;notchecked
function populate()
	return
EndFunction

State NotInstalled
function populate()
	return
EndFunction
EndState


State Installed
function populate()
	myKeyword = Keyword.GetKeyword("_FWContraceptionItem")
	if myKeyword
		WMAE_VendorItemsApothecary.AddForm(myKeyword)
			Debug.Notification("_FWContraceptionItem populated")
	EndIf
	myKeyword = Keyword.GetKeyword("_BFPanty")
	if myKeyword
		WMAE_VendorItemsApothecary.AddForm(myKeyword)
			Debug.Notification("_BFPanty populated")
	EndIf
	myPotion = Game.getFormFromFile(0x000038C9, "BeeingFemale.esm") as Potion
	WMAE_Litem_FemalePotion.addForm(myPotion,1,2)
	Debug.Notification("_BFContraceptionLow populated")
	myPotion = Game.getFormFromFile(0x000038CB, "BeeingFemale.esm") as Potion
	WMAE_Litem_FemalePotion.addForm(myPotion,1,2)
	Debug.Notification("_BFContraceptionMedium populated")
	myPotion = Game.getFormFromFile(0x0000B4D5, "BeeingFemale.esm") as Potion
	WMAE_Litem_FemalePotion.addForm(myPotion,1,2)
	Debug.Notification("_BF_WashOutFluid75 populated")
	myPotion = Game.getFormFromFile(0x0000E55B, "BeeingFemale.esm") as Potion
	WMAE_Litem_FemalePotion.addForm(myPotion,1,2)
	Debug.Notification("_BF_WashOutFluid100 populated")	
	
	myItem = Game.getFormFromFile(0x00064358, "BeeingFemale.esm") as Armor
	WMAE_Litem_FemaleItem.addForm(myItem,1,2)
	Debug.Notification("_BFTamponNormal populated")
	myItem = Game.getFormFromFile(0x0000AF61, "BeeingFemale.esm") as Armor
	WMAE_Litem_FemaleItem.addForm(myItem,1,2)
	Debug.Notification("_BFArmorPantyNormal populated")

    return
EndFunction


EndState

; END

 

 

Primarily I am interested to use stuff from various mods to create merchandise for an immersive herbalist/healer. Thus the soft dependencies.

Because of that,  it doesn't matter to me if BF is stable or not. It's just another soft dependency with useful stuff like Fertility Mode and others are.

 

15 hours ago, Lupine00 said:

In that case, Potion is a vanilla type, so there is no risk from the type... in your script.

Ah! :classic_lightbulb::classic_smile: Now I think I understood what you meant.

 

Link to comment
37 minutes ago, darkconsole said:

i've seen issues with special edition where, in oldrim, your undefined types would still work fine if you wrapped them with the proper checks, but in sse it seems to do an additional pass during loading to make sure all the referenced types exist and won't load a script if one is missing even if it was in an unreachable path because of checks.

Global functions are the way around this, because the global does not have to be loaded until it is executed, and as long as your global functions are written purely in terms of vanilla types, there won't be any problems with type references.

 

 

Papyrus is compiled, very crudely so; it runs an crude virtual machine, and on loading the compiled code for a non-global it has to make that code into a live executable version. At that point it attempts to resolve external references for all symbols. Of course vanilla types always resolve. If you refer to a type that doesn't have a definition, whether it game from your mod, or another mod, it's an error at compile time, and it's an error during that resolve phase too.

 

 

It's a good thing that a strict check is in SSE, as in LE it leads to so much strange instability. I'm not sure what LE is doing, but the worst case is when it can find the foreign type's script file, but that script file can't be properly loaded because its ESP hasn't been loaded.

 

Does SSE detect that case as well? Non-global script present, but the script's ESP isn't loaded.

Link to comment
4 hours ago, Lupine00 said:

Global functions are the way around this, because the global does not have to be loaded until it is executed, and as long as your global functions are written purely in terms of vanilla types, there won't be any problems with type references.

 

the loading pre-pass kills this method in sse too. seems like sse is just up shits creek.

 

[07/31/2020 - 03:09:22PM] Error: Unable to link types associated with static function "ActorArousalExhib" on object "dse_dm_ExternSexlabAroused".
[07/31/2020 - 03:09:22PM] Error: Unable to link types associated with static function "ActorArousalUpdate" on object "dse_dm_ExternSexlabAroused".
[07/31/2020 - 03:09:22PM] Error: Unable to link types associated with static function "ActorArousalGet" on object "dse_dm_ExternSexlabAroused".

[edit] i'll probably end up keeping this tho, that way the file can be overwritten with a patch to enable or disable.

Link to comment
10 hours ago, darkconsole said:

the loading pre-pass kills this method in sse too. seems like sse is just up shits creek.

At first glance that's caused by defining the global's function interface in terms of unavailable types. Globals need to use only vanilla types in their interface to be useful for this purpose.

 

e.g.

 

Actor Function Foo(Int index) Global ; this is fine

ssl_Animation Function Bar(String animName) Global ; this will fail

Link to comment
18 minutes ago, Lupine00 said:

At first glance that's caused by defining the global's function interface in terms of unavailable types. Globals need to use only vanilla types in their interface to be useful for this purpose.

 

Int Function ActorArousalGet(dse_dm_QuestController Main, Actor Who) Global
{ask sla for an actor's arousal.}

	If(Main.Aroused == None)
		Return 0
	EndIf

	Return (Main.Aroused as slaFrameworkScr).GetActorExposure(Who)
EndFunction

 

granted dse_dm_QuestController isn't vanilla, but its one that cant ever be missing if this code even exists. unless its *that* anal. i ended up just making a fomod installer that replaces the script with empty funcs if you disable sla.

 

Link to comment

Seems like in SSE global functions are being fully parsed after the first invocation. Safe bet is to make one script (with global functions) per soft dependency and do not call functions from it unless you are sure dependency is loaded.

P.S. in your sample there's checkSoftDeps > 2 and not checkSoftDeps >= 2 which makes it +1 cycle for init.

Is it intentional ?

Edited by crajjjj
Link to comment
×
×
  • 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