core: add server-side lua runtime for clientless players
This commit is contained in:
1 parent
5ac9ed673c
commit
05fd748537
9 files changed
+320
-1
No files matched your search
@@ -0,0 +1,198 @@
|
||||
#include "ClientlessLuaEngine.h"
|
||||
#include "ClientlessPlayerMgr.h"
|
||||
#include "Log.h"
|
||||
#include "Player.h"
|
||||
#include <lua.hpp>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace
|
||||
{
|
||||
uint32 BoundGuid(lua_State* L) { return uint32(lua_tointeger(L, lua_upvalueindex(1))); }
|
||||
|
||||
std::unordered_map<std::string, std::string> ReadMap(lua_State* L, int index)
|
||||
{
|
||||
std::unordered_map<std::string, std::string> out;
|
||||
if (!lua_istable(L, index)) return out;
|
||||
index = lua_absindex(L, index);
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, index) != 0)
|
||||
{
|
||||
if (lua_type(L, -2) == LUA_TSTRING)
|
||||
{
|
||||
std::string key = lua_tostring(L, -2), value;
|
||||
switch (lua_type(L, -1))
|
||||
{
|
||||
case LUA_TSTRING: value = lua_tostring(L, -1); break;
|
||||
case LUA_TNUMBER:
|
||||
value = lua_isinteger(L, -1) ? std::to_string(lua_tointeger(L, -1)) : std::to_string(lua_tonumber(L, -1));
|
||||
break;
|
||||
case LUA_TBOOLEAN: value = lua_toboolean(L, -1) ? "true" : "false"; break;
|
||||
default: break;
|
||||
}
|
||||
if (!value.empty()) out.emplace(std::move(key), std::move(value));
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
int LuaIntend(lua_State* L)
|
||||
{
|
||||
ClientlessIntent intent;
|
||||
intent.Name = luaL_checkstring(L, 1);
|
||||
if (lua_gettop(L) >= 2) intent.Parameters = ReadMap(L, 2);
|
||||
lua_pushboolean(L, sClientlessPlayerMgr->SetIntent(BoundGuid(L), std::move(intent)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int LuaClearIntent(lua_State* L)
|
||||
{
|
||||
lua_pushboolean(L, sClientlessPlayerMgr->ClearIntent(BoundGuid(L)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int LuaQueueAction(lua_State* L)
|
||||
{
|
||||
ClientlessAction action;
|
||||
action.Name = luaL_checkstring(L, 1);
|
||||
if (lua_gettop(L) >= 2) action.Parameters = ReadMap(L, 2);
|
||||
lua_pushboolean(L, sClientlessPlayerMgr->QueueAction(BoundGuid(L), std::move(action)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int LuaCharacterGuid(lua_State* L)
|
||||
{
|
||||
lua_pushinteger(L, BoundGuid(L));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int LuaLog(lua_State* L)
|
||||
{
|
||||
TC_LOG_INFO("server.worldserver", "ClientlessLua[%u]: %s", BoundGuid(L), luaL_checkstring(L, 1));
|
||||
return 0;
|
||||
}
|
||||
|
||||
void BoundFn(lua_State* L, int env, char const* name, lua_CFunction fn, uint32 guid)
|
||||
{
|
||||
env = lua_absindex(L, env);
|
||||
lua_pushinteger(L, guid);
|
||||
lua_pushcclosure(L, fn, 1);
|
||||
lua_setfield(L, env, name);
|
||||
}
|
||||
|
||||
class ClientlessAdventureLuaController final : public ClientlessPlayerController
|
||||
{
|
||||
public:
|
||||
void OnPlayerOnline(Player* player) override { sClientlessLuaEngine->ExecuteAdventureScript(player); }
|
||||
};
|
||||
}
|
||||
|
||||
ClientlessLuaEngine* ClientlessLuaEngine::instance()
|
||||
{
|
||||
static ClientlessLuaEngine instance;
|
||||
return &instance;
|
||||
}
|
||||
|
||||
ClientlessLuaEngine::ClientlessLuaEngine()
|
||||
{
|
||||
_scriptRoot = std::getenv("VCORE_CLIENTLESS_SCRIPT_DIR") ? std::getenv("VCORE_CLIENTLESS_SCRIPT_DIR") : "scripts/clientless";
|
||||
_state = luaL_newstate();
|
||||
if (!_state)
|
||||
{
|
||||
TC_LOG_ERROR("server.worldserver", "ClientlessLua: failed to create Lua state.");
|
||||
return;
|
||||
}
|
||||
luaL_openlibs(_state);
|
||||
for (char const* g : { "io", "os", "package", "debug", "dofile", "loadfile", "require" })
|
||||
{
|
||||
lua_pushnil(_state);
|
||||
lua_setglobal(_state, g);
|
||||
}
|
||||
TC_LOG_INFO("server.worldserver", "ClientlessLua initialized, script root '%s'.", _scriptRoot.c_str());
|
||||
}
|
||||
|
||||
ClientlessLuaEngine::~ClientlessLuaEngine()
|
||||
{
|
||||
if (_state) lua_close(_state);
|
||||
}
|
||||
|
||||
void ClientlessLuaEngine::PushSandboxEnvironment(Player* player)
|
||||
{
|
||||
lua_newtable(_state);
|
||||
int env = lua_gettop(_state);
|
||||
lua_newtable(_state);
|
||||
lua_pushglobaltable(_state);
|
||||
lua_setfield(_state, -2, "__index");
|
||||
lua_setmetatable(_state, env);
|
||||
|
||||
uint32 guid = player->GetGUID().GetCounter();
|
||||
BoundFn(_state, env, "intend", LuaIntend, guid);
|
||||
BoundFn(_state, env, "clear_intent", LuaClearIntent, guid);
|
||||
BoundFn(_state, env, "queue_action", LuaQueueAction, guid);
|
||||
BoundFn(_state, env, "character_guid", LuaCharacterGuid, guid);
|
||||
BoundFn(_state, env, "log", LuaLog, guid);
|
||||
lua_pushstring(_state, player->GetName().c_str());
|
||||
lua_setfield(_state, env, "character_name");
|
||||
}
|
||||
|
||||
bool ClientlessLuaEngine::ExecuteScript(Player* player, std::string const& path, std::string const& kind)
|
||||
{
|
||||
if (!_state || !player) return false;
|
||||
int base = lua_gettop(_state);
|
||||
if (luaL_loadfile(_state, path.c_str()) != LUA_OK)
|
||||
{
|
||||
TC_LOG_ERROR("server.worldserver", "ClientlessLua[%u]: load failed for %s '%s': %s",
|
||||
player->GetGUID().GetCounter(), kind.c_str(), path.c_str(), lua_tostring(_state, -1));
|
||||
lua_settop(_state, base);
|
||||
return false;
|
||||
}
|
||||
PushSandboxEnvironment(player);
|
||||
if (!lua_setupvalue(_state, base + 1, 1))
|
||||
{
|
||||
TC_LOG_ERROR("server.worldserver", "ClientlessLua[%u]: '%s' has no _ENV upvalue.", player->GetGUID().GetCounter(), path.c_str());
|
||||
lua_settop(_state, base);
|
||||
return false;
|
||||
}
|
||||
if (lua_pcall(_state, 0, 0, 0) != LUA_OK)
|
||||
{
|
||||
TC_LOG_ERROR("server.worldserver", "ClientlessLua[%u]: runtime error in '%s': %s",
|
||||
player->GetGUID().GetCounter(), path.c_str(), lua_tostring(_state, -1));
|
||||
lua_settop(_state, base);
|
||||
return false;
|
||||
}
|
||||
TC_LOG_INFO("server.worldserver", "ClientlessLua[%u]: executed %s script '%s'.",
|
||||
player->GetGUID().GetCounter(), kind.c_str(), path.c_str());
|
||||
lua_settop(_state, base);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClientlessLuaEngine::ExecuteAdventureScript(Player* player)
|
||||
{
|
||||
if (!player) return false;
|
||||
namespace fs = std::filesystem;
|
||||
std::string name = player->GetName();
|
||||
std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return char(std::tolower(c)); });
|
||||
fs::path root(_scriptRoot);
|
||||
fs::path custom = root / "adventure" / "overrides" / (name + ".lua");
|
||||
if (fs::exists(custom)) return ExecuteScript(player, custom.string(), "adventure override");
|
||||
return ExecuteScript(player, (root / "adventure" / "generic.lua").string(), "generic adventure");
|
||||
}
|
||||
|
||||
bool ClientlessLuaEngine::ExecuteTestScript(Player* player, std::string const& scriptName)
|
||||
{
|
||||
if (!player || scriptName.empty() || scriptName.find("..") != std::string::npos || scriptName.find('/') != std::string::npos || scriptName.find('\\') != std::string::npos)
|
||||
return false;
|
||||
std::filesystem::path path = std::filesystem::path(_scriptRoot) / "tests" / (scriptName + ".lua");
|
||||
return ExecuteScript(player, path.string(), "test");
|
||||
}
|
||||
|
||||
std::unique_ptr<ClientlessPlayerController> CreateClientlessAdventureLuaController()
|
||||
{
|
||||
return std::make_unique<ClientlessAdventureLuaController>();
|
||||
}
|
||||
Reference in new issue
Block a user