Fix stuck group followers and druid mount preparation

This commit is contained in:
Nico committed 2026-11-05 13:06:25 +01:00
1 parent 7f10f724f2
commit b3d2d69251
12 files changed
+502 -29

No files matched your search

@@ -0,0 +1,31 @@
# Druidengestalten vor dem Aufsitzen
Talzul (97, Druide Level 66) konnte in der Höllenfeuerhalbinsel seinen
Flugmountzauber 32245 nicht starten. Die gespeicherte Katzengestalt (Aura 768)
wurde von der bisherigen Aufsitzvorbereitung nicht entfernt.
`ClientlessMountPreparation::LeaveDruidForm` entfernt bei Druiden sämtliche
`SPELL_AURA_MOD_SHAPESHIFT`-Auren vor dem Aufsitzen. Es gibt keine Liste einzelner
Gestaltzauber: Katze, Bär und frühere Ränge, Reise-, Wasser-, Mondkin-, Baum- und
beide Flugformen fallen unter dieselbe Vorbereitung. Andere Klassen und
gewöhnliche Verwandlungs-/Begleitauren werden nicht verändert.
Die Vorbereitung ist bei Bodenmounts, freien Reiseflügen und dem Flugmount zum
Gruppenfolgen eingebunden. Eine Druidenform wird bei Kampf, Tod, Flug, Sturz,
Wasser, Taxi, Transport, Fahrzeug oder laufendem Teleport nicht entfernt.
Die zuständige Bewegungslogik muss zunächst einen sicheren Zustand herstellen.
Ein bereits aktives Flugmount wird wie bisher beibehalten. Flugberechtigung,
gelernte Mountzauber, Gebietsfreigaben und native Zauberprüfungen bleiben aktiv.
Mountablehnungen beim freien Flug und Gruppenflug protokollieren jetzt zusätzlich
den konkreten `SpellCastResult`, die GUID und den Mountzauber.
Die Tests `test_group_flight_mount.py` und `test_adventure_world_presence.py`
führen die gemeinsame Produktionsvorbereitung und die tatsächlichen Aufsitz-
methoden aus. Sie decken die genannten Gestalten, sicheren Formwechsel,
verweigerten Wechsel in Kampf/Flug sowie bestehende Mount-, Worgen- und
Flugberechtigungsregeln ab. Zusätzlich bestanden
`test_flight_departure.py` und `test_adventure_group_travel.py`.
Build, Installation, Neustart und Livebefund stehen im aktuellen
`SESSION-RESUME.md`; lokale Belege liegen unter
`checkpoints/2026-11-05-druid-mount/`.
@@ -0,0 +1,47 @@
# Gruppenfolge: MMAP-Korridore und Wiederherstellung festhängender Bots
Der zusätzliche Sichtlinientest entfällt experimentell für vom lokalen
MMAP-Navigator akzeptierte Korridore. Boden-/Steigungsprüfung und Gebietsregeln
bleiben aktiv. Aufgezeichnete oder seitlich versetzte Graphensplines behalten
ihren Sichtlinientest, da sie keinen entsprechenden MMAP-Nachweis besitzen.
`AdventureGroupTravelMgr::Observe` erkennt fehlgeschlagene Folgenavigation
oder mindestens 60 Sekunden ohne mehr als einen Yard Positionsfortschritt bei
tatsächlich angeforderter Bewegung. Ein bestätigter Fehler bleibt bis zur
Bewegung, zum Wechsel des relevanten Zustands oder zur erreichten Formation
gespeichert. Normales Warten, Gruppenbildung und Halten einer Flugformation
lösen keine Wiederherstellung aus.
Die Teleportfreigabe setzt diesen Befund voraus:
- Spieler als Anführer, offene Welt: mindestens 100 Yards Abstand oder
mindestens 60 Sekunden bestätigtes Festhängen.
- Bot als Anführer, offene Welt: mindestens 300 Sekunden Festhängen.
- Dungeon/Raid: beide müssen bereits in derselben konkreten Instanz und Phase
sein, der lokale Instanzeintritt muss verarbeitet sein und der Abstand muss
mindestens 100 Yards betragen. Dann entfällt die zusätzliche Wartefrist.
Der Eintritt und das normale Warten auf den Anführer werden nicht ersetzt.
Vor jeder Wiederherstellung prüft die vorhandene Gebietsverwaltung Zielposition,
Gebiet und Zone auf Erlaubnis für diesen Bot. Instanzwechsel, BG-/Arenaeintritt,
laufende Teleports, Taxi, Transportmittel, Fahrzeuge, Sturz, Tod, Kampf des Bots
und sicherer Logout sperren die Wiederherstellung. Laufende Zauber werden nicht
für einen Teleport unterbrochen.
Zu einem Anführer in der Luft darf der Bot erst mit aktivem Flugmount und
Flugberechtigung am Ziel teleportieren. Er darf vorher einen regulären
Aufsitzversuch starten. Bei Ablehnung oder nach zehn Sekunden ohne Mount wartet
er auf Bodenkontakt des Anführers. Nach einem erlaubten Luftteleport übernimmt
wieder die bestehende Flugfolgenavigation. Der Teleport wird als
`GROUP_FOLLOW_RECOVERY` mit Anführer, Instanz, Abstand und Fehlerdauer protokolliert.
Automatisierte Prüfung: `test_adventure_group_travel.py` kompiliert und führt die
Produktionsmethoden einschließlich der Wiederherstellung aus. Neue Fälle decken
MMAP/LOS und beibehaltene Bodensicherheit, Distanz-/Zeitgrenzen, echte Bewegung,
absichtliches Warten, Gebietsablehnung, Transit, Logout, Instanzkopien,
Eintrittsgrenze und Mount-/Flugberechtigungen ab. Bestehende Tests für
Instanzgruppenschutz, Flugfolgen, Aufsitzen, gemeinsame Routen, Intentzustand und
Dungeonbrowser-Teleports wurden ebenfalls ausgeführt.
Build-/Installations- und Live-Ergebnis: siehe aktuellen SESSION-RESUME.md.
Die vollständige Akzeptanzprüfung mit Spieler-Anführer erfolgt im Spiel.
+128
View File
@@ -1,3 +1,131 @@
# 2026-11-05 – Commit des gesamten offenen Korrekturstands beauftragt
Nutzer hat den lokalen Commit der Änderungen angefordert. Zusammengefasst sind
Gruppenfolge-Recovery inklusive experimenteller MMAP-LOS-Ausnahme sowie die
Aufsitzvorbereitung für alle Druidengestalten, zugehörige Tests und Berichte.
Beide Änderungen sind gebaut, installiert und nach Neustart geprüft; Livebefunde
und Grenzen stehen unten. Keine neuen Codeänderungen für den Commit, kein Push.
---
# 2026-11-05 – Druidengestalten-Mountfix installiert, Talzul startet wieder
Aktueller Nutzerauftrag: Talzul-Mountproblem für ALLE Druidengestalten fixen.
Neue gemeinsame ClientlessMountPreparation.h LeaveDruidForm entfernt Kategorie
SPELL_AURA_MOD_SHAPESHIFT nur bei Druiden am sicheren Boden, nicht in Kampf/
Flug/Sturz/Wasser/Taxi/Transport/Fahrzeug/Teleport. Hook in GroundMount,
PrepareFollowingMount und Begin-Freiflug vor normalem Cast. Native Castchecks
und Berechtigungen unverändert; FLIGHT_MOUNT_REJECT loggt jetzt CastResult.
Keine Recoveryretryänderung in diesem Auftrag. Vorherige Gruppen-Recovery-WIP
ist installiert und bleibt erhalten. Kein Commit/Push beauftragt.
Tests group_flight_mount, adventure_world_presence (Produktionhelper+Methoden,
Katze/Bärenränge/Reise/Wasser/Mondkin/Baum/beideFlugformen), flight_departure,
adventure_group_travel PASS. Vollbuild-j10 PASS, Installation PASS nach vollständigem Prozess- UND tmux-Ende.
Loaderconf+nativ0+safeall beiBuildbeginn. AltPID25378 normaler shutdown10 erstnach
Buildende; 60.842s grace, 11 restlicheBots integriertFINALIZE,remaining0,
GDBexitednormally. Neuer passiverPID31410 frischesREADY+8085/8087 geprüft.
Loader vorher1 wiederhergestellt, alle vorher118Bots direktDBonlinebestätigt.
LiveTalzul97 zuerstnormal eingeloggt, direktangegriffen; Kampf regulärbeendet,
danach 12:23:20 FLIGHT_START spell32245 und FLIGHT_SEGMENT1 diagonalclimb.
Keine Operator-Form-/Intent-/Positionsänderung. Damit ursprüngliche Aufsitz-
blockade livebeseitigt; übrigeDruidengestaltenautomatisiert geprüft.
Belege checkpoints/2026-11-05-druid-mount/{before.json,build.log,shutdown.log,
install.log,startup-ready.log,startup.log,talzul-first.log,talzul-navigation.log,
roster-after.json,result.json}. Bericht2026-11-05-druid-mount-preparation.md.
Keine Commits/Push fürdiesen Auftrag; vorherige Gruppen-Recovery-WIP erhalten.
---
# 2026-11-05 – Talzul97 untersucht, keine Code-/Zustandsänderung
Nutzerauftrag nur untersuchen. Talzul Troll-Druide Lv66, solo, Map530
Höllenfeuerhalbinsel(346.7,2235.6,118.998), Freizeitintent leisure, mehrere
Fallbackziele poi_tele_1370->1162->1037. Native Flight status: enabled1,riding225,
outdoors1, mountSpell32245, Failed "native mount spell rejected", segments0.
Probe: nearFeet118.998=Z, mounted0,canFly0,flying0,falling0,keinWasser. Damit
kein Flugkorridor-/Bodenabstandsproblem: scheitert schon beim normalen Cast.
Character-Auras gespeichert:768(Katzengestalt),24932,47179; letzteKämpfe nutzten
1082. Wahrscheinliche Ursache: Katzenform nach Kampf bleibt, Mountbegin entfernt
nur GroundMount/MOUNTED, nicht SHAPESHIFT. Native Spell CheckShapeshift blockiert
solche Mountcasts. Tatsächlicher SpellCastResult wird bei ClientlessFlightMgr
Begin umZeile564 NICHT geloggt, daher konkrete Ablehnungsnummer noch offen.
12:12:30 Start+5Recoveries selben Ort/Route innerhalb1Sekunde, blockedEdges0,
RECOVERY_FAILED danachIntentFallback30s. Fehler besteht schon vor letztemFix.
Fix bei Auftrag: normale Reisezustandsvorbereitung verlässt unvereinbare Form
vor Mountcast (keine getriggerten Spells/umgangene Berechtigung), exakten Cast-
Result loggen und deterministische Castablehnung nicht fünfmal sofort wiederholen.
Keine Teleports/Entzauberung/Intentänderung durchgeführt. Bestehende Gruppen-
Recovery Source-WIP und Doc-WIP sind erwartet und unverändert erhalten.
---
# 2026-11-05 – Gruppenfolge-Recovery installiert und geprüft
Aktueller Auftrag: experimentell LOS bei validiertem MMAP deaktivieren und
nachgewiesen festhängende Gruppenfolger bergen. Nutzer bestätigte außerhalb
von Instanzen Spielerführer 100 Yards ODER 60 Sekunden; Botführer 300 Sekunden.
Bereits gleiche konkrete Dungeon-/Raidinstanz: 100 Yards ohne Zusatzfrist,
keinen Instanzeintritt und kein normales Eingangswarten ersetzen.
Implementiert in AdventureGroupTravelMgr/Policy: Positionsfortschritt >1 Yard,
expliziter Navigationsfehler oder 60 Sekunden angeforderte Bewegung ohne
Fortschritt; bestätigten Fehler bis echter Bewegung/Zustandswechsel erhalten.
Gebiet/Zone/Position über AdventureAreaMgr zwingend zulässig. Transit/Taxi/
Fahrzeug/Portaltransfer, Sturz, Kampf, Tod, Casting und sicherer Logout sperren.
Luftziel nur mit aktivem Flugmount UND Ziel-Flugrecht, optional regulärer Mount-
Versuch (10s), sonst auf Leader-Boden warten. MMAP-Validator überspringt nur
zusätzliche LOS; Bodenprüfung und aufgezeichnete/verschobene Straßen-LOS bleiben.
Details doc/clientless/2026-11-05-group-follow-recovery.md.
8 Regressionstests bestanden, letzter zusätzlicher Recoverytest ebenfalls.
Vollbuild -j10 und letzte Sourcefassung build-verified.log PASS, installiert.
Loader config/runtime0 und safe all bei jedem Buildstart; kein Shutdown vor
Buildende. Normaler Shutdown AltPID13497: grace60881ms, 16 restliche Bots durch
integriertes FINALIZE gesichert/ausgeloggt, remaining0, GDB exited normally.
Installation erst nach Worldserver-Prozessende; tmux/GDB endete unmittelbar
danach ebenfalls. Neuer passiver Start PID25378: frisches READY + beide Ports8085/8087,
Prozess lebt. Loader vorher1 wiederhergestellt; sämtliche118 vorherigen Bots
laut direktem DB-Abgleich wiederonline. Filterroster zählt117:155Renana online,
aber Loaderrotation state=replacing, nicht Logoutfehler.
Live: Torukan57 und Moggra87 haben vorher blockierten Unterstadtweg passiert,
beide jetzt um(1633,230,-43), vorher(1663,256,-62). Torukan NAV_COMPLETE
11:54:44, remaining0,repaths0,segments3. Keine Recoveryteleports dafür nötig.
Die Gruppe hat sich nach Neustart regulär neugebildet:87Leader statt130.
Teleportregeln vollständig automatisiert geprüft; gezielte Spieler-/Flug- und
Instanz-Akzeptanzprüfung durch Nutzer noch offen.
Belege checkpoints/2026-11-05-follow-recovery/{before.json,tests.log,
tests-recovery-final.log,build-verified.log,install.log,shutdown.log,startup.log}.
Kein Commit/Push für diesen Auftrag beauftragt; bestehende Diagnose-WIP erhalten.
---
# 2026-11-05 11:09 – Summonfix committed; Torukan/Moggra read-only untersucht
Nutzerauftragzuerstcommit,dannBotsprüfen:7f10f724f2 "Reject ownerless minion
summons before allocation" (2Cpp+Test+Bericht+Checkpoint5Dateien),keinPush.
DanachTorukan57Lv52undMoggra87Lv47nativuntersucht. Gruppe3LeaderDarlan130Lv48.
BeideinUnterstadtMap0(1663.455,256.192,-62.178), Leader(1632.014,347.887,-61.270).
AlleübernehmenRestintentLeader, nichtdiepersönlichenInstanzpläne; Zielwechselt
wegenLeaderFallbackzwischenGasthäusern(WestlPestlandeu.a328015/328422/328550).
KeinfreierFlugmöglichbeidenFollowern(riding150). KompletteMMAPpfadeberechnet,
aberAdventureGroupTravelMgr::ValidateGroundPathblockiertLOS: FollowerSegment7
(1647.873,274.032,-60.197)->(1644.342,275.909,-61.183), LeaderSegment8eigene
Stelle. BeideAdaptRoadVersuche(auchSide0)abgewiesen. Alternativpfadzusätzlich
GroundSampleheightError3.102/floorStep3.463überLimits1.75/2.458. 5Recoveries,
failedEdgeDbId0/blockedEdges0=gleicherEinstiegwirdwiederholt; Followersdanach
"group destination unavailable locally; waiting for leader". Leaderauchblockiert.
GroundPointändertbeiAdaptRoadalleWegpunktZaufGetHeight(z+3,dist18), danach
Ground+LOSCheckZ+1. VerdachtNativeMMAPwegwirddurchBodensnap/engeGeometrie
unbrauchbar, aberursprünglichePunkte/hitGeometrynochNICHTverglichen. Keine
pauschaleBehauptungLOSseiFalsePositive, keineGuardabschaltung. BeiFixauftrag
OriginalMMAP-/angepasstePunktevergleichenundlokaleAlternativenbehandeln.
NurDiagnose, keineTeleport/Gruppe/Intent/Codeänderung. Belegeignored
checkpoints/2026-11-05-torukan-moggra/{native-status.log,navigation.log,diagnosis.json}.
ServerPID13497weiteronline. AktuellerCheckpointnachCommitnaturgemäßneuesDoc-WIP.
---
# 2026-11-05 11:03 – Summon-Besitzerprüfung FERTIG installiert
Nutzerauftragguard/loggingvorMinionerzeugungumgesetzt. Object.cppMapFactory+
@@ -1,5 +1,6 @@
#include "AdventureGroupTravelMgr.h"
#include "AdventureAreaMgr.h"
#include "AdventureGroupMgr.h"
#include "AdventureGroupTravelPolicy.h"
#include "ClientlessTravelMgr.h"
#include "ClientlessTravelGraphMgr.h"
@@ -143,11 +144,12 @@ void AdventureGroupTravelMgr::Observe(Player* player,uint32 diff)
if(inserted.second)
{
sClientlessActionExecutorMgr->SetGroundPathAdapter(player->GetGUID().GetCounter(),[](Player* member,std::vector<Position>& path){return sAdventureGroupTravelMgr->AdaptRoad(member,path);});
sClientlessTravelMgr->SetPathValidator(player->GetGUID().GetCounter(),[](Player* member,std::vector<Position> const& path){return sAdventureGroupTravelMgr->ValidateGroundPath(member,path);});
sClientlessTravelMgr->SetPathValidator(player->GetGUID().GetCounter(),[](Player* member,std::vector<Position> const& path){return sAdventureGroupTravelMgr->ValidateGroundPath(member,path,true);});
}
if(auto flight=sClientlessFlightMgr->GetStatus(player->GetGUID().GetCounter()))
if(flight->State==ClientlessFlightState::Mounting || flight->State==ClientlessFlightState::Flying)record.FlightOwned=true;
if(group->GetLeaderGUID()==player->GetGUID())return;
RecoverFollower(player,record);
if(player->GetMap()->IsDungeon())
{
record.Pending.clear();record.SeparationMs=0;record.ProtectedTravel=true;
@@ -197,6 +199,123 @@ void AdventureGroupTravelMgr::Observe(Player* player,uint32 diff)
record.SeparationMs=AdventureGroupTravelPolicy::Separation(record.SeparationMs,elapsed,!near,record.ProtectedTravel || !player->IsAlive());
if(leader->GetSession()->IsClientless())record.SeparationMs=0; // late bot recruits retain their agreed rendezvous behavior
}
// This is a last-resort recovery of a failed follow, never a fast-travel rule.
void AdventureGroupTravelMgr::RecoverFollower(Player* player,Record& record)
{
auto group=player->GetGroup();
auto leader=group?ObjectAccessor::FindPlayer(group->GetLeaderGUID()):nullptr;
auto& recovery=record.RecoveryState;
auto now=GameTime::GetGameTime();
uint32 guid=player->GetGUID().GetCounter();
auto reset=[&]()
{
recovery={};
if(leader)recovery.Leader=leader->GetGUID();
recovery.Map=player->GetMapId();recovery.Instance=player->GetInstanceId();
recovery.PositionAtProgress.Relocate(player);recovery.ProgressAt=now;
};
if(!leader || leader==player){reset();return;}
if(recovery.Leader!=leader->GetGUID() || recovery.Map!=player->GetMapId() ||
recovery.Instance!=player->GetInstanceId() || !recovery.ProgressAt)
{reset();return;} // discard failures from outside/another instance or group
if(recovery.SampleAt==now)return;
recovery.SampleAt=now;
bool botLeader=leader->GetSession()->IsClientless();
auto inTransit=[](Player* member)
{
if(member->IsBeingTeleported() || member->IsInFlight() || member->GetTransport() || member->GetVehicle())return true;
auto ride=sClientlessTransportMgr->GetStatus(member->GetGUID().GetCounter());
return ride && ride->Phase!=ClientlessTransportPhase::Failed && ride->Phase!=ClientlessTransportPhase::Arrived;
};
if(!player->IsAlive() || !leader->IsAlive() || player->IsInCombat() || player->IsCharmed() ||
player->IsFalling() || leader->IsFalling() || inTransit(player) || inTransit(leader) ||
leader->HasUnitState(UNIT_STATE_CASTING) || (player->HasUnitState(UNIT_STATE_CASTING) && !recovery.MountUntil) ||
sClientlessPlayerMgr->IsShutdownDraining() || sClientlessPlayerMgr->IsSafeLogoutPending(guid) ||
(botLeader && sAdventureGroupMgr->Forming(player)))
{reset();return;}
bool instance=player->GetMap()->IsDungeon() || leader->GetMap()->IsDungeon();
// Require membership in the same live instance, not merely the same map ID.
// Normal rendezvous at the entrance has no navigation failure and stays put.
if(!player->IsInPhase(leader) || (instance && (record.EntryLeader!=leader->GetGUID() ||
record.EntryMap!=player->GetMapId() || record.EntryInstance!=player->GetInstanceId() ||
player->GetMap()!=leader->GetMap() ||
player->GetInstanceId()!=leader->GetInstanceId())) ||
(!instance && (player->GetMap()->Instanceable() || leader->GetMap()->Instanceable())))
{reset();return;}
if(recovery.PositionAtProgress.GetExactDist(player)>1.0f)
{reset();return;}
auto nav=sClientlessTravelMgr->GetStatus(guid);
auto action=sClientlessActionExecutorMgr->GetStatus(guid);
bool travelAction=action && (action->ActionName=="travel_to_node" || action->ActionName=="travel_to_location");
auto flight=sClientlessFlightMgr->GetStatus(guid);
if(flight && flight->FollowingPlayer && flight->State==ClientlessFlightState::Flying &&
player->GetExactDist(flight->Target)<2)
{reset();return;} // holding an assigned aerial formation is deliberate
bool failed=(flight && flight->FollowingPlayer && flight->State==ClientlessFlightState::Failed) || recovery.BlockedAt || (recovery.FailedAt && now-recovery.FailedAt<=5) ||
(nav && (nav->State==ClientlessTravelState::Stuck || nav->State==ClientlessTravelState::PathRejected)) ||
(travelAction && action->State==ClientlessActionExecutionState::Failed);
bool requested=(recovery.RequestedAt && now-recovery.RequestedAt<=5) ||
(nav && nav->State==ClientlessTravelState::Moving) ||
(flight && flight->State==ClientlessFlightState::Flying &&
(player->IsSplineEnabled() || (flight->FollowingPlayer && player->GetExactDist(flight->Target)>=2)));
if(!failed && !(requested && now-recovery.ProgressAt>=60) && !recovery.MountUntil)
{
// Retain a confirmed failure while mounting for this recovery.
if(!recovery.MountUntil){recovery.BlockedAt=0;if(!requested)recovery.ProgressAt=now;}
return;
}
if(!recovery.BlockedAt)recovery.BlockedAt=failed?now:recovery.ProgressAt;
float distance=player->GetMapId()==leader->GetMapId()?player->GetExactDist(leader):100000.0f;
if(!AdventureGroupTravelPolicy::Recover(instance,botLeader,distance,now-recovery.BlockedAt))return;
Position target;target.Relocate(leader);
// Check both the resolved destination and the leader's native area/zone.
// In particular, a long-distance recovery must never bypass faction/level rules.
if(!sAdventureAreaMgr->EnsureLoaded() || !sAdventureAreaMgr->AllowsArea(player,leader->GetAreaId()) ||
!sAdventureAreaMgr->AllowsArea(player,leader->GetZoneId()) ||
!sAdventureAreaMgr->AllowsPosition(player,leader->GetMapId(),target))
{record.Detail="stuck follow recovery denied by destination area rules";return;}
float ground=leader->GetMap()->GetHeight(leader->GetPhaseShift(),target.GetPositionX(),target.GetPositionY(),target.GetPositionZ()+2,true,6);
bool airborne=leader->IsFlying() || !std::isfinite(ground) || ground<=INVALID_HEIGHT || std::fabs(ground-target.GetPositionZ())>3;
if(airborne)
{
if(recovery.WaitForGround)return;
if(!sClientlessFlightMgr->Enabled(player) || !std::isfinite(sClientlessFlightMgr->Estimate(player,leader->GetMapId(),target,target,true)))
{record.Detail="stuck follow recovery waiting for permitted ground destination";return;}
if(!player->IsMounted() || !player->CanFly())
{
if(recovery.MountUntil)
{
if(now>=recovery.MountUntil){recovery.WaitForGround=true;record.Detail="recovery mount failed; waiting for leader to land";}
return;
}
if(!sClientlessFlightMgr->PrepareFollowingMount(player))
{recovery.WaitForGround=true;record.Detail="recovery mount unavailable; waiting for leader to land";return;}
recovery.MountUntil=now+10;return;
}
}
else {recovery.WaitForGround=false;recovery.MountUntil=0;}
auto blocked=now-recovery.BlockedAt;
sClientlessPlayerMgr->ClearQueuedActions(guid);
sClientlessActionExecutorMgr->Clear(guid);
sClientlessFlightMgr->Clear(guid);
player->StopMoving();
bool teleported=player->TeleportTo(leader->GetMapId(),target.GetPositionX(),target.GetPositionY(),target.GetPositionZ(),
target.GetOrientation(),TELE_TO_NONE,leader->GetInstanceId());
if(!teleported){record.Detail="stuck follow recovery teleport rejected";reset();return;}
if(player->IsBeingTeleported())player->GetSession()->CompleteClientlessTeleport();
record.Pending.clear();record.RouteOwned=false;record.FlightOwned=false;record.SeparationMs=0;
if(airborne && player->IsMounted() && player->CanFly())
{
player->AddUnitMovementFlag(MOVEMENTFLAG_FLYING);
std::string error;
record.FlightOwned=sClientlessFlightMgr->Follow(player,target,error);
}
record.Detail="recovered genuinely stuck follower to group leader";
TC_LOG_INFO("server.adventurebot.navigation",
"GROUP_FOLLOW_RECOVERY guid=%u leader=%u botLeader=%u map=%u instance=%u distance=%.1f blockedSeconds=%u airborne=%u",
guid,leader->GetGUID().GetCounter(),botLeader,player->GetMapId(),player->GetInstanceId(),distance,uint32(blocked),airborne);
reset();
}
bool AdventureGroupTravelMgr::Airborne(Player* player) const
{
if(!player->IsAlive())return false;
@@ -449,16 +568,17 @@ bool AdventureGroupTravelMgr::Follow(Player* player)
}
if(!leader->IsMounted() && player->IsMounted() && player->GetExactDist(leader)<12)sClientlessLuaEngine->DismountGroundMount(player,"group leader on foot");
if(player->GetExactDist(target)<1.2f)
{sClientlessTravelMgr->CancelTravel(guid);record.Detail="holding assigned formation position";return true;}
{record.RecoveryState.BlockedAt=0;record.RecoveryState.RequestedAt=0;record.RecoveryState.FailedAt=0;sClientlessTravelMgr->CancelTravel(guid);record.Detail="holding assigned formation position";return true;}
auto nav=sClientlessTravelMgr->GetStatus(guid);
if(nav && nav->State==ClientlessTravelState::Moving && nav->Target.GetExactDist(target)<0.75f)
{
if(auto active=sClientlessTravelMgr->GetActivePath(guid))if(!ValidateGroundPath(player,*active))
{sClientlessTravelMgr->CancelTravel(guid);record.Detail="active follow corridor became unsafe";}
if(auto active=sClientlessTravelMgr->GetActivePath(guid))if(!ValidateGroundPath(player,*active,true))
{record.RecoveryState.FailedAt=GameTime::GetGameTime();sClientlessTravelMgr->CancelTravel(guid);record.Detail="active follow corridor became unsafe";}
return true;
}
record.RecoveryState.RequestedAt=GameTime::GetGameTime();
if(!SafePath(player,target))
{sClientlessTravelMgr->CancelTravel(guid);record.Detail="no safe complete follow path; waiting";return true;}
{record.RecoveryState.FailedAt=GameTime::GetGameTime();sClientlessTravelMgr->CancelTravel(guid);record.Detail="no safe complete follow path; waiting";return true;}
if(leader->IsMounted() && !(flightLeader && player->CanFly()))
{
auto profile=sClientlessLuaEngine->GetAdventureProfile(guid);
@@ -466,6 +586,7 @@ bool AdventureGroupTravelMgr::Follow(Player* player)
}
else if(!leader->IsMounted() && player->IsMounted() && player->GetExactDist(leader)<12)sClientlessLuaEngine->DismountGroundMount(player,"group leader on foot");
bool started=sClientlessTravelMgr->StartTravel(player,target,true);
if(!started)record.RecoveryState.FailedAt=GameTime::GetGameTime();
record.Detail=started?"safe ground formation":"follow spline rejected; waiting";return true;
}
bool AdventureGroupTravelMgr::AdaptRoad(Player* player,std::vector<Position>& path)
@@ -515,7 +636,7 @@ bool AdventureGroupTravelMgr::AdaptRoad(Player* player,std::vector<Position>& pa
}
return false;
}
bool AdventureGroupTravelMgr::ValidateGroundPath(Player* player,std::vector<Position> const& path) const
bool AdventureGroupTravelMgr::ValidateGroundPath(Player* player,std::vector<Position> const& path,bool mmapValidated) const
{
for(size_t i=1;i<path.size();++i)
{
@@ -536,7 +657,9 @@ bool AdventureGroupTravelMgr::ValidateGroundPath(Player* player,std::vector<Posi
}
last=floor;
}
if(!player->GetMap()->isInLineOfSight(player->GetPhaseShift(),a.GetPositionX(),a.GetPositionY(),a.GetPositionZ()+1,b.GetPositionX(),b.GetPositionY(),b.GetPositionZ()+1,LINEOFSIGHT_ALL_CHECKS,VMAP::ModelIgnoreFlags(0)))
// Native validated MMAP corridors already establish traversability.
// Recorded/formation-shifted splines retain the additional geometry guard.
if(!mmapValidated && !player->GetMap()->isInLineOfSight(player->GetPhaseShift(),a.GetPositionX(),a.GetPositionY(),a.GetPositionZ()+1,b.GetPositionX(),b.GetPositionY(),b.GetPositionZ()+1,LINEOFSIGHT_ALL_CHECKS,VMAP::ModelIgnoreFlags(0)))
{
TC_LOG_WARN("server.adventurebot.navigation",
"GROUP_GROUND_REJECT guid=%u map=%u check='line-of-sight' segment=%u from=(%.3f %.3f %.3f) to=(%.3f %.3f %.3f)",
@@ -18,7 +18,7 @@ public:
bool Follow(Player* player);
bool HoldForInstanceLeader(Player* player);
bool AdaptRoad(Player* player, std::vector<Position>& path);
bool ValidateGroundPath(Player* player, std::vector<Position> const& path) const;
bool ValidateGroundPath(Player* player, std::vector<Position> const& path, bool mmapValidated = false) const;
bool Protected(Player* player) const;
bool Airborne(Player* player) const;
bool Separated(Player* player) const;
@@ -41,8 +41,17 @@ private:
time_t SampleAt = 0, Began = 0, Arrived = 0;
uint32 Generation = 0;
};
struct Recovery
{
ObjectGuid Leader;
uint32 Map = 0, Instance = 0;
Position PositionAtProgress;
time_t ProgressAt = 0, BlockedAt = 0, SampleAt = 0, RequestedAt = 0, FailedAt = 0, MountUntil = 0;
bool WaitForGround = false;
};
struct Record
{
Recovery RecoveryState;
ObjectGuid LeaderGuid;
uint32 Timer = 0, SeparationMs = 0, Generation = 0, BotMap = 0;
Position ProgressPosition;
@@ -55,6 +64,7 @@ private:
std::deque<Leader> Pending;
};
struct Width { time_t At = 0; Position Center; unsigned Columns = 1; };
void RecoverFollower(Player* player, Record& record);
void SampleLeader(Player* player, Leader& state, time_t now);
bool GroundPoint(Player* player, Position& point) const;
bool SafePath(Player* player, Position const& point) const;
@@ -12,6 +12,12 @@ namespace AdventureGroupTravelPolicy
constexpr float LocalFollowRange = 100;
constexpr float MaxGroundStep = 1.75f;
constexpr float MaxGroundSlope = 0.8f;
inline bool Recover(bool instance, bool botLeader, float distance, std::int64_t blockedSeconds)
{
if (distance < 2 || blockedSeconds < 0) return false;
if (instance) return distance >= 100;
return botLeader ? blockedSeconds >= 300 : distance >= 100 || blockedSeconds >= 60;
}
struct Offset { float Behind, Side, Height; };
inline Offset Formation(unsigned slot, unsigned columns, bool moving, bool mounted, bool flying)
{
+11 -2
View File
@@ -1,6 +1,7 @@
#include "AdventureAreaMgr.h"
#include "ClientlessTravelTelemetry.h"
#include "ClientlessFlightMgr.h"
#include "ClientlessMountPreparation.h"
#include <cstring>
#include "ClientlessLuaEngine.h"
#include "ClientlessTravelMgr.h"
@@ -248,10 +249,14 @@ bool ClientlessFlightMgr::PrepareFollowingMount(Player* player)
total+=weight;if(urand(1,total)<=weight)spell=mount.DisplayId;
}
if(!spell)return false;
if(!ClientlessMountPreparation::LeaveDruidForm(player))return false;
sClientlessTravelMgr->CancelTravel(player->GetGUID().GetCounter());player->StopMoving();
sClientlessLuaEngine->DismountGroundMount(player,"group flying mount");
player->RemoveAurasByType(SPELL_AURA_MOUNTED);
return player->CastSpell(player,spell,false)==SPELL_CAST_OK;
auto result=player->CastSpell(player,spell,false);
if(result!=SPELL_CAST_OK)
TC_LOG_WARN("server.adventurebot.navigation","FLIGHT_MOUNT_REJECT guid=%u spell=%u result=%u context='group-follow'",player->GetGUID().GetCounter(),spell,uint32(result));
return result==SPELL_CAST_OK;
}
void ClientlessFlightMgr::HoldFollowing(Player* player,Record& record)
@@ -521,6 +526,8 @@ bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::s
ClientlessTravelTelemetry::Note(player,"flight-landing-rejected",error);
return false;
}
if(!existingMount && !ClientlessMountPreparation::LeaveDruidForm(player))
{error="druid form cannot be left safely before mounting";return false;}
sClientlessTravelMgr->CancelTravel(player->GetGUID().GetCounter());
player->StopMoving();
if(!existingMount)
@@ -561,9 +568,11 @@ bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::s
Store(player->GetGUID().GetCounter(),std::move(record));return true;
}
Store(player->GetGUID().GetCounter(),std::move(record));
if (!existingMount && player->CastSpell(player, spell, false) != SPELL_CAST_OK)
auto mountResult=existingMount?SPELL_CAST_OK:player->CastSpell(player,spell,false);
if (mountResult != SPELL_CAST_OK)
{
Fail(player, _records.at(player->GetGUID().GetCounter()), "native mount spell rejected");
TC_LOG_WARN("server.adventurebot.navigation","FLIGHT_MOUNT_REJECT guid=%u spell=%u result=%u context='free-flight'",player->GetGUID().GetCounter(),spell,uint32(mountResult));
error = "native mount spell rejected";
return false;
}
@@ -1,5 +1,6 @@
#include "AdventureProfessionPolicy.h"
#include "ClientlessLuaEngine.h"
#include "ClientlessMountPreparation.h"
#include "AdventureRotationPolicy.h"
#include "ClientlessPlayerMgr.h"
#include "ClientlessLoginRecovery.h"
@@ -647,6 +648,8 @@ bool ClientlessLuaEngine::PrepareGroundMount(Player* player, float travelDistanc
return false;
if (travelDistance < profile->GroundMountMinDistance || !player->IsOutdoors())
return false;
if (!ClientlessMountPreparation::LeaveDruidForm(player))
return false;
if (player->IsMounted())
return true;
@@ -0,0 +1,26 @@
#ifndef VCORE_CLIENTLESS_MOUNT_PREPARATION_H
#define VCORE_CLIENTLESS_MOUNT_PREPARATION_H
#include "Player.h"
namespace ClientlessMountPreparation
{
// Cancel the aura category, not individual spell IDs: covers every druid
// form and its ranks, including travel, aquatic, flight and talent forms.
inline bool LeaveDruidForm(Player* player)
{
if (player->getClass() != CLASS_DRUID || !player->HasAuraType(SPELL_AURA_MOD_SHAPESHIFT))
return true;
// Never drop a flying/swimming druid out of a form in mid-transition.
// The normal landing/transport owner must finish before mounting.
if (!player->IsAlive() || player->IsInCombat() || player->IsFlying() || player->IsFalling() ||
player->IsInWater() || player->IsInFlight() || player->GetTransport() || player->GetVehicle() ||
player->IsBeingTeleported())
return false;
player->RemoveAurasByType(SPELL_AURA_MOD_SHAPESHIFT);
return !player->HasAuraType(SPELL_AURA_MOD_SHAPESHIFT);
}
}
#endif
+82 -10
View File
@@ -26,7 +26,7 @@ using uint32=unsigned;
#define TC_LOG_WARN(...) ((void)0)
#define TC_LOG_INFO(...) ((void)0)
constexpr float INVALID_HEIGHT=-100000;
constexpr int LINEOFSIGHT_ALL_CHECKS=0,CMSG_AREATRIGGER=0;
constexpr int LINEOFSIGHT_ALL_CHECKS=0,CMSG_AREATRIGGER=0,TELE_TO_NONE=0,MOVEMENTFLAG_FLYING=1,UNIT_STATE_CASTING=2;
namespace VMAP{enum class ModelIgnoreFlags{None};}
struct ObjectGuid{unsigned id=0;unsigned GetCounter()const{return id;}void Clear(){id=0;}explicit operator bool()const{return id;}bool operator==(ObjectGuid b)const{return id==b.id;}bool operator!=(ObjectGuid b)const{return id!=b.id;}bool operator<(ObjectGuid b)const{return id<b.id;}};
namespace std{template<>struct hash<ObjectGuid>{size_t operator()(ObjectGuid g)const{return g.id;}};}
@@ -38,17 +38,20 @@ struct Position{
float GetExactDist(Position const*p)const{return GetExactDist(*p);}float GetExactDist(float x,float y,float z)const{return GetExactDist(Position{x,y,z});}
float GetExactDist2d(Position const&p)const{return std::hypot(m_positionX-p.m_positionX,m_positionY-p.m_positionY);}
};
struct Map{bool cliff=false,los=true,narrow=false,dungeon=false;bool IsDungeon(){return dungeon;}
struct Map{bool cliff=false,los=true,narrow=false,dungeon=false;bool IsDungeon(){return dungeon;}bool Instanceable(){return dungeon;}
float GetHeight(unsigned phase,float x,float y,float,bool,float){assert(phase==169);if(cliff && x>4 && x<15)return -1000;if(narrow && std::fabs(y)>2)return -1000;return 0;}
bool isInLineOfSight(unsigned,float,float,float,float,float,float,int,VMAP::ModelIgnoreFlags){return los;}
}map;
struct Session{bool clientless=false;bool IsClientless(){return clientless;}void HandleAreaTriggerOpcode(struct WorldPacket&){}};
struct Session{bool clientless=false;bool IsClientless(){return clientless;}void CompleteClientlessTeleport(){}void HandleAreaTriggerOpcode(struct WorldPacket&){}};
struct Group{ObjectGuid leader{21};struct Slot{ObjectGuid guid;};std::vector<Slot>slots{{{21}},{{26}},{{27}}};ObjectGuid GetLeaderGUID(){return leader;}auto const&GetMemberSlots(){return slots;}}group;
struct Player:Position{
ObjectGuid guid;bool alive=true,combat=false,teleport=false,charmed=false,flying=false,falling=false,mounted=false,moving=false,taxi=false,transport=false,phase=true,visible=true;unsigned mapid=0,instance=0,stops=0;Group*g=&group;Session session;
ObjectGuid guid;bool alive=true,combat=false,teleport=false,charmed=false,flying=false,falling=false,mounted=false,moving=false,taxi=false,transport=false,phase=true,visible=true;unsigned mapid=0,instance=0,stops=0;Group*g=&group;Session session;bool vehicle=false,areaAllowed=true,casting=false;bool HasUnitState(unsigned){return casting;}unsigned teleports=0;
Player(unsigned id):guid{id}{}ObjectGuid GetGUID(){return guid;}Group*GetGroup(){return g;}Map*GetMap(){return &map;}unsigned GetMapId(){return mapid;}unsigned GetInstanceId(){return instance;}bool IsSplineEnabled(){return moving;}void StopMoving(){moving=false;++stops;}unsigned GetPhaseShift(){return 169;}
Session*GetSession(){return &session;}bool IsAlive(){return alive;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool IsCharmed(){return charmed;}
bool CanFly(){return mounted && flightCapable;}bool flightCapable=false;bool IsFlying(){return flying;}bool IsFalling(){return falling;}bool IsMounted(){return mounted;}bool isMoving(){return moving;}bool IsInFlight(){return taxi;}bool GetTransport(){return transport;}
bool GetVehicle(){return vehicle;}unsigned GetAreaId(){return areaAllowed?1:2;}unsigned GetZoneId(){return 1;}
bool TeleportTo(unsigned m,float x,float y,float z,float o,unsigned,unsigned inst){++teleports;mapid=m;instance=inst;Relocate(x,y,z,o);return true;}
void AddUnitMovementFlag(unsigned){flying=true;}
bool IsInMap(Player*p){return mapid==p->mapid;}bool IsInPhase(Player*p){return phase && p->phase;}bool CanSeeOrDetect(Player*p){return visible && p->visible;}bool IsInAreaTriggerRadius(void*){return false;}
};
struct WorldPacket{WorldPacket(int,int){}template<class T>WorldPacket&operator<<(T){return *this;}};
@@ -57,9 +60,9 @@ namespace ObjectAccessor{std::unordered_map<ObjectGuid,Player*>players;Player*Fi
namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}}
struct Areas{void*LookupEntry(unsigned){return nullptr;}}sAreaTriggerStore;
enum class ClientlessActionExecutionState{Moving,SelectingTarget,Completed,Failed};
struct Executor{struct Status{ClientlessActionExecutionState State=ClientlessActionExecutionState::Completed;}status;unsigned clears=0,adapters=0;auto GetStatus(unsigned){return &status;}bool RequestStopAtNode(unsigned){return true;}void Clear(unsigned){++clears;status.State=ClientlessActionExecutionState::Completed;}void SetGroundPathAdapter(unsigned,std::function<bool(Player*,std::vector<Position>&)> adapter){adapters+=adapter?1:-1;}}executor;auto sClientlessActionExecutorMgr=&executor;
struct Executor{struct Status{ClientlessActionExecutionState State=ClientlessActionExecutionState::Completed;std::string ActionName;}status;unsigned clears=0,adapters=0;auto GetStatus(unsigned){return &status;}bool RequestStopAtNode(unsigned){return true;}void Clear(unsigned){++clears;status.State=ClientlessActionExecutionState::Completed;}void SetGroundPathAdapter(unsigned,std::function<bool(Player*,std::vector<Position>&)> adapter){adapters+=adapter?1:-1;}}executor;auto sClientlessActionExecutorMgr=&executor;
struct ClientlessAction{std::string Name;std::unordered_map<std::string,std::string>Parameters;};
struct Core{unsigned queued=0;std::vector<std::string>nodes;unsigned GetQueuedActionCount(unsigned){return queued;}bool QueueAction(unsigned,ClientlessAction action){++queued;nodes.push_back(action.Parameters["node"]);return true;}void ClearQueuedActions(unsigned){queued=0;}}core;auto sClientlessPlayerMgr=&core;
struct Core{bool draining=true,safeLogout=false;bool IsShutdownDraining(){return draining;}bool IsSafeLogoutPending(unsigned){return safeLogout;}unsigned queued=0;std::vector<std::string>nodes;unsigned GetQueuedActionCount(unsigned){return queued;}bool QueueAction(unsigned,ClientlessAction action){++queued;nodes.push_back(action.Parameters["node"]);return true;}void ClearQueuedActions(unsigned){queued=0;}}core;auto sClientlessPlayerMgr=&core;
enum class ClientlessTravelState{Moving,Arrived,Stuck,PathRejected};
struct ClientlessPathDiagnostics{bool Complete=true,Suspicious=false;float PathLength=20;};
struct Travel{struct Status{ClientlessTravelState State=ClientlessTravelState::Arrived;Position Target;}status;bool valid=true;unsigned starts=0,cancels=0,validators=0;std::vector<Position>targets;
@@ -73,17 +76,18 @@ enum class ClientlessTransportPhase{Waiting,Riding,Arrived,Failed};
struct TransportStatus{ClientlessTransportPhase Phase=ClientlessTransportPhase::Failed;};struct Stop{unsigned Map=0;struct Point{Position Pos;};std::vector<Point>Points{{{0,0,0}}};};
struct Transport{TransportStatus status;auto GetStatus(unsigned){return &status;}Stop*GetStop(std::string const&){return nullptr;}bool Start(Player*,std::string const&,std::string const&,std::string&){return false;}}transport;auto sClientlessTransportMgr=&transport;
enum class ClientlessFlightState{Mounting,Flying,Arrived,Failed};
struct Flight{struct Status{ClientlessFlightState State=ClientlessFlightState::Failed;bool FollowingPlayer=false;}status;bool enabled=false;unsigned follows=0,lands=0;auto GetStatus(unsigned){return &status;}bool Enabled(Player*){return enabled;}bool PrepareFollowingMount(Player*){return false;}
struct Flight{struct Status{ClientlessFlightState State=ClientlessFlightState::Failed;bool FollowingPlayer=false;Position Target;}status;bool enabled=false,mountSucceeds=false,permitted=true;unsigned follows=0,lands=0,mountAttempts=0;float Estimate(Player*,unsigned,Position const&,Position const&,bool){return permitted?0:INFINITY;}void Clear(unsigned){status.State=ClientlessFlightState::Failed;}auto GetStatus(unsigned){return &status;}bool Enabled(Player*){return enabled;}bool PrepareFollowingMount(Player*){++mountAttempts;return mountSucceeds;}
bool Follow(Player*,Position const&,std::string&){++follows;status.State=ClientlessFlightState::Flying;status.FollowingPlayer=true;return true;}bool LandFollowing(Player*){++lands;status.FollowingPlayer=false;return true;}}flight;auto sClientlessFlightMgr=&flight;
struct Profile{float GroundMountMinDistance=60;};struct Lua{Profile profile;auto GetAdventureProfile(unsigned){return &profile;}void PrepareGroundMount(Player*p,float,char const*){p->mounted=true;}void DismountGroundMount(Player*p,char const*){p->mounted=false;}}lua;auto sClientlessLuaEngine=&lua;
struct AreaAccess{bool allowed=true;bool AllowsPosition(Player*,unsigned,Position const&){return allowed;}}access;auto sAdventureAreaMgr=&access;
struct AreaAccess{bool allowed=true;bool EnsureLoaded(){return true;}bool AllowsArea(Player*,unsigned a){return a==1;}bool AllowsPosition(Player*,unsigned,Position const&){return allowed;}}access;auto sAdventureAreaMgr=&access;
struct ChatHandler{void SendSysMessage(char const*){}template<class...T>void PSendSysMessage(char const*,T...){}};
struct GroupMgr{bool forming=false;bool Forming(Player*){return forming;}}groupMgr;auto sAdventureGroupMgr=&groupMgr;
'''+h+r'''
AdventureGroupTravelMgr* AdventureGroupTravelMgr::instance(){static AdventureGroupTravelMgr m;return &m;}
AdventureGroupTravelMgr::Leader observed;
void AdventureGroupTravelMgr::SampleLeader(Player*,Leader&state,time_t){state=observed;}
'''+method(' bool Executing(')+method(' Position OffsetPoint(')
for sig in ['void AdventureGroupTravelMgr::Observe(', 'bool AdventureGroupTravelMgr::Airborne(', 'bool AdventureGroupTravelMgr::Protected(', 'bool AdventureGroupTravelMgr::Separated(', 'bool AdventureGroupTravelMgr::GroundPoint(', 'bool AdventureGroupTravelMgr::SafePath(', 'unsigned AdventureGroupTravelMgr::Columns(', 'Position AdventureGroupTravelMgr::Formation(', 'bool AdventureGroupTravelMgr::Route(', 'bool AdventureGroupTravelMgr::JourneyFollow(', 'bool AdventureGroupTravelMgr::HoldForInstanceLeader(', 'bool AdventureGroupTravelMgr::Follow(', 'bool AdventureGroupTravelMgr::AdaptRoad(', 'bool AdventureGroupTravelMgr::ValidateGroundPath(', 'void AdventureGroupTravelMgr::Clear(']:code+=method(sig)+'\n'
for sig in ['void AdventureGroupTravelMgr::RecoverFollower(', 'void AdventureGroupTravelMgr::Observe(', 'bool AdventureGroupTravelMgr::Airborne(', 'bool AdventureGroupTravelMgr::Protected(', 'bool AdventureGroupTravelMgr::Separated(', 'bool AdventureGroupTravelMgr::GroundPoint(', 'bool AdventureGroupTravelMgr::SafePath(', 'unsigned AdventureGroupTravelMgr::Columns(', 'Position AdventureGroupTravelMgr::Formation(', 'bool AdventureGroupTravelMgr::Route(', 'bool AdventureGroupTravelMgr::JourneyFollow(', 'bool AdventureGroupTravelMgr::HoldForInstanceLeader(', 'bool AdventureGroupTravelMgr::Follow(', 'bool AdventureGroupTravelMgr::AdaptRoad(', 'bool AdventureGroupTravelMgr::ValidateGroundPath(', 'void AdventureGroupTravelMgr::Clear(']:code+=method(sig)+'\n'
code+=r'''
int main(){
using namespace AdventureGroupTravelPolicy;
@@ -195,10 +199,78 @@ int main(){
assert(mgr._records.count(26)&&executor.adapters==2&&travel.validators==2);
auto cancelsBefore=travel.cancels;a.g=nullptr;mgr.Observe(&a,500);
assert(!mgr._records.count(26)&&executor.adapters==1&&travel.validators==1&&travel.cancels==cancelsBefore+1);
// MMAP-proven corridors may round a wall; arbitrary recorded paths still need LOS.
a=Player(26);leader=Player(21);map={};map.los=false;
std::vector<Position> aroundWall{{0,0,0},{10,0,0}};
assert(!mgr.ValidateGroundPath(&a,aroundWall));
assert(mgr.ValidateGroundPath(&a,aroundWall,true));
map.cliff=true;assert(!mgr.ValidateGroundPath(&a,aroundWall,true));map={};
// Exercise the production recovery implementation with native-state substitutes.
core.draining=false;core.safeLogout=false;flight.enabled=false;flight.status={};
executor.status={};travel.status.State=ClientlessTravelState::Arrived;
AdventureGroupTravelMgr::Record rec;
auto begin=[&](float distance,bool bot=false){
a=Player(26);leader=Player(21);leader.m_positionX=distance;leader.session.clientless=bot;
rec={};map={};core.safeLogout=false;groupMgr.forming=false;access.allowed=true;
executor.status={};travel.status.State=ClientlessTravelState::Arrived;flight.status={};
flight.enabled=false;flight.mountSucceeds=false;flight.permitted=true;
GameTime::now+=1000;mgr.RecoverFollower(&a,rec);
};
auto step=[&](unsigned seconds=1){GameTime::now+=seconds;mgr.RecoverFollower(&a,rec);};
auto failed=[&](){rec.RecoveryState.FailedAt=GameTime::now;};
begin(120);step(600);assert(a.teleports==0); // idle is never stuck
begin(120);failed();step();assert(a.teleports==1); // human: >=100 and failed
begin(99);failed();step();assert(a.teleports==0);
step(59);assert(a.teleports==0);
failed();step();assert(a.teleports==1); // human: 60s confirmed failure
begin(120,true);failed();step();
step(299);assert(a.teleports==0);step();assert(a.teleports==1); // bot: 300 seconds
begin(120);flight.status.FollowingPlayer=true;flight.status.State=ClientlessFlightState::Flying;flight.status.Target=a;
step(600);assert(a.teleports==0); // deliberate aerial formation hold
begin(120);flight.status.FollowingPlayer=true;flight.status.State=ClientlessFlightState::Failed;step();assert(a.teleports==1);
begin(120);travel.status.State=ClientlessTravelState::Moving;
step(59);assert(a.teleports==0);step();assert(a.teleports==1); // silent movement stall
begin(120);travel.status.State=ClientlessTravelState::Moving;
for(int i=0;i<10;++i){a.m_positionX+=2;step(20);}assert(a.teleports==0); // real progress
begin(120);access.allowed=false;failed();step();assert(a.teleports==0);
begin(120);leader.areaAllowed=false;failed();step();assert(a.teleports==0);
begin(120);leader.taxi=true;failed();step();assert(a.teleports==0);
begin(120);a.transport=true;failed();step();assert(a.teleports==0);
begin(120);executor.status.ActionName="travel_to_location";executor.status.State=ClientlessActionExecutionState::Failed;step();assert(a.teleports==1);
begin(120);a.phase=false;failed();step();assert(a.teleports==0);
begin(120);a.combat=true;failed();step();assert(a.teleports==0);
begin(120);a.alive=false;failed();step();assert(a.teleports==0);
begin(120);leader.teleport=true;failed();step();assert(a.teleports==0);
begin(120);leader.casting=true;failed();step();assert(a.teleports==0);
begin(120);leader.vehicle=true;failed();step();assert(a.teleports==0);
begin(120);transport.status.Phase=ClientlessTransportPhase::Waiting;failed();step();assert(a.teleports==0);transport.status.Phase=ClientlessTransportPhase::Failed;
begin(120);core.safeLogout=true;failed();step();assert(a.teleports==0);
begin(120,true);groupMgr.forming=true;failed();step(600);assert(a.teleports==0);
begin(120);map.dungeon=true;a.instance=1;leader.instance=2;failed();step();assert(a.teleports==0); // different copies
begin(120);map.dungeon=true;a.instance=leader.instance=1;
step();rec.EntryLeader=leader.guid;rec.EntryMap=a.mapid;rec.EntryInstance=a.instance;failed();step();assert(a.teleports==1); // already in same copy
begin(120,true);map.dungeon=true;a.instance=leader.instance=1;step();
rec.EntryLeader=leader.guid;rec.EntryMap=a.mapid;rec.EntryInstance=a.instance;failed();step();assert(a.teleports==1);
begin(99);map.dungeon=true;a.instance=leader.instance=1;step();failed();step(600);assert(a.teleports==0);
begin(120);map.dungeon=true;a.instance=leader.instance=1;step();step(600);assert(a.teleports==0); // entrance wait
begin(120);failed();a.instance=1;map.dungeon=true;leader.instance=1;step();assert(a.teleports==0); // discard pre-entry failure
begin(120);leader.flying=true;leader.m_positionZ=50;failed();step();assert(a.teleports==0); // ground bot, no flying skill
begin(120);leader.flying=true;leader.m_positionZ=50;flight.enabled=true;failed();step();assert(a.teleports==0);
assert(rec.RecoveryState.WaitForGround); // mount failed => await landing
leader.flying=false;leader.m_positionZ=0;failed();step();assert(a.teleports==1);
begin(120);leader.flying=true;leader.m_positionZ=50;flight.enabled=true;flight.mountSucceeds=true;
failed();step();assert(a.teleports==0&&rec.RecoveryState.MountUntil);
a.mounted=a.flightCapable=true;failed();step();assert(a.teleports==1&&a.flying);
begin(120);leader.flying=true;leader.m_positionZ=50;flight.enabled=true;flight.mountSucceeds=true;failed();step();
a.mounted=a.flightCapable=true;access.allowed=false;failed();step();assert(a.teleports==0); // recheck after mounting
begin(120);leader.flying=true;leader.m_positionZ=50;flight.enabled=true;flight.permitted=false;a.mounted=a.flightCapable=true;
failed();step();assert(a.teleports==0); // destination flight permission
begin(120);leader.flying=true;leader.m_positionZ=50;flight.enabled=true;flight.mountSucceeds=true;
failed();step();failed();step(10);assert(a.teleports==0&&rec.RecoveryState.WaitForGround); // cast timeout
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['g++','-std=c++17','-I',str(root/'src/server/game/World'),str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Production group travel: distinct slots, phase169 cliff safety, width, mounts, flight/kick landing, taxi grace, portal queue, bounded separation and recorded-road validation passed')
print('Production group travel: distinct slots, phase169 cliff safety, width, mounts, flight/kick landing, taxi grace, portal queue, bounded separation, recorded-road validation, MMAP LOS bypass and guarded stuck-follower recovery passed')
@@ -16,6 +16,7 @@ code=r'''
#include <ctime>
using uint32=unsigned;
#define TC_LOG_INFO(...) ((void)0)
constexpr int CLASS_DRUID=11,SPELL_AURA_MOD_SHAPESHIFT=9;
constexpr int CHARACTER_FLAG_PVP_DESIRED=65536,PLAYER_FLAGS=1,PLAYER_FLAGS_IN_PVP=512,RACE_WORGEN=22,
SPELL_TWO_FORMS_RACIAL=68996,SPELL_ALTERED_FORM_RACIAL=97709,SPELL_AURA_WORGEN_ALTERED_FORM=1,UNIT_STATE_CASTING=2,MOVE_RUN=3;
namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}}
@@ -23,6 +24,7 @@ unsigned roll=0;unsigned urand(unsigned,unsigned){return roll;}
struct Guid{unsigned GetCounter(){return 26;}};
struct Map{bool dungeon=false;bool IsDungeon(){return dungeon;}}map;
struct Player{
unsigned klass=1,form=0;unsigned getClass(){return klass;}bool IsFalling(){return false;}bool IsInWater(){return false;}bool GetVehicle(){return false;}void RemoveAurasByType(int type){assert(type==SPELL_AURA_MOD_SHAPESHIFT);form=0;}
unsigned race=22,level=60,flags=0,characterFlags=0,display=0,casts=0,updates=0;
bool world=true,alive=true,combat=false,mounted=false,transport=false,taxi=false,flying=false,teleport=false,casting=false,outdoors=true,pvp=false,fail=false;
float speed=1;std::unordered_set<unsigned>spells,auras;struct{unsigned EndTimer=10;}pvpInfo;
@@ -30,7 +32,7 @@ struct Player{
bool IsPvP(){return pvp;}void UpdatePvP(bool on,bool){pvp=on;pvpInfo.EndTimer=0;++updates;}
unsigned getRace(){return race;}unsigned getLevel(){return level;}bool IsAlive(){return alive;}bool IsInCombat(){return combat;}
bool IsMounted(){return mounted;}bool GetTransport(){return transport;}bool IsInFlight(){return taxi;}bool IsFlying(){return flying;}
bool IsBeingTeleported(){return teleport;}bool HasUnitState(int){return casting;}bool HasAuraType(int){return auras.count(97709);}
bool IsBeingTeleported(){return teleport;}bool HasUnitState(int){return casting;}bool HasAuraType(int type){return type==SPELL_AURA_MOD_SHAPESHIFT?form!=0:auras.count(97709);}
bool HasSpell(unsigned id){return spells.count(id);}void LearnSpell(unsigned id,bool){spells.insert(id);}
bool HasAura(unsigned id){return auras.count(id);}bool IsOutdoors(){return outdoors;}
void CastSpell(Player*,unsigned id,bool){++casts;if(fail)return;if(id==97709)auras.insert(id);if(id==68996)auras.erase(97709);if(id==87840){auras.insert(id);mounted=true;display=29422;speed=2;}}
@@ -43,7 +45,7 @@ struct ClientlessLuaEngine{
ClientlessAdventureProfile*GetAdventureProfile(unsigned){return &profile;}unsigned SelectGroundMountDisplayId(unsigned){return 2404;}
void UpdateWorldPresence(Player*,std::string const&,bool=false);bool PrepareGroundMount(Player*,float,char const*);void DismountGroundMount(Player*,char const*);
};
'''+fn('void ClientlessLuaEngine::UpdateWorldPresence(')+fn('bool ClientlessLuaEngine::PrepareGroundMount(')+fn('void ClientlessLuaEngine::DismountGroundMount(')+r'''
'''+'\n'.join(v for v in (root/'src/server/game/World/ClientlessMountPreparation.h').read_text().splitlines() if not v.startswith('#'))+fn('void ClientlessLuaEngine::UpdateWorldPresence(')+fn('bool ClientlessLuaEngine::PrepareGroundMount(')+fn('void ClientlessLuaEngine::DismountGroundMount(')+r'''
int main(){
ClientlessLuaEngine e;Player p;e.UpdateWorldPresence(&p,"adventure");
assert(p.pvp&&p.pvpInfo.EndTimer==0&&(p.flags&512)&&(p.characterFlags&65536)&&p.HasAura(97709));
@@ -63,6 +65,14 @@ int main(){
// Failed native casts are throttled, not spammed every world tick.
p.fail=true;GameTime::now+=6;e.UpdateWorldPresence(&p,"instance");auto casts=p.casts;
e.UpdateWorldPresence(&p,"instance");assert(p.casts==casts);
p.klass=CLASS_DRUID;p.race=8;p.fail=false;
for(unsigned spell:{768u,5487u,9634u,783u,1066u,24858u,33891u,33943u,40120u}){
p.form=spell;p.mounted=false;assert(e.PrepareGroundMount(&p,100,"druid")&&!p.form&&p.mounted);
}
p.form=5487;p.mounted=false;p.combat=true;
assert(!e.PrepareGroundMount(&p,100,"combat")&&p.form==5487&&!p.mounted);p.combat=false;
p.form=33943;p.flying=true;assert(!e.PrepareGroundMount(&p,100,"airborne")&&p.form==33943);p.flying=false;
p.form=0;
p.race=1;p.pvp=false;p.pvpInfo.EndTimer=5;e.UpdateWorldPresence(&p,"rest");assert(p.pvp&&!p.pvpInfo.EndTimer);
}
'''
@@ -70,4 +80,4 @@ with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['g++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Production world presence: persistent PvP, Worgen intent/arrival decisions, travel deferral, native Running Wild, human horse and aura cleanup passed')
print('Production world presence: all druid mount forms, combat/airborne guards, persistent PvP, Worgen intent/arrival decisions, travel deferral, native Running Wild, human horse and aura cleanup passed')
+14 -6
View File
@@ -14,7 +14,8 @@ code=r'''
#include <cassert>
#include <vector>
using uint32=unsigned;using uint8=unsigned char;
constexpr int MAP_COMMON=0,UNIT_STATE_CASTING=1,MOUNT_CAPABILITY_FLAG_FLYING=1,SPELL_AURA_MOUNTED=1,SPELL_CAST_OK=0;
#define TC_LOG_WARN(...) ((void)0)
constexpr int CLASS_DRUID=11,SPELL_AURA_MOD_SHAPESHIFT=2,MAP_COMMON=0,UNIT_STATE_CASTING=1,MOUNT_CAPABILITY_FLAG_FLYING=1,SPELL_AURA_MOUNTED=1,SPELL_CAST_OK=0;
constexpr int MOVEMENTFLAG_FLYING=1,UNIT_FIELD_BYTES_1=1,UNIT_BYTES_1_OFFSET_ANIM_TIER=1,SMSG_MOVE_UPDATE=1;
enum class AnimTier:uint8{Ground=0,Fly=3};
struct Position{void Relocate(Position const&){}};
@@ -24,15 +25,16 @@ struct WorldPacket{explicit WorldPacket(int){}};
namespace GameTime{unsigned GetGameTimeMS(){return 100;}}
struct Player{
bool alive=true,combat=false,teleport=false,transport=false,taxi=false,falling=false,water=false,outdoors=true,mounted=true,fly=false,casting=false,learned=true;
unsigned klass=1,form=0;unsigned getClass(){return klass;}bool HasAuraType(int){return form!=0;}bool GetVehicle(){return false;}
unsigned flags=0,tier=0,stops=0,updates=0,animations=0,removals=0,casts=0;int castResult=SPELL_CAST_OK;
struct Capability{unsigned Flags=MOUNT_CAPABILITY_FLAG_FLYING;}capability;
struct{Guid guid;Position pos;unsigned time=0;}m_movementInfo;
bool IsAlive(){return alive;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool GetTransport(){return transport;}bool IsInFlight(){return taxi;}
bool IsFalling(){return falling;}bool IsInWater(){return water;}bool IsOutdoors(){return outdoors;}Map*GetMap(){return &map;}bool IsMounted(){return mounted;}bool CanFly(){return fly;}
bool IsFalling(){return falling;}bool IsInWater(){return water;}bool IsOutdoors(){return outdoors;}Map*GetMap(){return &map;}bool IsMounted(){return mounted;}bool CanFly(){return fly;}bool IsFlying(){return fly;}
bool HasUnitState(int){return casting;}unsigned GetZoneId(){return 1519;}Guid GetGUID(){return {};}
bool HasSpell(unsigned){return learned;}Capability*GetMountCapability(unsigned){return &capability;}
void StopMoving(){++stops;}void RemoveAurasByType(int){++removals;mounted=false;}
int CastSpell(Player*,unsigned spell,bool triggered){assert(spell==123&&!triggered);++casts;return castResult;}
void StopMoving(){++stops;}void RemoveAurasByType(int aura){++removals;if(aura==SPELL_AURA_MOD_SHAPESHIFT)form=0;else mounted=false;}
int CastSpell(Player*,unsigned spell,bool triggered){assert(spell==123&&!triggered);++casts;assert(!form);return castResult;}
bool HasUnitMovementFlag(int flag){return flags&flag;}void AddUnitMovementFlag(int flag){flags|=flag;}
unsigned GetByteValue(int,int){return tier;}void SetAnimTier(AnimTier t){tier=unsigned(t);++animations;}
Position GetPosition(){return {};}void WriteMovementInfo(WorldPacket&){assert(flags&MOVEMENTFLAG_FLYING);}
@@ -48,7 +50,7 @@ unsigned FlightType(void*){return 1;}unsigned urand(unsigned low,unsigned){retur
struct ClientlessFlightMgr{struct Record{bool Moving=false;};bool enabled=true;
bool Enabled(Player*){return enabled;}bool PrepareFollowingMount(Player*);void HoldFollowing(Player*,Record&);
};
'''+method('bool ClientlessFlightMgr::PrepareFollowingMount(')+method('void ClientlessFlightMgr::HoldFollowing(')+r'''
'''+'\n'.join(v for v in (root/'src/server/game/World/ClientlessMountPreparation.h').read_text().splitlines() if not v.startswith('#'))+method('bool ClientlessFlightMgr::PrepareFollowingMount(')+method('void ClientlessFlightMgr::HoldFollowing(')+r'''
int main(){
ClientlessFlightMgr m;Player p;
assert(m.PrepareFollowingMount(&p)&&p.casts==1&&p.removals==1&&travel.cancels==1);
@@ -65,6 +67,12 @@ int main(){
p.capability.Flags=0;assert(!m.PrepareFollowingMount(&p));p.capability.Flags=1;
profile.FlightMounts[0].Weight=0;assert(!m.PrepareFollowingMount(&p));profile.FlightMounts[0].Weight=1;
p.castResult=1;assert(!m.PrepareFollowingMount(&p));
p.castResult=SPELL_CAST_OK;p.klass=CLASS_DRUID;
// Every shapeshift aura uses the same native cancellation path, including ranks.
for(unsigned spell:{768u,5487u,9634u,783u,1066u,24858u,33891u,33943u,40120u}){
p.form=spell;auto casts=p.casts;assert(m.PrepareFollowingMount(&p)&&!p.form&&p.casts==casts+1);
}
p.form=33943;p.fly=true;assert(!m.PrepareFollowingMount(&p)&&p.form==33943);p.fly=false;
ClientlessFlightMgr::Record r;r.Moving=true;
auto stops=p.stops;m.HoldFollowing(&p,r);
assert(!r.Moving&&p.stops==stops+1&&p.flags==MOVEMENTFLAG_FLYING&&p.tier==unsigned(AnimTier::Fly)&&p.updates==1);
@@ -76,4 +84,4 @@ with tempfile.TemporaryDirectory() as d:
p=Path(d);(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('Group flight mount: native admission, mount retention, casting wait, rejected capabilities, stationary flight animation and bounded publication passed')
print('Group flight mount: all druid forms, safe form cancellation, native admission, mount retention, casting wait, rejected capabilities, stationary flight animation and bounded publication passed')