core: add server-side lua runtime for clientless players
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s

This commit is contained in:
Nico committed 2026-10-12 16:12:18 +00:00
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>();
}