Quick Start
This guide takes you from "F4SE plugin that compiles" to "panel on screen" in six steps. End state: pressing F6 in-game opens a dark panel that says Hello from PrismaUI with an [X] button that closes it and unpauses the game.
Prerequisites:
- An F4SE plugin that already compiles (xmake or cmake, doesn't matter)
- PrismaUI F4 installed and active in your MO2 modlist
- Basic C++ familiarity
Step 1 Copy the API header
PrismaUI has no link-time dependency. There is nothing to add to your xmake.lua or CMakeLists.txt. The entire integration surface is one header you drop into your project.
Where to get it:
- From the framework source:
Framework/PrismaUI_F4_OG/src/PrismaUI_F4_API.h - Or from the Nexus download: inside the
SDK/folder in the optional files archive
What to do:
- Copy
PrismaUI_F4_API.hinto your plugin'ssrc/folder. - Add one include to your main
.cpp:
#include "PrismaUI_F4_API.h"
That's it. No linker flags. No additional libraries. The header resolves the API at runtime via GetProcAddress against the running PrismaUI_F4.dll.
Step 2 Request the API
Declare two globals at the top of your main .cpp:
static PRISMA_UI_API::IVPrismaUI2* g_api = nullptr;
static PrismaView g_view = 0;
Then, in your F4SE message handler, acquire the API pointer on kGameDataReady:
static void F4SEMessageHandler(F4SE::MessagingInterface::Message* msg)
{
switch (msg->type) {
case F4SE::MessagingInterface::kGameDataReady:
g_api = PRISMA_UI_API::RequestPluginAPI<PRISMA_UI_API::IVPrismaUI2>();
if (!g_api) {
REX::CRITICAL("PrismaUI_F4 not found, is it installed and active in MO2?");
}
break;
// ... other cases below
}
}
Why kGameDataReady and not F4SEPlugin_Load?
PrismaUI_F4.dll may not be loaded yet when F4SEPlugin_Load runs F4SE loads plugins in an undefined order. By kGameDataReady, all F4SE plugins are fully initialised and GetModuleHandleW("PrismaUI_F4.dll") is guaranteed to succeed. Calling RequestPluginAPI earlier returns null every time.
Step 3 Create the view
Add handlers for kPostLoadGame and kNewGame in the same message handler. Create the view there, register your close listener inside OnDomReady, then hide the view immediately:
case F4SE::MessagingInterface::kPostLoadGame:
case F4SE::MessagingInterface::kNewGame:
if (g_api && (g_view == 0 || !g_api->IsValid(g_view))) {
g_view = g_api->CreateView("Interface/MyMod/hello.html",
[](PrismaView v) {
// OnDomReady, the HTML is loaded and JS is running.
// Register listeners HERE, not before.
g_api->RegisterJSListener(v, "close", [](const char*) {
g_api->Unfocus(g_view);
g_api->Hide(g_view);
});
});
g_api->Hide(g_view); // Views start visible, hide immediately.
}
break;
Key points:
RegisterJSListenermust be called insideOnDomReady, not before it. The DOM doesn't exist yet before that callback fires.- The
"close"string is the JS function name. Your HTML will callwindow.close()to trigger it. HideafterCreateViewis not optional, views are visible by default, so if you skip this your panel will flash on screen every time a save loads.- The
g_view == 0 || !g_api->IsValid(g_view)guard prevents creating a second view if the player loads another save in the same session.
Step 4 Toggle it open with F6
Add a toggle function and wire it to F6. The cleanest approach is the BSInputEventUser pattern from the example plugin (copy keyhandler/ from example-f4se-plugin/src/ if you want the full helper), but a direct approach also works. Here is the toggle function:
static void TogglePanel()
{
if (!g_api || !g_api->IsValid(g_view)) return;
if (g_api->IsHidden(g_view)) {
g_api->Show(g_view);
g_api->Focus(g_view, /*pauseGame=*/true);
} else {
g_api->Unfocus(g_view);
g_api->Hide(g_view);
}
}
Wire it to F6 in kGameDataReady using the KeyHandler from the example plugin:
case F4SE::MessagingInterface::kGameDataReady:
g_api = PRISMA_UI_API::RequestPluginAPI<PRISMA_UI_API::IVPrismaUI2>();
if (!g_api) { REX::CRITICAL("PrismaUI not found"); break; }
KeyHandler::RegisterSink();
KeyHandler::GetSingleton()->Register(
0x40, // F6 DirectInput scan code
KeyEventType::KEY_DOWN,
TogglePanel);
break;
Focus(view, true) pauses the game while the panel is open. When the [X] button fires the "close" listener, Unfocus releases the pause and returns control to the player.
Step 5 The HTML file
Create this file at:
Data/PrismaUI_F4/views/Interface/MyMod/hello.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Hello</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: transparent;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
font-family: sans-serif;
}
.panel {
background: #1a1a1a;
color: #ffffff;
border: 1px solid #444;
border-radius: 6px;
padding: 32px 40px;
min-width: 320px;
text-align: center;
position: relative;
}
.panel h1 {
font-size: 1.4rem;
font-weight: 400;
letter-spacing: 0.05em;
}
.close-btn {
position: absolute;
top: 10px;
right: 14px;
background: none;
border: none;
color: #aaa;
font-size: 1.1rem;
cursor: pointer;
line-height: 1;
}
.close-btn:hover { color: #fff; }
</style>
</head>
<body>
<div class="panel">
<button class="close-btn" onclick="window.close()">✕</button>
<h1>Hello from PrismaUI</h1>
</div>
</body>
</html>
window.close() calls the JS listener you registered in Step 3. No external scripts, no CDN fonts, everything is inline so it loads instantly.
Step 6 Deploy and test
-
Copy the HTML to your MO2 mod folder for your plugin:
<MO2>\mods\<YourMod>\Data\PrismaUI_F4\views\Interface\MyMod\hello.html -
Build your DLL and deploy it as usual.
-
Load a save. Press F6.
Expected result:
- The panel appears in the centre of the screen.
- The game pauses (your cursor should be active).
- Clicking [X] closes the panel and unpauses the game.
- Pressing F6 again reopens it.
Nothing appears when I press F6
Open %USERPROFILE%\Documents\My Games\Fallout4\F4SE\PrismaUI_F4.log. Look for lines with [error] or failed. Common causes: the HTML path passed to CreateView doesn't match where you put the file; the MO2 mod is not active; PrismaUI_F4 is not installed.
Panel opens but [X] does nothing
The JS function name in RegisterJSListener and the function called in HTML must match exactly. You registered "close" in C++, so the HTML must call window.close(). If you renamed one but not the other, the click silently goes nowhere.
Game crashes on load (or on F6)
RequestPluginAPI returned null and you didn't check it. g_api is null, so the first call through it crashes. Confirm PrismaUI_F4.dll is in F4SE/plugins/ and shows up in the F4SE log as loaded. Also confirm you are calling RequestPluginAPI on kGameDataReady, not in F4SEPlugin_Load.
Panel appears but the game does not pause
Focus was called without pauseGame = true, or it was called before the view was shown. Call Show before Focus.
What's next
- Send data to your panel: call
g_api->InteropCall(g_view, "fnName", jsonString)from C++ to callwindow.fnName(arg)in JS. Good for updating values while the panel is open. - Receive data from JS: add more
RegisterJSListenercalls insideOnDomReadyfor each event your HTML needs to send back. - Console debugging: register a
ConsoleMessageCallback(seeIVPrismaUI2::RegisterConsoleCallback) to mirror JSconsole.logoutput intoPrismaUI_F4.log. - Full API reference: see API Reference for every method, and View Lifecycle for timing rules when doing more complex work.