Modern Frameworks
React, Vue, Svelte, Solid, and similar frameworks can be used with PrismaUI_F4 2.1.0 when you ship the result as compatible local HTML/CSS/JavaScript assets.
The in-game runtime is Ultralight 1.4.0, not previous browser runtime. Test the actual production bundle under PrismaUI and avoid making required UI behavior depend on a browser feature you have only tested in Chrome.
The bridge timing problem
Your C++ DOM-ready callback can run before framework components finish mounting. If C++ calls a JavaScript function that a React/Vue/Svelte component has not registered yet, that first message can be lost.
A robust pattern is:
- expose C++-callable
window.*functions at JavaScript module-load time; - let those functions write into a framework-agnostic store;
- let components subscribe to that store;
- from Prisma's DOM-ready callback, register JS-to-C++ listeners and push initial data.
React example
// store.ts
import { create } from 'zustand';
type PlayerState = {
health: number;
maxHealth: number;
};
type Store = {
player: PlayerState | null;
setPlayer: (player: PlayerState) => void;
};
export const useGameStore = create<Store>((set) => ({
player: null,
setPlayer: (player) => set({ player }),
}));
window.setPlayerState = (json: string) => {
useGameStore.getState().setPlayer(JSON.parse(json));
};
Import that module before rendering the application:
import './store';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
C++ can then push state after DOM-ready:
static void ClosePanel(const char*)
{
if (!g_api || !g_view) return;
g_api->Unfocus(g_view);
g_api->Hide(g_view);
}
static void OnDomReady(PrismaView view)
{
g_api->RegisterJSListener(view, "closePanel", ClosePanel);
g_api->InteropCall(
view,
"setPlayerState",
R"({"health":100,"maxHealth":100})");
}
JavaScript to C++
Register page-facing listeners from the DOM-ready path. Prisma-delivered JS listener callbacks are marshalled to the game thread in 2.1.0.
static void SaveSettingsFromJS(const char* json)
{
if (!json) return;
SaveSettings(json);
}
static void OnDomReady(PrismaView view)
{
g_api->RegisterJSListener(view, "saveSettings", SaveSettingsFromJS);
}
export function save(settings: object) {
window.saveSettings(JSON.stringify(settings));
}
TypeScript declarations
export {};
declare global {
interface Window {
setPlayerState: (json: string) => void;
closePanel: () => void;
saveSettings: (json: string) => void;
onTranslationsReady?: () => void;
t?: (key: string) => string;
L10N?: Record<string, string>;
}
}
Translations with a framework app
RegisterTranslations is a runtime Invoke in 2.1.0. It does not make window.t available before your bundle's module-level code executes.
Register translations in DOM-ready, then call an application hook that reads them:
static void OnDomReady(PrismaView view)
{
g_api->RegisterTranslations(view, "MyPlugin_F4");
g_api->Invoke(view, "window.onTranslationsReady && window.onTranslationsReady()");
}
window.onTranslationsReady = () => {
const t = (key: string) => window.t?.(key) ?? key;
// Update your store or framework state here.
};
Do not build module-level constants from window.t unless your own bootstrap code guarantees that translation registration has already happened.
Development outside Fallout
Use your normal Vite/Webpack/dev-server workflow for layout and application logic, with mock data when Prisma is absent.
if (import.meta.env.DEV) {
window.setPlayerState(JSON.stringify({ health: 85, maxHealth: 100 }));
}
The browser preview is a development convenience, not proof that an API is available in Ultralight 1.4.0. Feature-detect anything outside the tested baseline.
Bundle dependencies locally
Do not ship a production page that requires:
- Google Fonts;
- jsDelivr/unpkg;
- a remote JavaScript module;
- a remote stylesheet;
- browser WebSocket/Worker infrastructure.
Bundle those files with the mod instead.
A typical Vite build can emit:
Data/PrismaUI_F4/views/MyPlugin/
├── index.html
└── assets/
├── app.js
├── app.css
└── ui.woff2
If the mod needs real networking, keep that in native C++ and bridge the resulting data into the view.
Multiple screens
You do not have to use exactly one Prisma view per plugin. Use one routed frontend when screens share lifecycle/state, and multiple views when they are genuinely separate surfaces such as a persistent HUD plus an independent panel.
For V10 projects:
- mark passive HUDs
ViewRole::kWidget; - mark interactive menus
ViewRole::kPanel; - use
IsAnyPanelVisiblebefore opening a cooperating panel; - capability-check
InputRegionsbefore usingFocusOverlay/SetInputRegions.
Key rules
| Rule | Reason |
|---|---|
Expose C++-called window.* functions at module load | They exist before components mount |
| Register Prisma JS listeners at DOM-ready | Avoid view/context timing ambiguity |
| Register translations at DOM-ready before translation-dependent init | 2.1.0 injects them through Invoke |
Use InteropCall for normal named C++ -> JS messages | Simple structured bridge path |
| Use JSON strings for structured payloads | Stable language boundary |
| Bundle required web dependencies locally | Matches the 2.1.0 sandbox and offline mod packaging |
| Test the built bundle in Ultralight | Chrome/Vite success is not runtime proof |
See Translations, HTML Views, Networking, View Lifecycle, and API Reference.