#include "ClientlessTestBotMgr.h" #include "ClientlessCharacterFactory.h" #include "ClientlessPlayerMgr.h" #include "ClientlessTravelMgr.h" #include "Chat.h" #include "Base64.h" #include "Config.h" #include "CombatPackets.h" #include "Creature.h" #include "GameObject.h" #include "GossipDef.h" #include "NPCPackets.h" #include "Bag.h" #include "DBCStores.h" #include "Item.h" #include "Log.h" #include "Map.h" #include "MotionMaster.h" #include "MovementPackets.h" #include "GameClient.h" #include "PathGenerator.h" #include "ObjectAccessor.h" #include "ObjectMgr.h" #include "Opcodes.h" #include "Player.h" #include "QuestPackets.h" #include "SpellInfo.h" #include "SpellMgr.h" #include "SpellPackets.h" #include "Stats.h" #include "World.h" #include "WorldPacket.h" #include "WorldSession.h" #include #include #include #include #include #include #include #include #include #include #include namespace { namespace fs=std::filesystem; using Clock=std::chrono::steady_clock; struct TestController final:ClientlessPlayerController { static void ClaimMover(Player* player) { auto client=player->GetGameClient();if(!client)return; Unit* mover=player; if(auto vehicle=player->GetVehicleBase())if(client->IsAllowedToMove(vehicle))mover=vehicle; if(!client->IsAllowedToMove(mover) || client->GetActivelyMovedUnit()==mover)return; // Emulate the client's normal control acknowledgement. The native // handler enforces the server's allowed-mover list, including vehicles. WorldPackets::Movement::SetActiveMover packet{WorldPacket(CMSG_SET_ACTIVE_MOVER)};packet.ActiveMover=mover->GetGUID(); player->GetSession()->HandleSetActiveMoverOpcode(packet); } void OnPlayerOnline(Player* player)override{ClaimMover(player);} void Update(Player* player,uint32)override{ClaimMover(player);} }; struct Run { std::string Id, State="running", Error; lua_State* Lua=nullptr; lua_State* Thread=nullptr; Clock::time_point Started=Clock::now(), Wake=Started; uint32 TimeoutSeconds=300; uint64 StartedUnixMs=uint64(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()); uint32 Checks=0, Failures=0, Created=0; bool Creating=false; size_t Memory=0; std::string CreateError; std::set Bots; std::vector Events; ~Run(){if(Lua)lua_close(Lua);} }; std::map> runs; std::map leases; bool accepting=true; fs::path Root(){return sConfigMgr->GetStringDefault("Clientless.TestBot.Root","testbots");} bool Key(std::string const& s) { return !s.empty() && s.size()<=80 && s.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-")==std::string::npos; } std::string Quote(std::string const& s) { std::string out="\"";char const* hex="0123456789abcdef"; for(unsigned char c:s){if(c=='"'||c=='\\'){out+='\\';out+=char(c);}else if(c<32){out+="\\u00";out+=hex[c>>4];out+=hex[c&15];}else out+=char(c);} return out+'"'; } bool Write(fs::path const& path,std::string const& value) { fs::create_directories(path.parent_path());auto tmp=path;tmp+=".tmp"; std::ofstream out(tmp,std::ios::binary|std::ios::trunc);out<(Clock::now()-r.Started).count() <<",\"checks\":"<CancelTravel(guid); sClientlessPlayerMgr->StopPlayer(guid); leases.erase(guid); } Publish(r); if(r.Lua){lua_close(r.Lua);r.Lua=nullptr;r.Thread=nullptr;} r.Events.clear();r.Events.shrink_to_fit(); TC_LOG_INFO("server.worldserver","TESTBOT_RESULT run=%s state=%s checks=%u failures=%u error='%s'",r.Id.c_str(),r.State.c_str(),r.Checks,r.Failures,r.Error.c_str()); } Run& Context(lua_State* L){return *static_cast(lua_touserdata(L,lua_upvalueindex(1)));} uint32 Number(lua_State* L,int index,uint32 low=1,uint32 high=0xFFFFFFFFu) { auto v=luaL_checkinteger(L,index);luaL_argcheck(L,v>=low && uint64(v)<=high,index,"integer out of range");return uint32(v); } float Coordinate(lua_State* L,int i) { auto v=luaL_checknumber(L,i);luaL_argcheck(L,std::isfinite(v) && std::fabs(v)<100000,i,"invalid coordinate");return float(v); } uint32 Owned(lua_State* L) { auto guid=Number(L,1);luaL_argcheck(L,Context(L).Bots.count(guid),1,"bot does not belong to this run");return guid; } Player* Actor(lua_State* L) { auto session=sWorld->FindClientlessSession(Owned(L));auto player=session?session->GetPlayer():nullptr; luaL_argcheck(L,player && player->IsInWorld() && !player->IsBeingTeleported(),1,"bot not online or teleport pending");return player; } ObjectGuid Target(lua_State* L,int index=2) { auto s=luaL_checkstring(L,index);char* end=nullptr;auto raw=std::strtoull(s,&end,10); luaL_argcheck(L,*s && end && !*end && raw,index,"expected full GUID string from creature/object lookup");return ObjectGuid(uint64(raw)); } int Spawn(lua_State* L) { auto guid=Number(L,1);auto& r=Context(L); if(leases.count(guid) && leases.at(guid)!=r.Id)return luaL_error(L,"bot leased by another run"); std::ifstream file(Root()/"registry"/(std::to_string(guid)+".bot"));uint32 account=0;file>>account; if(!account)return luaL_error(L,"not a registered test character; create it with testbot.create first"); if(sClientlessPlayerMgr->IsManaged(guid) || !sClientlessPlayerMgr->StartPlayer(account,guid,std::make_unique())) return luaL_error(L,"test character is already managed or login failed"); r.Bots.insert(guid);leases[guid]=r.Id;lua_pushinteger(L,guid);return 1; } int Online(lua_State* L) { auto session=sWorld->FindClientlessSession(Owned(L));auto player=session?session->GetPlayer():nullptr; lua_pushboolean(L,player && player->IsInWorld() && !player->IsBeingTeleported());return 1; } int Create(lua_State* L) { auto& r=Context(L);if(r.Creating)return luaL_error(L,"creation already pending"); auto account=Number(L,1);ClientlessCharacterFactory::Definition d; d.Name=luaL_checkstring(L,2);d.Race=uint8(Number(L,3,1,255));d.Class=uint8(Number(L,4,1,11)); d.Level=uint8(Number(L,5,1,85));d.Gender=uint8(luaL_optinteger(L,6,0));d.Seed=uint32(r.Bots.size()+1); r.Creating=true;r.Created=0;r.CreateError.clear();std::weak_ptr weak=runs.at(r.Id); ClientlessCharacterFactory::CreateAsync(account,d,{}, {},[weak,account](ClientlessCharacterFactory::Result result) { try { if(result.Guid && !Write(Root()/"registry"/(std::to_string(result.Guid)+".bot"),std::to_string(account))) result.Error="Cannot persist test character registry"; } catch(std::exception const& e){result.Error=e.what();} if(auto run=weak.lock()){run->Creating=false;run->Created=result.Guid;run->CreateError=result.Error;if(result.Guid)run->Bots.insert(result.Guid);if(run->State!="running")Publish(*run);} }); return 0; } int CreateResult(lua_State* L) { auto& r=Context(L);lua_pushboolean(L,r.Creating);lua_pushinteger(L,r.Created);lua_pushstring(L,r.CreateError.c_str()); return 3; } int Wait(lua_State* L) { auto ms=Number(L,1,0,60000);Context(L).Wake=Clock::now()+std::chrono::milliseconds(ms);return lua_yield(L,0); } int Snapshot(lua_State* L) { auto p=Actor(L);lua_newtable(L); auto n=[&](char const* k,lua_Integer v){lua_pushinteger(L,v);lua_setfield(L,-2,k);}; auto f=[&](char const* k,float v){lua_pushnumber(L,v);lua_setfield(L,-2,k);}; n("strength",p->GetStat(StatType::Strength));n("agility",p->GetStat(StatType::Agility));n("stamina",p->GetStat(StatType::Stamina));n("intellect",p->GetStat(StatType::Intellect));n("spirit",p->GetStat(StatType::Spirit)); auto mover=p->GetGameClient()?p->GetGameClient()->GetActivelyMovedUnit():nullptr;lua_pushstring(L,mover?std::to_string(mover->GetGUID().GetRawValue()).c_str():"");lua_setfield(L,-2,"mover_guid"); n("area",p->GetAreaId());n("zone",p->GetZoneId());lua_pushstring(L,p->GetLootGUID()?std::to_string(p->GetLootGUID().GetRawValue()).c_str():"");lua_setfield(L,-2,"loot_guid"); n("guid",p->GetGUID().GetCounter());n("level",p->getLevel());n("map",p->GetMapId());n("health",p->GetHealth());n("max_health",p->GetMaxHealth());n("power",p->GetPower(p->GetPowerType()));n("max_power",p->GetMaxPower(p->GetPowerType())); auto vehicle=p->GetVehicleCreatureBase();n("vehicle_entry",vehicle?vehicle->GetEntry():0);lua_pushstring(L,vehicle?std::to_string(vehicle->GetGUID().GetRawValue()).c_str():"");lua_setfield(L,-2,"vehicle_guid"); n("moving",p->IsSplineEnabled());n("alive",p->IsAlive());n("combat",p->IsInCombat());n("transport",bool(p->GetTransport()));f("x",p->GetPositionX());f("y",p->GetPositionY());f("z",p->GetPositionZ());return 1; } int Teleport(lua_State* L) { auto p=Actor(L);auto map=Number(L,2,0,65535);auto x=Coordinate(L,3),y=Coordinate(L,4),z=Coordinate(L,5),o=Coordinate(L,6); lua_pushboolean(L,p->TeleportTo(map,x,y,z,o));return 1; } int Level(lua_State* L){auto p=Actor(L);p->GiveLevel(uint8(Number(L,2,1,85)));p->InitTalentForLevel();p->SetFullHealth();return 0;} int Stats(lua_State* L) { auto p=Actor(L);p->SetMaxHealth(Number(L,2,1,100000000));p->SetFullHealth(); if(lua_gettop(L)>=3){auto power=Number(L,3,0,100000000);p->SetMaxPower(p->GetPowerType(),power);p->SetPower(p->GetPowerType(),power);}return 0; } int Stat(lua_State* L) { auto p=Actor(L);auto stat=Number(L,2,0,4);p->GetStats().SetBaseStatValue(StatType(stat),int32(Number(L,3,0,1000000)));return 0; } int Learn(lua_State* L){auto p=Actor(L);auto id=Number(L,2);if(!sSpellMgr->GetSpellInfo(id))return luaL_error(L,"unknown spell");p->LearnSpell(id,false);return 0;} int Item(lua_State* L){auto p=Actor(L);lua_pushboolean(L,p->StoreNewItemInBestSlots(Number(L,2),Number(L,3,1,1000)));return 1;} int Equip(lua_State* L) { auto p=Actor(L);auto slot=uint8(Number(L,2,0,22));uint16 dest=0;auto item=Number(L,3); if(p->GetItemByPos(INVENTORY_SLOT_BAG_0,slot))return luaL_error(L,"equipment slot occupied; clear it explicitly first"); lua_pushboolean(L,p->CanEquipNewItem(slot,dest,item,false)==EQUIP_ERR_OK && p->EquipNewItem(dest,item,true));return 1; } int ClearSlot(lua_State* L){auto p=Actor(L);p->DestroyItem(INVENTORY_SLOT_BAG_0,uint8(Number(L,2,0,22)),true);return 0;} int RemoveItem(lua_State* L){auto p=Actor(L);p->DestroyItemCount(Number(L,2),Number(L,3,1,10000),true);return 0;} int Skill(lua_State* L) { auto p=Actor(L);auto skill=Number(L,2,1,65535),value=Number(L,3,0,600),maximum=Number(L,4,0,600); if(!sSkillLineStore.LookupEntry(skill) || value>maximum)return luaL_error(L,"invalid skill or skill value exceeds maximum"); p->SetSkill(uint16(skill),p->GetSkillStep(uint16(skill)),uint16(value),uint16(maximum));lua_pushboolean(L,p->GetPureSkillValue(skill)==value);return 1; } int HasAura(lua_State* L){auto p=Actor(L);lua_pushboolean(L,p->HasAura(Number(L,2)));return 1;} int RemoveAura(lua_State* L){auto p=Actor(L);p->RemoveAurasDueToSpell(Number(L,2));return 0;} int Count(lua_State* L){auto p=Actor(L);lua_pushinteger(L,p->GetItemCount(Number(L,2),false));return 1;} int Quest(lua_State* L){auto p=Actor(L);auto id=Number(L,2);lua_pushinteger(L,p->GetQuestStatus(id));lua_pushboolean(L,p->GetQuestRewardStatus(id));return 2;} int Find(lua_State* L) { auto p=Actor(L);auto spawn=Number(L,2);bool object=lua_toboolean(L,lua_upvalueindex(2)); ObjectGuid found; if(object){auto const& store=p->GetMap()->GetGameObjectBySpawnIdStore();auto range=store.equal_range(spawn);for(auto i=range.first;i!=range.second;++i)if(p->CanSeeOrDetect(i->second)){found=i->second->GetGUID();break;}} else {auto const& store=p->GetMap()->GetCreatureBySpawnIdStore();auto range=store.equal_range(spawn);for(auto i=range.first;i!=range.second;++i)if(p->CanSeeOrDetect(i->second)){found=i->second->GetGUID();break;}} if(found)lua_pushstring(L,std::to_string(found.GetRawValue()).c_str());else lua_pushnil(L);return 1; } int Nearest(lua_State* L) { auto p=Actor(L);auto entry=Number(L,2);auto radius=luaL_optnumber(L,3,40); luaL_argcheck(L,std::isfinite(radius) && radius>0 && radius<=100,3,"radius must be 0..100 yards"); WorldObject* object=lua_toboolean(L,lua_upvalueindex(2))?static_cast(p->FindNearestGameObject(entry,float(radius))):static_cast(p->FindNearestCreature(entry,float(radius),true)); if(object && p->CanSeeOrDetect(object))lua_pushstring(L,std::to_string(object->GetGUID().GetRawValue()).c_str());else lua_pushnil(L);return 1; } int NpcHello(lua_State* L) { auto p=Actor(L);WorldPackets::NPC::Hello packet{WorldPacket(CMSG_GOSSIP_HELLO)};packet.Unit=Target(L);p->GetSession()->HandleGossipHelloOpcode(packet);return 0; } int Menu(lua_State* L) { auto p=Actor(L);auto& menu=p->PlayerTalkClass->GetGossipMenu();lua_newtable(L); lua_pushinteger(L,menu.GetMenuId());lua_setfield(L,-2,"id");lua_pushstring(L,std::to_string(p->PlayerTalkClass->GetInteractionData().SourceGuid.GetRawValue()).c_str());lua_setfield(L,-2,"source_guid");lua_newtable(L);unsigned index=1; for(auto const& option:menu.GetMenuItems()) { lua_newtable(L);lua_pushinteger(L,option.first);lua_setfield(L,-2,"id");lua_pushstring(L,option.second.Message.c_str());lua_setfield(L,-2,"text"); lua_pushboolean(L,option.second.IsCoded);lua_setfield(L,-2,"coded");lua_pushinteger(L,option.second.OptionType);lua_setfield(L,-2,"action");lua_rawseti(L,-2,index++); } lua_setfield(L,-2,"options");return 1; } int SpellClick(lua_State* L){auto p=Actor(L);WorldPacket packet(CMSG_SPELLCLICK);packet<GetSession()->HandleSpellClick(packet);return 0;} int ExitVehicle(lua_State* L){auto p=Actor(L);WorldPacket packet(CMSG_REQUEST_VEHICLE_EXIT);p->GetSession()->HandleRequestVehicleExit(packet);return 0;} int RequestCast(lua_State* L) { auto p=Actor(L);WorldPackets::Spells::CastSpell packet{WorldPacket(CMSG_CAST_SPELL)};packet.Cast.SpellID=int32(Number(L,2)); auto target=lua_gettop(L)>=3?Target(L,3):p->GetGUID();packet.Cast.Target.Flags=target.IsGameObject()?TARGET_FLAG_GAMEOBJECT:TARGET_FLAG_UNIT;packet.Cast.Target.Unit=target;p->GetSession()->HandleCastSpellOpcode(packet);return 0; } int RequestCastAt(lua_State* L) { auto p=Actor(L);WorldPackets::Spells::CastSpell packet{WorldPacket(CMSG_CAST_SPELL)};packet.Cast.SpellID=int32(Number(L,2)); WorldPackets::Spells::TargetLocation location;location.Location.Relocate(Coordinate(L,3),Coordinate(L,4),Coordinate(L,5)); packet.Cast.Target.Flags=TARGET_FLAG_DEST_LOCATION;packet.Cast.Target.DstLocation=location;p->GetSession()->HandleCastSpellOpcode(packet);return 0; } int UseItemAt(lua_State* L) { auto p=Actor(L);auto bag=uint8(Number(L,2,0,255)),slot=uint8(Number(L,3,0,255));auto item=p->GetItemByPos(bag,slot);if(!item)return luaL_error(L,"item slot empty"); WorldPackets::Spells::UseItem packet{WorldPacket(CMSG_USE_ITEM)};packet.PackSlot=bag;packet.Slot=slot;packet.CastItem=item->GetGUID();packet.Cast.SpellID=int32(Number(L,4)); WorldPackets::Spells::TargetLocation location;location.Location.Relocate(Coordinate(L,5),Coordinate(L,6),Coordinate(L,7)); packet.Cast.Target.Flags=TARGET_FLAG_DEST_LOCATION;packet.Cast.Target.DstLocation=location;p->GetSession()->HandleUseItemOpcode(packet);return 0; } int AreaTrigger(lua_State* L){auto p=Actor(L);WorldPacket packet(CMSG_AREATRIGGER);packet<GetSession()->HandleAreaTriggerOpcode(packet);return 0;} int Inventory(lua_State* L) { auto p=Actor(L);lua_newtable(L);unsigned index=1; auto add=[&](uint8 bag,uint8 slot) { auto item=p->GetItemByPos(bag,slot);if(!item)return; lua_newtable(L);lua_pushinteger(L,bag);lua_setfield(L,-2,"bag");lua_pushinteger(L,slot);lua_setfield(L,-2,"slot"); lua_pushinteger(L,item->GetEntry());lua_setfield(L,-2,"item");lua_pushinteger(L,item->GetCount());lua_setfield(L,-2,"count");lua_pushstring(L,std::to_string(item->GetGUID().GetRawValue()).c_str());lua_setfield(L,-2,"guid"); lua_newtable(L);unsigned effectIndex=1; for(auto const& effect:item->GetTemplate()->Effects)if(effect.SpellID) { lua_newtable(L);lua_pushinteger(L,effect.SpellID);lua_setfield(L,-2,"id");lua_pushinteger(L,effect.Trigger);lua_setfield(L,-2,"trigger");lua_rawseti(L,-2,effectIndex++); } lua_setfield(L,-2,"spells");lua_rawseti(L,-2,index++); }; for(uint8 slot=0;slotGetItemByPos(INVENTORY_SLOT_BAG_0,bag))if(auto container=item->ToBag())for(uint8 slot=0;slotGetBagSize();++slot)add(bag,slot); return 1; } int Use(lua_State* L){auto p=Actor(L);WorldPacket packet(CMSG_GAMEOBJ_USE,8);packet<GetSession()->HandleGameObjectUseOpcode(packet);return 0;} int Hello(lua_State* L){auto p=Actor(L);WorldPackets::Quest::QuestGiverHello packet{WorldPacket(CMSG_QUEST_GIVER_HELLO)};packet.QuestGiverGUID=Target(L);p->GetSession()->HandleQuestgiverHelloOpcode(packet);return 0;} int Accept(lua_State* L){auto p=Actor(L);WorldPackets::Quest::QuestGiverAcceptQuest packet{WorldPacket(CMSG_QUEST_GIVER_ACCEPT_QUEST)};packet.QuestGiverGUID=Target(L);packet.QuestID=Number(L,3);p->GetSession()->HandleQuestgiverAcceptQuestOpcode(packet);return 0;} int Abandon(lua_State* L) { auto p=Actor(L);auto slot=p->FindQuestSlot(Number(L,2));if(slot>=MAX_QUEST_LOG_SIZE)return luaL_error(L,"quest not in log"); WorldPackets::Quest::QuestLogRemoveQuest packet{WorldPacket(CMSG_QUEST_LOG_REMOVE_QUEST)};packet.Entry=uint8(slot);p->GetSession()->HandleQuestLogRemoveQuest(packet);return 0; } int Reward(lua_State* L){auto p=Actor(L);WorldPackets::Quest::QuestGiverChooseReward packet{WorldPacket(CMSG_QUEST_GIVER_CHOOSE_REWARD)};packet.QuestGiverGUID=Target(L);packet.QuestID=int32(Number(L,3));packet.ItemChoiceID=int32(Number(L,4,0,5));p->GetSession()->HandleQuestgiverChooseRewardOpcode(packet);return 0;} int Gossip(lua_State* L) { auto p=Actor(L);auto guid=Target(L);auto menu=Number(L,3,0),option=Number(L,4,0);WorldPacket packet(CMSG_GOSSIP_SELECT_OPTION);packet<PlayerTalkClass->IsGossipOptionCoded(option))packet<GetSession()->HandleGossipSelectOptionOpcode(packet);return 0; } int Cast(lua_State* L) { auto p=Actor(L);auto id=Number(L,2);auto target=lua_gettop(L)>=3?ObjectAccessor::GetUnit(*p,Target(L,3)):p; if(!p->HasSpell(id)||!target)return luaL_error(L,"spell not learned or target not loaded"); lua_pushinteger(L,p->CastSpell(target,id,false));return 1; } int TargetState(lua_State* L) { auto p=Actor(L);auto target=ObjectAccessor::GetUnit(*p,Target(L));if(!target){lua_pushnil(L);return 1;} lua_newtable(L);lua_pushinteger(L,target->GetMaxHealth());lua_setfield(L,-2,"max_health");lua_pushinteger(L,target->getLevel());lua_setfield(L,-2,"level");lua_pushinteger(L,target->GetEntry());lua_setfield(L,-2,"entry");lua_pushinteger(L,target->GetHealth());lua_setfield(L,-2,"health");lua_pushboolean(L,target->IsAlive());lua_setfield(L,-2,"alive");lua_pushnumber(L,p->GetDistance(target));lua_setfield(L,-2,"distance"); lua_pushnumber(L,target->GetPositionX());lua_setfield(L,-2,"x");lua_pushnumber(L,target->GetPositionY());lua_setfield(L,-2,"y");lua_pushnumber(L,target->GetPositionZ());lua_setfield(L,-2,"z");return 1; } int Objectives(lua_State* L) { auto p=Actor(L);auto slot=p->FindQuestSlot(Number(L,2));lua_newtable(L); if(slotGetQuestSlotCounter(slot,i));lua_rawseti(L,-2,i+1);} return 1; } int RequestReward(lua_State* L) { auto p=Actor(L);WorldPackets::Quest::QuestGiverCompleteQuest packet{WorldPacket(CMSG_QUEST_GIVER_COMPLETE_QUEST)}; packet.QuestGiverGUID=Target(L);packet.QuestID=int32(Number(L,3));p->GetSession()->HandleQuestgiverCompleteQuest(packet);return 0; } int Loot(lua_State* L){auto p=Actor(L);WorldPacket packet(CMSG_LOOT);packet<GetSession()->HandleLootOpcode(packet);return 0;} int LootItem(lua_State* L){auto p=Actor(L);WorldPacket packet(CMSG_AUTOSTORE_LOOT_ITEM);packet<GetSession()->HandleAutostoreLootItemOpcode(packet);return 0;} int LootRelease(lua_State* L){auto p=Actor(L);WorldPacket packet(CMSG_LOOT_RELEASE);packet<GetSession()->HandleLootReleaseOpcode(packet);return 0;} int UseItem(lua_State* L) { auto p=Actor(L);auto bag=uint8(Number(L,2,0,255)),slot=uint8(Number(L,3,0,255));auto item=p->GetItemByPos(bag,slot);if(!item)return luaL_error(L,"item slot empty"); WorldPackets::Spells::UseItem packet{WorldPacket(CMSG_USE_ITEM)};packet.PackSlot=bag;packet.Slot=slot;packet.CastItem=item->GetGUID();packet.Cast.SpellID=int32(Number(L,4,0)); auto target=lua_gettop(L)>=5?Target(L,5):p->GetGUID();packet.Cast.Target.Flags=target.IsGameObject()?TARGET_FLAG_GAMEOBJECT:TARGET_FLAG_UNIT;packet.Cast.Target.Unit=target;p->GetSession()->HandleUseItemOpcode(packet);return 0; } int Move(lua_State* L) { auto p=Actor(L);auto x=Coordinate(L,2),y=Coordinate(L,3),z=Coordinate(L,4); // Use native local navigation directly: no Adventure area/travel policy. PathGenerator path(p);bool valid=path.CalculatePath(x,y,z) && (path.GetPathType()&PATHFIND_NORMAL); if(valid)p->GetMotionMaster()->MovePoint(0,x,y,z,true); lua_pushboolean(L,valid);return 1; } int Stop(lua_State* L){auto p=Actor(L);p->AttackStop();p->GetMotionMaster()->Clear();p->StopMoving();return 0;} int Now(lua_State* L){lua_pushinteger(L,std::chrono::duration_cast(Clock::now()-Context(L).Started).count());return 1;} int Face(lua_State* L){auto p=Actor(L);auto target=ObjectAccessor::GetUnit(*p,Target(L));if(!target)return luaL_error(L,"target not loaded");p->SetFacingToObject(target);return 0;} int Attack(lua_State* L) { auto p=Actor(L);WorldPackets::Combat::AttackSwing packet{WorldPacket(CMSG_ATTACK_SWING)};packet.Victim=Target(L);p->GetSession()->HandleAttackSwingOpcode(packet); lua_pushboolean(L,p->GetVictim() && p->GetVictim()->GetGUID()==packet.Victim);return 1; } std::string Json(lua_State* L,int i,unsigned depth=0) { if(depth>5)return "null"; switch(lua_type(L,i)) { case LUA_TBOOLEAN:return lua_toboolean(L,i)?"true":"false"; case LUA_TNUMBER:{auto n=lua_tonumber(L,i);if(!std::isfinite(n))return "null";std::ostringstream o;o.precision(16);o<=100 || out.size()>=8192){lua_pop(L,2);break;} std::string key; if(lua_type(L,-2)==LUA_TSTRING)key=std::string(lua_tostring(L,-2)).substr(0,512); else if(lua_isinteger(L,-2))key=std::to_string(lua_tointeger(L,-2)); if(!key.empty()){++count;if(out.size()>1)out+=',';out+=Quote(key)+":"+Json(L,-1,depth+1);} lua_pop(L,1); }return out+"}";} default:return "null"; } } int Record(lua_State* L) { auto& r=Context(L);if(r.Events.size()>=1000)return luaL_error(L,"event limit exceeded"); auto label=std::string(luaL_checkstring(L,1)).substr(0,512);r.Events.push_back("{\"label\":"+Quote(label)+",\"value\":"+Json(L,2)+"}");return 0; } int Check(lua_State* L) { auto& r=Context(L);++r.Checks;bool ok=lua_toboolean(L,1);if(!ok)++r.Failures; if(r.Events.size()>=1000)return luaL_error(L,"event limit exceeded"); r.Events.push_back("{\"check\":"+Quote(luaL_checkstring(L,2))+",\"passed\":"+(ok?"true":"false")+"}");lua_pushboolean(L,ok);return 1; } void* Allocate(void* user,void* ptr,size_t oldSize,size_t newSize) { auto& run=*static_cast(user);if(!ptr)oldSize=0; if(!newSize){std::free(ptr);run.Memory-=oldSize;return nullptr;} if(newSize>8*1024*1024 || run.Memory-oldSize>8*1024*1024-newSize)return nullptr; void* next=std::realloc(ptr,newSize);if(next)run.Memory=run.Memory-oldSize+newSize;return next; } void Hook(lua_State* L,lua_Debug*){luaL_error(L,"Lua instruction budget exceeded; yield with testbot.wait/await");} void Bind(lua_State* L,Run* r,char const* name,lua_CFunction fn,int flag=0) { lua_pushlightuserdata(L,r);lua_pushboolean(L,flag);lua_pushcclosure(L,fn,2);lua_setfield(L,-2,name); } bool Start(std::string const& id,fs::path path,std::string& error) { if(!accepting){error="test job admission paused";return false;} if(!Key(id)){error="invalid run id";return false;} if(runs.size()>=128)for(auto it=runs.begin();it!=runs.end();) {if(it->second->State!="running")it=runs.erase(it);else ++it;} if(runs.count(id)||fs::exists(Root()/"results"/(id+".json"))){error="run id already exists";return false;} for(auto const& entry:runs)if(entry.second->State=="running"){error="another test is running";return false;} if(!fs::exists(path)||fs::file_size(path)>1024*1024){error="script missing or exceeds 1MiB";return false;} auto r=std::make_shared();r->Id=id;r->TimeoutSeconds=uint32(std::clamp(sConfigMgr->GetIntDefault("Clientless.TestBot.TimeoutSeconds",300),1,86400));r->Lua=lua_newstate(Allocate,r.get());if(!r->Lua){error="Lua allocation failed";return false;} auto L=r->Lua;luaL_openlibs(L); for(auto name:{"io","os","package","debug","dofile","loadfile","require","load","collectgarbage","coroutine","pcall","xpcall"}){lua_pushnil(L);lua_setglobal(L,name);} lua_newtable(L); for(auto const& fn:std::initializer_list>{ {"npc_hello",NpcHello},{"menu",Menu},{"spellclick",SpellClick},{"exit_vehicle",ExitVehicle},{"request_cast",RequestCast},{"request_cast_at",RequestCastAt},{"use_item_at",UseItemAt},{"area_trigger",AreaTrigger},{"inventory",Inventory},{"stop",Stop},{"now_ms",Now},{"spawn",Spawn},{"online",Online},{"_create",Create},{"_create_result",CreateResult},{"wait",Wait},{"state",Snapshot},{"teleport",Teleport},{"level",Level},{"stats",Stats},{"stat",Stat},{"learn",Learn},{"item",Item},{"equip",Equip},{"clear_slot",ClearSlot},{"remove_item",RemoveItem},{"skill",Skill},{"has_aura",HasAura},{"remove_aura",RemoveAura},{"item_count",Count},{"quest",Quest},{"use",Use},{"hello",Hello},{"accept",Accept},{"abandon",Abandon},{"reward",Reward},{"gossip",Gossip},{"cast",Cast},{"move",Move},{"face",Face},{"attack",Attack},{"target_state",TargetState},{"objectives",Objectives},{"request_reward",RequestReward},{"loot",Loot},{"loot_item",LootItem},{"loot_release",LootRelease},{"use_item",UseItem},{"record",Record},{"check",Check}})Bind(L,r.get(),fn.first,fn.second); Bind(L,r.get(),"nearest_creature",Nearest);Bind(L,r.get(),"nearest_object",Nearest,1); Bind(L,r.get(),"creature",Find);Bind(L,r.get(),"object",Find,1);lua_setglobal(L,"testbot"); char const* helpers=R"lua( function testbot.await(predicate, timeout_ms, label) local deadline=testbot.now_ms()+(timeout_ms or 10000) while not predicate() do assert(testbot.now_ms()Thread=lua_newthread(L);if(luaL_loadfile(r->Thread,path.string().c_str())!=LUA_OK){error=lua_tostring(r->Thread,-1);return false;} runs[id]=r;Publish(*r);return true; } } ClientlessTestBotMgr* ClientlessTestBotMgr::instance(){static ClientlessTestBotMgr m;return &m;} void ClientlessTestBotMgr::Update(uint32 diff) { try { if(!sConfigMgr->GetBoolDefault("Clientless.TestBot.Enabled",false)) {for(auto const& e:runs)if(e.second->State=="running")Finish(*e.second,"cancelled","test controller disabled");return;} static uint32 poll=0;poll+=diff; if(poll>=1000) { poll=0;for(auto dir:{"requests","scripts","results","registry","archive"})fs::create_directories(Root()/dir); // A claimed request is never replayed after a worldserver restart. static bool recovered=false;if(!recovered) { recovered=true; for(auto const& e:fs::directory_iterator(Root()/"results"))if(e.path().extension()==".json" && Key(e.path().stem().string())) { std::ifstream input(e.path());std::string previous((std::istreambuf_iterator(input)),{}); if(previous.find("\"state\":\"running\"")!=std::string::npos) {Run r;r.Id=e.path().stem().string();r.Events.push_back("{\"previous_result\":"+Quote(previous)+"}");Finish(r,"interrupted","worldserver restarted during test");} } for(auto const& e:fs::directory_iterator(Root()/"archive"))if(e.path().extension()==".lua") {auto id=e.path().stem().string();if(Key(id)&&!fs::exists(Root()/"results"/(id+".json"))){Run r;r.Id=id;Finish(r,"interrupted","worldserver restarted before result publication");}} } bool busy=false;for(auto const& e:runs)busy|=e.second->State=="running"; if(accepting && !busy)for(auto const& e:fs::directory_iterator(Root()/"requests"))if(e.is_regular_file() && e.path().extension()==".lua" && Key(e.path().stem().string())) { auto id=e.path().stem().string();auto archive=Root()/"archive"/e.path().filename(); if(fs::exists(archive)||fs::exists(Root()/"results"/(id+".json")))continue; fs::rename(e.path(),archive);std::string error;if(!Start(id,archive,error)){Run r;r.Id=id;Finish(r,"failed",error);}break; } } for(auto const& entry:runs) { auto& r=*entry.second;if(r.State!="running")continue; auto now=Clock::now();if(now-r.Started>std::chrono::seconds(r.TimeoutSeconds)){Finish(r,"timed_out","configured run deadline exceeded");continue;} if(nowGetBoolDefault("Clientless.TestBot.Enabled",false)){h->SendSysMessage("Testbot controller disabled (Clientless.TestBot.Enabled).");return true;} std::istringstream in(args?args:"");std::string op,id,script,extra;in>>op>>id; if(op=="pause" || op=="resume"){accepting=op=="resume";h->PSendSysMessage("Testbot job admission %s; active runs remain controlled separately.",accepting?"resumed":"paused");return true;} if(op=="run" && (in>>script) && !(in>>extra) && Key(script)) {std::string error;bool ok=Start(id,Root()/"scripts"/(script+".lua"),error);h->PSendSysMessage("Testbot run %s: %s",id.c_str(),ok?"accepted":error.c_str());return true;} if(op=="submit" && Key(id) && (in>>script) && !(in>>extra)) { if(script.size()>87384){h->SendSysMessage("Encoded script exceeds 64KiB payload limit.");return true;} auto decoded=Trinity::Encoding::Base64::Decode(script); if(!decoded || decoded->size()>65536 || decoded->empty()){h->SendSysMessage("Invalid base64 or script exceeds 64KiB.");return true;} if(runs.count(id)||fs::exists(Root()/"requests"/(id+".lua"))||fs::exists(Root()/"archive"/(id+".lua"))||fs::exists(Root()/"results"/(id+".json"))) {h->SendSysMessage("Run id already exists.");return true;} if(!Write(Root()/"requests"/(id+".lua"),std::string(decoded->begin(),decoded->end()))) {h->SendSysMessage("Cannot persist test request.");return true;} h->PSendSysMessage("Testbot run %s: queued",id.c_str());return true; } if(op=="result" && Key(id)) { std::ifstream file(Root()/"results"/(id+".json")); if(!file){h->SendSysMessage("Testbot result not available yet.");return true;} std::string text((std::istreambuf_iterator(file)),{});h->SendSysMessage(text.c_str());return true; } auto it=runs.find(id); if(op=="cancel" && it!=runs.end() && it->second->State=="running"){Finish(*it->second,"cancelled","cancelled by operator");return true;} if(op=="status" && it!=runs.end()){auto const& r=*it->second;h->PSendSysMessage("Testbot run %s: state=%s checks=%u failures=%u error='%s'",id.c_str(),r.State.c_str(),r.Checks,r.Failures,r.Error.c_str());return true;} h->SendSysMessage("Usage: clientless test pause | resume | run | status | cancel | submit | result . Requests: /requests/.lua; results: results/.json");return true; } catch(std::exception const& e){h->PSendSysMessage("Testbot command failed: %s",e.what());return true;} }