#include "PCH.h"
#include "PapyrusInterface.h"
#include "ScreenCapture.h"
#include "logger.h"
namespace PapyrusInterface {
// Global state
std::atomic<bool> g_isRunning{false};
std::atomic<bool> g_cancelFlag{false};
std::atomic<bool> g_resultReady{false}; // New flag for polling
std::mutex g_resultMutex;
std::string g_result = "Success";
// Helper: Convert string to lowercase
std::string ToLowerCase(const std::string& str) {
std::string lower = str;
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
return lower;
}
// Helper: Validate path
bool IsValidPath(const std::string& pathStr) {
try {
std::filesystem::path path(pathStr);
// Check if path is absolute
if (!path.is_absolute()) {
logger::warn("Path is not absolute: {}", pathStr);
return false;
}
// Check for illegal characters (Windows specific)
const std::string illegal = "<>:\"|?*";
std::string filename = path.filename().string();
if (filename.find_first_of(illegal) != std::string::npos) {
logger::warn("Path contains illegal characters: {}", pathStr);
return false;
}
// Try to create directory if it doesn't exist
std::filesystem::path dir = path.parent_path();
if (!std::filesystem::exists(dir)) {
if (!std::filesystem::create_directories(dir)) {
logger::warn("Failed to create directory: {}", dir.string());
return false;
}
}
// Check write permissions by trying to create a temp file
std::filesystem::path testFile = dir / ".printscreen_test";
std::ofstream test(testFile);
if (!test.good()) {
logger::warn("No write permission for path: {}", dir.string());
return false;
}
test.close();
std::filesystem::remove(testFile);
return true;
}
catch (const std::exception& e) {
logger::error("Path validation error: {}", e.what());
return false;
}
}
// Worker thread function (simplified - no ModEvent)
void CaptureWorkerThread(
std::string basePath,
std::string imageType,
float jpgCompression,
std::string compressionMode,
float gifMultiFrameDuration
) {
logger::info("Capture worker thread started");
logger::info("Parameters: basePath={}, imageType={}, jpgCompression={}, compressionMode={}, gifDuration={}",
basePath, imageType, jpgCompression, compressionMode, gifMultiFrameDuration);
// Convert to wide string for Windows APIs
std::wstring wBasePath(basePath.begin(), basePath.end());
// Setup capture parameters
ScreenCapture::CaptureParams params;
params.basePath = wBasePath;
params.format = ScreenCapture::StringToImageFormat(imageType);
params.jpegQuality = jpgCompression;
params.cancelFlag = &g_cancelFlag;
// Set GIF duration correctly
if (params.format == ScreenCapture::ImageFormat::GIF_MULTIFRAME) {
params.gifDuration = gifMultiFrameDuration;
if (params.gifDuration <= 0.0f || params.gifDuration > 30.0f) {
params.gifDuration = 3.0f; // Default 3 seconds
}
}
// Parse compression mode
if (params.format == ScreenCapture::ImageFormat::TIF) {
params.tiffMode = ScreenCapture::StringToTiffCompression(compressionMode);
}
else if (params.format == ScreenCapture::ImageFormat::DDS) {
params.ddsMode = ScreenCapture::StringToDDSCompression(compressionMode);
}
// Perform capture
auto result = ScreenCapture::CaptureScreen(params);
// Update global result (no ModEvent - just set flags for polling)
{
std::lock_guard<std::mutex> lock(g_resultMutex);
if (g_cancelFlag.load()) {
g_result = "Cancelled";
}
else {
g_result = result.message;
}
g_isRunning = false;
g_resultReady = true; // Signal that result is ready
}
logger::info("Capture completed with result: {}", g_result);
}
// Papyrus: TakePhoto (Latent)
RE::BSFixedString TakePhoto(
RE::StaticFunctionTag*,
RE::BSFixedString basePath,
RE::BSFixedString imageType,
float jpgCompression,
RE::BSFixedString compressionMode,
float gifMultiFrameDuration
) {
std::lock_guard<std::mutex> lock(g_resultMutex);
// Check if already running
if (g_isRunning.load()) {
g_result = "Already running";
logger::warn("TakePhoto called while capture already running");
return RE::BSFixedString(g_result.c_str());
}
// Convert parameters to lowercase
std::string basePathStr = ToLowerCase(basePath.c_str());
std::string imageTypeStr = ToLowerCase(imageType.c_str());
std::string compressionModeStr = ToLowerCase(compressionMode.c_str());
// Validate parameters
if (basePathStr.empty()) {
g_result = "Invalid base path: empty";
logger::error("{}", g_result);
return RE::BSFixedString(g_result.c_str());
}
// Validate image type
std::vector<std::string> validTypes = {
"png", "jpg", "jpeg", "bmp", "tif", "tiff",
"gif", "gif_multiframe", "gif_multi", "dds"
};
if (std::find(validTypes.begin(), validTypes.end(), imageTypeStr) == validTypes.end()) {
g_result = "Invalid image type: " + imageTypeStr;
logger::error("{}", g_result);
return RE::BSFixedString(g_result.c_str());
}
// Validate JPEG compression (0-100)
if ((imageTypeStr == "jpg" || imageTypeStr == "jpeg") &&
(jpgCompression < 0.0f || jpgCompression > 100.0f)) {
g_result = "Invalid JPEG compression: must be 0-100";
logger::error("{}", g_result);
return RE::BSFixedString(g_result.c_str());
}
// Validate GIF duration (1-30 seconds)
if ((imageTypeStr == "gif_multiframe" || imageTypeStr == "gif_multi") &&
(gifMultiFrameDuration < 1.0f || gifMultiFrameDuration > 30.0f)) {
g_result = "Invalid GIF duration: must be 1-30 seconds";
logger::error("{}", g_result);
return RE::BSFixedString(g_result.c_str());
}
// Reset flags
g_cancelFlag = false;
g_isRunning = true;
g_resultReady = false; // Clear the ready flag
g_result = "Started";
// Start worker thread with correct parameters
std::thread captureThread(
CaptureWorkerThread,
basePathStr,
imageTypeStr,
jpgCompression,
compressionModeStr,
gifMultiFrameDuration
);
captureThread.detach();
logger::info("TakePhoto started capture thread");
return RE::BSFixedString(g_result.c_str());
}
// Papyrus: Get_Result (Enhanced for polling)
RE::BSFixedString Get_Result(RE::StaticFunctionTag*) {
std::lock_guard<std::mutex> lock(g_resultMutex);
// Check if we have a new result ready
if (g_resultReady.load()) {
g_resultReady = false; // Reset flag so we only trigger once
// Return special callback codes that Papyrus can detect
if (g_result == "Success") {
logger::info("Returning callback: Success");
return RE::BSFixedString("CALLBACK_SUCCESS");
} else if (g_result == "Cancelled") {
logger::info("Returning callback: Cancelled");
return RE::BSFixedString("CALLBACK_CANCELLED");
} else {
// Error case - prefix with CALLBACK_ERROR:
std::string errorResult = "CALLBACK_ERROR:" + g_result;
logger::info("Returning callback: {}", errorResult);
return RE::BSFixedString(errorResult.c_str());
}
}
// No new result - return current status
if (g_isRunning.load()) {
return RE::BSFixedString("Running");
}
// Return last result
return RE::BSFixedString(g_result.c_str());
}
// Papyrus: Cancel (Non-Latent)
bool Cancel(RE::StaticFunctionTag*) {
if (!g_isRunning.load()) {
logger::warn("Cancel called but no capture is running");
return false;
}
g_cancelFlag = true;
logger::info("Capture cancellation requested");
return true;
}
// Papyrus: CheckPath (Non-Latent)
bool CheckPath(RE::StaticFunctionTag*, RE::BSFixedString path) {
std::string pathStr = ToLowerCase(path.c_str());
if (pathStr.empty()) {
logger::warn("CheckPath called with empty path");
return false;
}
bool valid = IsValidPath(pathStr);
logger::info("CheckPath('{}') = {}", pathStr, valid);
return valid;
}
// Register all Papyrus functions
bool RegisterFunctions(RE::BSScript::IVirtualMachine* vm) {
if (!vm) {
logger::error("Virtual machine is null");
return false;
}
// Register functions with the script name
const char* scriptName = "PrintScreen_Formula_Script";
// TakePhoto - Latent function
vm->RegisterFunction(
"TakePhoto",
scriptName,
TakePhoto
);
vm->SetCallableFromTasklets(scriptName, "TakePhoto", true);
// Get_Result - Non-Latent (enhanced for polling)
vm->RegisterFunction(
"Get_Result",
scriptName,
Get_Result
);
// Cancel - Non-Latent
vm->RegisterFunction(
"Cancel",
scriptName,
Cancel
);
// CheckPath - Non-Latent
vm->RegisterFunction(
"CheckPath",
scriptName,
CheckPath
);
logger::info("Papyrus functions registered successfully for polling method");
return true;
}
}