Table of Contents

Papyrus Native Functions

A Papyrus script can declare a function native and leave its body to an SKSE plugin. In SkyrimForge, a C# class marked [PapyrusScript] implements those functions. The build writes the script's declarations to a .psc file, and SkyrimForge registers the class's methods with the game once the game's data has loaded.

A Script of Global Functions

using SkyrimForge;
using SkyrimForge.Game;
using SkyrimForge.Papyrus;

namespace MyMod;

[PapyrusScript]
public sealed class MyModNatives(GameTarget gameTarget)
{
    /// <summary>The number of gold coins in the player's inventory.</summary>
    public int GetPlayerGold() => gameTarget.Player.GetGoldAmount();

    public int Add(int first, int second) => first + second;
}

The build writes Scripts\Source\MyModNatives.psc in the project folder:

Scriptname MyModNatives Hidden

; Generated by SkyrimForge from MyMod.MyModNatives. Do not edit.

; The number of gold coins in the player's inventory.
int Function GetPlayerGold() global native

int Function Add(int first, int second) global native

Any script then calls the functions through the script's name: int gold = MyModNatives.GetPlayerGold().

  • The script's name is the class's name. Papyrus script names share one namespace across every mod, so include the mod's name in the class name.
  • SkyrimForge constructs the class as it constructs an IPlugin class: its one public constructor declares the services it uses. Like an IPlugin class, it has a log and saved state of its own, stored under its full name or the identity [PluginIdentity] states. An exception a method throws is written to that log, and the calling script receives None.
  • Every public method the class declares is a native function, static methods included. Its <summary> is written into the .psc as a comment.
  • A parameter or return value is int, float, bool, string, a form view for which Papyrus has a script type (such as Actor, ObjectReference, or Form), or a one-dimensional array of one of those. A nullable type declares the type it wraps.

A Script Attached to Forms

A script that extends a form type can be attached to a form of that type, in the Creation Kit or at runtime. Extends names the form view, and a method whose first parameter is marked [PapyrusSelf] receives the form to which the script is attached. That method is a non-global native function, and the .psc declares it without the [PapyrusSelf] parameter.

[PapyrusScript(Extends = typeof(ObjectReference))]
public sealed class MyModLeverNatives
{
    public string Describe([PapyrusSelf] ObjectReference self) => self.GetDisplayFullName();
}
Scriptname MyModLeverNatives extends ObjectReference

; Generated by SkyrimForge from MyMod.MyModLeverNatives. Do not edit.

string Function Describe() native

The generated script declares only native functions. A Papyrus script that extends it adds the events and properties only Papyrus can declare, and calls the native functions on itself:

Scriptname MyModLever extends MyModLeverNatives

Event OnActivate(ObjectReference akActionRef)
    Debug.Notification(Describe())
EndEvent

A script that extends no form type is declared Hidden, since it cannot be attached to a form.

Compiling the Scripts

The build writes a .psc only during a build, not while the code is edited, and only when the class has no errors. It also deletes a .psc in Scripts\Source that it wrote for a class the project no longer declares. A .psc without the generated comment is left as it is, so Scripts\Source can also contain scripts written by hand, such as MyModLever.psc above.

The game loads compiled scripts, so each .psc is compiled to a .pex with the Creation Kit's Papyrus compiler, which imports the game's own script sources:

PapyrusCompiler.exe MyModNatives -f="<Skyrim>\Data\Source\Scripts\TESV_Papyrus_Flags.flg" -i="Scripts\Source;<Skyrim>\Data\Source\Scripts" -o="Scripts"

The .pex files go in the mod's Scripts folder, beside its SKSE folder. Until a script's .pex is installed, registering its class's methods fails, and the class's log names the missing script.

Build Errors

The build reports what Papyrus cannot declare as an error at the declaration in question:

  • SFPS001: a parameter or return value of a type for which Papyrus has no equivalent, such as long, double, or Character (which Papyrus calls Actor).
  • SFPS002: a method Papyrus cannot declare: a generic method, a parameter or return value passed by reference, more than 11 parameters, two methods whose names differ only in case or that overload one name, a public property, or a [PapyrusSelf] parameter that is not the first, that is not of the Extends type, or that is in a class without Extends.
  • SFPS003: a class Papyrus cannot declare as a script: a name that is not a Papyrus identifier, a class that is static, abstract, or generic, a class that also implements IPlugin, an Extends type with no Papyrus script type, or two classes whose names differ only in case.

Functions the Attributes Do Not Cover

A native function that makes the calling script wait for a result, or one that implements a script the plugin does not own, is registered through IPapyrusFunctionRegistrar, and its .psc is written by hand. Registering loads the script and throws InvalidOperationException where no script call would reach the function, naming the script's declaration and the registration's:

MyMod.Add would be unbound, so a call would return None without running: the script declares "int Function Add(int first, int second) global native",
and the registration declares "int Function Add(string, int) global native".

A registration whose script does not exist, or declares no function of its name, throws the same way. So does registering a function a second time, by the same plugin or another, and the earlier registration stays bound.