Inter-mod Communication

Revision as of 13:22, 19 October 2015 by imported>Plplecuyer (→‎Custom Events)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Overview

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

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

How To Determine If A Mod Is Installed

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

How To Get A Form From Another Mod

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 GetFromFromFile function, along with the plugin name, to get access to the form.

Example:

Form newCompanionForm = Game.GetFormFromFile("SpecialCompanion.esp", 0x00015A4F)
Actor newCompanion = newCompanionForm 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

How To Communicate With Another Mod

There are a few ways to communicate with the other mod via Papyrus - functions, properties, remote events, and custom events, covered below.

Functions

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

Properties

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 = aCharacter.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

Remote Events

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

Custom Events

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", actorThatEntered)
  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

See Also