From 1dc8ca56776017381a03a6503455e9ce75e7ac38 Mon Sep 17 00:00:00 2001 From: Leonid Pospelov Date: Wed, 26 Jun 2024 11:39:45 +0500 Subject: [PATCH 1/3] feat(skyrim-platform): writePlugin/getPluginSourceCode: add overrideFolder arg & support INI setting --- 1js/JsEngine.h | 2 +- .../dev/sp-plugins-path-override-folder.md | 1 + docs/release/dev/sp-plugins-path-use-.md | 1 + .../codegen/convert-files/Definitions.txt | 4 +- .../codegen/convert-files/skyrimPlatform.ts | 16 ++-- .../platform_se/skyrim_platform/DevApi.cpp | 79 +++++++++++++++++-- .../platform_se/skyrim_platform/Settings.h | 42 ++++++++++ .../skyrim_platform/SkyrimPlatform.cpp | 27 +------ 8 files changed, 133 insertions(+), 39 deletions(-) create mode 100644 docs/release/dev/sp-plugins-path-override-folder.md create mode 100644 docs/release/dev/sp-plugins-path-use-.md diff --git a/1js/JsEngine.h b/1js/JsEngine.h index 12b345c518..1b1bbe98f8 100644 --- a/1js/JsEngine.h +++ b/1js/JsEngine.h @@ -508,7 +508,7 @@ class JsValue { // A bit ugly reinterpret_cast, but it's a hot path. // We do not want to modify the ref counter for each argument. - // This is also unit tested, so we would know if it breaks. + // This is also unit tested, so we will know if it breaks. return i < n ? reinterpret_cast(arr[i]) : *undefined; } diff --git a/docs/release/dev/sp-plugins-path-override-folder.md b/docs/release/dev/sp-plugins-path-override-folder.md new file mode 100644 index 0000000000..3d7a114d47 --- /dev/null +++ b/docs/release/dev/sp-plugins-path-override-folder.md @@ -0,0 +1 @@ +New optional argument `overrideFolder` for `writePlugin` and `getPluginSourceCode` methods: An optional argument `overrideFolder` is now available. This folder can be outside the list of plugin folders defined in `PluginFolders`. While this folder will be writable and readable, SkyrimPlatform will not monitor or load plugins from it. `overrideFolder` is relative to `Data/Platform`. diff --git a/docs/release/dev/sp-plugins-path-use-.md b/docs/release/dev/sp-plugins-path-use-.md new file mode 100644 index 0000000000..808069a5d2 --- /dev/null +++ b/docs/release/dev/sp-plugins-path-use-.md @@ -0,0 +1 @@ +Updated `writePlugin` and `getPluginSourceCode` methods: These methods now support the `PluginFolders` INI setting. diff --git a/skyrim-platform/src/platform_se/codegen/convert-files/Definitions.txt b/skyrim-platform/src/platform_se/codegen/convert-files/Definitions.txt index 2ecb50d579..0d04594e2e 100644 --- a/skyrim-platform/src/platform_se/codegen/convert-files/Definitions.txt +++ b/skyrim-platform/src/platform_se/codegen/convert-files/Definitions.txt @@ -7,8 +7,8 @@ export declare function writeLogs(pluginName: string, ...arguments: unknown[]): export declare function setPrintConsolePrefixesEnabled(enabled: boolean): void export declare function callNative(className: string, functionName: string, self?: PapyrusObject, ...args: PapyrusValue[]): PapyrusValue export declare function getJsMemoryUsage(): number -export declare function getPluginSourceCode(pluginName: string): string -export declare function writePlugin(pluginName: string, newSources: string): string +export declare function getPluginSourceCode(pluginName: string, overrideFolder?: string): string // overrideFolder is relative to Data/Platform +export declare function writePlugin(pluginName: string, newSources: string, overrideFolder?: string): string // overrideFolder is relative to Data/Platform export declare function getPlatformVersion(): string export declare function disableCtrlPrtScnHotkey(): void export declare function blockPapyrusEvents(block: boolean): void diff --git a/skyrim-platform/src/platform_se/codegen/convert-files/skyrimPlatform.ts b/skyrim-platform/src/platform_se/codegen/convert-files/skyrimPlatform.ts index dde7cb3ae7..3d13ca1666 100644 --- a/skyrim-platform/src/platform_se/codegen/convert-files/skyrimPlatform.ts +++ b/skyrim-platform/src/platform_se/codegen/convert-files/skyrimPlatform.ts @@ -12,8 +12,8 @@ export declare function writeLogs(pluginName: string, ...arguments: unknown[]): export declare function setPrintConsolePrefixesEnabled(enabled: boolean): void export declare function callNative(className: string, functionName: string, self?: PapyrusObject, ...args: PapyrusValue[]): PapyrusValue export declare function getJsMemoryUsage(): number -export declare function getPluginSourceCode(pluginName: string): string -export declare function writePlugin(pluginName: string, newSources: string): string +export declare function getPluginSourceCode(pluginName: string, overrideFolder?: string): string // overrideFolder is relative to Data/Platform +export declare function writePlugin(pluginName: string, newSources: string, overrideFolder?: string): string // overrideFolder is relative to Data/Platform export declare function getPlatformVersion(): string export declare function disableCtrlPrtScnHotkey(): void export declare function blockPapyrusEvents(block: boolean): void @@ -1525,16 +1525,16 @@ export declare class Hooks { export declare let hooks: Hooks export declare class HttpResponse { - body: string; - status: number; - error: string; + body: string; + status: number; + error: string; } export type HttpHeaders = Record export declare class HttpClient { - constructor(url: string); - get(path: string, options?: { headers?: HttpHeaders }, callback?: (result: HttpResponse) => void): Promise; - post(path: string, options: { body: string, contentType: string, headers?: HttpHeaders }, callback?: (result: HttpResponse) => void): Promise; + constructor(url: string); + get(path: string, options?: { headers?: HttpHeaders }, callback?: (result: HttpResponse) => void): Promise; + post(path: string, options: { body: string, contentType: string, headers?: HttpHeaders }, callback?: (result: HttpResponse) => void): Promise; } export declare function createText(xPos: number, yPos: number, text: string, color: number[], name?: string): number; //default name is Tavern diff --git a/skyrim-platform/src/platform_se/skyrim_platform/DevApi.cpp b/skyrim-platform/src/platform_se/skyrim_platform/DevApi.cpp index 018c3ff6c1..c5077a5ede 100644 --- a/skyrim-platform/src/platform_se/skyrim_platform/DevApi.cpp +++ b/skyrim-platform/src/platform_se/skyrim_platform/DevApi.cpp @@ -3,11 +3,28 @@ #include "InvalidArgumentException.h" #include "NullPointerException.h" #include "PapyrusTESModPlatform.h" +#include "Settings.h" #include "Validators.h" std::shared_ptr DevApi::jsEngine = nullptr; DevApi::NativeExportsMap DevApi::nativeExportsMap; +namespace { +bool CreateDirectoryRecursive(const std::string& dirName, std::error_code& err) +{ + err.clear(); + if (!std::filesystem::create_directories(dirName, err)) { + if (std::filesystem::exists(dirName)) { + // The folder already exists: + err.clear(); + return true; + } + return false; + } + return true; +} +} + JsValue DevApi::Require( const JsFunctionArguments& args, const std::vector& pluginLoadDirectories) @@ -64,12 +81,38 @@ JsValue DevApi::AddNativeExports(const JsFunctionArguments& args) } namespace { -std::filesystem::path GetPluginPath(const std::string& pluginName) +std::filesystem::path GetPluginPath(const std::string& pluginName, + std::optional folderOverride) { if (!ValidateFilename(pluginName, /*allowDots*/ false)) { throw InvalidArgumentException("pluginName", pluginName); } - return std::filesystem::path("Data/Platform/Plugins") / (pluginName + ".js"); + + // Folder override is alowed to be not in list of plugin folders + // In this case it will be writable, but SkyrimPlatform will not monitor and + // load plugins from it. + if (folderOverride) { + if (!ValidateFilename(folderOverride->data(), /*allowDots*/ false)) { + throw InvalidArgumentException("folderOverride", *folderOverride); + } + + return std::filesystem::path("Data/Platform") / *folderOverride / + (pluginName + ".js"); + } + + auto pluginFolders = Settings::GetPlatformSettings()->GetPluginFolders(); + + if (!pluginFolders) { + throw NullPointerException("pluginFolders"); + } + + if (pluginFolders->empty()) { + throw std::runtime_error("No plugin folders found"); + } + + auto folder = pluginFolders->front(); + + return folder / (pluginName + ".js"); } } @@ -77,7 +120,16 @@ JsValue DevApi::GetPluginSourceCode(const JsFunctionArguments& args) { // TODO: Support multifile plugins? auto pluginName = args[1].ToString(); - return Viet::ReadFileIntoString(GetPluginPath(pluginName)); + + std::optional overrideFolder; + if (args.GetSize() >= 3) { + auto t = args[2].GetType(); + if (t != JsValue::Type::Undefined && t != JsValue::Type::Null) { + overrideFolder = args[2].ToString(); + } + } + + return Viet::ReadFileIntoString(GetPluginPath(pluginName, overrideFolder)); } JsValue DevApi::WritePlugin(const JsFunctionArguments& args) @@ -86,13 +138,30 @@ JsValue DevApi::WritePlugin(const JsFunctionArguments& args) auto pluginName = args[1].ToString(); auto newSources = args[2].ToString(); - auto path = GetPluginPath(pluginName); + std::optional overrideFolder; + if (args.GetSize() >= 4) { + auto t = args[3].GetType(); + if (t != JsValue::Type::Undefined && t != JsValue::Type::Null) { + overrideFolder = args[3].ToString(); + } + } + + auto path = GetPluginPath(pluginName, overrideFolder); + + std::error_code err; + CreateDirectoryRecursive(path.parent_path().string(), err); + if (err) { + throw std::runtime_error("Failed to create directory " + + path.parent_path().string() + ": " + + err.message()); + } std::ofstream f(path); f << newSources; f.close(); - if (!f) + if (!f) { throw std::runtime_error("Failed to write into " + path.string()); + } return JsValue::Undefined(); } diff --git a/skyrim-platform/src/platform_se/skyrim_platform/Settings.h b/skyrim-platform/src/platform_se/skyrim_platform/Settings.h index 8df70a5fa7..a0d9481923 100644 --- a/skyrim-platform/src/platform_se/skyrim_platform/Settings.h +++ b/skyrim-platform/src/platform_se/skyrim_platform/Settings.h @@ -1,4 +1,10 @@ #pragma once +#include +#include +#include +#include +#include +#include namespace Settings { @@ -171,6 +177,42 @@ class File return ini.GetValue(section, key, defaultValue); } + std::unique_ptr> GetPluginFolders() + { + return GetPathsSemicolonSeparated( + "Main", "PluginFolders", + "Data/Platform/Plugins;Data/Platform/PluginsDev"); + } + + std::unique_ptr> + GetPathsSemicolonSeparated(const char* section, const char* key, + const char* defaultValue) + { + std::string utf8pluginFoldersSemicolonSeparated = + GetString("Main", "PluginFolders", + "Data/Platform/Plugins;Data/Platform/PluginsDev"); + + std::istringstream ss(utf8pluginFoldersSemicolonSeparated); + std::string folder; + + if (utf8pluginFoldersSemicolonSeparated.find_first_of("\"") != + std::string::npos) { + throw std::runtime_error( + "Invalid path with quotes in PluginFolders setting. Please remove " + "quotes and restart the game."); + } + + auto result = std::make_unique>(); + + while (std::getline(ss, folder, ';')) { + if (!folder.empty()) { + result->emplace_back(folder); + } + } + + return result; + } + bool SetString(const char* section, const char* key, const char* value, const char* comment = nullptr) { diff --git a/skyrim-platform/src/platform_se/skyrim_platform/SkyrimPlatform.cpp b/skyrim-platform/src/platform_se/skyrim_platform/SkyrimPlatform.cpp index 56bc68c8ee..2725af7903 100644 --- a/skyrim-platform/src/platform_se/skyrim_platform/SkyrimPlatform.cpp +++ b/skyrim-platform/src/platform_se/skyrim_platform/SkyrimPlatform.cpp @@ -130,31 +130,12 @@ class CommonExecutionListener : public TickListener const std::vector& GetFileDirs() const { if (!pluginFolders) { - auto settings = Settings::GetPlatformSettings(); - std::string utf8pluginFoldersSemicolonSeparated = - settings->GetString("Main", "PluginFolders", - "Data/Platform/Plugins;Data/Platform/PluginsDev"); - - std::istringstream ss(utf8pluginFoldersSemicolonSeparated); - std::string folder; - - if (utf8pluginFoldersSemicolonSeparated.find_first_of("\"") != - std::string::npos) { + try { + pluginFolders = Settings::GetPlatformSettings()->GetPluginFolders(); + } catch (std::exception& e) { pluginFolders = std::make_unique>(); - throw std::runtime_error( - "Invalid path with quotes in PluginFolders setting. Please remove " - "quotes and restart the game."); + throw; } - - auto result = std::make_unique>(); - - while (std::getline(ss, folder, ';')) { - if (!folder.empty()) { - result->emplace_back(folder); - } - } - - pluginFolders = std::move(result); } return *pluginFolders; From 81a01c1f90b93fcafa8c4509fefce796e00735a3 Mon Sep 17 00:00:00 2001 From: Leonid Pospelov Date: Wed, 26 Jun 2024 11:40:08 +0500 Subject: [PATCH 2/3] . --- .../dev/{sp-plugins-path-use-.md => sp-plugins-path-use.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/release/dev/{sp-plugins-path-use-.md => sp-plugins-path-use.md} (100%) diff --git a/docs/release/dev/sp-plugins-path-use-.md b/docs/release/dev/sp-plugins-path-use.md similarity index 100% rename from docs/release/dev/sp-plugins-path-use-.md rename to docs/release/dev/sp-plugins-path-use.md From c0938f2bd3e6273f8bcd90708b449e74fddd9642 Mon Sep 17 00:00:00 2001 From: Leonid Pospelov Date: Wed, 26 Jun 2024 11:42:09 +0500 Subject: [PATCH 3/3] excess upd --- .../codegen/convert-files/skyrimPlatform.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/skyrim-platform/src/platform_se/codegen/convert-files/skyrimPlatform.ts b/skyrim-platform/src/platform_se/codegen/convert-files/skyrimPlatform.ts index 3d13ca1666..120a2eb9a6 100644 --- a/skyrim-platform/src/platform_se/codegen/convert-files/skyrimPlatform.ts +++ b/skyrim-platform/src/platform_se/codegen/convert-files/skyrimPlatform.ts @@ -1525,16 +1525,16 @@ export declare class Hooks { export declare let hooks: Hooks export declare class HttpResponse { - body: string; - status: number; - error: string; + body: string; + status: number; + error: string; } export type HttpHeaders = Record export declare class HttpClient { - constructor(url: string); - get(path: string, options?: { headers?: HttpHeaders }, callback?: (result: HttpResponse) => void): Promise; - post(path: string, options: { body: string, contentType: string, headers?: HttpHeaders }, callback?: (result: HttpResponse) => void): Promise; + constructor(url: string); + get(path: string, options?: { headers?: HttpHeaders }, callback?: (result: HttpResponse) => void): Promise; + post(path: string, options: { body: string, contentType: string, headers?: HttpHeaders }, callback?: (result: HttpResponse) => void): Promise; } export declare function createText(xPos: number, yPos: number, text: string, color: number[], name?: string): number; //default name is Tavern