Keep queued adventure bots active and preserve browser admission rules

Reuse formation performances while waiting for dungeon, raid or PvP admission, and accept compatible formation invitations without active recruitment or interrupting battleground offers.

Require the full low-population wait for every participating queue entry and acknowledge connected clientless members during native group teleportation.

Add regression coverage and document builds, live Yarumak/Seraellin checks and separately preserved shutdown crash findings.
This commit is contained in:
Nico committed 2026-11-04 14:19:38 +01:00
1 parent d1aceaf583
commit aba5aeba2c
15 files changed
+517 -76

No files matched your search

+76
View File
@@ -0,0 +1,76 @@
# Dungeonbrowser: Warteaktivitäten, Einladungen und Gruppenbeitritt
Bots im Dungeon-/Raidintent führen während der Gruppenwartephase in der
Hauptstadt die lokalen performativen Aktionen der Gruppenbildung aus.
Aktionsauswahl und lokale Umgebung sind vom tatsächlichen Instanzziel getrennt;
Intent, Tagesplan und Dungeonziel werden dafür nicht umgeschrieben. Die
bestehenden Szenen-/Spaziergangsexecutoren werden wiederverwendet. Ein nativer
Instanztransfer beendet die alte Stadtaktivität.
Ein solo in der Hauptstadt wartender und nativ eingereihter Bot rekrutiert
nicht selbst. Ein Bot in der Gruppenbildung kann ihn für den passenden
Folgeintent einladen. Annahme prüft weiter Fraktion, Level, Instanzzugang,
Rollenbedarf und Kapazität. Eine passende solche Einladung hängt nicht erneut
vom Geselligkeitswurf ab; die persönliche Browserqueue wird vor der nativen
Annahme verlassen. Laufende Vorschläge werden nicht durch diesen Sonderfall
abgebrochen.
Der Low-Pop-Fallback prüft nun die Wartezeit aller beteiligten Queueeinträge.
Ein alter Botqueueeintrag verkürzt die 60 Sekunden eines frisch eingereihten
Spielers oder einer Gruppe nicht. Vollständige normale Gruppen behalten den
normalen sofortigen Matchpfad. Browserbeitritt prüft weiterhin native
Zugangsbeschränkungen und Clientless-Zulassung.
Die native Vorschlagsportierung verwendet verbundene Spieler auch während
eines kurzzeitigen Out-of-World-Transfers und bestätigt Clientless-Teleports
unmittelbar. Clientlose Mitglieder werden vor den vom echten Client zu
bestätigenden Menschen portiert; menschliche Bestätigungen bleiben erhalten.
Dies ist eine gezielte Absicherung des Übergangs; die vom Nutzer beobachtete
konkrete menschliche Browsergruppe wurde nicht nachträglich rekonstruiert.
Regressionen: native Low-Pop-Frist mit altem Bot/neuem Spieler bei 0/59/60s,
Native-Teleportbatch einschließlich Out-of-World/Offline-Fällen, performative
Aktionsauswahl bei unverändertem Dungeonintent, passive Wartebots und Annahme
kompatibler Formationseinladungen einschließlich Level-/Zugang-/Intentablehnung.
Bestehende Gruppenmanager-, Browser-, Interaktions-, Mikro-, Freizeit- und
Sozialtests wurden zusätzlich ausgeführt. Live-Abnahme eines echten
Spieler-Browserbeitritts bleibt erforderlich.
Artefakte: checkpoints/2026-11-04-browser-wait/.
## Live-Stand
Installiert, frisches Ready und Ports8085/8087 geprüft. Alle ursprünglichen118
Bots online, Loader wieder aktiv, keine Logoutanforderungen ausstehend.
Yarumak44 bleibt in Phase WaitingGroup mit intent instance und Zielat_4363;
LEISURE_WALK_PLAN mit70.11ydSpannweite und8Wegpunkten sowie MICRO_START
für group_search_walk live belegt. Kein Intent-/Zielwechsel für Aktivitäten.
Beim Abschalten der vorherigen Version trat nach automatischem60s-Restbot-
Audit eine andere Assertion auf: Unit299 noch in Owner-ControlledSet,
Unit::RemoveFromWorld9519, Pet::RemoveFromWorld, Map::~Map. Dump und passende
alte Binärdatei gesichert; kein Unit/Pet-Code für diesen Befund geändert.
Der Testserver wurde nach vollständigem Prozessende installiert und unter
passivem GDB wieder gestartet. Dieser Shutdownbefund ist noch nicht behoben.
## Erweiterung auf PvP
Die performativen Warteaktivitäten gelten auch für PvP-Intents in der
Hauptstadt. Solo in der Schlachtfeldwarteschlange wartende Bots können passende
Einladungen aus einer PvP-Gruppenbildung annehmen, ohne selbst zu rekrutieren.
Native Fraktions-, Level-/Bracket- und Zugangsprüfungen bleiben wirksam.
Ein bereits vorliegendes Schlachtfeldangebot wird nicht für eine solche
Einladung abgebrochen. Beim Schlachtfeldeintritt wird die alte Stadtaktivität
beendet. QueueEnabled/JoinEnabled werden dadurch nicht geändert;
Battleground.JoinEnabled bleibt in dieser Testkonfiguration 0.
Acht betroffene Regressionstests bestanden, einschließlich passiver PvP-
Kandidatenwahl, Annahme kompatibler Einladungen, Schutz aktiver BG-Angebote,
PvP-Aktionsauswahl aus dem Gruppenbildungskatalog und nativer BG-Brackets.
Artefakte: checkpoints/2026-11-04-pvp-wait/.
PvP-Erweiterung installiert und live bestätigt: Seraellin79 führt bei weiterhin
aktivem PvP-Intent die performative Aktion profession_tools aus. Server und
118Bots wieder aktiv. Beim vorherigen Shutdown trat nach60sAudit ein separater
Transportdestruktor-Assert (_passengers.empty(), Transport.cpp94) auf; Dump und
passendes altes Binary gesichert. Dieser Shutdownfehler ist noch offen.
+132
View File
@@ -1,3 +1,135 @@
# 2026-11-04 – Lokaler Commit der Browser- und PvP-Erweiterungen
Nutzer hat den Commit der letzten Änderungen angefordert. Umfang: performative
Warteaktivitäten für Dungeon/Raid/PvP, kompatible passive Gruppeneinladungen,
Low-Pop-Frist für alle beteiligten Queueeinträge und Clientless-Gruppenportierung,
samt Tests und Dokumentation. Bereits gebaut, installiert und wie unten
beschrieben geprüft. Die beiden offenen Shutdownbefunde bleiben dokumentiert;
für sie wurde in diesem Änderungssatz kein Fix implementiert. Dumps, gesicherte
Binärdateien und Laufzeitlogs bleiben außerhalb des Commits. Kein Push.
Die vorherigen Hinweise „kein Commit angefordert“ sind damit überholt.
---
# 2026-11-04 14:16 – PvP-Erweiterung installiert, Seraellin live geprüft
PvP-Wartephase nutzt nun ebenfalls performative Gruppenbildungsaktionen.
Passivwartende soloBG-Kandidaten können passende Formationseinladungen
annehmen, aktive BG-Angebote werden dabei nicht abgebrochen. Bei BG-Eintritt
alteStadtaktivitätresetten; Queue-/JoinConfigunverändert. AchtbetroffeneTests
PASS, -j10Build/Install erfolgreich. FrischesReady+Ports8085/8087 geprüft,
jetztPID187536 unterpassivemGDB. AlleOriginal118Bots online, LoaderDatei+nativ1,
shutdownPaused0, safeLogoutpending0. KeinindividuellesRecoverychecking/Teleport.
Seraellin79 liveintentpvp/PhaseWaitingGroup erhalten, MICRO_START profession_tools
undSocialscene imLog. Battleground.JoinEnabledweiter0; nichtfreigeschaltet.
Testartefakte checkpoints/2026-11-04-pvp-wait/ mit8Tests,build/install,
seraellin-live.log,native-final.log,result.json,operation.json,roster-after.json.
KeinCommit/Pushangefordert; vorherigesBrowser-WIPbewahrt.
WICHTIGneuerShutdownbefund: nachautomatischem60sAudit (16Restbots,60265ms)
SIGSEGVviaAssert Transport::~Transport Transport.cpp94 _passengers.empty().
NichtvorschnellmitvorigemPetassertgleichsetzen. Core20261104-135325 pluspassendes
Binary checkpoints/2026-11-04-pvp-wait/worldserver-before-pvp-wait bewahrt.
GDB/alterWorld184307vollständigbeendet VORInstall. Userinformiert; Transport-
Cleanup-Code NICHTgeändert. DieserBefundUNDvorigerUnit299/PetControlledSet-
ShutdownfehlerweiterOFFEN. AktuellerServerläuft, letztePvPänderungwarnichtim
abgestürztenProzessenthalten. NativeBacktracefileunterserver/debug/crash/
worldserver-20261104-135325.backtrace.log, keinevollenConnInfosausgeben.
KeineweitereaktiveBeobachtungnötig; echteGruppeneinladung/BG-PortweiterIngame.
---
# 2026-11-04 14:10 – PvP-Warteaktivitäten in Umsetzung
User beauftragt Erweiterung des letzten Browser-Wartefixes auf PvP. Bekannte
Browser-WIP bewahrt (nichtcommitted). WaitingGroup-Performanz fürpvp ergänzt,
kompatible Formationseinladungen an solo BG-Wartende erlaubt; aktive BG-Angebote
nicht unterbrechen, native Bracket-/Zugangregeln bleiben. Cityaktivität bei
BG-Eintritt beenden. Battleground.JoinEnabled bleibt0, nichtfreischalten.
Zwei gezielte Tests bereitsPASS (PvP-Selektor, passiveAnnahme/aktivesAngebot).
Loadervorher1/118Bots, beiBuildbeginnmanuell0 undsafeall; -j10Buildparallel.
Erst nachBuildende normalShutdown10 (automatisches60sAudit), dannProzessende,
Install/passiverGDB-Neustart/Ready/Ports, LoaderundOriginalbetriebwiederherstellen.
KeineeinzelnenRestbotprüfungen. BekannterPet-ShutdowncrashnochOFFEN ausvoriger
Runde, matchingBinary/124032Corebewahrt. AktuelleArtefakte2026-11-04-pvp-wait/.
KeinCommit/Pushangefordert. Seraellin79 hatPvP14Uhrbis15Uhr, liveprüfenwennbereit.
---
# 2026-11-04 13:56 – Browser-Warteaktivitäten installiert, Yarumak live aktiv
Alle14betroffenenRegressionstestsPASS; finaler-j10Build/Install erfolgreich.
NeustartPID184307 unterpassivemGDB, frischesReady undPorts8085/8087geprüft.
Original118Bots online; Loader Datei+nativ1, shutdownPaused0, logoutpending0.
Userwunschumgesetzt: WaitingGroupinHauptstadt nutzt lokale Gruppenbildungs-
Aktionen bei unverändertem Dungeon-/Raidintent; kein eigenes Rekrutieren in
nativerQueue, kompatible Formationseinladungen werden unterCanInvite/Rollen/
Level/Zugang/Kapazität angenommen. PersönlicheQueue vorAnnahmebeenden.
Lowpop wartet volleFristfürALLE beteiligtenQueueeinträge, alterBotkannneuem
SpielerkeineFristverkürzen. NativeGruppenportierung überConnectedStore, Bots
zuerst/sofortigesACK, menschlicheClientbestätigungbewahrt.
Yarumak44 liveweiterinstanceat_4363/WaitingGroup, group_search_walk gestartet:
70.11ydStadtspaziergang/8Wegpunkte. KeinmanuellerIntent/TeleportzumTestnötig.
EchteSpielerbrowsergruppe/volleLowpop60sundkompatibleEinladunglive bleiben
Ingameabnahme; EntscheidungslogikdurchneueProduktionscodeTestsabgedeckt.
WICHTIG: vorInstallationalterServer172527 bei normalemShutdown nach60sAudit
(17Restbots) andererAbsturz: Unit299 inOwnerControlledSet, Unit.cpp9519ABORT;
StackPet::RemoveFromWorld -> Map::~Map. Core20261104-124032 pluspassendes
Binary checkpoints/2026-11-04-browser-wait/worldserver-before-browser-wait
bewahrt; Unit/Pet-Code NICHTgeändert, konkreterShutdownfehlernochOFFEN.
Nutzerinformiert. KeinZusammenhangmitBrowseränderungenbehaupten: alterProzess
liefnochohneaktuellenBrowserfix. SourceWIPpreserved,keinCommit/Pushangefordert.
Artefakte checkpoints/2026-11-04-browser-wait/{build*.log,install.log,
startup.log,native-final.log,yarumak-live.log,result.json,operation.json,
shutdown*.jsonl,shutdown-crash-frames.log,test*.log}. NeueTests:
browser_low_population_wait, browser_group_teleport, browser_wait_activities;
groups-TestumqueuedKandidaten/Annahme/Fremdintent-/Level-/Zugangablehnungergänzt,
Interaktions-/Freizeitadapterminimalangepasst. Doc2026-11-04-browser-wait.md.
KeineweiteraktiveBeobachtungnötig; finalerIngameBrowserbeitrittvomNutzer.
---
# 2026-11-04 13:53 – Browserfix gebaut, alter Server beim Shutdown abgebrochen
Build-final/installable -j10 erfolgreich, alle14betroffenenTestsPASS. Source
noch nicht installiert. NormalShutdown10 nach Buildende ausgelöst, integrierte
60s-Frist mit17Restbots/Audit ausgeführt. Danach anderer Shutdownfehler:
Unit299 noch in controlled list von Besitzerentry0, Unit::RemoveFromWorld9519
ABORT/SIGSEGV. Passiver GDB schreibt core worldserver-20261104-124032.core.
Passendes Binary checkpoints/2026-11-04-browser-wait/worldserver-before-browser-
wait bewahrt. Warten bis Prozess172527/GDB beendet, erst dann cmakeinstall.
Keinen Unit-Code/Assertion geändert. Nutzer informiert. Danach frischerpassiver
Start/Ready/Ports, Originalroster und Loader1 wiederherstellen; automatischer
Recovery vertrauen. Yarumak44 noch im13UhrDungeonintent liveprüfen (14Uhrbeginnt
Gruppenbildung, ggfvorherbeobachten). EchteSpielerbrowsergruppe bleibtIngame-
Abnahme. AktuellerBuild enthält Warteaktivitäten/Einladungen/LowpopalleQueue-
fristen/verbundeneBots+sofortigeTeleportACK. Kein Commit angefordert.
---
# 2026-11-04 – Browser-Warteaktivitäten und Dungeonbeitritt in Arbeit
Nutzer beauftragt performative Gruppenbildungsaktionen beim Dungeonbrowser-
Warten, ohne eigene Rekrutierung; kompatible Einladungen aus Gruppenbildung
mit folgendem Dungeonintent annehmen. Außerdem Low-Pop60s-Frist beachten und
Bots bei nativem Browserbeitritt vollständig mitteleportieren.
Source-WIP auf autorisiertem Branch, vorher tracked clean (d1aceaf583).
Änderungen: WaitingGroup lokale Aktionsumgebung/OnSite-Executor wiederverwenden;
Browserwartende Kandidaten akzeptieren passende Formationseinladung nach
CanInvite/Level/Rollen/Zugang, eigene Queue vor Annahme verlassen; Low-Pop
prüft Wartezeit aller Queueeinträge statt nur Seed; native Gruppenportierung
bestätigt CPC-Teleports sofort. Diagnose Yarumak44 zuvor WaitingGroup, Boden
korrekt, kein Travel. Neue Artefakte checkpoints/2026-11-04-browser-wait/.
Loader war1, nun0; sicherer Logout bei Buildbeginn angefordert, Build-j10
läuft parallel. Erst nach erfolgreichem Build normaler Shutdown10, dann
Prozessende, Install, passiver GDB-Neustart/Ready/Ports, Loader wieder1.
Keine separate5min-Frist/manuelle Restbotliste mehr. Tests/Live noch ausstehend.
Kein Commit/Push für aktuelle Änderung angefordert.
---
# 2026-11-04 – Lokaler Commit auf Nutzerwunsch
Nutzer hat nun den lokalen Commit angefordert. Zusammengefasst werden die
+16 -3
View File
@@ -1174,10 +1174,23 @@ void LFGMgr::MakeNewGroup(LfgProposal const& proposal)
// Update group info
grp->SendUpdate();
// Teleport Player
for (GuidList::const_iterator it = playersToTeleport.begin(); it != playersToTeleport.end(); ++it)
if (Player* player = ObjectAccessor::FindPlayer(*it))
// Complete clientless entries before waiting for human transfer acknowledgements.
// Resolve connected players even during the brief out-of-world transfer state.
GuidList botsToTeleport;
for (auto it = playersToTeleport.begin(); it != playersToTeleport.end(); )
{
auto current = it++;
if (ClientlessBrowserPolicy::Bot(ObjectAccessor::FindConnectedPlayer(*current)))
botsToTeleport.splice(botsToTeleport.end(), playersToTeleport, current);
}
playersToTeleport.splice(playersToTeleport.begin(), botsToTeleport);
for (auto guid : playersToTeleport)
if (Player* player = ObjectAccessor::FindConnectedPlayer(guid))
{
TeleportPlayer(player, false);
if (player->GetSession()->IsClientless() && player->IsBeingTeleported())
player->GetSession()->CompleteClientlessTeleport();
}
}
uint32 LFGMgr::AddProposal(LfgProposal& proposal)
+9 -1
View File
@@ -424,7 +424,15 @@ uint8 LFGQueue::FindLowPopulationGroups()
playerCount += uint32(itrData->second.roles.size());
}
if (playerCount < minPlayers)
// Every participating queue entry must have received the full
// fallback window, including a human joining long-waiting bots.
bool waited = std::all_of(check.begin(), check.end(), [&](ObjectGuid guid)
{
auto entry = QueueDataStore.find(guid);
return entry != QueueDataStore.end() && now >= entry->second.joinTime &&
uint64(now - entry->second.joinTime) >= fallbackDelay;
});
if (playerCount < minPlayers || !waited)
continue;
TC_LOG_DEBUG("lfg.queue.lowpopulation",
+90 -60
View File
@@ -503,6 +503,14 @@ void AdventureBehaviorMgr::Begin(Player * player, Record & r)
(!r.ExpeditionStarted && !sAdventureGroupMgr->Complete(player))))
{
r.State=Phase::WaitingGroup;r.Detail="waiting for a suitable group / native browser queue";
r.Anchor.Relocate(player);r.AnchorMap=player->GetMapId();r.WaitingLocation.clear();
float nearest=std::numeric_limits<float>::max();
for (auto const& site:sAdventureLocationMgr->Locations())
if (site.Point.MapId==r.AnchorMap && Allowed(player,site,"groupformation"))
{
float distance=player->GetExactDist(site.Point.X,site.Point.Y,site.Point.Z);
if (distance<nearest) {nearest=distance;r.WaitingLocation=site.Point.Id;}
}
sAdventureGroupMgr->QueueBrowser(player,r.Intent);
return;
}
@@ -624,7 +632,7 @@ void AdventureBehaviorMgr::ResetMicro(Player * player, Record & r)
bool AdventureBehaviorMgr::LocalMove(Player * player, Record & r, Position const & target, float maxPath, bool riding)
{
auto l = sAdventureLocationMgr->Find(r.Location);
auto l = sAdventureLocationMgr->Find(r.State==Phase::WaitingGroup?r.WaitingLocation:r.Location);
if (! l || player->GetMapId() != r.AnchorMap || r.Anchor.GetExactDist(target) > (r.Leisure.PlannedWalk ? 250.0f : (r.Intent=="economy" || r.Intent=="adventure" || r.Intent=="profession") ? 200.0f : r.Intent=="leisure" ? 120.0f : 60.0f)) return false;
uint32 const boundary=l->IsArea?l->AreaId:((r.Intent=="leisure" || r.Intent=="economy" || r.Intent=="adventure" || r.Intent=="profession")?l->ZoneId:0);
if (boundary)
@@ -701,10 +709,10 @@ bool AdventureBehaviorMgr::LeisureAvailable(Player* p,Record const& r,AdventureM
auto rod=p->GetWeaponForAttack(BASE_ATTACK,true);float direction=0;
return rod && rod->GetTemplate()->GetSubClass()==ITEM_SUBCLASS_WEAPON_FISHING_POLE && FishingDirection(p,direction);
}
if (r.Intent=="groupformation") return a.Requirement=="walk";
if ((r.Intent=="groupformation" || r.State==Phase::WaitingGroup)) return a.Requirement=="walk";
if (r.Intent!="leisure") return false;
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
auto location=sAdventureLocationMgr->Find(r.Location);
auto location=sAdventureLocationMgr->Find(r.State==Phase::WaitingGroup?r.WaitingLocation:r.Location);
if (!profile || !location) return false;
if (a.Requirement=="training") return sAdventureCombatMgr->TrainingAvailable(p);
if (a.Requirement=="fish")
@@ -726,23 +734,23 @@ bool AdventureBehaviorMgr::LeisureAvailable(Player* p,Record const& r,AdventureM
bool AdventureBehaviorMgr::PlanLeisureWalk(Player* p, Record& r)
{
auto& s=r.Leisure;
auto location=sAdventureLocationMgr->Find(r.Location);
auto location=sAdventureLocationMgr->Find(r.State==Phase::WaitingGroup?r.WaitingLocation:r.Location);
uint32 const boundary=location->IsArea?location->AreaId:location->ZoneId;
// Choose one substantial destination, then follow its actual ground corridor.
// Randomness selects the outing, never the next individual footstep.
float const offset=std::uniform_real_distribution<float>(0,6.2831853f)(r.Random);
float const budget=r.Intent=="groupformation"?220.0f:std::min(220.0f,std::max(100.0f,float(r.MicroTimer)*0.001f*1.0f));
float const budget=(r.Intent=="groupformation" || r.State==Phase::WaitingGroup)?220.0f:std::min(220.0f,std::max(100.0f,float(r.MicroTimer)*0.001f*1.0f));
float best=0;
for (unsigned i=0;i<16;++i)
{
float const angle=offset+float(i%8)*6.2831853f/8;
float const radius=r.Intent=="groupformation"?(i<8?110.0f:70.0f):(i<8?std::min(150.0f,budget*0.7f):80.0f);
float const radius=(r.Intent=="groupformation" || r.State==Phase::WaitingGroup)?(i<8?110.0f:70.0f):(i<8?std::min(150.0f,budget*0.7f):80.0f);
Position goal;
goal.Relocate(s.Origin.GetPositionX()+std::cos(angle)*radius,
s.Origin.GetPositionY()+std::sin(angle)*radius,s.Origin.GetPositionZ());
float const z=p->GetMap()->GetHeight(p->GetPhaseShift(),goal.GetPositionX(),goal.GetPositionY(),
s.Origin.GetPositionZ()+2,true,15);
if (!std::isfinite(z) || z<=INVALID_HEIGHT+1 || std::fabs(z-s.Origin.GetPositionZ())>(r.Intent=="groupformation"?20.0f:3.0f)) continue;
if (!std::isfinite(z) || z<=INVALID_HEIGHT+1 || std::fabs(z-s.Origin.GetPositionZ())>((r.Intent=="groupformation" || r.State==Phase::WaitingGroup)?20.0f:3.0f)) continue;
goal.m_positionZ=z;
PathGenerator navigation(p);
navigation.SetPathLengthLimit(240);
@@ -763,13 +771,13 @@ bool AdventureBehaviorMgr::PlanLeisureWalk(Player* p, Record& r)
p->GetMap()->GetTerrain()->GetFullTerrainStatusForPosition(p->GetPhaseShift(),p->GetMapId(),point.x,point.y,point.z,terrain);
uint32 zone=0,area=0;
p->GetMap()->GetTerrain()->GetZoneAndAreaId(p->GetPhaseShift(),p->GetMapId(),zone,area,target);
if ((r.Intent!="groupformation" && !terrain.outdoors) || (boundary && area!=boundary && zone!=boundary)) {valid=false;break;}
if (((r.Intent!="groupformation" && r.State!=Phase::WaitingGroup) && !terrain.outdoors) || (boundary && area!=boundary && zone!=boundary)) {valid=false;break;}
endsOutdoors=terrain.outdoors;
length+=step;section+=step;last=target;
if (section>=20) {outbound.push_back(target);section=0;}
}
float const span=s.Origin.GetExactDist(last);
if (!valid || length>budget || span<65.0f || (r.Intent=="groupformation" && !endsOutdoors) || length>span*2.5f || last.GetExactDist(goal)>1.5f) continue;
if (!valid || length>budget || span<65.0f || ((r.Intent=="groupformation" || r.State==Phase::WaitingGroup) && !endsOutdoors) || length>span*2.5f || last.GetExactDist(goal)>1.5f) continue;
if (outbound.empty() || outbound.back().GetExactDist(last)>2) outbound.push_back(last);
else outbound.back()=last;
if (span>best)
@@ -1159,9 +1167,9 @@ bool AdventureBehaviorMgr::UpdateRecovery(Player* player, Record& r, uint32 diff
AdventureSocialContext AdventureBehaviorMgr::SocialContext(Player* player,Record& r)
{
auto profile=sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter());
auto location=sAdventureLocationMgr->Find(r.Location);
auto location=sAdventureLocationMgr->Find(r.State==Phase::WaitingGroup?r.WaitingLocation:r.Location);
return {r.Anchor,r.AnchorMap,location&&location->Point.Category=="festival",
profile?profile->Locale:"enUS",r.Intent,r.Random,r.TargetHistory,r.VariantHistory,
profile?profile->Locale:"enUS",r.State==Phase::WaitingGroup?"groupformation":r.Intent,r.Random,r.TargetHistory,r.VariantHistory,
[this,player,&r](Position const& target,float maxPath){return LocalMove(player,r,target,maxPath);},
r.Intent != "rest" || (location && location->Point.Category == "rest_ground"),
location && r.Intent=="economy"?AdventureEconomyPolicy::LocationRequirement(location->Point.Category):"",
@@ -1189,7 +1197,7 @@ void AdventureBehaviorMgr::Micro(Player * player, Record & r)
auto profile = sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter());
if (! profile) return;
auto l = sAdventureLocationMgr->Find(r.Location);
auto l = sAdventureLocationMgr->Find(r.State==Phase::WaitingGroup?r.WaitingLocation:r.Location);
if (! l) return;
auto social=SocialContext(player,r);
if (r.Intent=="rest")
@@ -1259,7 +1267,7 @@ void AdventureBehaviorMgr::Micro(Player * player, Record & r)
{
auto a=ResolvedAction(catalog);
if(a.Kind=="unsupported")continue;
auto w = a.Intents.find(r.Intent=="pvp"?"adventure":r.Intent);
auto w = a.Intents.find(r.State==Phase::WaitingGroup?"groupformation":r.Intent=="pvp"?"adventure":r.Intent);
if (w == a.Intents.end()) continue;
if ((a.Kind=="leisure" || a.Kind=="outing") && !LeisureAvailable(player,r,a)) continue;
if (a.Kind=="recovery" && !RecoveryAvailable(player,r,a)) continue;
@@ -1479,7 +1487,18 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff)
if (entry == _records.end() || ! entry->second.Enabled || ! player->IsInWorld()) return;
auto & r = entry->second;
ClientlessTravelTelemetry::Tick(player);
if(player->InBattleground())return;
if(player->InBattleground())
{
if(r.State==Phase::WaitingGroup)ResetMicro(player,r);
return;
}
if (r.State==Phase::WaitingGroup && (player->GetMap()->IsDungeon() || player->GetMapId()!=r.AnchorMap))
{
// Native transfer owns movement; discard the old city activity.
ResetMicro(player,r);r.WaitingLocation.clear();
if (player->GetMap()->IsDungeon()) return;
r.Anchor.Relocate(player);r.AnchorMap=player->GetMapId();
}
if(UpdateNavigationRescue(player,r))return;
if(r.AreaRevision!=sAdventureAreaMgr->Revision())
{
@@ -1612,8 +1631,13 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff)
// Once a native browser owns this group, never race it with physical travel.
auto group=player->GetGroup();
if(world && (r.ExpeditionStarted || sAdventureGroupMgr->Complete(player)) && (!group || !group->isLFGGroup()) && !player->isUsingLfg() && !player->InBattlegroundQueue())
{r.Dirty=true;r.State=Phase::Idle;}
else sAdventureGroupMgr->QueueBrowser(player,r.Intent);
{ResetMicro(player,r);r.Dirty=true;r.State=Phase::Idle;}
else
{
sAdventureGroupMgr->QueueBrowser(player,r.Intent);
if (AdventureGroupPolicy::Capital(player->GetZoneId()) && !r.WaitingLocation.empty() &&
(r.Intent=="instance" || r.Intent=="raid" || r.Intent=="pvp")) UpdateActivities(player,r);
}
return;
}
if (r.State == Phase::Travel)
@@ -1667,53 +1691,59 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff)
{
if ((r.Intent=="instance" || r.Intent=="raid")) {UpdateInstance(player,r,now);return;}
if(sAdventureGroupMgr->FollowMember(player))return;
if (r.Recovery.Active)
{
if (UpdateRecovery(player,r,1000)) return;
ResetMicro(player,r);r.MicroTimer=3000;return;
}
if(r.Social.Active)
{
if(UpdateSocial(player,r,1000))return;
ResetMicro(player,r);r.MicroTimer=3000;return;
}
if (r.Micro.empty() && r.MicroTimer) return;
if (player->GetMapId() != r.AnchorMap)
{
Fallback(player, r);
return;
}
auto travel = sClientlessTravelMgr->GetStatus(guid);
bool moving = travel && travel->State == ClientlessTravelState::Moving;
if (! r.Micro.empty())
{
if (! r.GestureTimer && r.Micro == "drink")
{
player->SendPlaySpellVisualKit(SPELL_VISUAL_KIT_DRINK, 0, 0);
r.GestureTimer = 7000;
}
if (! moving && r.WorkPending)
{
auto object = player->GetMap()->GetGameObject(r.WorkObject);
if (! object || player->GetExactDist(object) > 4 || ! player->IsWithinLOSInMap(object))
{
ResetMicro(player, r);
r.MicroTimer = 3000;
return;
}
player->SetFacingToObject(object);
player->SetUInt32Value(UNIT_NPC_EMOTESTATE, r.WorkEmote);
r.WorkPending = false;
r.Detail = "performing work demonstration";
}
if (r.MicroTimer || moving) return;
ResetMicro(player, r);
}
Micro(player, r);
UpdateActivities(player,r);
}
}
void AdventureBehaviorMgr::UpdateActivities(Player* player, Record& r)
{
uint32 guid=player->GetGUID().GetCounter();
if(sAdventureGroupMgr->FollowMember(player))return;
if (r.Recovery.Active)
{
if (UpdateRecovery(player,r,1000)) return;
ResetMicro(player,r);r.MicroTimer=3000;return;
}
if(r.Social.Active)
{
if(UpdateSocial(player,r,1000))return;
ResetMicro(player,r);r.MicroTimer=3000;return;
}
if (r.Micro.empty() && r.MicroTimer) return;
if (player->GetMapId() != r.AnchorMap)
{
Fallback(player, r);
return;
}
auto travel = sClientlessTravelMgr->GetStatus(guid);
bool moving = travel && travel->State == ClientlessTravelState::Moving;
if (! r.Micro.empty())
{
if (! r.GestureTimer && r.Micro == "drink")
{
player->SendPlaySpellVisualKit(SPELL_VISUAL_KIT_DRINK, 0, 0);
r.GestureTimer = 7000;
}
if (! moving && r.WorkPending)
{
auto object = player->GetMap()->GetGameObject(r.WorkObject);
if (! object || player->GetExactDist(object) > 4 || ! player->IsWithinLOSInMap(object))
{
ResetMicro(player, r);
r.MicroTimer = 3000;
return;
}
player->SetFacingToObject(object);
player->SetUInt32Value(UNIT_NPC_EMOTESTATE, r.WorkEmote);
r.WorkPending = false;
r.Detail = "performing work demonstration";
}
if (r.MicroTimer || moving) return;
ResetMicro(player, r);
}
Micro(player, r);
}
bool AdventureBehaviorMgr::Command(ChatHandler * handler, char const * args)
{
std::istringstream in(args ? args : "");
+2 -1
View File
@@ -63,7 +63,7 @@ private:
bool Enabled=false, Schedule=false, Publishing=false, Queued=false, Dirty=true, GroupOverride=false;
Phase State=Phase::Idle;
std::string Intent, Location, WantedIntent, WantedLocation, Detail, Micro, ManualNextIntent, ManualNextLocation;
std::string PersonalIntent,PersonalLocation;
std::string PersonalIntent,PersonalLocation,WaitingLocation;
time_t PersonalDeadline=0;
time_t Deadline=0;
AdventureGroupPolicy::Departure Departure;
@@ -120,6 +120,7 @@ private:
void Queue(Player* player, Record& record, std::string const& action, std::string const& parameter, std::string const& value, bool hard = false);
void ResetMicro(Player* player, Record& record);
void Micro(Player* player, Record& record);
void UpdateActivities(Player* player, Record& record);
AdventureSocialContext SocialContext(Player* player, Record& record);
bool UpdateSocial(Player* player, Record& record, uint32 diff);
bool LocalMove(Player* player, Record& record, Position const& target, float maxPath, bool riding=false);
+22 -5
View File
@@ -148,6 +148,21 @@ void AdventureGroupMgr::Offline(Player* p)
CancelBrowser(p);
_records.erase(p->GetGUID());
}
bool AdventureGroupMgr::WaitingForBrowserGroup(Player* player) const
{
if (!Ready(player) || player->GetGroup() || !AdventureGroupPolicy::Capital(player->GetZoneId())) return false;
auto plan=sAdventureBehaviorMgr->GroupPlan(player);
if (!plan.Enabled || plan.Forming) return false;
if (plan.Intent=="pvp")
{
if (!player->InBattlegroundQueue() || player->InBattleground()) return false;
for (uint32 slot=0;slot<PLAYER_MAX_BATTLEGROUND_QUEUES;++slot)
if (player->IsInvitedForBattlegroundQueueType(player->GetBattlegroundQueueTypeId(slot))) return false;
return true;
}
return (plan.Intent=="instance" || plan.Intent=="raid") &&
sLFGMgr->GetState(player->GetGUID())==lfg::LFG_STATE_QUEUED;
}
bool AdventureGroupMgr::Accept(Player* invited,Player* inviter)
{
if(!CanInvite(inviter,invited))return false;
@@ -157,10 +172,11 @@ bool AdventureGroupMgr::Accept(Player* invited,Player* inviter)
if(it==_records.end() || it->second.Inviter!=inviter->GetGUID() || it->second.InviteUntil<GameTime::GetGameTime())return false;
auto a=sAdventureBehaviorMgr->GroupPlan(inviter),b=sAdventureBehaviorMgr->GroupPlan(invited);
auto intent=Activity(a);
if(!a.Enabled || !b.Enabled || a.Forming!=b.Forming ||
bool waiting=WaitingForBrowserGroup(invited);
if(!a.Enabled || !b.Enabled || (a.Forming!=b.Forming && !(a.Forming && waiting)) ||
!AdventureGroupPolicy::Compatible(intent,Activity(b),inviter->getLevel(),invited->getLevel()))return false;
auto group=inviter->GetGroup();
return (!group || (group->GetLeaderGUID()==inviter->GetGUID() && group->GetMembersCount()<Capacity(a))) && frand(0,1)<Sociability(invited);
return (!group || (group->GetLeaderGUID()==inviter->GetGUID() && group->GetMembersCount()<Capacity(a))) && (waiting || frand(0,1)<Sociability(invited));
}
bool AdventureGroupMgr::CanInvite(Player* leader,Player* member)
@@ -548,7 +564,7 @@ void AdventureGroupMgr::Update(Player* p)
MaybeRunFormationManager(now);
CheckFullGroup(p);
if(PlayerLeadership(p))return;
if(!Ready(p) || p->isUsingLfg() || p->InBattlegroundQueue())return;
if(!Ready(p) || p->isUsingLfg() || p->InBattlegroundQueue() || WaitingForBrowserGroup(p))return;
if(EnforceCapacity(p))return;
auto plan=sAdventureBehaviorMgr->GroupPlan(p);if(!plan.Enabled)return;
auto intent=Activity(plan);if(!AdventureGroupPolicy::Eligible(intent))return;
@@ -592,9 +608,10 @@ void AdventureGroupMgr::Update(Player* p)
{
if(guid==p->GetGUID())continue;
auto other=ObjectAccessor::FindPlayer(guid);
if(!Ready(other) || other->isUsingLfg() || other->InBattlegroundQueue() || other->GetGroupInvite() || other->GetTeam()!=p->GetTeam())continue;
bool waiting=WaitingForBrowserGroup(other);
if(!Ready(other) || (other->isUsingLfg() && !(plan.Forming && waiting)) || (other->InBattlegroundQueue() && !(plan.Forming && waiting)) || other->GetGroupInvite() || other->GetTeam()!=p->GetTeam())continue;
auto peerPlan=sAdventureBehaviorMgr->GroupPlan(other);
if(!peerPlan.Enabled || peerPlan.Forming!=plan.Forming ||
if(!peerPlan.Enabled || (peerPlan.Forming!=plan.Forming && !(plan.Forming && waiting)) ||
!AdventureGroupPolicy::Compatible(intent,Activity(peerPlan),p->getLevel(),other->getLevel()))continue;
if(!ProfessionCompatible(p,other,intent))continue;
if(auto existing=other->GetGroup())
@@ -21,6 +21,7 @@ public:
static AdventureGroupMgr* instance();
void Update(Player* player);
bool Complete(Player* player);
bool WaitingForBrowserGroup(Player* player) const;
bool NeedsRole(Player* leader,Player* member);
void UpdateBrowser(Player* player);
void QueueBrowser(Player* player,std::string const& intent);
@@ -132,6 +132,7 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
(inviter->GetSession()->IsClientless() || AdventureGroupPolicy::AcceptPlayerLevel(p->getLevel(),inviter->getLevel())) &&
(inviter->GetSession()->IsClientless()?sAdventureGroupMgr->Accept(p,inviter):
frand(0,1)<AdventureInteractionPolicy::Willingness(sAdventureBehaviorMgr->InteractionIntent(p),Sociability(p)));
if (accept) sAdventureGroupMgr->CancelBrowser(p);
WorldPackets::Party::PartyInviteResponse response{WorldPacket(CMSG_PARTY_INVITE_RESPONSE)};response.Accept=accept;
p->GetSession()->HandlePartyInviteResponseOpcode(response);r.Invite.Clear();
TC_LOG_INFO("server.adventurebot","INTERACTION_INVITE guid=%u leader=%u accepted=%u",p->GetGUID().GetCounter(),leader.GetCounter(),accept);
+31 -3
View File
@@ -27,14 +27,16 @@ namespace std{template<>struct hash<ObjectGuid>{size_t operator()(ObjectGuid g)c
struct Player;struct ChatHandler;
struct Map{bool IsDungeon(){return false;}}map;
struct Member{Player*player=nullptr;Member*nextMember=nullptr;Player*GetSource(){return player;}Member*next(){return nextMember;}};
constexpr int GROUP_REMOVEMETHOD_LEAVE=1;
constexpr unsigned PLAYER_MAX_BATTLEGROUND_QUEUES=3;constexpr int GROUP_REMOVEMETHOD_LEAVE=1;
struct Group{struct Slot{ObjectGuid guid;};std::vector<Slot>slots;auto const&GetMemberSlots(){return slots;}void RemoveMember(ObjectGuid,int);Member*members=nullptr;Member*GetFirstMember(){return members;}void ChangeLeader(ObjectGuid g){leader=g;}void SendUpdate(){}ObjectGuid leader;unsigned count=2;bool raid=false;ObjectGuid GetLeaderGUID(){return leader;}unsigned GetMembersCount(){return count;}bool isRaidGroup(){return raid;}void ConvertToRaid(){raid=true;}void ConvertToGroup(){raid=false;}};
struct WorldPacket{unsigned value=0;WorldPacket(int,int=0){}WorldPacket&operator<<(unsigned n){value=n;return *this;}};using Difficulty=unsigned;constexpr int MSG_SET_DUNGEON_DIFFICULTY=3;constexpr int MSG_SET_RAID_DIFFICULTY=2,RAID_DIFFICULTY_25MAN_NORMAL=1;constexpr int ALLIANCE=0,LANG_COMMON=7,LANG_ORCISH=1;constexpr int CMSG_PARTY_INVITE=1,EMOTE_ONESHOT_WAVE=1;
namespace WorldPackets{namespace Party{struct PartyInviteClient{PartyInviteClient(WorldPacket&&){}std::string TargetName;ObjectGuid TargetGUID;};}}
struct Session{Player*owner;bool clientless=true;bool IsClientless(){return clientless;}void HandlePartyInviteOpcode(WorldPackets::Party::PartyInviteClient&);void HandleSetRaidDifficultyOpcode(WorldPacket&);void HandleSetDungeonDifficultyOpcode(WorldPacket&p){HandleSetRaidDifficultyOpcode(p);}};
struct Player{ObjectGuid guid;Group*group=nullptr,*invite=nullptr;Session session{this};bool combat=false;unsigned zone=1519,level=60,team=0;float distance=10;
unsigned difficulty=0;unsigned GetRaidDifficulty(){return difficulty;}unsigned GetDifficulty(bool){return difficulty;}ObjectGuid GetGUID(){return guid;}bool IsInWorld(){return true;}bool IsAlive(){return true;}bool IsInCombat(){return combat;}
bool isUsingLfg(){return false;}bool InBattlegroundQueue(){return false;}
bool queued=false;bool isUsingLfg(){return queued;}bool bgqueued=false,bg=false,bgoffer=false;
bool InBattlegroundQueue(){return bgqueued;}bool InBattleground(){return bg;}
unsigned GetBattlegroundQueueTypeId(unsigned){return 1;}bool IsInvitedForBattlegroundQueueType(unsigned){return bgoffer;}
bool IsBeingTeleported(){return false;}bool GetTransport(){return false;}bool IsFlying(){return false;}bool IsInFlight(){return false;}bool IsCharmed(){return false;}Map*GetMap(){return &map;}
unsigned GetZoneId(){return zone;}unsigned getLevel(){return level;}unsigned GetTeam(){return team;}
Group*GetGroup(){return group;}Group*GetGroupInvite(){return invite;}Session*GetSession(){return &session;}std::string GetName(){return std::to_string(guid.id);}
@@ -72,8 +74,9 @@ bool AdventureGroupMgr::NeedsRole(Player*,Player*){return true;}
void AdventureGroupMgr::MaybeRunFormationManager(time_t){}
bool AdventureGroupMgr::Complete(Player*p){auto group=p->GetGroup();if(!group)return false;auto plan=sAdventureBehaviorMgr->GroupPlan(p);return group->GetMembersCount()==(plan.Capacity?plan.Capacity:AdventureGroupPolicy::Capacity(plan.Forming?plan.NextIntent:plan.Intent));}
'''
code+=r'namespace lfg{constexpr int LFG_STATE_QUEUED=1;} struct LfgMgr{int GetState(ObjectGuid guid){auto p=ObjectAccessor::FindPlayer(guid);return p&&p->queued?1:0;}}lfgMgr;auto sLFGMgr=&lfgMgr;'
code+=fun('float Sociability(')+'\n'+fun('bool ProfessionCompatible(')+'\n'+fun('bool Ready(')+'\n'+fun('std::string Activity(')+'\n'+fun('unsigned Capacity(')
for name in ['std::string AdventureGroupMgr::RecruitmentText(', 'bool AdventureGroupMgr::RecruitPlayer(', 'bool AdventureGroupMgr::BotLed(', 'bool AdventureGroupMgr::FollowMember(', 'bool AdventureGroupMgr::Forming(', 'bool AdventureGroupMgr::PlannedTransition(', 'bool AdventureGroupMgr::CanInvite(', 'bool AdventureGroupMgr::Accept(', 'void AdventureGroupMgr::CheckFullGroup(', 'void AdventureGroupMgr::Update(', 'void AdventureGroupMgr::Invite(', 'void AdventureGroupMgr::RequestPlayerAdmission(', 'bool AdventureGroupMgr::PlayerLeadership(', 'bool AdventureGroupMgr::EnforceCapacity(']:code+='\n'+fun(name)
for name in ['std::string AdventureGroupMgr::RecruitmentText(', 'bool AdventureGroupMgr::RecruitPlayer(', 'bool AdventureGroupMgr::BotLed(', 'bool AdventureGroupMgr::FollowMember(', 'bool AdventureGroupMgr::Forming(', 'bool AdventureGroupMgr::PlannedTransition(', 'bool AdventureGroupMgr::CanInvite(', 'bool AdventureGroupMgr::WaitingForBrowserGroup(', 'bool AdventureGroupMgr::Accept(', 'void AdventureGroupMgr::CheckFullGroup(', 'void AdventureGroupMgr::Update(', 'void AdventureGroupMgr::Invite(', 'void AdventureGroupMgr::RequestPlayerAdmission(', 'bool AdventureGroupMgr::PlayerLeadership(', 'bool AdventureGroupMgr::EnforceCapacity(']:code+='\n'+fun(name)
code+=r'''
int main(){
using namespace AdventureGroupPolicy;
@@ -143,6 +146,31 @@ int main(){
mgr._records[a.guid].SearchAt=0;mgr._records[a.guid].Partners.clear();mgr._records[b.guid].Partners.clear();mgr._records[b.guid].InviteUntil=0;
mgr.Update(&a);assert(a.difficulty==RAID_DIFFICULTY_25MAN_NORMAL&&party.raid&&invites==3&&mgr.Accept(&b,&a));
party.count=20;assert(!mgr.Accept(&b,&a));
// Queued bots remain passive but accept compatible formation invitations.
{AdventureGroupMgr waiting;Player leader,member;leader.guid={950};member.guid={951};
ObjectAccessor::players[leader.guid]=&leader;ObjectAccessor::players[member.guid]=&member;
behavior.plans[950]={true,true,"groupformation","capital","instance","target",3600};
behavior.plans[951]={true,false,"instance","target",{}, {},3600};member.queued=true;
waiting._records[leader.guid]={};waiting._records[member.guid]={};int before=invites;
waiting.Update(&member);assert(invites==before);
waiting.Update(&leader);assert(invites==before+1&&member.GetGroupInvite());
profile.Personality["sociability"]=0;assert(waiting.Accept(&member,&leader));
locations.levelEligible=false;assert(!waiting.Accept(&member,&leader));locations.levelEligible=true;
member.level=30;assert(!waiting.Accept(&member,&leader));member.level=60;
behavior.plans[950].NextIntent="raid";assert(!waiting.Accept(&member,&leader));
// The same passive admission applies to PvP queues, but never steals an active BG offer.
behavior.plans[950].NextIntent="pvp";behavior.plans[951].Intent="pvp";
member.queued=false;member.bgqueued=true;assert(waiting.WaitingForBrowserGroup(&member));
member.invite=nullptr;waiting._records.clear();waiting._records[leader.guid]={};waiting._records[member.guid]={};
profile.Personality["sociability"]=1;int bgBefore=invites;
waiting.Update(&member);assert(invites==bgBefore);
waiting.Update(&leader);assert(invites==bgBefore+1&&member.GetGroupInvite());
profile.Personality["sociability"]=0;assert(waiting.Accept(&member,&leader));
member.bgoffer=true;assert(!waiting.WaitingForBrowserGroup(&member)&&!waiting.Accept(&member,&leader));
member.bgoffer=false;member.bg=true;assert(!waiting.WaitingForBrowserGroup(&member));member.bg=false;
member.bgqueued=false;assert(!waiting.WaitingForBrowserGroup(&member));
profile.Personality["sociability"]=1;ObjectAccessor::players.erase(leader.guid);ObjectAccessor::players.erase(member.guid);
invites=before;}
// Solo recruitment outside formation requires actual local contact.
a.group=nullptr;b.invite=nullptr;behavior.plans[29]={true,false,"social","city",{}, {},3600};behavior.plans[31]=behavior.plans[29];
mgr._records.clear();mgr._records[a.guid]={};mgr._records[b.guid]={};mgr.Update(&a);assert(invites==3);
@@ -68,7 +68,7 @@ struct Travel {Nav nav;bool active=false,valid=true;int starts=0,cancels=0;Nav*G
enum class ClientlessActionExecutionState{Moving,SelectingTarget,Completed};struct Execution {ClientlessActionExecutionState State=ClientlessActionExecutionState::Completed;};
struct Executor {Execution status;Execution*GetStatus(unsigned){return &status;}}executor;auto sClientlessActionExecutorMgr=&executor;
struct Graph {void EnsureLoaded(){}bool FindNearestNodeToPosition(unsigned,float,float,float,std::string&node,float&distance){node="entrance";distance=10;return true;}}graph;auto sClientlessTravelGraphMgr=&graph;
struct BotGroups{void Offline(Player*){}void Update(Player*){}bool Accept(Player*,Player*){return false;}bool botLed=false;bool BotLed(Player*){return botLed;}bool Forming(Player*){return false;}bool PlannedTransition(Player*){return false;}void Report(ChatHandler*,Player*){}}botGroups;auto sAdventureGroupMgr=&botGroups;
struct BotGroups{unsigned browserCancels=0;void CancelBrowser(Player*){++browserCancels;}void Offline(Player*){}void Update(Player*){}bool Accept(Player*,Player*){return false;}bool botLed=false;bool BotLed(Player*){return botLed;}bool Forming(Player*){return false;}bool PlannedTransition(Player*){return false;}void Report(ChatHandler*,Player*){}}botGroups;auto sAdventureGroupMgr=&botGroups;
struct GroupTravel{bool protectedTravel=false,separated=false;int follows=0;bool Follow(Player*p){++follows;return p->GetGroup()!=nullptr;}bool Protected(Player*){return protectedTravel;}bool Separated(Player*){return separated;}void Clear(Player*){}void Offline(Player*){}void Report(ChatHandler*,Player*){}}groupTravel;auto sAdventureGroupTravelMgr=&groupTravel;
'''+header+'\n'+source+r'''
int main(){
+2 -2
View File
@@ -89,10 +89,10 @@ struct Manager{void ClearQueuedActions(uint32){}}manager;auto sClientlessPlayerM
struct Groups{unsigned cancellations=0;void CancelBrowser(Player*){++cancellations;}}groups;auto sAdventureGroupMgr=&groups;
struct Executor{bool RequestStopAtNode(uint32){return false;}}executor;auto sClientlessActionExecutorMgr=&executor;
class AdventureBehaviorMgr{public:
enum class Phase{Idle,OnSite,Switching};
enum class Phase{Idle,OnSite,Switching,WaitingGroup};
struct Record{
AdventureInstancePolicy::Visit Instance;
std::string Intent="leisure",Location="city",WantedIntent,WantedLocation,Detail;Position Anchor;uint32 AnchorMap=0,MicroTimer=240000,RetryTimer=0;std::mt19937 Random{26};
std::string Intent="leisure",Location="city",WaitingLocation="city",WantedIntent,WantedLocation,Detail;Position Anchor;uint32 AnchorMap=0,MicroTimer=240000,RetryTimer=0;std::mt19937 Random{26};
time_t Deadline=0,ExpeditionUntil=0;bool Dirty=false,ExpeditionStarted=false;std::string ExpeditionIntent;std::unordered_set<std::string> Tried;Phase State=Phase::OnSite;
struct LeisureScene{bool Active=false,Flying=false,Moving=false,StopRequested=false,Returning=false,PlannedWalk=false,Progressive=false;std::string Kind;Position Origin;std::vector<Position> Route,WalkTrace;std::size_t Step=0,OutboundSteps=0;uint32 Elapsed=0,LegElapsed=0,PauseMs=0,CompanionWait=0,CompletedLegs=0;ClientlessCompanionScene Companion;}Leisure;
};
@@ -0,0 +1,37 @@
"""Execute the native proposal teleport batch with transfer/session adapters."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/DungeonFinding/LFGMgr.cpp').read_text()
a=s.index(' // Complete clientless entries');z=s.index('\n}\n\nuint32 LFGMgr::AddProposal',a)
code=r'''
#include <cassert>
#include <list>
#include <map>
#include <vector>
using ObjectGuid=unsigned;using GuidList=std::list<ObjectGuid>;
std::vector<unsigned> order;
struct Player;
struct Session{bool bot;Player*player;bool IsClientless(){return bot;}void CompleteClientlessTeleport();};
struct Player{unsigned id;bool world=true,pending=false;unsigned ack=0;Session session;Player(unsigned id,bool bot):id(id),session{bot,this}{}Session*GetSession(){return &session;}bool IsBeingTeleported(){return pending;}};
void Session::CompleteClientlessTeleport(){++player->ack;player->pending=false;player->world=true;}
namespace ObjectAccessor{std::map<unsigned,Player*>players;Player*FindConnectedPlayer(unsigned id){return players[id];}}
namespace ClientlessBrowserPolicy{bool Bot(Player*p){return p&&p->session.bot;}}
struct LFGMgr{void Run(GuidList playersToTeleport);void TeleportPlayer(Player*p,bool out){assert(!out);order.push_back(p->id);p->world=false;p->pending=true;}};
void LFGMgr::Run(GuidList playersToTeleport){
'''+s[a:z]+r'''
}
int main(){
Player human(1,false),bot(2,true),transitioning(3,true);transitioning.world=false;
ObjectAccessor::players={{1,&human},{2,&bot},{3,&transitioning},{4,nullptr}};
LFGMgr mgr;mgr.Run({1,2,3,4});
assert((order==std::vector<unsigned>{2,3,1}));
assert(bot.ack==1&&transitioning.ack==1&&bot.world&&transitioning.world);
assert(human.ack==0&&human.pending&&!human.world);
}
'''
with tempfile.TemporaryDirectory() as folder:
p=Path(folder);(p/'test.cpp').write_text(code)
subprocess.run(['c++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Native group teleport: all connected bots included and acknowledged, human client acknowledgement preserved, offline members skipped.')
@@ -0,0 +1,45 @@
"""Run the native fallback against old bot / freshly queued human combinations."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/DungeonFinding/LFGQueue.cpp').read_text()
a=s.index('uint8 LFGQueue::FindLowPopulationGroups()');z=s.index('\n/**',a)
code=r'''
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <ctime>
#include <list>
#include <map>
#include <string>
using uint8=uint8_t;using uint32=uint32_t;using uint64=uint64_t;using ObjectGuid=unsigned;using GuidList=std::list<ObjectGuid>;
#define TC_LOG_DEBUG(...) ((void)0)
constexpr unsigned CONFIG_DUNGEON_FINDER_LOW_POPULATION_FALLBACK_DELAY=0,CONFIG_DUNGEON_FINDER_LOW_POPULATION_MIN_PLAYERS=1;
struct World{unsigned getIntConfig(unsigned id){return id?1:60;}}world;auto sWorld=&world;
namespace GameTime{time_t now=1000;time_t GetGameTime(){return now;}}
enum{LFG_COMPATIBLES_MATCH};
struct Data{time_t joinTime;std::map<unsigned,unsigned>roles{{1,8}};std::string bestCompatible="both";};
using LfgQueueData=Data;using LfgQueueDataContainer=std::map<unsigned,Data>;
struct LFGQueue{
LfgQueueDataContainer QueueDataStore;GuidList currentQueueStore{1,2};unsigned matches=0;
uint8 FindLowPopulationGroups();void FindBestCompatibleInQueue(LfgQueueDataContainer::iterator){}
GuidList GetQueueGuidsFromKey(std::string){return currentQueueStore;}
int CheckCompatibility(GuidList const&,bool incomplete){assert(incomplete);++matches;currentQueueStore.clear();return LFG_COMPATIBLES_MATCH;}
};
'''+s[a:z]+r'''
int main(){
LFGQueue q;q.QueueDataStore[1]={0};q.QueueDataStore[2]={1000};
assert(q.FindLowPopulationGroups()==0&&q.matches==0);
GameTime::now=1059;assert(q.FindLowPopulationGroups()==0&&q.matches==0);
GameTime::now=1060;assert(q.FindLowPopulationGroups()==1&&q.matches==1);
LFGQueue future;future.QueueDataStore[1]={0};future.QueueDataStore[2]={2000};
assert(future.FindLowPopulationGroups()==0);
LFGQueue same;same.QueueDataStore[1]={1000};same.QueueDataStore[2]={1000};
assert(same.FindLowPopulationGroups()==1);
}
'''
with tempfile.TemporaryDirectory() as folder:
p=Path(folder);(p/'test.cpp').write_text(code)
subprocess.run(['c++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Native low-pop fallback: old bots cannot bypass a new human/group 60-second wait; 59/60-second and future timestamps passed.')
@@ -0,0 +1,52 @@
"""Run the shared performance executor with a real waiting-state action selector."""
from pathlib import Path
import subprocess,tempfile,re
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text()
a=s.index('void AdventureBehaviorMgr::UpdateActivities(');z=s.index('\nbool AdventureBehaviorMgr::Command',a)
selector=re.search(r'auto w = a.Intents.find\([^;]+;',s).group()
code=r'''
#include <cassert>
#include <map>
#include <string>
using uint32=unsigned;
constexpr int SPELL_VISUAL_KIT_DRINK=1,UNIT_NPC_EMOTESTATE=1;
struct Object{bool IsWithinLOSInMap(Object*){return true;}}object;
struct Map{unsigned id=1;Object*GetGameObject(unsigned){return &object;}}map;
struct Guid{unsigned GetCounter(){return 44;}};
struct Player{Guid GetGUID(){return {};}unsigned mapid=1;unsigned GetMapId(){return mapid;}Map*GetMap(){return &map;}float GetExactDist(Object*){return 1;}bool IsWithinLOSInMap(Object*){return true;}void SetFacingToObject(Object*){}void SetUInt32Value(int,unsigned){}void SendPlaySpellVisualKit(int,int,int){}};
enum class ClientlessTravelState{Moving,Arrived};
struct Travel{struct Status{ClientlessTravelState State=ClientlessTravelState::Arrived;}status;Status*GetStatus(unsigned){return &status;}}travel;auto sClientlessTravelMgr=&travel;
struct Group{bool FollowMember(Player*){return false;}}group;auto sAdventureGroupMgr=&group;
struct AdventureBehaviorMgr{
enum class Phase{WaitingGroup,OnSite};struct Record{Phase State=Phase::WaitingGroup;std::string Intent="instance",Location="remote-dungeon",Micro,Detail;unsigned AnchorMap=1,MicroTimer=0,GestureTimer=0,WorkObject=0,WorkEmote=0;bool WorkPending=false;struct{bool Active=false;}Recovery,Social;};
unsigned calls=0,resets=0;std::string action;
void UpdateActivities(Player*,Record&);
void ResetMicro(Player*,Record&r){++resets;r.Micro.clear();r.Recovery.Active=r.Social.Active=false;}
bool UpdateRecovery(Player*,Record&,unsigned){return false;}bool UpdateSocial(Player*,Record&,unsigned){return true;}
void Fallback(Player*,Record&){assert(false);}
void Micro(Player*,Record&r){
struct{std::map<std::string,int>Intents{{"groupformation",1},{"pvp",2},{"adventure",3},{"instance",4}};}a;
'''+selector+r'''
action=w->first;++calls;r.Micro="performative";r.MicroTimer=5000;
}
};
'''+s[a:z]+r'''
int main(){
AdventureBehaviorMgr mgr;AdventureBehaviorMgr::Record r;Player p;
mgr.UpdateActivities(&p,r);assert(mgr.calls==1&&mgr.action=="groupformation"&&r.Intent=="instance"&&r.Location=="remote-dungeon");
mgr.UpdateActivities(&p,r);assert(mgr.calls==1); // ongoing action is not restarted
r.MicroTimer=0;travel.status.State=ClientlessTravelState::Moving;mgr.UpdateActivities(&p,r);assert(mgr.calls==1);
travel.status.State=ClientlessTravelState::Arrived;mgr.UpdateActivities(&p,r);assert(mgr.calls==2&&mgr.resets==1);
r.Social.Active=true;mgr.UpdateActivities(&p,r);assert(mgr.calls==2);
r.Social.Active=false;r.Micro.clear();r.MicroTimer=0;r.Intent="pvp";
mgr.UpdateActivities(&p,r);assert(mgr.action=="groupformation"&&r.Intent=="pvp");
r.Micro.clear();r.MicroTimer=0;r.State=AdventureBehaviorMgr::Phase::OnSite;r.Intent="pvp";
mgr.UpdateActivities(&p,r);assert(mgr.action=="adventure");
}
'''
with tempfile.TemporaryDirectory() as folder:
p=Path(folder);(p/'test.cpp').write_text(code)
subprocess.run(['c++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Waiting activities: formation catalog selected without changing dungeon intent/destination; action, movement and social lifetimes preserved.')