#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 #include #include #include #include #include #include #include #include namespace { // World-thread only. Script effects are committed only after a successful reload. struct ReloadEffects { std::vector 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 ReadMap(lua_State* L, int index) { std::unordered_map 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 ReadFloatDictionary(lua_State* L, int table, char const* key) { std::unordered_map 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 ReadMountDictionary(lua_State* L, int movementTable, char const* field = "ground_mounts") { std::vector 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(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(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 CreateClientlessAdventureLuaController() { return std::make_unique(); }