Files
Viano-Core/src/server/game/World/ClientlessLuaEngine.cpp
T
Nico 0173a434f4 Improve AdventureBot recruitment, world presence and performance
Manage Arthoran as a persistent level-60 tank profile, add Worgen forms and mounts, enable PvP and recruit through native LFG with preparation actions and player invitations.

Reduce repeated route and flight-planning work, bound failed-travel retries and add runtime timing diagnostics. Include 86 passing tests and measurements with 110 online bots.
2026-10-29 01:36:35 +01:00

730 lines
29 KiB
C++

#include "AdventureProfessionPolicy.h"
#include "ClientlessLuaEngine.h"
#include "AdventureRotationPolicy.h"
#include "ClientlessPlayerMgr.h"
#include "ClientlessActionExecutorMgr.h"
#include "ClientlessTravelMgr.h"
#include "AdventureBehaviorMgr.h"
#include "AdventureCombatMgr.h"
#include "AdventureInteractionMgr.h"
#include "AdventureGroupMgr.h"
#include "AdventureGroupTravelMgr.h"
#include "AdventureGeneratorMgr.h"
#include "AdventurePopulationMgr.h"
#include "AdventureSupplyMgr.h"
#include "AdventureSocialActions.h"
#include "Config.h"
#include "Log.h"
#include "Player.h"
#include "Random.h"
#include "GameTime.h"
#include "Map.h"
#include <lua.hpp>
#include <algorithm>
#include <cmath>
#include <cctype>
#include <cstdlib>
#include <filesystem>
#include <memory>
#include <string>
#include <unordered_map>
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)
{
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);
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;
}
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;
}
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);
}
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, char const* field = "ground_mounts")
{
std::vector<ClientlessWeightedMount> mounts;
movementTable = lua_absindex(L, movementTable);
lua_getfield(L, movementTable, field);
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 && std::isfinite(weight) && weight > 0.0 && weight <= 1000000.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->LoadAdventureProfile(player);
AdventureSocialActions::Prepare(player);
sClientlessLuaEngine->ExecuteAdventureScript(player);
sAdventureBehaviorMgr->Online(player);
sAdventureSupplyMgr->Online(player);
sAdventurePopulationMgr->Online(player);
}
void OnIntentChanged(Player* player, ClientlessIntent const* intent) override
{ sAdventureBehaviorMgr->IntentChanged(player, intent); }
void Update(Player* player, uint32 diff) override
{
sClientlessLuaEngine->UpdateWorldPresence(player,sAdventureBehaviorMgr->InteractionIntent(player));
sAdventureGroupTravelMgr->Observe(player,diff);
sAdventurePopulationMgr->Maintain(player,diff);
if(sAdventureGroupTravelMgr->Airborne(player)){sAdventureGroupTravelMgr->Follow(player);return;}
if(sAdventureSupplyMgr->Update(player,diff,sAdventureCombatMgr->Busy(player)))return;
if(!sAdventureCombatMgr->Update(player,diff) && !sAdventureInteractionMgr->Update(player,diff))
sAdventureBehaviorMgr->Update(player,diff);
}
void OnSharedTargetDamage(Player* player,ObjectGuid helper,ObjectGuid target) override
{ sAdventureCombatMgr->SharedTargetDamage(player,helper,target); }
void OnGroupJoinRequest(Player* player,Player* requester) override
{sAdventureGroupMgr->RequestPlayerAdmission(player,requester);}
void OnTextEmote(Player* player, Player* source, uint32 emote) override
{ sAdventureInteractionMgr->Emote(player,source,emote); }
void OnDamageTaken(Player* player, Unit* attacker, uint32 damage) override
{ sAdventureCombatMgr->DamageTaken(player, attacker, damage); }
void OnPlayerLogout(Player* player) override
{ sClientlessLuaEngine->ForgetWorldPresence(player->GetGUID().GetCounter()); sAdventurePopulationMgr->Offline(player); sAdventureInteractionMgr->Offline(player); sAdventureSupplyMgr->Offline(player); sAdventureCombatMgr->Offline(player); sAdventureBehaviorMgr->Offline(player); }
};
}
ClientlessLuaEngine* ClientlessLuaEngine::instance()
{
static ClientlessLuaEngine instance;
return &instance;
}
ClientlessLuaEngine::ClientlessLuaEngine()
{
_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)
{
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::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;
std::string generated;
bool databaseProfile=!fs::exists(specific) && AdventureGeneratorMgr::Profile(player->GetGUID().GetCounter(),generated);
if(databaseProfile)chosen=fs::path("generated:"+std::to_string(player->GetGUID().GetCounter()));
int base = lua_gettop(_state);
int loaded=databaseProfile?luaL_loadbuffer(_state,generated.data(),generated.size(),chosen.string().c_str()):luaL_loadfile(_state,chosen.string().c_str());
if (loaded != 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");
profile.Background = ReadStringField(_state, -1, "background");
profile.Vocation = ReadStringField(_state, -1, "vocation");
profile.ProfileVersion = uint32(std::clamp(ReadFloatField(_state,-1,"profile_version",0),0.0f,100.0f));
profile.Level = uint32(std::clamp(ReadFloatField(_state,-1,"level",0),0.0f,85.0f));
profile.Class = uint32(std::clamp(ReadFloatField(_state,-1,"class",0),0.0f,11.0f));
profile.Specialization = uint32(std::clamp(ReadFloatField(_state,-1,"specialization",0),0.0f,2.0f));
lua_getfield(_state,-1,"seed");
if(lua_isinteger(_state,-1)){auto value=lua_tointeger(_state,-1);if(value>=0 && value<=0xFFFFFFFFll)profile.Seed=uint32(value);}
lua_pop(_state,1);
profile.SoloSpecialization=uint32(std::clamp(ReadFloatField(_state,-1,"solo_specialization",
float(AdventureRotationPolicy::SoloSpec(profile.Class,profile.Seed))),0.0f,2.0f));
profile.Role = ReadStringField(_state,-1,"role","damage");
if(profile.Role!="tank" && profile.Role!="healer" && profile.Role!="damage")profile.Role="damage";
profile.HomeZone = uint32(std::max(0.0f,ReadFloatField(_state,-1,"home_zone",0)));
profile.HomeCapital = uint32(std::max(0.0f,ReadFloatField(_state,-1,"home_capital",0)));
profile.FavoriteZone = uint32(std::max(0.0f,ReadFloatField(_state,-1,"favorite_zone",0)));
profile.FavoriteRegion = ReadStringField(_state,-1,"favorite_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);
profile.CanFreeFly = ReadBoolField(_state, -1, "can_free_fly", false);
profile.FlightMounts = ReadMountDictionary(_state, -1, "flight_mounts");
profile.Companions = ReadMountDictionary(_state,-1,"companions");
profile.CompanionSpell = uint32(std::max(0.0f, ReadFloatField(_state, -1, "companion_spell", 0)));
profile.CompanionLeashItem = uint32(std::max(0.0f, ReadFloatField(_state, -1, "companion_leash_item", 0)));
}
lua_pop(_state, 1);
lua_getfield(_state,rootTable,"inventory");
if(lua_istable(_state,-1))
{
lua_getfield(_state,-1,"keep_items");
if(lua_istable(_state,-1))for(size_t i=1;i<=lua_rawlen(_state,-1);++i)
{
lua_rawgeti(_state,-1,i);auto id=lua_tointeger(_state,-1);
if(id>0 && id<=0xFFFFFFFFll)profile.KeepItems.insert(uint32(id));
lua_pop(_state,1);
}
lua_pop(_state,1);
}
lua_pop(_state,1);
lua_getfield(_state,rootTable,"zone_affinity");
if(lua_istable(_state,-1))
{
lua_pushnil(_state);
while(lua_next(_state,-2))
{
auto id=lua_tointeger(_state,-2);auto weight=lua_tonumber(_state,-1);
if(lua_isinteger(_state,-2) && lua_isnumber(_state,-1) && id>0 && id<=0xFFFFFFFFll && std::isfinite(weight))
profile.ZoneAffinity[uint32(id)]=float(std::clamp(weight,0.0,100.0));
lua_pop(_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");
profile.MicroActionAffinity = ReadFloatDictionary(_state, rootTable, "micro_action_affinity");
lua_getfield(_state, rootTable, "behavior");
if (lua_istable(_state, -1))
{
profile.AutonomousBehavior = ReadBoolField(_state, -1, "enabled", false);
profile.DailySchedule = ReadBoolField(_state, -1, "daily_schedule", false);
profile.AllowRetreat = ReadBoolField(_state, -1, "allow_retreat", true);
profile.InitialIntent = ReadStringField(_state, -1, "initial_intent");
profile.InitialLocation = ReadStringField(_state, -1, "initial_location");
profile.Locale = ReadStringField(_state, -1, "locale", "enUS");
profile.InitialDurationSeconds = uint32(std::clamp(ReadFloatField(_state, -1, "initial_duration_seconds", 3600), 30.0f, 86400.0f));
}
lua_pop(_state, 1);
lua_getfield(_state,rootTable,"professions");
if(lua_istable(_state,-1))
{
lua_getfield(_state,-1,"primary");
if(lua_istable(_state,-1))
for(size_t index=1;index<=lua_rawlen(_state,-1);++index)
{
lua_rawgeti(_state,-1,index);
if(lua_type(_state,-1)==LUA_TSTRING)
{
std::string name=lua_tostring(_state,-1);
if(AdventureProfessionPolicy::PrimaryName(name))profile.PrimaryProfessions.insert(name);
else TC_LOG_WARN("server.adventurebot","PROFILE_UNKNOWN_PROFESSION guid=%u name='%s'",player->GetGUID().GetCounter(),name.c_str());
}
lua_pop(_state,1);
}
lua_pop(_state,1);
}
lua_pop(_state,1);
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()));
TC_LOG_INFO("server.adventurebot","PROFILE_PROFESSIONS guid=%u primary=%u",guid,uint32(stored.PrimaryProfessions.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;
}
uint32 ClientlessLuaEngine::SelectCompanionSpell(uint32 guid) const
{
auto profile=GetAdventureProfile(guid);
if(!profile)return 0;
uint32 total=0;
for(auto const& pet:profile->Companions)total+=pet.Weight;
if(!total)return profile->CompanionSpell;
auto roll=urand(1,total);
for(auto const& pet:profile->Companions){if(roll<=pet.Weight)return pet.DisplayId;roll-=pet.Weight;}
return 0;
}
void ClientlessLuaEngine::UpdateWorldPresence(Player* player, std::string const& intent, bool arrived)
{
if (!player || !player->IsInWorld()) return;
player->AddCharacterFlag(CHARACTER_FLAG_PVP_DESIRED);
player->SetFlag(PLAYER_FLAGS, PLAYER_FLAGS_IN_PVP);
if (!player->IsPvP() || player->pvpInfo.EndTimer) player->UpdatePvP(true,true);
if (player->getRace()!=RACE_WORGEN) return;
if (!player->HasSpell(SPELL_TWO_FORMS_RACIAL)) player->LearnSpell(SPELL_TWO_FORMS_RACIAL,false);
if (player->getLevel()>=20 && !player->HasSpell(458)) player->LearnSpell(458,false);
bool active=intent=="adventure" || intent=="instance" || intent=="raid" || player->IsInCombat() || player->GetMap()->IsDungeon();
auto guid=player->GetGUID().GetCounter();
auto wanted=_worgenForms.find(guid);
if (arrived && !active) wanted=_worgenForms.insert_or_assign(guid,urand(0,1)!=0).first;
if (active) wanted=_worgenForms.insert_or_assign(guid,true).first;
if (wanted==_worgenForms.end() || !player->IsAlive() || player->IsMounted() ||
player->GetTransport() || player->IsInFlight() || player->IsFlying() || player->IsBeingTeleported() ||
player->HasUnitState(UNIT_STATE_CASTING)) return;
bool altered=player->HasAuraType(SPELL_AURA_WORGEN_ALTERED_FORM);
if (altered==wanted->second || _worgenNextAttempt[guid]>GameTime::GetGameTime()) return;
_worgenNextAttempt[guid]=GameTime::GetGameTime()+5;
if (wanted->second) player->CastSpell(player,SPELL_ALTERED_FORM_RACIAL,true);
else if (player->HasSpell(SPELL_TWO_FORMS_RACIAL)) player->CastSpell(player,SPELL_TWO_FORMS_RACIAL,true);
TC_LOG_INFO("server.adventurebot","WORGEN_FORM guid=%u intent='%s' desired=%u arrived=%u",guid,intent.c_str(),wanted->second,arrived);
}
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() && player->getRace()!=RACE_WORGEN))
return false;
if (travelDistance < profile->GroundMountMinDistance || !player->IsOutdoors())
return false;
if (player->IsMounted())
return true;
if (player->getRace()==RACE_WORGEN && player->HasAuraType(SPELL_AURA_WORGEN_ALTERED_FORM) && player->getLevel()>=20)
{
if (!player->HasSpell(87840)) player->LearnSpell(87840,false);
player->CastSpell(player,87840,true);
if (player->HasAura(87840) && player->IsMounted())
{
TC_LOG_INFO("server.adventurebot.navigation","GROUND_RUNNING_WILD guid=%u",player->GetGUID().GetCounter());
return true;
}
return false;
}
uint32 const displayId = player->getRace()==RACE_WORGEN?2404: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->RemoveAurasDueToSpell(87840);
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;
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::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)
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>();
}