Fix stuck group followers and druid mount preparation
This commit is contained in:
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.
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -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 ↦}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,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 ↦}bool IsMounted(){return mounted;}bool CanFly(){return fly;}
|
||||
bool IsFalling(){return falling;}bool IsInWater(){return water;}bool IsOutdoors(){return outdoors;}Map*GetMap(){return ↦}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')
|
||||
Reference in new issue
Block a user