importet Areateleports as Nodes
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s

This commit is contained in:
Nico committed 2026-10-16 09:36:33 +00:00
1 parent 93c3df1c95
commit fe7b5e0f02
12 files changed
+2087 -218

No files matched your search

+262 -1
View File
@@ -2,6 +2,7 @@
#include "ClientlessPlayerMgr.h"
#include "Log.h"
#include "Player.h"
#include "Random.h"
#include <lua.hpp>
#include <algorithm>
#include <cctype>
@@ -86,10 +87,92 @@ void BoundFn(lua_State* L, int env, char const* name, lua_CFunction fn, uint32 g
lua_setfield(L, env, name);
}
std::string ReadStringField(lua_State* L, int table, char const* key, std::string const& fallback = {})
{
table = lua_absindex(L, table);
lua_getfield(L, table, key);
std::string value = lua_isstring(L, -1) ? lua_tostring(L, -1) : fallback;
lua_pop(L, 1);
return value;
}
bool ReadBoolField(lua_State* L, int table, char const* key, bool fallback)
{
table = lua_absindex(L, table);
lua_getfield(L, table, key);
bool value = lua_isboolean(L, -1) ? lua_toboolean(L, -1) != 0 : fallback;
lua_pop(L, 1);
return value;
}
float ReadFloatField(lua_State* L, int table, char const* key, float fallback)
{
table = lua_absindex(L, table);
lua_getfield(L, table, key);
float value = lua_isnumber(L, -1) ? float(lua_tonumber(L, -1)) : fallback;
lua_pop(L, 1);
return value;
}
std::unordered_map<std::string, float> ReadFloatDictionary(lua_State* L, int table, char const* key)
{
std::unordered_map<std::string, float> out;
table = lua_absindex(L, table);
lua_getfield(L, table, key);
if (!lua_istable(L, -1))
{
lua_pop(L, 1);
return out;
}
int dict = lua_absindex(L, -1);
lua_pushnil(L);
while (lua_next(L, dict) != 0)
{
if (lua_type(L, -2) == LUA_TSTRING && lua_isnumber(L, -1))
out[lua_tostring(L, -2)] = float(lua_tonumber(L, -1));
lua_pop(L, 1);
}
lua_pop(L, 1);
return out;
}
std::vector<ClientlessWeightedMount> ReadMountDictionary(lua_State* L, int movementTable)
{
std::vector<ClientlessWeightedMount> mounts;
movementTable = lua_absindex(L, movementTable);
lua_getfield(L, movementTable, "ground_mounts");
if (!lua_istable(L, -1))
{
lua_pop(L, 1);
return mounts;
}
int dict = lua_absindex(L, -1);
lua_pushnil(L);
while (lua_next(L, dict) != 0)
{
if (lua_isinteger(L, -2) && lua_isnumber(L, -1))
{
lua_Integer id = lua_tointeger(L, -2);
lua_Number weight = lua_tonumber(L, -1);
if (id > 0 && id <= 0xFFFFFFFFll && weight > 0.0)
mounts.push_back({ uint32(id), uint32(std::max<lua_Number>(1.0, weight)) });
}
lua_pop(L, 1);
}
lua_pop(L, 1);
return mounts;
}
class ClientlessAdventureLuaController final : public ClientlessPlayerController
{
public:
void OnPlayerOnline(Player* player) override { sClientlessLuaEngine->ExecuteAdventureScript(player); }
void OnPlayerOnline(Player* player) override
{
sClientlessLuaEngine->LoadAdventureProfile(player);
sClientlessLuaEngine->ExecuteAdventureScript(player);
}
};
}
@@ -172,6 +255,184 @@ bool ClientlessLuaEngine::ExecuteScript(Player* player, std::string const& path,
return true;
}
bool ClientlessLuaEngine::LoadAdventureProfile(Player* player)
{
if (!_state || !player)
return false;
namespace fs = std::filesystem;
std::string lowerName = player->GetName();
std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(),
[](unsigned char c) { return char(std::tolower(c)); });
fs::path root(_scriptRoot);
fs::path specific = root / "adventure" / "profiles" / (lowerName + ".lua");
fs::path generic = root / "adventure" / "profiles" / "generic.lua";
fs::path chosen = fs::exists(specific) ? specific : generic;
int base = lua_gettop(_state);
if (luaL_loadfile(_state, chosen.string().c_str()) != LUA_OK)
{
TC_LOG_ERROR("server.adventurebot",
"[AdventureBot][Profile] guid=%u name='%s' load failed path='%s': %s",
player->GetGUID().GetCounter(), player->GetName().c_str(),
chosen.string().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.adventurebot",
"[AdventureBot][Profile] guid=%u path='%s' has no _ENV upvalue",
player->GetGUID().GetCounter(), chosen.string().c_str());
lua_settop(_state, base);
return false;
}
if (lua_pcall(_state, 0, 1, 0) != LUA_OK)
{
TC_LOG_ERROR("server.adventurebot",
"[AdventureBot][Profile] guid=%u runtime error path='%s': %s",
player->GetGUID().GetCounter(), chosen.string().c_str(), lua_tostring(_state, -1));
lua_settop(_state, base);
return false;
}
if (!lua_istable(_state, -1))
{
TC_LOG_ERROR("server.adventurebot",
"[AdventureBot][Profile] guid=%u path='%s' must return a table",
player->GetGUID().GetCounter(), chosen.string().c_str());
lua_settop(_state, base);
return false;
}
int rootTable = lua_absindex(_state, -1);
ClientlessAdventureProfile profile;
profile.CharacterName = player->GetName();
profile.SourcePath = chosen.string();
lua_getfield(_state, rootTable, "identity");
if (lua_istable(_state, -1))
{
profile.CharacterName = ReadStringField(_state, -1, "name", profile.CharacterName);
profile.HomeRegion = ReadStringField(_state, -1, "home_region");
}
lua_pop(_state, 1);
lua_getfield(_state, rootTable, "movement");
if (lua_istable(_state, -1))
{
profile.CanGroundMount = ReadBoolField(_state, -1, "can_ground_mount", false);
profile.GroundMountMinDistance = std::max(0.0f,
ReadFloatField(_state, -1, "ground_mount_min_distance", 60.0f));
profile.GroundMountSpeedRate = std::max(1.0f,
ReadFloatField(_state, -1, "ground_mount_speed_rate", 1.6f));
profile.GroundMounts = ReadMountDictionary(_state, -1);
}
lua_pop(_state, 1);
profile.Personality = ReadFloatDictionary(_state, rootTable, "personality");
profile.RegionAffinity = ReadFloatDictionary(_state, rootTable, "region_affinity");
profile.ActivityAffinity = ReadFloatDictionary(_state, rootTable, "activity_affinity");
uint32 const guid = player->GetGUID().GetCounter();
_profiles[guid] = std::move(profile);
ClientlessAdventureProfile const& stored = _profiles.at(guid);
TC_LOG_INFO("server.adventurebot",
"[AdventureBot][Profile] loaded guid=%u name='%s' source='%s' home='%s' "
"canGroundMount=%u mounts=%u mountMinDistance=%.1f mountSpeedRate=%.2f "
"personality=%u regionAffinity=%u activityAffinity=%u",
guid, player->GetName().c_str(), stored.SourcePath.c_str(), stored.HomeRegion.c_str(),
stored.CanGroundMount ? 1u : 0u, uint32(stored.GroundMounts.size()),
stored.GroundMountMinDistance, stored.GroundMountSpeedRate,
uint32(stored.Personality.size()), uint32(stored.RegionAffinity.size()),
uint32(stored.ActivityAffinity.size()));
if (stored.CanGroundMount && stored.GroundMounts.empty())
TC_LOG_WARN("server.adventurebot",
"[AdventureBot][Profile] guid=%u enables ground mounts but defines no ground_mounts", guid);
lua_settop(_state, base);
return true;
}
ClientlessAdventureProfile const* ClientlessLuaEngine::GetAdventureProfile(uint32 characterGuid) const
{
auto itr = _profiles.find(characterGuid);
return itr != _profiles.end() ? &itr->second : nullptr;
}
uint32 ClientlessLuaEngine::SelectGroundMountDisplayId(uint32 characterGuid) const
{
ClientlessAdventureProfile const* profile = GetAdventureProfile(characterGuid);
if (!profile || profile->GroundMounts.empty())
return 0;
uint64 total = 0;
for (ClientlessWeightedMount const& mount : profile->GroundMounts)
total += mount.Weight;
if (!total)
return 0;
uint32 const cappedTotal = uint32(std::min<uint64>(total, 0xFFFFFFFFull));
uint32 roll = urand(1u, cappedTotal);
for (ClientlessWeightedMount const& mount : profile->GroundMounts)
{
if (roll <= mount.Weight)
return mount.DisplayId;
roll -= std::min(roll, mount.Weight);
}
return profile->GroundMounts.back().DisplayId;
}
bool ClientlessLuaEngine::PrepareGroundMount(Player* player, float travelDistance, char const* context)
{
if (!player || !player->IsInWorld())
return false;
ClientlessAdventureProfile const* profile = GetAdventureProfile(player->GetGUID().GetCounter());
if (!profile || !profile->CanGroundMount || profile->GroundMounts.empty())
return false;
if (travelDistance < profile->GroundMountMinDistance || !player->IsOutdoors())
return false;
if (player->IsMounted())
return true;
uint32 const displayId = SelectGroundMountDisplayId(player->GetGUID().GetCounter());
if (!displayId)
return false;
// The AdventureBot profile is authoritative for this capability. Intentionally
// no level, riding-skill, spellbook, inventory or economy eligibility check.
player->Mount(displayId);
player->SetSpeedRate(MOVE_RUN, profile->GroundMountSpeedRate);
TC_LOG_INFO("server.adventurebot.navigation",
"GROUND_MOUNT guid=%u name='%s' displayId=%u distance=%.2f speedRate=%.2f context='%s'",
player->GetGUID().GetCounter(), player->GetName().c_str(), displayId,
travelDistance, profile->GroundMountSpeedRate, context ? context : "");
return true;
}
void ClientlessLuaEngine::DismountGroundMount(Player* player, char const* reason)
{
if (!player || !player->IsMounted())
return;
uint32 const oldDisplay = player->GetMountID();
player->Dismount();
player->UpdateSpeed(MOVE_RUN);
TC_LOG_INFO("server.adventurebot.navigation",
"GROUND_DISMOUNT guid=%u name='%s' displayId=%u reason='%s'",
player->GetGUID().GetCounter(), player->GetName().c_str(), oldDisplay,
reason ? reason : "");
}
bool ClientlessLuaEngine::ExecuteAdventureScript(Player* player)
{
if (!player) return false;