Implement clientless transports, Lua reload and same-map portals

Add native transport recording and execution with grid lifecycle handling, clientless teleport acknowledgements, and regression tests. Support targeted same-map portal imports and preserve disabled taxi nodes. Document live transport and portal verification and the remaining portal inventory.
This commit is contained in:
Nico committed 2026-10-26 11:27:56 +01:00
1 parent 01a9ba4b0b
commit 1164a72ea3
40 files changed
+4533 -163

No files matched your search

+96 -1
View File
@@ -1,5 +1,8 @@
#include "ClientlessLuaEngine.h"
#include "ClientlessPlayerMgr.h"
#include "ClientlessActionExecutorMgr.h"
#include "ClientlessTravelMgr.h"
#include "Config.h"
#include "Log.h"
#include "Player.h"
#include "Random.h"
@@ -14,6 +17,15 @@
namespace
{
// World-thread only. Script effects are committed only after a successful reload.
struct ReloadEffects
{
std::vector<ClientlessAction> Actions;
ClientlessIntent Intent;
bool IntentChanged = false;
};
ReloadEffects* pendingReload = nullptr;
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)
@@ -48,12 +60,30 @@ int LuaIntend(lua_State* L)
ClientlessIntent intent;
intent.Name = luaL_checkstring(L, 1);
if (lua_gettop(L) >= 2) intent.Parameters = ReadMap(L, 2);
if (pendingReload)
{
bool valid = !intent.Empty();
if (valid)
{
pendingReload->Intent = std::move(intent);
pendingReload->IntentChanged = true;
}
lua_pushboolean(L, valid);
return 1;
}
lua_pushboolean(L, sClientlessPlayerMgr->SetIntent(BoundGuid(L), std::move(intent)));
return 1;
}
int LuaClearIntent(lua_State* L)
{
if (pendingReload)
{
pendingReload->Intent = ClientlessIntent();
pendingReload->IntentChanged = true;
lua_pushboolean(L, true);
return 1;
}
lua_pushboolean(L, sClientlessPlayerMgr->ClearIntent(BoundGuid(L)));
return 1;
}
@@ -63,6 +93,13 @@ int LuaQueueAction(lua_State* L)
ClientlessAction action;
action.Name = luaL_checkstring(L, 1);
if (lua_gettop(L) >= 2) action.Parameters = ReadMap(L, 2);
if (pendingReload)
{
bool valid = !action.Name.empty();
if (valid) pendingReload->Actions.push_back(std::move(action));
lua_pushboolean(L, valid);
return 1;
}
lua_pushboolean(L, sClientlessPlayerMgr->QueueAction(BoundGuid(L), std::move(action)));
return 1;
}
@@ -184,7 +221,10 @@ ClientlessLuaEngine* ClientlessLuaEngine::instance()
ClientlessLuaEngine::ClientlessLuaEngine()
{
_scriptRoot = std::getenv("VCORE_CLIENTLESS_SCRIPT_DIR") ? std::getenv("VCORE_CLIENTLESS_SCRIPT_DIR") : "scripts/clientless";
_scriptRoot = sConfigMgr->GetStringDefault("Clientless.ScriptRoot", "scripts/clientless");
if (char const* overrideRoot = std::getenv("VCORE_CLIENTLESS_SCRIPT_DIR"))
if (*overrideRoot) _scriptRoot = overrideRoot;
_scriptRoot = std::filesystem::absolute(_scriptRoot).lexically_normal().string();
_state = luaL_newstate();
if (!_state)
{
@@ -445,6 +485,61 @@ bool ClientlessLuaEngine::ExecuteAdventureScript(Player* player)
return ExecuteScript(player, (root / "adventure" / "generic.lua").string(), "generic adventure");
}
bool ClientlessLuaEngine::ReloadAdventureScript(Player* player, std::string& detail)
{
if (!player || !player->IsInWorld() || !sClientlessPlayerMgr->IsManaged(player->GetGUID().GetCounter()))
{
detail = "Reload requires an online managed clientless player.";
return false;
}
uint32 guid = player->GetGUID().GetCounter();
auto action = sClientlessActionExecutorMgr->GetStatus(guid);
auto travel = sClientlessTravelMgr->GetStatus(guid);
if (pendingReload || sClientlessPlayerMgr->GetQueuedActionCount(guid) ||
(action && (action->State == ClientlessActionExecutionState::SelectingTarget ||
action->State == ClientlessActionExecutionState::Moving)) ||
(travel && travel->State == ClientlessTravelState::Moving) ||
player->GetTransport() || player->IsInFlight() || player->IsBeingTeleported())
{
detail = "Reload refused: bot has queued actions, is travelling or is aboard a transport. Wait until idle.";
return false;
}
auto previous = _profiles.find(guid);
bool hadProfile = previous != _profiles.end();
ClientlessAdventureProfile backup = hadProfile ? previous->second : ClientlessAdventureProfile();
ReloadEffects effects;
bool success;
{
struct Capture
{
explicit Capture(ReloadEffects& effects) { pendingReload = &effects; }
~Capture() { pendingReload = nullptr; }
} capture(effects);
success = LoadAdventureProfile(player) && ExecuteAdventureScript(player);
}
if (!success)
{
if (hadProfile) _profiles[guid] = std::move(backup);
else _profiles.erase(guid);
detail = "Lua reload failed. Previous profile retained; no new actions or intent applied. See server log.";
}
else
{
if (effects.IntentChanged)
{
if (effects.Intent.Empty()) sClientlessPlayerMgr->ClearIntent(guid);
else sClientlessPlayerMgr->SetIntent(guid, std::move(effects.Intent));
}
for (auto& queued : effects.Actions)
sClientlessPlayerMgr->QueueAction(guid, std::move(queued));
detail = "Profile and adventure script reloaded; queued actions=" + std::to_string(effects.Actions.size()) + ".";
}
TC_LOG_INFO("server.adventurebot.navigation", "LUA_RELOAD guid=%u success=%u root='%s' detail='%s'",
guid, success ? 1u : 0u, _scriptRoot.c_str(), detail.c_str());
return success;
}
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)