From ce3c79020e0e03c31682ffc89cbdeae20167eac0 Mon Sep 17 00:00:00 2001 From: Vitali Kolebyanov Date: Sat, 2 May 2026 15:09:08 +0400 Subject: [PATCH 1/9] Introduce stateful callbacks and `InterfaceVersion::V3` for enhanced plugin API functionality. --- src/API/API.cpp | 37 ++++++---- src/API/API.h | 129 ++++++++++++++++++++++------------- src/PrismaUI/Core.h | 2 +- src/PrismaUI/ViewManager.cpp | 4 +- src/PrismaUI/ViewManager.h | 2 +- src/PrismaUI_API.h | 27 +++++++- 6 files changed, 135 insertions(+), 66 deletions(-) diff --git a/src/API/API.cpp b/src/API/API.cpp index acec43f..3a1fe88 100644 --- a/src/API/API.cpp +++ b/src/API/API.cpp @@ -5,20 +5,11 @@ PrismaView PluginAPI::PrismaUIInterface::CreateView(const char* htmlPath, PRISMA_UI_API::OnDomReadyCallback onDomReadyCallback) noexcept { - if (!htmlPath) { - return 0; - } - - std::function domReadyWrapper = nullptr; - if (onDomReadyCallback) { - domReadyWrapper = [onDomReadyCallback](PrismaUI::Core::PrismaViewId viewId) { - SKSE::GetTaskInterface()->AddTask([callback = onDomReadyCallback, id = viewId]() { - callback(id); - }); - }; - } + auto callback = onDomReadyCallback + ? std::make_optional(onDomReadyCallback) + : std::nullopt; - return PrismaUI::ViewManager::Create(htmlPath, domReadyWrapper); + return CreateViewInternal(htmlPath, callback); } void PluginAPI::PrismaUIInterface::Invoke(PrismaView view, const char* script, PRISMA_UI_API::JSCallback callback) noexcept @@ -240,3 +231,23 @@ void PluginAPI::PrismaUIInterface::RegisterConsoleCallback(PrismaView view, PRIS PrismaUI::ViewManager::RegisterConsoleCallback(view, nullptr); } } + +PrismaView PluginAPI::PrismaUIInterface::CreateViewWithState( + const char* htmlPath, PRISMA_UI_API::OnDomReadyCallbackWithState onDomReadyCallback, void* state) noexcept +{ + auto callback = onDomReadyCallback + ? std::make_optional([onDomReadyCallback, state](auto id) { onDomReadyCallback(id, state); }) + : std::nullopt; + + return CreateViewInternal(htmlPath, callback); +} + +void PluginAPI::PrismaUIInterface::InvokeWithState(PrismaView view, const char* script, + PRISMA_UI_API::JSCallbackWithState callback, void* state) noexcept +{ +} + +void PluginAPI::PrismaUIInterface::RegisterJSListenerWithState(PrismaView view, const char* functionName, + PRISMA_UI_API::JSListenerCallbackWithState callback, void* state) noexcept +{ +} diff --git a/src/API/API.h b/src/API/API.h index dcacdde..3d21823 100644 --- a/src/API/API.h +++ b/src/API/API.h @@ -1,54 +1,87 @@ #pragma once #include "PrismaUI_API.h" +#include "PrismaUI/ViewManager.h" -class PluginAPI -{ - using LatestInterface = PRISMA_UI_API::IVPrismaUI2; +class PluginAPI { + using LatestInterface = PRISMA_UI_API::IVPrismaUI3; public: - class PrismaUIInterface : public LatestInterface - { - private: - PrismaUIInterface() noexcept {}; - virtual ~PrismaUIInterface() noexcept {}; - - public: - static PrismaUIInterface* GetSingleton() noexcept - { - static PrismaUIInterface singleton; - return std::addressof(singleton); - } - - // IVPrismaUI1 (order needs to match PrismaUI_API.h exactly for correct vtable layout) - - virtual PrismaView CreateView(const char* htmlPath, PRISMA_UI_API::OnDomReadyCallback onDomReadyCallback = nullptr) noexcept override; - virtual void Invoke(PrismaView view, const char* script, PRISMA_UI_API::JSCallback callback = nullptr) noexcept override; - virtual void InteropCall(PrismaView view, const char* functionName, const char* argument) noexcept override; - virtual void RegisterJSListener(PrismaView view, const char* fnName, PRISMA_UI_API::JSListenerCallback callback) noexcept override; - virtual bool HasFocus(PrismaView view) noexcept override; - virtual bool Focus(PrismaView view, bool pauseGame = false, bool disableFocusMenu = false) noexcept override; - virtual void Unfocus(PrismaView view) noexcept override; - virtual void Show(PrismaView view) noexcept override; - virtual void Hide(PrismaView view) noexcept override; - virtual bool IsHidden(PrismaView view) noexcept override; - virtual int GetScrollingPixelSize(PrismaView view) noexcept override; - virtual void SetScrollingPixelSize(PrismaView view, int pixelSize) noexcept override; - virtual bool IsValid(PrismaView view) noexcept override; - virtual void Destroy(PrismaView view) noexcept override; - virtual void SetOrder(PrismaView view, int order) noexcept override; - virtual int GetOrder(PrismaView view) noexcept override; - virtual void CreateInspectorView(PrismaView view) noexcept override; - virtual void SetInspectorVisibility(PrismaView view, bool visible) noexcept override; - virtual bool IsInspectorVisible(PrismaView view) noexcept override; - virtual void SetInspectorBounds(PrismaView view, float topLeftX, float topLeftY, unsigned int width, unsigned int height) noexcept override; - virtual bool HasAnyActiveFocus() noexcept override; - - // IVPrismaUI2 - - virtual void RegisterConsoleCallback(PrismaView view, PRISMA_UI_API::ConsoleMessageCallback callback) noexcept override; - - private: - unsigned long apiTID = 0; - }; -}; + class PrismaUIInterface : public LatestInterface { + private: + PrismaUIInterface() noexcept { + }; + + virtual ~PrismaUIInterface() noexcept { + }; + + public: + static PrismaUIInterface* GetSingleton() noexcept { + static PrismaUIInterface singleton; + return std::addressof(singleton); + } + + // IVPrismaUI1 (order needs to match PrismaUI_API.h exactly for correct vtable layout) + + PrismaView CreateView(const char* htmlPath, + PRISMA_UI_API::OnDomReadyCallback onDomReadyCallback = nullptr) noexcept override; + void Invoke(PrismaView view, const char* script, + PRISMA_UI_API::JSCallback callback = nullptr) noexcept override; + void InteropCall(PrismaView view, const char* functionName, const char* argument) noexcept override; + void RegisterJSListener(PrismaView view, const char* fnName, + PRISMA_UI_API::JSListenerCallback callback) noexcept override; + bool HasFocus(PrismaView view) noexcept override; + bool Focus(PrismaView view, bool pauseGame = false, bool disableFocusMenu = false) noexcept override; + void Unfocus(PrismaView view) noexcept override; + void Show(PrismaView view) noexcept override; + void Hide(PrismaView view) noexcept override; + bool IsHidden(PrismaView view) noexcept override; + int GetScrollingPixelSize(PrismaView view) noexcept override; + void SetScrollingPixelSize(PrismaView view, int pixelSize) noexcept override; + bool IsValid(PrismaView view) noexcept override; + void Destroy(PrismaView view) noexcept override; + void SetOrder(PrismaView view, int order) noexcept override; + int GetOrder(PrismaView view) noexcept override; + void CreateInspectorView(PrismaView view) noexcept override; + void SetInspectorVisibility(PrismaView view, bool visible) noexcept override; + bool IsInspectorVisible(PrismaView view) noexcept override; + void SetInspectorBounds(PrismaView view, float topLeftX, float topLeftY, unsigned int width, + unsigned int height) noexcept override; + bool HasAnyActiveFocus() noexcept override; + + // IVPrismaUI2 + + void RegisterConsoleCallback(PrismaView view, PRISMA_UI_API::ConsoleMessageCallback callback) noexcept override; + + // IVPrismaUI3 + + PrismaView CreateViewWithState( + const char* htmlPath, PRISMA_UI_API::OnDomReadyCallbackWithState onDomReadyCallback, void* state) noexcept override; + void InvokeWithState(PrismaView view, const char* script, PRISMA_UI_API::JSCallbackWithState callback, + void* state) noexcept override; + void RegisterJSListenerWithState(PrismaView view, const char* functionName, + PRISMA_UI_API::JSListenerCallbackWithState callback, void* state) noexcept override; + + private: + template TCallback> + PrismaView CreateViewInternal(const char* htmlPath, std::optional onDomReadyCallback) + { + if (!htmlPath) { + return 0; + } + + std::move_only_function domReadyWrapper = nullptr; + if (onDomReadyCallback.has_value()) { + domReadyWrapper = [callback = std::move(onDomReadyCallback.value())](PrismaUI::Core::PrismaViewId viewId) { + SKSE::GetTaskInterface()->AddTask([callback = std::move(callback), id = viewId] { + std::invoke(callback, id); + }); + }; + } + + return PrismaUI::ViewManager::Create(htmlPath, std::move(domReadyWrapper)); + } + + unsigned long apiTID = 0; + }; +}; \ No newline at end of file diff --git a/src/PrismaUI/Core.h b/src/PrismaUI/Core.h index 5cfd46f..eedc4b4 100644 --- a/src/PrismaUI/Core.h +++ b/src/PrismaUI/Core.h @@ -60,7 +60,7 @@ namespace PrismaUI::Core { std::unique_ptr loadListener; std::unique_ptr viewListener; std::atomic isLoadingFinished = false; - std::function domReadyCallback; + std::move_only_function domReadyCallback; std::function consoleMessageCallback; int scrollingPixelSize = 28; std::atomic isPaused = false; diff --git a/src/PrismaUI/ViewManager.cpp b/src/PrismaUI/ViewManager.cpp index 91ef344..6be9e56 100644 --- a/src/PrismaUI/ViewManager.cpp +++ b/src/PrismaUI/ViewManager.cpp @@ -9,7 +9,7 @@ namespace PrismaUI::ViewManager { using namespace Core; - Core::PrismaViewId Create(const std::string& htmlPath, std::function onDomReadyCallback) { + Core::PrismaViewId Create(const std::string& htmlPath, std::move_only_function onDomReadyCallback) { bool expected_init = false; if (coreInitialized.compare_exchange_strong(expected_init, true)) { Core::InitializeCoreSystem(); @@ -39,7 +39,7 @@ namespace PrismaUI::ViewManager { viewData->htmlPathToLoad = fileUrl; viewData->originalUrl = fileUrl; // Store for recovery after exceptions viewData->isHidden = false; - viewData->domReadyCallback = onDomReadyCallback; + viewData->domReadyCallback = std::move(onDomReadyCallback); { std::unique_lock lock(viewsMutex); diff --git a/src/PrismaUI/ViewManager.h b/src/PrismaUI/ViewManager.h index 72ddfe8..f24ad03 100644 --- a/src/PrismaUI/ViewManager.h +++ b/src/PrismaUI/ViewManager.h @@ -24,7 +24,7 @@ namespace PrismaUI::ViewManager { using namespace ultralight; Core::PrismaViewId Create(const std::string& htmlPath, - std::function onDomReadyCallback = nullptr); + std::move_only_function onDomReadyCallback = nullptr); void Show(const Core::PrismaViewId& viewId); void Hide(const Core::PrismaViewId& viewId); bool IsHidden(const Core::PrismaViewId& viewId); diff --git a/src/PrismaUI_API.h b/src/PrismaUI_API.h index 94ebb6d..bf5fe3b 100644 --- a/src/PrismaUI_API.h +++ b/src/PrismaUI_API.h @@ -20,11 +20,14 @@ namespace PRISMA_UI_API { constexpr const auto PrismaUIPluginName = "PrismaUI"; // Available PrismaUI interface versions - enum class InterfaceVersion : uint8_t { V1, V2 }; + enum class InterfaceVersion : uint8_t { V1, V2, V3 }; typedef void (*OnDomReadyCallback)(PrismaView view); + typedef void (*OnDomReadyCallbackWithState)(PrismaView view, void* state); typedef void (*JSCallback)(const char* result); + typedef void (*JSCallbackWithState)(const char* result, void* state); typedef void (*JSListenerCallback)(const char* argument); + typedef void (*JSListenerCallbackWithState)(const char* argument, void* state); // JavaScript console message severity level for use with RegisterConsoleCallback(). enum class ConsoleMessageLevel : uint8_t { Log = 0, Warning, Error, Debug, Info }; @@ -116,6 +119,23 @@ namespace PRISMA_UI_API { virtual void RegisterConsoleCallback(PrismaView view, ConsoleMessageCallback callback) noexcept = 0; }; + // PrismaUI modder interface v3 (extends v2) + class IVPrismaUI3 : public IVPrismaUI2 { + protected: + ~IVPrismaUI3() = default; + + public: + virtual PrismaView CreateViewWithState( + const char* htmlPath, OnDomReadyCallbackWithState onDomReadyCallback = nullptr, + void* state = nullptr) noexcept = 0; + + virtual void InvokeWithState(PrismaView view, const char* script, JSCallbackWithState callback = nullptr, + void* state = nullptr) noexcept = 0; + + virtual void RegisterJSListenerWithState(PrismaView view, const char* functionName, + JSListenerCallbackWithState callback, void* state = nullptr) noexcept = 0; + }; + // Maps interface types to InterfaceVersion enum values. // compile-time constraint -- only request interface versions that actually exist. template @@ -131,6 +151,11 @@ namespace PRISMA_UI_API { static constexpr InterfaceVersion version = InterfaceVersion::V2; }; + template <> + struct InterfaceVersionMap { + static constexpr InterfaceVersion version = InterfaceVersion::V3; + }; + typedef void* (*RequestPluginAPIFunc)(InterfaceVersion interfaceVersion); /// Request the PrismaUI API interface. From 9d5c04f300018c9d3c0aa9f46335233247aa385d Mon Sep 17 00:00:00 2001 From: Vitali Kolebyanov Date: Sat, 2 May 2026 15:18:30 +0400 Subject: [PATCH 2/9] Refactor CommonLibSSE build configuration to streamline directory setup and remove redundant build type handling. --- cmake/commonlibsse.cmake | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/cmake/commonlibsse.cmake b/cmake/commonlibsse.cmake index db1eafc..20d5186 100644 --- a/cmake/commonlibsse.cmake +++ b/cmake/commonlibsse.cmake @@ -3,6 +3,7 @@ # CommonLibSSE-NG configuration set(CommonLibPath "external/commonlibsse-ng") set(CommonLibName "CommonLibSSE") +set(CommonLibBuildDir "${CMAKE_BINARY_DIR}/external_builds/${CommonLibName}") # Extract version from CommonLibSSE's CMakeLists.txt (project() VERSION doesn't propagate to parent scope) file(READ "${CMAKE_SOURCE_DIR}/${CommonLibPath}/CMakeLists.txt" _commonlib_cmakelists_content) @@ -10,21 +11,15 @@ string(REGEX MATCH "VERSION[ \t]+([0-9]+\\.[0-9]+\\.[0-9]+)" _ "${_commonlib_cma set(COMMONLIBSSE_VERSION "${CMAKE_MATCH_1}" CACHE STRING "CommonLibSSE-NG version" FORCE) message(STATUS "Configuring CommonLibSSE-NG version ${COMMONLIBSSE_VERSION}") -# Save original build type -set(_saved_build_type "${CMAKE_BUILD_TYPE}") - -# Always build CommonLibSSE in Release mode to disable assertions and enable optimizations -set(CMAKE_BUILD_TYPE "Release") add_definitions(-D_CRT_SECURE_NO_WARNINGS) -add_definitions(-DNDEBUG) # Disable CommonLibSSE tests when building as subdirectory set(BUILD_TESTS OFF CACHE BOOL "Disable CommonLibSSE tests" FORCE) # Check if CommonLibSSE has already been built -set(COMMONLIB_STAMP_FILE "${BUILD_ROOT}/external_builds/${CommonLibName}.stamp") +set(COMMONLIB_STAMP_FILE "${CommonLibBuildDir}/${CommonLibName}.stamp") # Add CommonLibSSE-NG as a subdirectory with EXCLUDE_FROM_ALL -add_subdirectory("${CommonLibPath}" "${BUILD_ROOT}/external_builds/${CommonLibName}" EXCLUDE_FROM_ALL) +add_subdirectory("${CommonLibPath}" "${CommonLibBuildDir}" EXCLUDE_FROM_ALL) # Create stamp file after configuration (informational only) if(NOT EXISTS "${COMMONLIB_STAMP_FILE}") @@ -34,8 +29,5 @@ endif() # Include the CommonLibSSE helper cmake functions (provides add_commonlibsse_plugin macro) include("${CommonLibPath}/cmake/CommonLibSSE.cmake") -# Restore original build type for the main project -set(CMAKE_BUILD_TYPE "${_saved_build_type}") - # Expose CommonLibSSE version to C++ code add_compile_definitions(COMMONLIBSSE_VERSION="${COMMONLIBSSE_VERSION}") From 0a6ae1014b3ee85badda9a7db4b2fb6b58b1bf88 Mon Sep 17 00:00:00 2001 From: Vitali Kolebyanov Date: Sat, 2 May 2026 16:32:56 +0400 Subject: [PATCH 3/9] Refactor callback handling with `std::move_only_function` and introduce internal API helpers for streamlined invocation and listener registration. --- src/API/API.cpp | 131 +++++++++++++++++++++------------ src/API/API.h | 24 ++---- src/PrismaUI/Communication.cpp | 8 +- src/PrismaUI/Communication.h | 2 +- 4 files changed, 95 insertions(+), 70 deletions(-) diff --git a/src/API/API.cpp b/src/API/API.cpp index 3a1fe88..309ea5b 100644 --- a/src/API/API.cpp +++ b/src/API/API.cpp @@ -5,44 +5,12 @@ PrismaView PluginAPI::PrismaUIInterface::CreateView(const char* htmlPath, PRISMA_UI_API::OnDomReadyCallback onDomReadyCallback) noexcept { - auto callback = onDomReadyCallback - ? std::make_optional(onDomReadyCallback) - : std::nullopt; - - return CreateViewInternal(htmlPath, callback); + return CreateViewInternal(htmlPath, onDomReadyCallback); } void PluginAPI::PrismaUIInterface::Invoke(PrismaView view, const char* script, PRISMA_UI_API::JSCallback callback) noexcept { - if (!view || !script) { - return; - } - - std::string processedScript; - - if (isValidUTF8(script)) { - processedScript = script; - } - else { - processedScript = convertFromANSIToUTF8(script); - if (processedScript.empty()) { - return; // Conversion failed, cannot safely invoke - } - } - - ultralight::String _script(processedScript.c_str()); - - std::function callbackWrapper = nullptr; - - if (callback) { - callbackWrapper = [callback](const std::string& result) { - SKSE::GetTaskInterface()->AddTask([targetCallback = callback, data = result]() { - targetCallback(data.c_str()); - }); - }; - } - - return PrismaUI::Communication::Invoke(view, _script, callbackWrapper); + InvokeInternal(view, script, callback); } void PluginAPI::PrismaUIInterface::InteropCall(PrismaView view, const char* functionName, const char* argument) noexcept @@ -68,17 +36,7 @@ void PluginAPI::PrismaUIInterface::InteropCall(PrismaView view, const char* func void PluginAPI::PrismaUIInterface::RegisterJSListener(PrismaView view, const char* fnName, PRISMA_UI_API::JSListenerCallback callback) noexcept { - if (!view || !fnName || !callback) { - return; - } - - std::function callbackWrapper = [callback](const std::string& arg) { - SKSE::GetTaskInterface()->AddTask([targetCallback = callback, data = arg]() { - targetCallback(data.c_str()); - }); - }; - - return PrismaUI::Communication::RegisterJSListener(view, fnName, callbackWrapper); + RegisterJSListenerInternal(view, fnName, callback); } bool PluginAPI::PrismaUIInterface::HasFocus(PrismaView view) noexcept @@ -236,8 +194,8 @@ PrismaView PluginAPI::PrismaUIInterface::CreateViewWithState( const char* htmlPath, PRISMA_UI_API::OnDomReadyCallbackWithState onDomReadyCallback, void* state) noexcept { auto callback = onDomReadyCallback - ? std::make_optional([onDomReadyCallback, state](auto id) { onDomReadyCallback(id, state); }) - : std::nullopt; + ? std::function([onDomReadyCallback, state](auto id) { onDomReadyCallback(id, state); }) + : nullptr; return CreateViewInternal(htmlPath, callback); } @@ -245,9 +203,88 @@ PrismaView PluginAPI::PrismaUIInterface::CreateViewWithState( void PluginAPI::PrismaUIInterface::InvokeWithState(PrismaView view, const char* script, PRISMA_UI_API::JSCallbackWithState callback, void* state) noexcept { + auto callbackFunc = callback + ? std::function([callback, state](auto id) { callback(id, state); }) + : nullptr; + + InvokeInternal(view, script, callbackFunc); } void PluginAPI::PrismaUIInterface::RegisterJSListenerWithState(PrismaView view, const char* functionName, PRISMA_UI_API::JSListenerCallbackWithState callback, void* state) noexcept { + if (!callback) { + return; + } + + RegisterJSListenerInternal(view, functionName, [callback, state](auto argument) { callback(argument, state); }); +} + +PrismaView PluginAPI::PrismaUIInterface::CreateViewInternal( + const char* htmlPath, std::function onDomReadyCallback) noexcept +{ + if (!htmlPath) { + return 0; + } + + std::move_only_function domReadyWrapper = nullptr; + if (onDomReadyCallback) { + domReadyWrapper = [callback = std::move(onDomReadyCallback)](PrismaUI::Core::PrismaViewId viewId) { + SKSE::GetTaskInterface()->AddTask([callback = std::move(callback), id = viewId] { + std::invoke(callback, id); + }); + }; + } + + return PrismaUI::ViewManager::Create(htmlPath, std::move(domReadyWrapper)); +} + +void PluginAPI::PrismaUIInterface::InvokeInternal(PrismaView view, const char* script, + std::function callback) noexcept +{ + if (!view || !script) { + return; + } + + std::string processedScript; + + if (isValidUTF8(script)) { + processedScript = script; + } + else { + processedScript = convertFromANSIToUTF8(script); + if (processedScript.empty()) { + return; // Conversion failed, cannot safely invoke + } + } + + ultralight::String _script(processedScript.c_str()); + + std::move_only_function callbackWrapper = nullptr; + + if (callback) { + callbackWrapper = [callback = std::move(callback)](const std::string& result) { + SKSE::GetTaskInterface()->AddTask([targetCallback = std::move(callback), data = result] { + targetCallback(data.c_str()); + }); + }; + } + + return PrismaUI::Communication::Invoke(view, _script, std::move(callbackWrapper)); +} + +void PluginAPI::PrismaUIInterface::RegisterJSListenerInternal(PrismaView view, const char* functionName, + std::function callback) noexcept +{ + if (!view || !functionName || !callback) { + return; + } + + PrismaUI::Core::SimpleJSCallback callbackWrapper = [callback = std::move(callback)](const std::string& arg) { + SKSE::GetTaskInterface()->AddTask([targetCallback = std::move(callback), data = arg] { + targetCallback(data.c_str()); + }); + }; + + return PrismaUI::Communication::RegisterJSListener(view, functionName, callbackWrapper); } diff --git a/src/API/API.h b/src/API/API.h index 3d21823..5242295 100644 --- a/src/API/API.h +++ b/src/API/API.h @@ -63,24 +63,12 @@ class PluginAPI { PRISMA_UI_API::JSListenerCallbackWithState callback, void* state) noexcept override; private: - template TCallback> - PrismaView CreateViewInternal(const char* htmlPath, std::optional onDomReadyCallback) - { - if (!htmlPath) { - return 0; - } - - std::move_only_function domReadyWrapper = nullptr; - if (onDomReadyCallback.has_value()) { - domReadyWrapper = [callback = std::move(onDomReadyCallback.value())](PrismaUI::Core::PrismaViewId viewId) { - SKSE::GetTaskInterface()->AddTask([callback = std::move(callback), id = viewId] { - std::invoke(callback, id); - }); - }; - } - - return PrismaUI::ViewManager::Create(htmlPath, std::move(domReadyWrapper)); - } + static PrismaView CreateViewInternal( + const char* htmlPath, std::function onDomReadyCallback) noexcept; + static void InvokeInternal( + PrismaView view, const char* script, std::function callback) noexcept; + static void RegisterJSListenerInternal(PrismaView view, const char* functionName, + std::function callback) noexcept; unsigned long apiTID = 0; }; diff --git a/src/PrismaUI/Communication.cpp b/src/PrismaUI/Communication.cpp index f1ce1f5..10757b8 100644 --- a/src/PrismaUI/Communication.cpp +++ b/src/PrismaUI/Communication.cpp @@ -7,7 +7,7 @@ namespace PrismaUI::Communication { using namespace Core; using namespace ViewManager; - void Invoke(const Core::PrismaViewId& viewId, const String& script, std::function callback) { + void Invoke(const Core::PrismaViewId& viewId, const String& script, std::move_only_function callback) { std::shared_ptr viewData = nullptr; { std::shared_lock lock(viewsMutex); @@ -25,7 +25,7 @@ namespace PrismaUI::Communication { return; } - ultralightThread.submit([view_ptr = viewData->ultralightView, script_copy = script, callback]() { + ultralightThread.submit([view_ptr = viewData->ultralightView, script_copy = script, callback = std::move(callback)] mutable { String result = ""; if (view_ptr) { try { @@ -55,7 +55,7 @@ namespace PrismaUI::Communication { JSCallbackData data; data.viewId = viewId; data.name = name; - data.callback = callback; + data.callback = std::move(callback); jsCallbacks[std::make_pair(viewId, name)] = std::move(data); logger::debug("RegisterJSListener: Registered callback '{}' for view [{}]", name, viewId); } @@ -168,7 +168,7 @@ namespace PrismaUI::Communication { Core::PrismaViewId retrievedViewId = callbackDataPtr->viewId; std::string retrievedName = callbackDataPtr->name; - Core::SimpleJSCallback targetCallback = callbackDataPtr->callback; + Core::SimpleJSCallback& targetCallback = callbackDataPtr->callback; logger::debug("JSCallbackDispatcher: Retrieved from C++ pointer -> View ID: '{}', Name: '{}'", retrievedViewId, retrievedName); diff --git a/src/PrismaUI/Communication.h b/src/PrismaUI/Communication.h index 4139a77..a74d58f 100644 --- a/src/PrismaUI/Communication.h +++ b/src/PrismaUI/Communication.h @@ -16,7 +16,7 @@ namespace PrismaUI::Core { namespace PrismaUI::Communication { void Invoke(const Core::PrismaViewId& viewId, const ultralight::String& script, - std::function callback = nullptr); + std::move_only_function callback = nullptr); void BindJSCallbacks(const Core::PrismaViewId& viewId); JSValueRef InvokeCppCallback(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception); From be6968d4914d5212758eb723d2e36343ad80038e Mon Sep 17 00:00:00 2001 From: Vitali Kolebyanov Date: Sat, 2 May 2026 16:50:46 +0400 Subject: [PATCH 4/9] Update `SimpleJSCallback` to use `const std::string&` for improved clarity and efficiency. --- src/PrismaUI/Communication.h | 2 +- src/PrismaUI/Core.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PrismaUI/Communication.h b/src/PrismaUI/Communication.h index a74d58f..d539e37 100644 --- a/src/PrismaUI/Communication.h +++ b/src/PrismaUI/Communication.h @@ -11,7 +11,7 @@ namespace PrismaUI::Core { typedef uint64_t PrismaViewId; - using SimpleJSCallback = std::function; + using SimpleJSCallback = std::function; } namespace PrismaUI::Communication { diff --git a/src/PrismaUI/Core.h b/src/PrismaUI/Core.h index eedc4b4..127cf8b 100644 --- a/src/PrismaUI/Core.h +++ b/src/PrismaUI/Core.h @@ -126,7 +126,7 @@ namespace PrismaUI::Core { extern std::map> views; extern std::shared_mutex viewsMutex; - using SimpleJSCallback = std::function; + using SimpleJSCallback = std::function; struct JSCallbackData { PrismaViewId viewId; From 4dee7aa999f4dbb6ad45c4d09a3f9c6d81002614 Mon Sep 17 00:00:00 2001 From: Vitali Kolebyanov Date: Sat, 2 May 2026 17:06:04 +0400 Subject: [PATCH 5/9] Add support for `InterfaceVersion::V3` in `RequestPluginAPI`. --- src/main.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index 23d9a76..609d31b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -57,6 +57,9 @@ RequestPluginAPI(const PRISMA_UI_API::InterfaceVersion a_interfaceVersion) { case PRISMA_UI_API::InterfaceVersion::V2: logger::info("RequestPluginAPI returned V2 interface"); return static_cast(api); + case PRISMA_UI_API::InterfaceVersion::V3: + logger::info("RequestPluginAPI returned V3 interface"); + return static_cast(api); default: logger::info("RequestPluginAPI requested unsupported interface version"); return nullptr; From bf316eb71109597117fae06900aff1e07028c344 Mon Sep 17 00:00:00 2001 From: Vitali Kolebyanov Date: Sat, 2 May 2026 18:00:56 +0400 Subject: [PATCH 6/9] Refactor `PrismaUIInterface` methods to use V2 naming and improve callback state handling. --- src/API/API.cpp | 18 +++++++++--------- src/API/API.h | 12 ++++++------ src/PrismaUI_API.h | 12 ++++++------ 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/API/API.cpp b/src/API/API.cpp index 309ea5b..30a765a 100644 --- a/src/API/API.cpp +++ b/src/API/API.cpp @@ -190,34 +190,34 @@ void PluginAPI::PrismaUIInterface::RegisterConsoleCallback(PrismaView view, PRIS } } -PrismaView PluginAPI::PrismaUIInterface::CreateViewWithState( - const char* htmlPath, PRISMA_UI_API::OnDomReadyCallbackWithState onDomReadyCallback, void* state) noexcept +PrismaView PluginAPI::PrismaUIInterface::CreateViewV2( + const char* htmlPath, PRISMA_UI_API::OnDomReadyCallbackWithState onDomReadyCallback, void* callbackState) noexcept { auto callback = onDomReadyCallback - ? std::function([onDomReadyCallback, state](auto id) { onDomReadyCallback(id, state); }) + ? std::function([onDomReadyCallback, callbackState](auto id) { onDomReadyCallback(id, callbackState); }) : nullptr; return CreateViewInternal(htmlPath, callback); } -void PluginAPI::PrismaUIInterface::InvokeWithState(PrismaView view, const char* script, - PRISMA_UI_API::JSCallbackWithState callback, void* state) noexcept +void PluginAPI::PrismaUIInterface::InvokeV2(PrismaView view, const char* script, + PRISMA_UI_API::JSCallbackWithState callback, void* callbackState) noexcept { auto callbackFunc = callback - ? std::function([callback, state](auto id) { callback(id, state); }) + ? std::function([callback, callbackState](auto id) { callback(id, callbackState); }) : nullptr; InvokeInternal(view, script, callbackFunc); } -void PluginAPI::PrismaUIInterface::RegisterJSListenerWithState(PrismaView view, const char* functionName, - PRISMA_UI_API::JSListenerCallbackWithState callback, void* state) noexcept +void PluginAPI::PrismaUIInterface::RegisterJSListenerV2(PrismaView view, const char* functionName, + PRISMA_UI_API::JSListenerCallbackWithState callback, void* callbackState) noexcept { if (!callback) { return; } - RegisterJSListenerInternal(view, functionName, [callback, state](auto argument) { callback(argument, state); }); + RegisterJSListenerInternal(view, functionName, [callback, callbackState](auto argument) { callback(argument, callbackState); }); } PrismaView PluginAPI::PrismaUIInterface::CreateViewInternal( diff --git a/src/API/API.h b/src/API/API.h index 5242295..3d13381 100644 --- a/src/API/API.h +++ b/src/API/API.h @@ -55,12 +55,12 @@ class PluginAPI { // IVPrismaUI3 - PrismaView CreateViewWithState( - const char* htmlPath, PRISMA_UI_API::OnDomReadyCallbackWithState onDomReadyCallback, void* state) noexcept override; - void InvokeWithState(PrismaView view, const char* script, PRISMA_UI_API::JSCallbackWithState callback, - void* state) noexcept override; - void RegisterJSListenerWithState(PrismaView view, const char* functionName, - PRISMA_UI_API::JSListenerCallbackWithState callback, void* state) noexcept override; + PrismaView CreateViewV2( + const char* htmlPath, PRISMA_UI_API::OnDomReadyCallbackWithState onDomReadyCallback, void* callbackState) noexcept override; + void InvokeV2(PrismaView view, const char* script, PRISMA_UI_API::JSCallbackWithState callback, + void* callbackState) noexcept override; + void RegisterJSListenerV2(PrismaView view, const char* functionName, + PRISMA_UI_API::JSListenerCallbackWithState callback, void* callbackState) noexcept override; private: static PrismaView CreateViewInternal( diff --git a/src/PrismaUI_API.h b/src/PrismaUI_API.h index bf5fe3b..5fc9d78 100644 --- a/src/PrismaUI_API.h +++ b/src/PrismaUI_API.h @@ -125,15 +125,15 @@ namespace PRISMA_UI_API { ~IVPrismaUI3() = default; public: - virtual PrismaView CreateViewWithState( + virtual PrismaView CreateViewV2( const char* htmlPath, OnDomReadyCallbackWithState onDomReadyCallback = nullptr, - void* state = nullptr) noexcept = 0; + void* callbackState = nullptr) noexcept = 0; - virtual void InvokeWithState(PrismaView view, const char* script, JSCallbackWithState callback = nullptr, - void* state = nullptr) noexcept = 0; + virtual void InvokeV2(PrismaView view, const char* script, JSCallbackWithState callback = nullptr, + void* callbackState = nullptr) noexcept = 0; - virtual void RegisterJSListenerWithState(PrismaView view, const char* functionName, - JSListenerCallbackWithState callback, void* state = nullptr) noexcept = 0; + virtual void RegisterJSListenerV2(PrismaView view, const char* functionName, + JSListenerCallbackWithState callback, void* callbackState = nullptr) noexcept = 0; }; // Maps interface types to InterfaceVersion enum values. From 087c8258e7ff7ff9adb72363946296f5f888a403 Mon Sep 17 00:00:00 2001 From: Vitali Kolebyanov Date: Sat, 2 May 2026 18:12:14 +0400 Subject: [PATCH 7/9] Introduce `RegisterConsoleCallbackV2` with stateful callback support and refactor related API internals. --- src/API/API.cpp | 15 +++++++++++++++ src/API/API.h | 2 ++ src/PrismaUI/Core.h | 2 +- src/PrismaUI/Listeners.cpp | 2 +- src/PrismaUI/ViewManager.cpp | 6 ++++-- src/PrismaUI/ViewManager.h | 5 +++-- src/PrismaUI_API.h | 4 ++++ 7 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/API/API.cpp b/src/API/API.cpp index 30a765a..80e7230 100644 --- a/src/API/API.cpp +++ b/src/API/API.cpp @@ -220,6 +220,21 @@ void PluginAPI::PrismaUIInterface::RegisterJSListenerV2(PrismaView view, const c RegisterJSListenerInternal(view, functionName, [callback, callbackState](auto argument) { callback(argument, callbackState); }); } +void PluginAPI::PrismaUIInterface::RegisterConsoleCallbackV2( + PrismaView view, PRISMA_UI_API::ConsoleMessageCallbackWithState callback, void* callbackState) noexcept +{ + if (!view || !callback) { + return; + } + + auto wrappedCallback = [callback, callbackState](PrismaUI::Core::PrismaViewId id, PRISMA_UI_API::ConsoleMessageLevel level, const std::string& msg) { + SKSE::GetTaskInterface()->AddTask([callback, id, level, msg, callbackState] { + callback(id, level, msg.c_str(), callbackState); + }); + }; + PrismaUI::ViewManager::RegisterConsoleCallback(view, wrappedCallback); +} + PrismaView PluginAPI::PrismaUIInterface::CreateViewInternal( const char* htmlPath, std::function onDomReadyCallback) noexcept { diff --git a/src/API/API.h b/src/API/API.h index 3d13381..d7c8bbf 100644 --- a/src/API/API.h +++ b/src/API/API.h @@ -61,6 +61,8 @@ class PluginAPI { void* callbackState) noexcept override; void RegisterJSListenerV2(PrismaView view, const char* functionName, PRISMA_UI_API::JSListenerCallbackWithState callback, void* callbackState) noexcept override; + void RegisterConsoleCallbackV2(PrismaView view, PRISMA_UI_API::ConsoleMessageCallbackWithState callback, + void* callbackState) noexcept override; private: static PrismaView CreateViewInternal( diff --git a/src/PrismaUI/Core.h b/src/PrismaUI/Core.h index 127cf8b..e156b6c 100644 --- a/src/PrismaUI/Core.h +++ b/src/PrismaUI/Core.h @@ -61,7 +61,7 @@ namespace PrismaUI::Core { std::unique_ptr viewListener; std::atomic isLoadingFinished = false; std::move_only_function domReadyCallback; - std::function consoleMessageCallback; + std::move_only_function consoleMessageCallback; int scrollingPixelSize = 28; std::atomic isPaused = false; int order = 0; diff --git a/src/PrismaUI/Listeners.cpp b/src/PrismaUI/Listeners.cpp index 94ba907..f4df9d5 100644 --- a/src/PrismaUI/Listeners.cpp +++ b/src/PrismaUI/Listeners.cpp @@ -88,7 +88,7 @@ namespace PrismaUI::Listeners { default: break; } auto msg = std::string(message.message().utf8().data()); - auto cb = it->second->consoleMessageCallback; + auto& cb = it->second->consoleMessageCallback; auto id = viewId_; lock.unlock(); cb(id, level, msg); diff --git a/src/PrismaUI/ViewManager.cpp b/src/PrismaUI/ViewManager.cpp index 6be9e56..1b3fdc6 100644 --- a/src/PrismaUI/ViewManager.cpp +++ b/src/PrismaUI/ViewManager.cpp @@ -611,8 +611,10 @@ namespace PrismaUI::ViewManager { } } - void RegisterConsoleCallback(const Core::PrismaViewId& viewId, - std::function callback) { + void RegisterConsoleCallback( + const Core::PrismaViewId& viewId, + std::move_only_function callback) + { std::unique_lock lock(viewsMutex); auto it = views.find(viewId); if (it != views.end() && it->second) { diff --git a/src/PrismaUI/ViewManager.h b/src/PrismaUI/ViewManager.h index f24ad03..37bd31b 100644 --- a/src/PrismaUI/ViewManager.h +++ b/src/PrismaUI/ViewManager.h @@ -48,6 +48,7 @@ namespace PrismaUI::ViewManager { bool HasAnyActiveFocus(); // Console message callback registration - void RegisterConsoleCallback(const Core::PrismaViewId& viewId, - std::function callback); + void RegisterConsoleCallback( + const Core::PrismaViewId& viewId, + std::move_only_function callback); } diff --git a/src/PrismaUI_API.h b/src/PrismaUI_API.h index 5fc9d78..6f3fcae 100644 --- a/src/PrismaUI_API.h +++ b/src/PrismaUI_API.h @@ -34,6 +34,7 @@ namespace PRISMA_UI_API { // Console message callback. typedef void (*ConsoleMessageCallback)(PrismaView view, ConsoleMessageLevel level, const char* message); + typedef void (*ConsoleMessageCallbackWithState)(PrismaView view, ConsoleMessageLevel level, const char* message, void* state); // PrismaUI modder interface v1 class IVPrismaUI1 { @@ -134,6 +135,9 @@ namespace PRISMA_UI_API { virtual void RegisterJSListenerV2(PrismaView view, const char* functionName, JSListenerCallbackWithState callback, void* callbackState = nullptr) noexcept = 0; + + virtual void RegisterConsoleCallbackV2(PrismaView view, ConsoleMessageCallbackWithState callback, + void* callbackState = nullptr) noexcept = 0; }; // Maps interface types to InterfaceVersion enum values. From cf0daa31481e1f99c3164062948fee9825384253 Mon Sep 17 00:00:00 2001 From: Vitali Kolebyanov Date: Sat, 2 May 2026 18:15:16 +0400 Subject: [PATCH 8/9] Add `IVPrismaUI3` with state-aware callback variants and update `RequestPluginAPI` for V3 support. --- src/PrismaUI_API.h | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/PrismaUI_API.h b/src/PrismaUI_API.h index 6f3fcae..610cb8b 100644 --- a/src/PrismaUI_API.h +++ b/src/PrismaUI_API.h @@ -121,21 +121,31 @@ namespace PRISMA_UI_API { }; // PrismaUI modder interface v3 (extends v2) + // Adds state-aware callback variants. PrismaUI stores and passes callbackState back unchanged; + // ownership and lifetime of callbackState remain the caller's responsibility. class IVPrismaUI3 : public IVPrismaUI2 { protected: ~IVPrismaUI3() = default; public: + // Create view and receive callbackState in the DOM-ready callback. + // callbackState may be nullptr. virtual PrismaView CreateViewV2( const char* htmlPath, OnDomReadyCallbackWithState onDomReadyCallback = nullptr, void* callbackState = nullptr) noexcept = 0; + // Send JS code to UI and receive callbackState in the result callback. + // callbackState may be nullptr. virtual void InvokeV2(PrismaView view, const char* script, JSCallbackWithState callback = nullptr, void* callbackState = nullptr) noexcept = 0; + // Register a JS listener and receive callbackState each time JavaScript calls it. + // callbackState may be nullptr. virtual void RegisterJSListenerV2(PrismaView view, const char* functionName, JSListenerCallbackWithState callback, void* callbackState = nullptr) noexcept = 0; + // Register a callback to receive JavaScript console messages with callbackState. + // callbackState may be nullptr. virtual void RegisterConsoleCallbackV2(PrismaView view, ConsoleMessageCallbackWithState callback, void* callbackState = nullptr) noexcept = 0; }; @@ -187,6 +197,7 @@ namespace PRISMA_UI_API { /// Usage: /// auto* m_prismaUI = PRISMA_UI_API::RequestPluginAPI(); /// auto* m_prismaUIv2 = PRISMA_UI_API::RequestPluginAPI(); + /// auto* m_prismaUIv3 = PRISMA_UI_API::RequestPluginAPI(); template [[nodiscard]] inline T* RequestPluginAPI() { return static_cast(RequestPluginAPI(InterfaceVersionMap::version)); From cf6ccfae87957f21cbe2ccab264076e37b55ec39 Mon Sep 17 00:00:00 2001 From: Vitali Kolebyanov Date: Sun, 3 May 2026 16:42:14 +0400 Subject: [PATCH 9/9] Fix debug CMake flags --- CMakeLists.txt | 4 ++-- CMakePresets.json | 10 ++-------- cmake/CompilerFlags.cmake | 28 +++++++++++++++++++--------- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c597b5b..73a238b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -114,7 +114,7 @@ add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD # Post-build: Create distribution folder structure set(DIST_ROOT "${CMAKE_SOURCE_DIR}/dist") -set(DIST_VERSION_DIR "${DIST_ROOT}/PrismaUI_${PROJECT_VERSION}") +set(DIST_VERSION_DIR "${DIST_ROOT}/PrismaUI_${PROJECT_VERSION}$<$:_Debug>") set(DIST_PRISMAUI_DIR "${DIST_VERSION_DIR}/PrismaUI") set(DIST_SKSE_PLUGINS_DIR "${DIST_VERSION_DIR}/SKSE/plugins") @@ -139,6 +139,6 @@ add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD "$<$>:$>" "${DIST_SKSE_PLUGINS_DIR}/" - COMMAND "${CMAKE_COMMAND}" -E echo "Creating distribution folder: dist/PrismaUI_${PROJECT_VERSION}" + COMMAND "${CMAKE_COMMAND}" -E echo "Creating distribution folder: ${DIST_VERSION_DIR}" ) diff --git a/CMakePresets.json b/CMakePresets.json index 3469a27..020934c 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -20,14 +20,8 @@ "VCPKG_TARGET_TRIPLET": "x64-windows-static", "VCPKG_INSTALLED_DIR": "${sourceDir}/build/vcpkg_installed", "VCPKG_INSTALL_OPTIONS": "--no-print-usage", - "CMAKE_MSVC_RUNTIME_LIBRARY": "MultiThreaded$<$:Debug>", - "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", - - "CMAKE_CXX_FLAGS": "/permissive- /Zc:preprocessor /MP /W4 /utf-8", - "CMAKE_C_FLAGS_DEBUG": "/DWIN32_LEAN_AND_MEAN /DNOMINMAX /DUNICODE /D_UNICODE", - "CMAKE_CXX_FLAGS_DEBUG": "/DWIN32_LEAN_AND_MEAN /DNOMINMAX /DUNICODE /D_UNICODE", - "CMAKE_C_FLAGS_RELEASE": "/DWIN32_LEAN_AND_MEAN /DNOMINMAX /DUNICODE /D_UNICODE", - "CMAKE_CXX_FLAGS_RELEASE": "/DWIN32_LEAN_AND_MEAN /DNOMINMAX /DUNICODE /D_UNICODE", + "CMAKE_MSVC_RUNTIME_LIBRARY": "MultiThreaded$<$:Debug>", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", "CMAKE_CXX_STANDARD": "23", "CMAKE_CXX_STANDARD_REQUIRED": "ON", diff --git a/cmake/CompilerFlags.cmake b/cmake/CompilerFlags.cmake index 93de28d..e0c98d6 100644 --- a/cmake/CompilerFlags.cmake +++ b/cmake/CompilerFlags.cmake @@ -1,11 +1,21 @@ -# CompilerFlags.cmake -# Configures compiler-specific flags and optimizations - -# Statically link the C runtime (avoids MSVC redistributable DLLs) -if(MSVC) - # Add compilation speed optimizations - # /d2ReducedOptimizeHugeFunctions - Faster optimization for large functions - string(APPEND CMAKE_CXX_FLAGS " /d2ReducedOptimizeHugeFunctions") +# CompilerFlags.cmake +# Configures compiler-specific flags and optimizations + +add_compile_definitions( + WIN32_LEAN_AND_MEAN + NOMINMAX + UNICODE + _UNICODE +) + +# Statically link the C runtime (avoids MSVC redistributable DLLs) +if(MSVC) + string(REGEX REPLACE "/EH[scra-]*" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + string(APPEND CMAKE_CXX_FLAGS " /permissive- /Zc:preprocessor /MP /W4 /utf-8") + + # Add compilation speed optimizations + # /d2ReducedOptimizeHugeFunctions - Faster optimization for large functions + string(APPEND CMAKE_CXX_FLAGS " /d2ReducedOptimizeHugeFunctions") # Code generation optimizations string(APPEND CMAKE_CXX_FLAGS " /Gy") # Function-level linking (enables dead code elimination) @@ -58,4 +68,4 @@ if(MSVC) set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /DEBUG /OPT:REF /OPT:ICF") -endif() \ No newline at end of file +endif()