mpv/player/scripting.c

452 lines
14 KiB
C
Raw Normal View History

/*
* This file is part of mpv.
*
Relicense some non-MPlayer source files to LGPL 2.1 or later This covers source files which were added in mplayer2 and mpv times only, and where all code is covered by LGPL relicensing agreements. There are probably more files to which this applies, but I'm being conservative here. A file named ao_sdl.c exists in MPlayer too, but the mpv one is a complete rewrite, and was added some time after the original ao_sdl.c was removed. The same applies to vo_sdl.c, for which the SDL2 API is radically different in addition (MPlayer supports SDL 1.2 only). common.c contains only code written by me. But common.h is a strange case: although it originally was named mp_common.h and exists in MPlayer too, by now it contains only definitions written by uau and me. The exceptions are the CONTROL_ defines - thus not changing the license of common.h yet. codec_tags.c contained once large tables generated from MPlayer's codecs.conf, but all of these tables were removed. From demux_playlist.c I'm removing a code fragment from someone who was not asked; this probably could be done later (see commit 15dccc37). misc.c is a bit complicated to reason about (it was split off mplayer.c and thus contains random functions out of this file), but actually all functions have been added post-MPlayer. Except get_relative_time(), which was written by uau, but looks similar to 3 different versions of something similar in each of the Unix/win32/OSX timer source files. I'm not sure what that means in regards to copyright, so I've just moved it into another still-GPL source file for now. screenshot.c once had some minor parts of MPlayer's vf_screenshot.c, but they're all gone.
2016-01-19 17:36:06 +00:00
* mpv is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* mpv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Relicense some non-MPlayer source files to LGPL 2.1 or later This covers source files which were added in mplayer2 and mpv times only, and where all code is covered by LGPL relicensing agreements. There are probably more files to which this applies, but I'm being conservative here. A file named ao_sdl.c exists in MPlayer too, but the mpv one is a complete rewrite, and was added some time after the original ao_sdl.c was removed. The same applies to vo_sdl.c, for which the SDL2 API is radically different in addition (MPlayer supports SDL 1.2 only). common.c contains only code written by me. But common.h is a strange case: although it originally was named mp_common.h and exists in MPlayer too, by now it contains only definitions written by uau and me. The exceptions are the CONTROL_ defines - thus not changing the license of common.h yet. codec_tags.c contained once large tables generated from MPlayer's codecs.conf, but all of these tables were removed. From demux_playlist.c I'm removing a code fragment from someone who was not asked; this probably could be done later (see commit 15dccc37). misc.c is a bit complicated to reason about (it was split off mplayer.c and thus contains random functions out of this file), but actually all functions have been added post-MPlayer. Except get_relative_time(), which was written by uau, but looks similar to 3 different versions of something similar in each of the Unix/win32/OSX timer source files. I'm not sure what that means in regards to copyright, so I've just moved it into another still-GPL source file for now. screenshot.c once had some minor parts of MPlayer's vf_screenshot.c, but they're all gone.
2016-01-19 17:36:06 +00:00
* GNU Lesser General Public License for more details.
*
Relicense some non-MPlayer source files to LGPL 2.1 or later This covers source files which were added in mplayer2 and mpv times only, and where all code is covered by LGPL relicensing agreements. There are probably more files to which this applies, but I'm being conservative here. A file named ao_sdl.c exists in MPlayer too, but the mpv one is a complete rewrite, and was added some time after the original ao_sdl.c was removed. The same applies to vo_sdl.c, for which the SDL2 API is radically different in addition (MPlayer supports SDL 1.2 only). common.c contains only code written by me. But common.h is a strange case: although it originally was named mp_common.h and exists in MPlayer too, by now it contains only definitions written by uau and me. The exceptions are the CONTROL_ defines - thus not changing the license of common.h yet. codec_tags.c contained once large tables generated from MPlayer's codecs.conf, but all of these tables were removed. From demux_playlist.c I'm removing a code fragment from someone who was not asked; this probably could be done later (see commit 15dccc37). misc.c is a bit complicated to reason about (it was split off mplayer.c and thus contains random functions out of this file), but actually all functions have been added post-MPlayer. Except get_relative_time(), which was written by uau, but looks similar to 3 different versions of something similar in each of the Unix/win32/OSX timer source files. I'm not sure what that means in regards to copyright, so I've just moved it into another still-GPL source file for now. screenshot.c once had some minor parts of MPlayer's vf_screenshot.c, but they're all gone.
2016-01-19 17:36:06 +00:00
* You should have received a copy of the GNU Lesser General Public
* License along with mpv. If not, see <http://www.gnu.org/licenses/>.
*/
#include <string.h>
#include <sys/types.h>
#include <math.h>
#include <assert.h>
#include "config.h"
#include "osdep/io.h"
#include "osdep/subprocess.h"
#include "osdep/threads.h"
#include "common/common.h"
#include "common/msg.h"
#include "input/input.h"
#include "options/m_config.h"
#include "options/parse_configfile.h"
#include "options/path.h"
#include "misc/bstr.h"
#include "core.h"
#include "client.h"
#include "libmpv/client.h"
cplugin: allow loading mpv_* symbols dynamically Defining MPV_CPLUGIN_DYNAMIC_SYM during plugin compilation will replace mpv_* functions with function pointers. Those pointer will be initialized when loading the plugin. It is recommended to use this symbol table when targeting Windows. The loader does not have notion of global symbols. Loading cplugin into mpv process will not allow this plugin to call any of the symbols that may be available in other modules. Instead cplugin has to link explicitly to specific PE binary, libmpv-2.dll/mpv.exe or any other binary that may have linked mpv statically. This limits portability of cplugin as it would need to be compiled separately for each of target PE binary that includes mpv's symbols. Which in practice is unrealictis, as we want one cplugin to be loaded without those restrictions. Instead of linking to any PE binary, we create function pointer for all mpv's exported symbols. For convinience names of entrypoints are redefined to those pointer so no changes are required in cplugin source code, except defining MPV_CPLUGIN_DYNAMIC_SYM. Those function pointer are exported to make them available for mpv to init with correct values during runtime, before calling `mpv_open_cplugin`. Note that those pointer are decorated with `selectany` attribute, so no need to worry about multiple definitions, linker will keep only single instance. This fixes cplugin usability on Windows. Without any API changes, only recompilation with -DMPV_CPLUGIN_DYNAMIC_SYM is needed.
2023-08-17 14:19:26 +00:00
#include "libmpv/render.h"
#include "libmpv/stream_cb.h"
extern const struct mp_scripting mp_scripting_lua;
extern const struct mp_scripting mp_scripting_cplugin;
extern const struct mp_scripting mp_scripting_js;
extern const struct mp_scripting mp_scripting_run;
static const struct mp_scripting *const scripting_backends[] = {
#if HAVE_LUA
&mp_scripting_lua,
#endif
#if HAVE_CPLUGINS
&mp_scripting_cplugin,
#endif
#if HAVE_JAVASCRIPT
&mp_scripting_js,
#endif
&mp_scripting_run,
NULL
};
static char *script_name_from_filename(void *talloc_ctx, const char *fname)
{
fname = mp_basename(fname);
if (fname[0] == '@')
fname += 1;
char *name = talloc_strdup(talloc_ctx, fname);
// Drop file extension
char *dot = strrchr(name, '.');
if (dot)
*dot = '\0';
// Turn it into a safe identifier - this is used with e.g. dispatching
// input via: "send scriptname ..."
for (int n = 0; name[n]; n++) {
char c = name[n];
if (!(c >= 'A' && c <= 'Z') && !(c >= 'a' && c <= 'z') &&
!(c >= '0' && c <= '9'))
name[n] = '_';
}
return talloc_asprintf(talloc_ctx, "%s", name);
}
static void run_script(struct mp_script_args *arg)
{
char *name = talloc_asprintf(NULL, "%s/%s", arg->backend->name,
mpv_client_name(arg->client));
2023-10-21 02:55:41 +00:00
mp_thread_set_name(name);
talloc_free(name);
if (arg->backend->load(arg) < 0)
2023-10-27 21:28:06 +00:00
MP_ERR(arg, "Could not load %s script %s\n", arg->backend->name, arg->filename);
mpv_destroy(arg->client);
talloc_free(arg);
}
2023-10-21 02:55:41 +00:00
static MP_THREAD_VOID script_thread(void *p)
{
struct mp_script_args *arg = p;
run_script(arg);
2023-10-21 02:55:41 +00:00
MP_THREAD_RETURN();
}
static int64_t mp_load_script(struct MPContext *mpctx, const char *fname)
{
char *ext = mp_splitext(fname, NULL);
if (ext && strcasecmp(ext, "disable") == 0)
return 0;
void *tmp = talloc_new(NULL);
const char *path = NULL;
char *script_name = NULL;
const struct mp_scripting *backend = NULL;
struct stat s;
if (!stat(fname, &s) && S_ISDIR(s.st_mode)) {
path = fname;
fname = NULL;
for (int n = 0; scripting_backends[n]; n++) {
const struct mp_scripting *b = scripting_backends[n];
char *filename = mp_tprintf(80, "main.%s", b->file_ext);
fname = mp_path_join(tmp, path, filename);
if (!stat(fname, &s) && S_ISREG(s.st_mode)) {
backend = b;
break;
}
talloc_free((void *)fname);
fname = NULL;
}
if (!fname) {
MP_ERR(mpctx, "Cannot find main.* for any supported scripting "
"backend in: %s\n", path);
talloc_free(tmp);
return -1;
}
script_name = talloc_strdup(tmp, path);
mp_path_strip_trailing_separator(script_name);
script_name = mp_basename(script_name);
} else {
for (int n = 0; scripting_backends[n]; n++) {
const struct mp_scripting *b = scripting_backends[n];
if (ext && strcasecmp(ext, b->file_ext) == 0) {
backend = b;
break;
}
}
script_name = script_name_from_filename(tmp, fname);
}
if (!backend) {
MP_ERR(mpctx, "Can't load unknown script: %s\n", fname);
talloc_free(tmp);
return -1;
}
struct mp_script_args *arg = talloc_ptrtype(NULL, arg);
*arg = (struct mp_script_args){
.mpctx = mpctx,
.filename = talloc_strdup(arg, fname),
.path = talloc_strdup(arg, path),
.backend = backend,
// Create the client before creating the thread; otherwise a race
// condition could happen, where MPContext is destroyed while the
// thread tries to create the client.
.client = mp_new_client(mpctx->clients, script_name),
};
talloc_free(tmp);
fname = NULL; // might have been freed so don't touch anymore
if (!arg->client) {
MP_ERR(mpctx, "Failed to create client for script: %s\n", arg->filename);
talloc_free(arg);
return -1;
}
mp_client_set_weak(arg->client);
arg->log = mp_client_get_log(arg->client);
int64_t id = mpv_client_id(arg->client);
2023-10-27 21:28:06 +00:00
MP_DBG(arg, "Loading %s script %s...\n", backend->name, arg->filename);
if (backend->no_thread) {
run_script(arg);
} else {
2023-10-21 02:55:41 +00:00
mp_thread thread;
if (mp_thread_create(&thread, script_thread, arg)) {
mpv_destroy(arg->client);
talloc_free(arg);
return -1;
}
mp_thread_detach(thread);
}
return id;
}
int64_t mp_load_user_script(struct MPContext *mpctx, const char *fname)
{
char *path = mp_get_user_path(NULL, mpctx->global, fname);
int64_t ret = mp_load_script(mpctx, path);
talloc_free(path);
return ret;
}
static int compare_filename(const void *pa, const void *pb)
{
char *a = (char *)pa;
char *b = (char *)pb;
return strcmp(a, b);
}
static char **list_script_files(void *talloc_ctx, char *path)
{
char **files = NULL;
int count = 0;
DIR *dp = opendir(path);
if (!dp)
return NULL;
struct dirent *ep;
while ((ep = readdir(dp))) {
if (ep->d_name[0] != '.') {
char *fname = mp_path_join(talloc_ctx, path, ep->d_name);
struct stat s;
if (!stat(fname, &s) && (S_ISREG(s.st_mode) || S_ISDIR(s.st_mode)))
MP_TARRAY_APPEND(talloc_ctx, files, count, fname);
}
}
closedir(dp);
if (files)
qsort(files, count, sizeof(char *), compare_filename);
MP_TARRAY_APPEND(talloc_ctx, files, count, NULL);
return files;
}
static void load_builtin_script(struct MPContext *mpctx, int slot, bool enable,
const char *fname)
{
assert(slot < MP_ARRAY_SIZE(mpctx->builtin_script_ids));
int64_t *pid = &mpctx->builtin_script_ids[slot];
if (*pid > 0 && !mp_client_id_exists(mpctx, *pid))
*pid = 0; // died
if ((*pid > 0) != enable) {
if (enable) {
*pid = mp_load_script(mpctx, fname);
} else {
char *name = mp_tprintf(22, "@%"PRIi64, *pid);
scripting: change when/how player waits for scripts being loaded Fundamentally, scripts are loaded asynchronously, but as a feature, there was code to wait until a script is loaded (for a certain arbitrary definition of "loaded"). This was done in scripting.c with the wait_loaded() function. This called mp_idle(), and since there are commands to load/unload scripts, it meant the player core loop could be entered recursively. I think this is a major complication and has some problems. For example, if you had a script that does 'os.execute("sleep inf")', then every time you ran a command to load an instance of the script would add a new stack frame of mp_idle(). This would lead to some sort of reentrancy horror that is hard to debug. Also misc/dispatch.c contains a somewhat tricky mess to support such recursive invocations. There were also some bugs due to this and due to unforeseen interactions with other messes. This scripting stuff was the only thing making use of that reentrancy, and future commands that have "logical" waiting for something should be implemented differently. So get rid of it. Change the code to wait only in the player initialization phase: the only place where it really has to wait is before playback is started, because scripts might want to set options or hooks that interact with playback initialization. Unloading of builtin scripts (can happen with e.g. "set osc no") is left asynchronous; the unloading wasn't too robust anyway, and this change won't make a difference if someone is trying to break it intentionally. Note that this is not in mp_initialize(), because mpv_initialize() uses this by locking the core, which would have the same problem. In the future, commands which logically wait should use different mechanisms. Originally I thought the current approach (that is removed with this commit) should be used, but it's too much of a mess and can't even be used in some cases. Examples are: - "loadfile" should be made blocking (needs to run the normal player code and manually unblock the thread issuing the command) - "add-sub" should not freeze the player until the URL is opened (needs to run opening on a separate thread) Possibly the current scripting behavior could be restored once new mechanisms exist, and if it turns out that anyone needs it. With this commit there should be no further instances of recursive playloop invocations (other than the case in the following commit), since all mp_idle()/mp_wait_events() calls are done strictly from the main thread (and not commands/properties or libmpv client API that "lock" the main thread).
2018-04-15 08:14:00 +00:00
mp_client_send_event(mpctx, name, 0, MPV_EVENT_SHUTDOWN, NULL);
}
}
}
void mp_load_builtin_scripts(struct MPContext *mpctx)
{
load_builtin_script(mpctx, 0, mpctx->opts->lua_load_osc, "@osc.lua");
load_builtin_script(mpctx, 1, mpctx->opts->lua_load_ytdl, "@ytdl_hook.lua");
load_builtin_script(mpctx, 2, mpctx->opts->lua_load_stats, "@stats.lua");
load_builtin_script(mpctx, 3, mpctx->opts->lua_load_console, "@console.lua");
load_builtin_script(mpctx, 4, mpctx->opts->lua_load_auto_profiles,
"@auto_profiles.lua");
load_builtin_script(mpctx, 5, mpctx->opts->lua_load_select, "@select.lua");
}
bool mp_load_scripts(struct MPContext *mpctx)
{
bool ok = true;
// Load scripts from options
char **files = mpctx->opts->script_files;
for (int n = 0; files && files[n]; n++) {
if (files[n][0])
ok &= mp_load_user_script(mpctx, files[n]) >= 0;
}
if (!mpctx->opts->auto_load_scripts)
return ok;
// Load all scripts
void *tmp = talloc_new(NULL);
char **scriptsdir = mp_find_all_config_files(tmp, mpctx->global, "scripts");
for (int i = 0; scriptsdir && scriptsdir[i]; i++) {
files = list_script_files(tmp, scriptsdir[i]);
for (int n = 0; files && files[n]; n++)
ok &= mp_load_script(mpctx, files[n]) >= 0;
}
talloc_free(tmp);
return ok;
}
#if HAVE_CPLUGINS
2024-05-01 19:39:19 +00:00
#ifndef _WIN32
#include <dlfcn.h>
2020-06-12 08:42:01 +00:00
#endif
#define MPV_DLOPEN_FN "mpv_open_cplugin"
typedef int (*mpv_open_cplugin)(mpv_handle *handle);
cplugin: allow loading mpv_* symbols dynamically Defining MPV_CPLUGIN_DYNAMIC_SYM during plugin compilation will replace mpv_* functions with function pointers. Those pointer will be initialized when loading the plugin. It is recommended to use this symbol table when targeting Windows. The loader does not have notion of global symbols. Loading cplugin into mpv process will not allow this plugin to call any of the symbols that may be available in other modules. Instead cplugin has to link explicitly to specific PE binary, libmpv-2.dll/mpv.exe or any other binary that may have linked mpv statically. This limits portability of cplugin as it would need to be compiled separately for each of target PE binary that includes mpv's symbols. Which in practice is unrealictis, as we want one cplugin to be loaded without those restrictions. Instead of linking to any PE binary, we create function pointer for all mpv's exported symbols. For convinience names of entrypoints are redefined to those pointer so no changes are required in cplugin source code, except defining MPV_CPLUGIN_DYNAMIC_SYM. Those function pointer are exported to make them available for mpv to init with correct values during runtime, before calling `mpv_open_cplugin`. Note that those pointer are decorated with `selectany` attribute, so no need to worry about multiple definitions, linker will keep only single instance. This fixes cplugin usability on Windows. Without any API changes, only recompilation with -DMPV_CPLUGIN_DYNAMIC_SYM is needed.
2023-08-17 14:19:26 +00:00
static void init_sym_table(struct mp_script_args *args, void *lib) {
#define INIT_SYM(name) \
{ \
void **sym = (void **)dlsym(lib, "pfn_" #name); \
if (sym) { \
if (*sym && *sym != &name) \
MP_ERR(args, "Overriding already set function " #name "\n"); \
*sym = &name; \
} \
}
INIT_SYM(mpv_client_api_version);
INIT_SYM(mpv_error_string);
INIT_SYM(mpv_free);
INIT_SYM(mpv_client_name);
INIT_SYM(mpv_client_id);
INIT_SYM(mpv_create);
INIT_SYM(mpv_initialize);
INIT_SYM(mpv_destroy);
INIT_SYM(mpv_terminate_destroy);
INIT_SYM(mpv_create_client);
INIT_SYM(mpv_create_weak_client);
INIT_SYM(mpv_load_config_file);
INIT_SYM(mpv_get_time_us);
INIT_SYM(mpv_free_node_contents);
INIT_SYM(mpv_set_option);
INIT_SYM(mpv_set_option_string);
INIT_SYM(mpv_command);
INIT_SYM(mpv_command_node);
INIT_SYM(mpv_command_ret);
INIT_SYM(mpv_command_string);
INIT_SYM(mpv_command_async);
INIT_SYM(mpv_command_node_async);
INIT_SYM(mpv_abort_async_command);
INIT_SYM(mpv_set_property);
INIT_SYM(mpv_set_property_string);
INIT_SYM(mpv_del_property);
INIT_SYM(mpv_set_property_async);
INIT_SYM(mpv_get_property);
INIT_SYM(mpv_get_property_string);
INIT_SYM(mpv_get_property_osd_string);
INIT_SYM(mpv_get_property_async);
INIT_SYM(mpv_observe_property);
INIT_SYM(mpv_unobserve_property);
INIT_SYM(mpv_event_name);
INIT_SYM(mpv_event_to_node);
INIT_SYM(mpv_request_event);
INIT_SYM(mpv_request_log_messages);
INIT_SYM(mpv_wait_event);
INIT_SYM(mpv_wakeup);
INIT_SYM(mpv_set_wakeup_callback);
INIT_SYM(mpv_wait_async_requests);
INIT_SYM(mpv_hook_add);
INIT_SYM(mpv_hook_continue);
INIT_SYM(mpv_get_wakeup_pipe);
INIT_SYM(mpv_render_context_create);
INIT_SYM(mpv_render_context_set_parameter);
INIT_SYM(mpv_render_context_get_info);
INIT_SYM(mpv_render_context_set_update_callback);
INIT_SYM(mpv_render_context_update);
INIT_SYM(mpv_render_context_render);
INIT_SYM(mpv_render_context_report_swap);
INIT_SYM(mpv_render_context_free);
INIT_SYM(mpv_stream_cb_add_ro);
#undef INIT_SYM
}
static int load_cplugin(struct mp_script_args *args)
{
void *lib = dlopen(args->filename, RTLD_NOW | RTLD_LOCAL);
if (!lib)
goto error;
// Note: once loaded, we never unload, as unloading the libraries linked to
// the plugin can cause random serious problems.
mpv_open_cplugin sym = (mpv_open_cplugin)dlsym(lib, MPV_DLOPEN_FN);
if (!sym)
goto error;
cplugin: allow loading mpv_* symbols dynamically Defining MPV_CPLUGIN_DYNAMIC_SYM during plugin compilation will replace mpv_* functions with function pointers. Those pointer will be initialized when loading the plugin. It is recommended to use this symbol table when targeting Windows. The loader does not have notion of global symbols. Loading cplugin into mpv process will not allow this plugin to call any of the symbols that may be available in other modules. Instead cplugin has to link explicitly to specific PE binary, libmpv-2.dll/mpv.exe or any other binary that may have linked mpv statically. This limits portability of cplugin as it would need to be compiled separately for each of target PE binary that includes mpv's symbols. Which in practice is unrealictis, as we want one cplugin to be loaded without those restrictions. Instead of linking to any PE binary, we create function pointer for all mpv's exported symbols. For convinience names of entrypoints are redefined to those pointer so no changes are required in cplugin source code, except defining MPV_CPLUGIN_DYNAMIC_SYM. Those function pointer are exported to make them available for mpv to init with correct values during runtime, before calling `mpv_open_cplugin`. Note that those pointer are decorated with `selectany` attribute, so no need to worry about multiple definitions, linker will keep only single instance. This fixes cplugin usability on Windows. Without any API changes, only recompilation with -DMPV_CPLUGIN_DYNAMIC_SYM is needed.
2023-08-17 14:19:26 +00:00
init_sym_table(args, lib);
return sym(args->client) ? -1 : 0;
error: ;
char *err = dlerror();
if (err)
MP_ERR(args, "C plugin error: '%s'\n", err);
return -1;
}
const struct mp_scripting mp_scripting_cplugin = {
.name = "cplugin",
2024-05-01 19:39:19 +00:00
#ifdef _WIN32
2020-06-12 08:42:01 +00:00
.file_ext = "dll",
#else
.file_ext = "so",
2020-06-12 08:42:01 +00:00
#endif
.load = load_cplugin,
};
#endif
static int load_run(struct mp_script_args *args)
{
// The arg->client object might die and with it args->log, so duplicate it.
args->log = mp_log_new(args, args->log, NULL);
int fds[2];
if (!mp_ipc_start_anon_client(args->mpctx->ipc_ctx, args->client, fds))
return -1;
args->client = NULL; // ownership lost
char *fdopt = fds[1] >= 0 ? mp_tprintf(80, "--mpv-ipc-fd=%d:%d", fds[0], fds[1])
: mp_tprintf(80, "--mpv-ipc-fd=%d", fds[0]);
struct mp_subprocess_opts opts = {
.exe = (char *)args->filename,
.args = (char *[]){(char *)args->filename, fdopt, NULL},
.fds = {
// Keep terminal stuff
{.fd = 0, .src_fd = 0,},
{.fd = 1, .src_fd = 1,},
{.fd = 2, .src_fd = 2,},
2020-07-09 10:26:54 +00:00
// Just hope these don't step over each other (e.g. fds[1] could be
// below 4, if the std FDs are missing).
{.fd = fds[0], .src_fd = fds[0], },
{.fd = fds[1], .src_fd = fds[1], },
},
.num_fds = fds[1] >= 0 ? 5 : 4,
.detach = true,
};
struct mp_subprocess_result res;
mp_subprocess(args->log, &opts, &res);
// Closing these will (probably) make the client exit, if it really died.
close(fds[0]);
if (fds[1] >= 0)
close(fds[1]);
return res.error;
}
const struct mp_scripting mp_scripting_run = {
.name = "ipc",
.file_ext = "run",
.no_thread = true,
.load = load_run,
};