diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bb08a3ec..2288758f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,6 +93,11 @@ Features: * The serial number is unchanged: the framework has no serial of its own, so it is borrowed from the IOKit entry for the same controller. * Needs macOS 11.0, the release that introduced `GCDeviceBattery`. * `[GCController controllers]` is filled by a system daemon, and only while the run loop is pumped, so the module pumps it for at most 50 ms -- but only after confirming that the framework claims a connected controller. The wait is therefore not paid on a machine with no controller, or with one Apple does not claim. +* Added the `general.preload.lua` option, which runs a Lua script as the config is read, so a file of helpers can be shared by several modules instead of being repeated in every one. (General) + * It runs before any module does, so a script that raises is reported as a config error and stops the run, instead of being printed as the failure of whichever module happened to come first. + * A config that sets it therefore starts the Lua interpreter even when no `lua:` format string is used. + * The script loads its own helpers with `dofile` or `loadfile`. `require` is not available, and neither is the rest of Lua's `package` library, so a config file still cannot load a `.so` / `.dll`. + * A build without Lua support rejects the option instead of accepting it and doing nothing. Bugfixes: * Fixed Base64 encoding producing incorrect output for some inputs. (General) diff --git a/doc/json_schema.json b/doc/json_schema.json index 232dae5c3..d4f4c5391 100644 --- a/doc/json_schema.json +++ b/doc/json_schema.json @@ -937,6 +937,17 @@ "type": "boolean", "description": "Whether to detect and display component versions. Mainly for benchmarking", "default": true + }, + "preload": { + "description": "Scripts to run while the config is read, before any module runs", + "type": "object", + "additionalProperties": false, + "properties": { + "lua": { + "type": "string", + "description": "Lua script to run once, as soon as the config is read, for example `dofile('/path/to/helpers.lua')`" + } + } } } }, diff --git a/src/options/general.c b/src/options/general.c index ee7204218..890f38e7d 100644 --- a/src/options/general.c +++ b/src/options/general.c @@ -2,6 +2,7 @@ #include "common/jsonconfig.h" #include "common/processing.h" #include "common/strutil.h" +#include "common/lua.h" #include "options/general.h" #include @@ -29,6 +30,38 @@ const char* ffOptionsParseGeneralJsonConfig(FFOptionsGeneral* options, yyjson_va options->detectVersion = yyjson_get_bool(val); } else if (unsafe_yyjson_equals_str(key, "playerName")) { ffStrbufSetJsonVal(&options->playerName, val); + } else if (unsafe_yyjson_equals_str(key, "preload")) { + if (!yyjson_is_obj(val)) { + return "Property 'general.preload' must be an object"; + } + + yyjson_val* script = yyjson_obj_get(val, "lua"); + if (script) { + if (!yyjson_is_str(script)) { + return "Property 'general.preload.lua' must be a string"; + } + +#if FF_HAVE_LUA + const char* error = ffLuaLoadState(); + if (error) { + return error; + } + + lua_State* L = luaData.L; + lua_settop(L, 0); + if (luaL_loadbuffer(L, unsafe_yyjson_get_str(script), unsafe_yyjson_get_len(script), "@general.preload.lua") != LUA_OK || + lua_pcall(L, 0, 0, 0) != LUA_OK) { + // The position Lua puts in front of a message is kept here: this is printed once, so naming the + // line of the preload script that raised it is useful. `lua_tolstring` only converts strings and + // numbers, so an error value of any other type leaves nothing to report. + return lua_tolstring(L, -1, nullptr) ?: "the script raised an error that is not a string"; + } + lua_settop(L, 0); + +#else + return "Property 'general.preload.lua' requires a build with Lua support"; +#endif + } } #if defined(__linux__) || defined(__FreeBSD__) || defined(__sun) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__HAIKU__) || defined(__GNU__) else if (unsafe_yyjson_equals_str(key, "dsForceDrm")) {