From 962d4176135d0066e42f3a5b942faf88223d2188 Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Mon, 30 Jan 2023 19:49:21 +0100 Subject: [PATCH] Add support for custom commands --- .../commands/CommandController.cpp | 12 +++ .../commands/CommandController.hpp | 2 + src/controllers/plugins/PluginController.cpp | 80 ++++++++++++++++++- src/controllers/plugins/PluginController.hpp | 53 +++++++++--- 4 files changed, 132 insertions(+), 15 deletions(-) diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index b17f001da..ad07ddca6 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -3228,6 +3228,18 @@ QString CommandController::execCommand(const QString &textNoEmoji, return text; } +bool CommandController::registerPluginCommand(const QString &commandName) +{ + if (this->commands_.contains(commandName)) + { + return false; + } + + this->commands_[commandName] = [commandName](const CommandContext &ctx) { + return getApp()->plugins->tryExecPluginCommand(commandName, ctx); + }; + return true; +} void CommandController::registerCommand(const QString &commandName, CommandFunctionVariants commandFunction) { diff --git a/src/controllers/commands/CommandController.hpp b/src/controllers/commands/CommandController.hpp index fcae24249..85b4ddf58 100644 --- a/src/controllers/commands/CommandController.hpp +++ b/src/controllers/commands/CommandController.hpp @@ -43,6 +43,8 @@ public: ChannelPtr channel, const Message *message = nullptr, std::unordered_map context = {}); + bool registerPluginCommand(const QString &commandName); + private: void load(Paths &paths); diff --git a/src/controllers/plugins/PluginController.cpp b/src/controllers/plugins/PluginController.cpp index 51eb72dd3..3fb59711e 100644 --- a/src/controllers/plugins/PluginController.cpp +++ b/src/controllers/plugins/PluginController.cpp @@ -2,6 +2,7 @@ #include "Application.hpp" #include "common/QLogging.hpp" +#include "controllers/commands/CommandContext.hpp" #include "messages/MessageBuilder.hpp" #include "providers/twitch/TwitchIrcServer.hpp" #include "singletons/WindowManager.hpp" @@ -56,11 +57,11 @@ void PluginController::load(QFileInfo index, QDir pluginDir) luaL_openlibs(l); this->loadChatterinoLib(l); - luaL_dofile(l, index.absoluteFilePath().toStdString().c_str()); - auto pluginName = pluginDir.dirName(); auto plugin = std::make_unique(pluginName, l); this->plugins.insert({pluginName, std::move(plugin)}); + + luaL_dofile(l, index.absoluteFilePath().toStdString().c_str()); qCInfo(chatterinoLua) << "Loaded" << pluginName << "plugin from" << index; } @@ -85,6 +86,49 @@ void PluginController::callEveryWithArgs( } } +QString PluginController::tryExecPluginCommand(const QString &commandName, + const CommandContext &ctx) +{ + for (auto &[name, plugin] : this->plugins) + { + if (auto it = plugin->ownedCommands.find(commandName); + it != plugin->ownedCommands.end()) + { + const auto &funcName = it->second; + + auto *L = plugin->state_; // NOLINT + lua_getfield(L, LUA_REGISTRYINDEX, funcName.toStdString().c_str()); + // put args on stack + lua_createtable(L, 0, 2); + auto outIdx = lua_gettop(L); + + lua_createtable(L, ctx.words.count(), 0); + auto wordsIdx = lua_gettop(L); + + int i = 1; + for (const auto &w : ctx.words) + { + lua_pushstring(L, w.toStdString().c_str()); + lua_seti(L, wordsIdx, i); + i += 1; + } + + lua_setfield(L, outIdx, "words"); + + lua_pushstring(L, ctx.channel->getName().toStdString().c_str()); + lua_setfield(L, outIdx, "channelName"); + + lua_pcall(L, 1, 0, 0); + return ""; + } + } + qCCritical(chatterinoLua) + << "Something's seriously up, no plugin owns command" << commandName + << "yet a call to execute it came in"; + assert(false && "missing plugin command owner"); + return ""; +} + constexpr int C_FALSE = 0; constexpr int C_TRUE = 1; @@ -94,6 +138,7 @@ int luaC2SystemMsg(lua_State *L) { if (lua_gettop(L) != 2) { + qCDebug(chatterinoLua) << "system_msg: need 2 args"; luaL_error(L, "need exactly 2 arguments"); // NOLINT lua_pushboolean(L, C_FALSE); return 1; @@ -104,17 +149,48 @@ int luaC2SystemMsg(lua_State *L) const auto chn = getApp()->twitch->getChannelOrEmpty(channel); if (chn->isEmpty()) { + qCDebug(chatterinoLua) << "system_msg: no channel" << channel; lua_pushboolean(L, C_FALSE); return 1; } + qCDebug(chatterinoLua) << "system_msg: OK!"; chn->addMessage(makeSystemMessage(text)); lua_pushboolean(L, C_TRUE); + return 1; +} + +int luaC2RegisterCommand(lua_State *L) +{ + auto *pl = getApp()->plugins->getPluginByStatePtr(L); + if (pl == nullptr) + { + luaL_error(L, "internal error: no plugin"); // NOLINT + return 0; + } + + const char *name = luaL_optstring(L, 1, NULL); + if (lua_isnoneornil(L, 2)) + { + // NOLINTNEXTLINE + luaL_error(L, "missing argument for register_command: function " + "\"pointer\""); + return 0; + } + + auto callbackSavedName = QString("c2commandcb-%1").arg(name); + lua_setfield(L, LUA_REGISTRYINDEX, callbackSavedName.toStdString().c_str()); + pl->registerCommand(name, callbackSavedName); + + // delete both name and callback + lua_pop(L, 2); + return 0; } // NOLINTNEXTLINE static const luaL_Reg C2LIB[] = { {"system_msg", luaC2SystemMsg}, + {"register_command", luaC2RegisterCommand}, {nullptr, nullptr}, }; } diff --git a/src/controllers/plugins/PluginController.hpp b/src/controllers/plugins/PluginController.hpp index e9e0952f4..679fb0347 100644 --- a/src/controllers/plugins/PluginController.hpp +++ b/src/controllers/plugins/PluginController.hpp @@ -1,6 +1,10 @@ #pragma once +#include "Application.hpp" +#include "common/QLogging.hpp" #include "common/Singleton.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "controllers/commands/CommandController.hpp" #include "singletons/Paths.hpp" #include @@ -16,18 +20,6 @@ struct lua_State; namespace chatterino { -//class Registration -//{ -//public: -// enum Type { -// COMMAND, -// }; -// -// Type type; -// QString name; -// const char *receiverFunctionName; -//}; - class Plugin { public: @@ -38,9 +30,28 @@ public: { } + bool registerCommand(const QString &name, const QString &functionName) + { + if (this->ownedCommands.find(name) != this->ownedCommands.end()) + { + return false; + } + + auto ok = getApp()->commands->registerPluginCommand(name); + if (!ok) + { + return false; + } + this->ownedCommands.insert({name, functionName}); + return true; + } + private: lua_State *state_; + // maps command name -> function name + std::map ownedCommands; + friend class PluginController; }; @@ -55,10 +66,26 @@ public: std::function &pl, lua_State *L)> argCb); + QString tryExecPluginCommand(const QString &commandName, + const CommandContext &ctx); + + // NOTE: this pointer does not own the Plugin, unique_ptr still owns it + // This is required to be public because of c functions + Plugin *getPluginByStatePtr(lua_State *L) + { + for (auto &[name, plugin] : this->plugins) + { + if (plugin->state_ == L) + { + return plugin.get(); + } + } + return nullptr; + } + private: void load(QFileInfo index, QDir pluginDir); void loadChatterinoLib(lua_State *l); - std::map> plugins; };