Difference between revisions of "Inter-mod Communication"
imported>Docclox m (→How To Get A Form From Another Mod: The arguments in GetGameFromFile were in the wrong order.) |
imported>Docclox m (Corrected an obvious cut-and-paste error.) |
||
(2 intermediate revisions by 2 users not shown) | |||
Line 40: | Line 40: | ||
Example: | Example: | ||
<source lang="papyrus"> | <source lang="papyrus"> | ||
Actor newCompanion = Game.GetFormFromFile(0x00015A4F, "SpecialCompanion.esp") as Actor | |||
if newCompanion | if newCompanion | ||
; We've got ahold of the new companion from that other mod, maybe we can give them one of our special armor pieces? | ; We've got ahold of the new companion from that other mod, maybe we can give them one of our special armor pieces? | ||
Line 118: | Line 117: | ||
; We can't use the normal "as" operator, because that would add a dependency on the other mod. | ; We can't use the normal "as" operator, because that would add a dependency on the other mod. | ||
; So instead we use the CastAs function to make sure we're pointing at the right script. | ; So instead we use the CastAs function to make sure we're pointing at the right script. | ||
ScriptObject bfVendor = | ScriptObject bfVendor = aVendor.CastAs("BeginningFantasyVendor") | ||
; Now set the properties, again, we don't need to cast because our parameter values are | ; Now set the properties, again, we don't need to cast because our parameter values are | ||
Line 183: | Line 182: | ||
Var[] params = new Var[1] | Var[] params = new Var[1] | ||
params[0] = actorThatEntered | params[0] = actorThatEntered | ||
relayScript.CallFunction("RegisterForSummon", | relayScript.CallFunction("RegisterForSummon", params) | ||
endIf | endIf | ||
endEvent | endEvent |
Latest revision as of 20:38, 22 June 2020
Overview[edit | edit source]
This page covers the basics of how to communicate between two mods when one of them may or may not actually be installed or activated.
Warning: Uninstalling mods while keeping with the same save game is not supported. If a user uninstalls a mod, they must go back to a save created before the mod was installed in the first place.
What Not To Do[edit | edit source]
First off, what not to do. When you refer to another script in your own script, you add what is called a "dependency". In other words, your script is now dependent on that other script existing. By extension, your script is now also dependent on any scripts that other script is as well. For example, if you have an ObjectReference property, you are now dependent on ObjectReference. But you are also dependent on Form (the parent of ObjectReference), Keyword (used as a function parameter a few times), and other scripts as well.
If one or more of the scripts you are dependent on do not exist, then your own script may be flagged as "invalid" and will refuse to run.
As such, in order to prevent your script from suddenly not working when a mod you want to talk to is not installed, you must no longer depend on it. Several functions have been created to let you interact with other scripts without depending on them. However the downside is these functions are clunkier to use, and the compiler cannot check your code, nor do any auto-casting that might be necessary. The upside, of course, is that now you can talk directly to another mod's scripts without worrying about your own scripts failing to work if that other mod is not installed.
How To Talk To Another Mod[edit | edit source]
How To Determine If A Mod Is Installed[edit | edit source]
As most of the functions here will error if called on scripts that do not exist, you'll want to make sure the script you are interested in does exist by checking to see if the mod it comes from is available. This is relatively simple, as you just need to call IsPluginInstalled, passing in the name of the esp or esm you are interested in.
Example:
bool modInstalled = Game.IsPluginInstalled("SuperCoolWeapons.esp")
if modInstalled
; Do cool stuff! Like grab a fancy stick from it
endIf
Of course, the game allows the user to make a save at any time, and to install mods between different runs of the game. So you probably won't want to just check once and keep the value around forever. You will probably want to take advantage of OnPlayerLoadGame to update your "is mod installed" value every time the player loads their game from a save.
Again, note that uninstallation of mods while using the same save is not supported - so you shouldn't have to worry about your flag ever going false once it's set to true.
See Also[edit | edit source]
How To Get A Form From Another Mod[edit | edit source]
Now that you've determined that another mod is installed, assuming you want to do something other than just call global functions they provide, you'll need to grab a form from it. You obviously cannot use a property set in the editor in your mod, because that would add a dependency between your two mods in the esp or esm itself.
So if you do want to get a form from the mod, you'll need to know the form ID of the form you are interested in. This is typically listed in the CK. Once you have the form ID, pass it off to the GetFormFromFile function, along with the plugin name, to get access to the form.
Example:
Actor newCompanion = Game.GetFormFromFile(0x00015A4F, "SpecialCompanion.esp") as Actor
if newCompanion
; We've got ahold of the new companion from that other mod, maybe we can give them one of our special armor pieces?
endIf
You'll note that the form ID used in the above example has the top two digits as "00", when most commonly in the CK the form ID will start with a different number. The top two digits of the form ID are based off of the mod's position in the load order, and so they are ignored by the GetFormFromFile function.
See Also[edit | edit source]
How To Communicate With Another Mod[edit | edit source]
There are a few ways to communicate with the other mod via Papyrus - functions, properties, remote events, and custom events, covered below.
Functions[edit | edit source]
One of the most common ways you will communicate with another mod is via calling functions that it provides. There are several functions provided, depending on what function you want to call and how you want to call it. Note that the compiler cannot do any type checking, or auto-casting for you, and so you will have to spend a little more effort calling the function then you might otherwise have to do with a normal function call.
Note that you will have to make sure that your parameter types match exactly. No passing an int when they want a float, or an Actor when they want an ObjectReference. You'll have to pre-cast the parameters yourself. Also, you'll want to make sure you're pointing at the right script before you call a member function, but since you can't cast (that would add a dependency), there is a CastAs function to do that for you.
Because of the complexity, you may want to make wrapper functions that hide most of the code from the rest of your script, as shown below.
As an example, let's assume the other mod has a script like this:
Scriptname BeginningFantasyCharacter extends Actor
Function ActivateCrystal(ObjectReference aCrystal, int aiManaCost) global
; code here
EndFunction
In order to call that function, maybe passing in a special crystal you made in your mod specifically for interacting with this mod, you would do the following:
; A helper function to make it easier for my mod to call a function in the BeginningFantasy mod
Function ActivateBFCrystal(Actor aCharacter, ObjectReference aCrystal, int aiManaCost) global
; We can't use the normal "as" operator, because that would add a dependency on the other mod.
; So instead we use the CastAs function to make sure we're pointing at the right script.
ScriptObject bfCharacter = aCharacter.CastAs("BeginningFantasyCharacter")
; Make sure we have a script to call a function on
if bfCharacter
; Now build a parameter list. We don't have to do any casting because we're just passing
; in our own parameters which are already the right types. If they weren't, we'd have to
; manually cast
Var[] params = new Var[2]
params[0] = aCrystal
params[1] = aiManaCost
bfCharacter.CallFunction("ActivateCrystal", params)
endIf
endFunction
There are also functions for calling global functions, and "NoWait" versions that don't wait for the called function to return, so your code will run in parallel with theirs (but you won't be able to get a return value from them).
See Also[edit | edit source]
- CallGlobalFunction - Utility
- CallGlobalFunctionNoWait - Utility
- CallFunction - ScriptObject
- CallFunctionNoWait - ScriptObject
- CastAs - ScriptObject
Properties[edit | edit source]
Of course, another thing you may want to do is access properties on a script on the other mod. This is slightly easier than calling a function, but it still falls under the same restrictions as calling a function with regards to setting a property value, and the object you call the property on.
As such, it is recommended you write wrapper functions to hide the uglier code.
For example, assume the mod you want to talk to has a script like this:
Scriptname BeginningFantasyVendor extends Actor
float Property StoreOpenTime Auto
float Property StoreCloseTime Auto
You may want to write a wrapper function as follows to set these properties:
Function SetBFVendorStoreHours(Actor aVendor, float aOpenTime, float aCloseTime)
; We can't use the normal "as" operator, because that would add a dependency on the other mod.
; So instead we use the CastAs function to make sure we're pointing at the right script.
ScriptObject bfVendor = aVendor.CastAs("BeginningFantasyVendor")
; Now set the properties, again, we don't need to cast because our parameter values are
; already the right types
bfVendor.SetPropertyValue("StoreOpenTime", aOpenTime)
bfVendor.SetPropertyValue("StoreCloseTime", aCloseTime)
endFunction
Of course, there is also a function to get a property value, and a "NoWait" version of set property value if you don't care to wait for the value to be changed.
See Also[edit | edit source]
- GetPropertyValue - ScriptObject
- SetPropertyValue - ScriptObject
- SetPropertyValueNoWait - ScriptObject
- CastAs - ScriptObject
Remote Events[edit | edit source]
Remote event registration doesn't depend on scripts that your mod won't have access to, as the only events are native ones that come with the game. As such, remote event registration can be done normally, once you have the source form via GetFormFromFile.
See Also[edit | edit source]
Custom Events[edit | edit source]
And the final way you will usually talk to another mod is via custom events. There is a catch, however, as custom events require you to add a dependency on the script the event comes from. We can get around this limitation with a "throwaway" script. One that we write and add to our mod, and then talk to via the above CallFunction/SetProperty/etc functions so we don't have a dependency on the throwaway script. Because we don't have a dependency on it, when the game fails to load that script because the mod it depends on is missing, we won't be adversely affected. That throwaway script will then talk directly to our script when it receives the event in question.
So here's an example of a script custom event that we may want to listen for:
Scriptname BeginningFantasyCharacter extends Actor
CustomEvent SummonCasted
To listen for the event, create the following "throwaway" script that is attached to the same form as your script which you really want to receive the event:
Scriptname BFSummonEventRelay extends ObjectReference
Function RegisterForSummon(Actor aCharacterWhoSummons)
BeginningFantasyCharacter eventSource = aCharacterWhoSummons as BeginningFantasyCharacter
if eventSource
RegisterForCustomEvent(eventSource, "SummonCasted")
endIf
EndFunction
Event BeginningFantasyCharacter.SummonCasted(BeginningFantasyCharacter aSender, Var[] aArgs)
; Now to relay the event to the other script on the same ref as we are
MySpecialObject targetObj = ((self as ObjectReference) as MySpecialObject)
targetObj.BFSummonCasted(aSender, aArgs)
EndEvent
And finally the script that receives the event:
Scriptname MySpecialObject extends ObjectReference
Event OnTriggerEnter(ObjectReference aActionRef)
; Use CastAs to get the throwaway script without a dependency
ScriptObject relayScript = CastAs("BFSummonEventRelay")
Actor actorThatEntered = aActionRef as Actor
if relayScript && actorThatEntered
; And now tell the throwaway script to register for the custom event
Var[] params = new Var[1]
params[0] = actorThatEntered
relayScript.CallFunction("RegisterForSummon", params)
endIf
endEvent
Function BFSummonCasted(ScriptObject aSource, Var[] aArgs)
; We get called when our throwaway script gets the event
; So do something special since they casted a summon inside this trigger
endFunction