API Reference — PrismaUI_F4
Overview
The public API is declared entirely in PrismaUI_F4_API.h. Copy that single header into your plugin’s src/ folder. You do not link against PrismaUI_F4 at compile time; the connection is made at runtime via GetProcAddress.
#include "PrismaUI_F4_API.h"
// On kGameDataReady:
auto* api = PRISMA_UI_API::RequestPluginAPI<PRISMA_UI_API::IVPrismaUI3>();
Types
PrismaView
typedef uint64_t PrismaView;
An opaque handle that identifies one HTML view. The value 0 means “no view” / invalid. Always check IsValid(view) before using a handle you haven’t used recently, particularly after a game reload.
ConsoleMessageLevel
enum class ConsoleMessageLevel : uint8_t {
Log = 0,
Warning,
Error,
Debug,
Info
};
Passed to ConsoleMessageCallback. Maps directly to the JavaScript console.* level.
Callback Types
// Called once when the HTML document's DOM is fully parsed and ready.
typedef void (*OnDomReadyCallback)(PrismaView view);
// Called with the string result of a JS expression evaluated via Invoke().
typedef void (*JSCallback)(const char* result);
// Called when JS code calls the registered listener function on window.
typedef void (*JSListenerCallback)(const char* argument);
// Called for every console.log/warn/error line from JS.
typedef void (*ConsoleMessageCallback)(
PrismaView view,
ConsoleMessageLevel level,
const char* message
);
Threading: OnDomReadyCallback is invoked on the main game thread (dispatched via F4SE::GetTaskInterface()->AddTask). JSListenerCallback fires on the Ultralight render thread — see Threading Warning below.
Interface Versions
| Type | Version | Adds |
|---|---|---|
IVPrismaUI1 | V1 | All core view operations |
IVPrismaUI2 | V2 | RegisterConsoleCallback |
IVPrismaUI3 | V3 | RegisterTranslations |
Always request the highest version you need. If the installed PrismaUI_F4 is older than your requested version, RequestPluginAPI returns nullptr — handle this gracefully. V3 is the current recommended version.
// Recommended — request V3
auto* api = PRISMA_UI_API::RequestPluginAPI<PRISMA_UI_API::IVPrismaUI3>();
if (!api) {
logger::error("PrismaUI V3 not available — update PrismaUI_F4");
return;
}
// V1/V2 still supported for backward compatibility
auto* api = PRISMA_UI_API::RequestPluginAPI<PRISMA_UI_API::IVPrismaUI2>();
auto* api = PRISMA_UI_API::RequestPluginAPI<PRISMA_UI_API::IVPrismaUI1>();
RequestPluginAPI
template <typename T>
[[nodiscard]] inline T* RequestPluginAPI();
Locates PrismaUI_F4.dll in the current process via GetModuleHandleW, calls its exported RequestPluginAPI function, and casts the result. Returns nullptr if:
- PrismaUI_F4 is not loaded
- The loaded version does not support the requested interface
Call timing: During or after F4SE::MessagingInterface::kGameDataReady. Do not call during F4SEPlugin_Load or F4SEPlugin_Query; F4SE may not have loaded PrismaUI_F4 yet.
Threading Warning: JS Listener Callbacks
JSListenerCallback functions registered via RegisterJSListener fire on the Ultralight render thread, not the game thread.
You must not access CommonLibF4 game objects (RE::*) directly from a JS listener callback. Doing so causes undefined behavior — GetSingleton() calls may return null or corrupt data, and crashes will appear non-deterministic.
Always dispatch game thread work via AddTask:
api->RegisterJSListener(view, "requestGameData", [](const char* s) {
// WRONG — RE:: access on Ultralight thread:
// auto* player = RE::PlayerCharacter::GetSingleton(); // crash
// CORRECT — capture and dispatch to game thread:
std::string arg = s ? s : "";
F4SE::GetTaskInterface()->AddTask([arg]() {
auto* player = RE::PlayerCharacter::GetSingleton();
// ... safe game thread access ...
});
});
InteropCall and Invoke calls made from within AddTask (game thread) are safe — they marshal internally.
IVPrismaUI1
CreateView
virtual PrismaView CreateView(
const char* htmlPath,
OnDomReadyCallback onDomReadyCallback = nullptr
) noexcept = 0;
Creates an HTML view and begins loading the specified file.
| Parameter | Description |
|---|---|
htmlPath | Path relative to Data/PrismaUI_F4/views/, e.g. "mymenu.html" or "Interface/MyPlugin/page.html". |
onDomReadyCallback | Optional. Called once on the main game thread when the DOM is fully parsed. Safe to call RegisterJSListener and Invoke here. |
Returns a non-zero PrismaView handle on success. The view starts hidden — call Show(view) when you want it visible.
Thread safety: Call from the main thread (inside an F4SE message handler or AddTask).
Create views on kPostLoadGame / kNewGame, not on kGameDataReady. The rendering system is ready after a game is loaded.
Invoke
virtual void Invoke(
PrismaView view,
const char* script,
JSCallback callback = nullptr
) noexcept = 0;
Evaluates an arbitrary JavaScript expression in the view’s context. The JSCallback result is delivered on the main game thread.
Example:
// Push data into the page
api->Invoke(view, "updateInventory('[{\"name\":\"Stimpack\",\"count\":5}]')");
// Read a value back
api->Invoke(view, "document.getElementById('hp').textContent", [](const char* val) {
logger::info("HP display: {}", val);
});
Encoding: Auto-converts ANSI game strings to UTF-8.
Performance: Higher overhead than InteropCall — posts to the Ultralight thread and waits for a round-trip. Use for one-shots and reads, not high-frequency calls.
InteropCall
virtual void InteropCall(
PrismaView view,
const char* functionName,
const char* argument
) noexcept = 0;
Calls a named JavaScript function via the JS Interop API (lower overhead than Invoke). The function must exist on the window object.
Example:
// C++
api->InteropCall(view, "onInventoryData", jsonString.c_str());
// JS (mymenu.html)
function onInventoryData(json) {
var items = JSON.parse(json);
// render items...
}
Encoding: Same ANSI → UTF-8 auto-conversion as Invoke.
Use InteropCall for anything called more than once per second or in tight loops.
RegisterJSListener
virtual void RegisterJSListener(
PrismaView view,
const char* functionName,
JSListenerCallback callback
) noexcept = 0;
Exposes a C++ callback to JavaScript. After registration, calling window.functionName(arg) from JS invokes the C++ callback.
Warning: Callback fires on the Ultralight render thread. See Threading Warning.
Best practice: Register listeners inside your OnDomReadyCallback.
Example:
// C++ — register in OnDomReady
api->RegisterJSListener(view, "onCloseRequest", [](const char* /*arg*/) {
// No RE:: access here — safe, this is just UI state
F4SE::GetTaskInterface()->AddTask([]() {
g_api->Unfocus(g_view);
g_api->Hide(g_view);
});
});
// JS — call from the page
onCloseRequest();
HasFocus
virtual bool HasFocus(PrismaView view) noexcept = 0;
Returns true if this view currently has input focus.
Focus
virtual bool Focus(
PrismaView view,
bool pauseGame = false,
bool disableFocusMenu = false
) noexcept = 0;
Gives input focus to the view, routing keyboard and mouse events to the HTML page.
| Parameter | Description |
|---|---|
pauseGame | If true, sets game time scale to 0. Restored on Unfocus. Use for menus that need exclusive player attention. |
disableFocusMenu | If false (default), PrismaUI shows a Scaleform FocusMenu overlay that manages the cursor and intercepts ESC to unfocus. If true, the overlay is suppressed — keyboard events reach the HTML keydown handler directly and the game’s existing cursor (e.g. PauseMenu cursor) remains active. |
When to use disableFocusMenu=true: When your view opens on top of an existing game menu that already shows a cursor (e.g. the PauseMenu). With false, the FocusMenu overlay intercepts ESC before your JS sees it and may conflict with the existing cursor. With true, your JS keydown handler is responsible for closing the view on ESC.
When to use disableFocusMenu=false (default): Standard toggle menus opened directly from gameplay with no other game menu present.
Returns true on success.
Unfocus
virtual void Unfocus(PrismaView view) noexcept = 0;
Removes focus from the view. Restores game input. If pauseGame was true on Focus, game time is restored.
Call Hide after Unfocus if you want the view invisible while not in use.
Show
virtual void Show(PrismaView view) noexcept = 0;
Makes a hidden view visible at the next Present call. Does not grant input focus.
Hide
virtual void Hide(PrismaView view) noexcept = 0;
Removes a visible view from the composite. Does not destroy the view or stop JavaScript execution.
IsHidden
virtual bool IsHidden(PrismaView view) noexcept = 0;
Returns true if the view is currently hidden.
GetScrollingPixelSize
virtual int GetScrollingPixelSize(PrismaView view) noexcept = 0;
Returns the number of pixels scrolled per mouse wheel tick. Default: 28 px.
SetScrollingPixelSize
virtual void SetScrollingPixelSize(PrismaView view, int pixelSize) noexcept = 0;
Sets the mouse wheel scroll amount in pixels.
IsValid
virtual bool IsValid(PrismaView view) noexcept = 0;
Returns true if the view handle is live and backed by a real Ultralight view. Check this before any operation on a stored handle, especially after a game reload.
Destroy
virtual void Destroy(PrismaView view) noexcept = 0;
Tears down the view completely. The handle becomes invalid. Rarely needed — views are typically kept alive for the session.
SetOrder
virtual void SetOrder(PrismaView view, int order) noexcept = 0;
Sets the rendering z-order. Higher values render on top. Default is 0.
GetOrder
virtual int GetOrder(PrismaView view) noexcept = 0;
Returns the current z-order.
CreateInspectorView
virtual void CreateInspectorView(PrismaView view) noexcept = 0;
Attaches a WebKit DevTools inspector to the view. Call once before using other inspector methods. Do not ship this call in released mods.
SetInspectorVisibility
virtual void SetInspectorVisibility(PrismaView view, bool visible) noexcept = 0;
Shows or hides the inspector overlay.
IsInspectorVisible
virtual bool IsInspectorVisible(PrismaView view) noexcept = 0;
Returns true if the inspector is currently visible.
SetInspectorBounds
virtual void SetInspectorBounds(
PrismaView view,
float topLeftX,
float topLeftY,
unsigned int width,
unsigned int height
) noexcept = 0;
Positions and sizes the inspector overlay in screen pixels.
HasAnyActiveFocus
virtual bool HasAnyActiveFocus() noexcept = 0;
Returns true if any PrismaUI view currently has input focus. Use in hotkey handlers to suppress game actions while a menu is open.
IVPrismaUI2
Extends IVPrismaUI1.
RegisterConsoleCallback
virtual void RegisterConsoleCallback(
PrismaView view,
ConsoleMessageCallback callback
) noexcept = 0;
Receives all console.log/warn/error/debug/info calls from the view’s JS context. Pass nullptr to unregister. Callback fires on the main game thread.
Always register this during development — JS errors are otherwise silent.
api->RegisterConsoleCallback(view,
[](PrismaView, PRISMA_UI_API::ConsoleMessageLevel lvl, const char* msg) {
const char* tag = lvl == PRISMA_UI_API::ConsoleMessageLevel::Error ? "[JS ERR] " :
lvl == PRISMA_UI_API::ConsoleMessageLevel::Warning ? "[JS WARN]" :
"[JS LOG] ";
logger::info("{} {}", tag, msg);
});
IVPrismaUI3
Extends IVPrismaUI2.
RegisterTranslations
virtual void RegisterTranslations(
PrismaView view,
const char* pluginName
) noexcept = 0;
Loads a translation file and injects window.L10N and window.t() into the view’s JavaScript context on every page load.
| Parameter | Description |
|---|---|
pluginName | Your plugin’s base name without extension, e.g. "MyPlugin_F4". |
Translation file location: Data\Interface\Translations\<pluginName>_<lang>.txt
where <lang> matches the game’s current language setting (e.g. en, de, fr).
Call after CreateView. Translations are re-injected automatically on each subsequent page load, so you only need to call this once per view.
In JS:
// window.t() returns the translated string for a key, or the key itself if not found
document.getElementById('title').textContent = t('ui.title');
// window.L10N is the raw object of all key → value pairs
console.log(L10N['ui.title']);
Translation file format (one entry per line):
ui.title My Plugin
ui.close Close
ui.settings Settings
Typical Call Sequence
F4SEPlugin_Load:
F4SE::Init(a_f4se)
messaging->RegisterListener(F4SEMessageHandler)
kGameDataReady:
RequestPluginAPI<IVPrismaUI3>() → g_api
[register key handler / event sink]
kPostLoadGame / kNewGame:
g_api->CreateView("page.html", OnDomReady) → g_view
g_api->RegisterConsoleCallback(g_view, ...)
g_api->RegisterTranslations(g_view, "MyPlugin_F4") // V3 only
OnDomReady (game thread):
g_api->RegisterJSListener(g_view, "fnName", callback)
g_api->Invoke(g_view, "init()")
Toggle (key / event):
if opening:
g_api->Show(g_view)
g_api->Focus(g_view, pauseGame, disableFocusMenu)
if closing:
g_api->Unfocus(g_view)
g_api->Hide(g_view)
JSListenerCallback (Ultralight thread — dispatch RE:: work):
F4SE::GetTaskInterface()->AddTask([capture]() {
// RE:: access here is safe
g_api->InteropCall(g_view, "result", data.c_str());
});
Papyrus Bridge API (window.prisma)
Overview
window.prisma is automatically injected by PrismaUI_F4 into every HTML view. It provides read-only access to Papyrus globals and script properties without requiring C++ code in your plugin.
Available methods:
await prisma.getGlobal(esp, formId)— Read aTESGlobalform valueawait prisma.getProperty(esp, formId, scriptName, propertyName)— Read anAutoproperty from a Papyrus script
Known Limitations
Property writes are not supported. Papyrus scripts finalize their property values at initialization. After that, F4SE cannot modify them from outside the Papyrus VM. This is a fundamental engine limitation, not a PrismaUI limitation.
Workaround: Use TESGlobal variables instead of properties. Globals can be read and written at runtime. Alternatively, if you need to modify script state, do it from C++ via F4SE::GetTaskInterface()->AddTask() and the Papyrus VM scripting interface.
Return Values
Both read methods return Promises and handle errors gracefully:
- Returns a
numberon success (including 0.0, which is a valid result) - Returns
nullif the form/plugin is not loaded, form doesn’t exist, or script/property name mismatch - Never throws — always guard with
if (val === null)
Example
// Read a global
const val = await prisma.getGlobal('MyMod.esp', '800');
if (val !== null) {
console.log('Global value:', val);
} else {
console.log('Form not found or plugin not loaded');
}
// Read a quest property (most reliable host for properties)
const propVal = await prisma.getProperty('MyMod.esp', '801', 'MyQuestScript', 'CurrentPhase');
if (propVal !== null && propVal !== undefined) {
console.log('Quest phase:', propVal);
}
Timing
window.prisma is available immediately — no wait needed. However, getProperty calls may return null if the Papyrus VM is not yet ready (e.g., if called before kPostLoadGame). For best results, call property reads after the game has finished loading.