Review to SKSE/Papyrus Latent Functions
Papyrus is fundamentally a threaded, cooperative‐multitasking language. Its built‑in functions fall into two camps: latent (sometimes called “delayed”) and non‑latent (or “non‑delayed/NoWait”).
-
What makes a function “latent”?
Latent functions are those that take non‑negligible real‑world time to execute and therefore suspend (“yield”) the current script thread until they complete. Under the hood, the Papyrus VM pauses your script, lets other scripts and engine systems run, and then resumes yours once the operation finishes. In the CK wiki’s terminology, these are calls “that will wait for X to happen before returning” (UESP). -
Latent vs. non‑latent behavior
-
Latent (Delayed) calls block the script across frames. For example,
Utility.Wait(2.0) ; pauses script for 2 secondsdoesn’t return until those 2 s have elapsed, at which point Papyrus continues execution on the next frame.
-
Non‑latent (NoWait) calls return immediately, even if the underlying engine action continues asynchronously (e.g.
DisableNoWait, simple math or string ops,Debug.Notification, etc.).
Internally, both SKSE and F4SE use a shared flag (often namedisLatentorNoWait) to distinguish these (Nexus Mods Forums).
-
-
Common Papyrus latent functions
-
Utility.Wait(float seconds) -
ObjectReference.Enable(bool fadeIn)(waits for the fade‑in) -
ObjectReference.Disable(bool fadeOut)(waits for fade‑out) -
Game.FastTravel(Location target)(suspends until load completes)
By contrast, their “NoWait” variants (e.g.EnableNoWait,DisableNoWait) return immediately without yielding (Gist).
-
-
Registering native functions in an SKSE plugin
SKSE plugins can expose custom “native” functions to Papyrus. In C++ you write something like:// C++ side std::string MyNativeFunction(RE::StaticFunctionTag*) { return "Hello from C++!"; } bool BindPapyrusFunctions(RE::BSScript::IVirtualMachine* vm) { vm->RegisterFunction("MyNativeFunction", // Papyrus name "MyPapyrusScript", // scriptName in .psc MyNativeFunction); // C++ callback return true; } extern "C" DLLEXPORT bool SKSEPlugin_Load(const SKSE::LoadInterface* skse) { SKSE::Init(skse); SKSE::GetPapyrusInterface()->Register(BindPapyrusFunctions); return true; }On the Papyrus side you’d declare:
ScriptName MyPapyrusScript String Function MyNativeFunction() Global Nativeand invoke it like any other script call (Skyrim.dev).
By default, these native functions act as non‑latent calls (they return immediately). If you need your C++ callback to behave as a latent function (i.e. suspend the script until some asynchronous C++ work finishes), you must explicitly register it with a “latent” flag or compile with SKSE’s internal Papyrus interface enabled (e.g. defining
_PPAPI), although this is generally unsupported out of the box (Nexus Mods Forums). -
Invoking and returning from a latent function
From the script author’s perspective, there’s no special syntax—just call it and then put the continuation code in the next line. For example:Event OnInit() Debug.Notification("Starting wait…") Utility.Wait(1.5) ; latent call – script yields here Debug.Notification("Wait complete!") EndEventWhen
Utility.Waitis called, Papyrus suspends your script thread, runs other tasks, and then resumes execution at the very next line once the wait is over. If you had registered a C++ native function as latent, it would behave identically: the script yields at the call site and only continues after your C++ code signals completion.
In summary:
-
Latent functions yield scripts across frames until their work is done.
-
Non‑latent functions run and return immediately.
-
SKSE plugins can register native Papyrus functions via
IVirtualMachine->RegisterFunction, which default to non‑latent—but can be made latent with special flags. -
Invoking a latent function is the same as any call; return occurs implicitly when the VM resumes your script on the next frame after the latent operation completes.