diff --git a/.gitignore b/.gitignore index 9ef72e70bf..136c902d96 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,14 @@ nbproject/* .vscode cmake-build-*/ .vs + +# Python bytecode and generated development-server diagnostics. +__pycache__/ +/doc/clientless/checkpoints/**/*.log +/doc/clientless/checkpoints/**/*.json +/doc/clientless/checkpoints/**/*.jsonl +/doc/clientless/checkpoints/**/*.tsv +/doc/clientless/checkpoints/**/*.txt +/doc/clientless/checkpoints/**/*.core +/doc/clientless/checkpoints/**/worldserver* +/doc/clientless/checkpoints/**/drop-database-connections diff --git a/doc/clientless/2026-11-02-instance-test-final.md b/doc/clientless/2026-11-02-instance-test-final.md new file mode 100644 index 0000000000..acb8179abc --- /dev/null +++ b/doc/clientless/2026-11-02-instance-test-final.md @@ -0,0 +1,136 @@ +# Abschließende Auswertung des Instanztests am 02.11.2026 + +Zeitraum: Gruppenbildung ab 06:00, Dungeon/Raid ab 07:00, Ende 08:00 MEZ. +Die zehn bekannten Bastion-Bots (159–167 und176) sind aus sämtlichen Kennzahlen +und Gruppenstatistiken dieser Auswertung ausgeschlossen. 18 Bots unter Stufe15 +waren bereits vom Instanztest ausgenommen. Ergebnis: Der Test ist fehlgeschlagen. + +## Ergebnis + +| Kennzahl | Ergebnis | +|---|---:| +| Ausgewertete Bots | 82 (38 Allianz, 44 Horde) | +| Geplante Aktivitäten für07:00 | 67 Dungeon, 15 Raid | +| Ankunft am vorgeschriebenen Treffpunkt vor07:00 bestätigt | 38/82 (46,3%) | +| Allianz: Goldene Rose | 18/38 (47,4%) | +| Horde: Broken Tusk | 20/44 (45,5%) | +| Native Gruppenmitgliedschaft im letzten Snapshot | 62/82, verteilt auf22 Gruppen | +| Ohne native Gruppe im letzten Snapshot | 20/82 | +| Vollständig besetzte Fünfergruppen im letzten Snapshot | 1 | +| Bestätigte tatsächliche Instanzeintritte | 0 | +| Bots mit mindestens einem belegten RECOVERY_FAILED | 35/82 | + +Die übrigen44 Bots haben keinen belegten rechtzeitigen Treffpunktabschluss; +das ist kein lückenloser Nachweis, dass sie niemals kurz dort gewesen sind. +Gleichwertige Ortsknoten im selben Gasthaus werden anerkannt: poi_guard_179 +liegt rund3 Yards von Gryshkas Zielpunkt entfernt; poi_npc_314039 liegt rund5,6 +Yards vom Goldene-Rose-Zielpunkt entfernt. Die vorläufige Auswertung anhand +exakter Zielschlüssel allein ergab33; mit diesen physisch gleichwertigen +Treffpunkten sind38 bestätigt. Ein bloßer Aliaswechsel im selben Gasthaus wird +nicht als Treffpunktfehler gewertet. + +Die22 verbliebenen Gruppen bestanden aus zehn Zweier-, sieben Dreier-, vier +Vierer- und einer Fünfergruppe. Die Zahlen beschreiben native Mitgliedschaft, +nicht räumliches Zusammensein oder erfolgreiche Anreise. + +## Gruppenbildung + +Der Bestand enthält7 Tanks,11 Heiler und64 Schadensausteiler. Allianz:3/7/28, +Horde:4/4/36. Bei strikt einem Tank und einem Heiler pro Dungeon können selbst +bei ignorierten Levelunterschieden höchstens sieben Fünfergruppen gleichzeitig +entstehen; Fraktions-, Level- und Raidanforderungen schränken dies weiter ein. +Eine vollständige Einteilung aller82 Bots war unter diesen Bedingungen nicht +realistisch. Das ist zunächst eine Eigenschaft der Testpopulation, kein Beweis +für einen Fehler jeder einzelnen abgelehnten Gruppenzuteilung. + +Von den22 Schlussgruppen hatten17 keinen Tank,14 keinen Heiler; nur eine hatte +beides.60 Bots standen zuletzt auf „waiting for a suitable group / native browser +queue“. Dies ist ein Zustand des Gruppenzugangs, kein Nachweis eines Fehlers der +Instanzzulassung. Gruppenübertritte sind belegt, darunter wiederholte Transfers +von Arthoran26 und Rixzek73 zwischen Gruppen. Mindestens ein Managerlauf wurde +während der Testvorbereitung manuell erzwungen. Die Instabilität ist ein +Untersuchungsbefund; diese Daten liefern keine unverfälschte automatische +Transferhäufigkeit. Die vom Nutzer abgelehnte Nachbesetzungsänderung ist weder +notwendig für diese Auswertung noch als Maßnahme vorgesehen. + +## Die vollständige Dungeon-Gruppe + +Arthoran26 (Tank), Sorana72 (Heiler), Gimmik41, Fizzira43 und Kargarrin45 +(Schaden) bildeten eine vollständige Fünfergruppe mit Ziel Versunkener Tempel +(instance_visit_at_446, Instanzkarte109). Alle fünf hatten eine native +Instanzplanung und waren auch um07:50 noch im Reisestatus. Es gibt für keinen +der fünf einen INSTANCE_ENTERED-Nachweis. + +Am Schluss standen Arthoran und Fizzira weiterhin auf Reise, mit fehlgeschlagenen +Flugaktionen im Sturmwinder Bereich. Gimmik, Kargarrin und Sorana standen auf +„outside instance: orientation and recovery rest“ im Bereich74. Dieser Text ist +kein Beleg eines absolvierten Dungeons: Die Implementierung wechselt auch ohne +Eintritt nach Ablauf der Eintrittsfrist in diesen Restzustand. Der Wechsel ist +ab07:56 sichtbar; die Frist endet hier planmäßig um07:55, fünf Minuten vor Ende +des Aktivitätenblocks. Die vollständige Besetzung allein genügte somit nicht, +um die Gruppe rechtzeitig zur Instanz zu bringen. + +## Reise und Intentwechsel + +35 Bots hatten mindestens einen belegten Recovery-Abbruch. Häufige Ursachen, +gezählt als betroffene Bots (mehrere Ursachen pro Bot möglich): + +| Recovery-Ursache | Bots | +|---|---:| +| Lokaler Bodenweg abgelehnt: route-local-PathRejected | 13 | +| Sicherer Rückflug nach fehlendem Flugkorridor | 8 | +| Bodenannäherung zum Abflugpunkt unterbrochen | 8 | +| Kein bestätigter Landeplatz mit letztem Bodenanschluss | 5 | +| Letzter lokaler Bodenweg abgelehnt | 4 | +| Kein Fortschritt beim verzögerten Routenstart | 4 | +| Aufgezeichnete Gruppenstraße vom Safety-Adapter abgelehnt | 3 | + +Im letzten Snapshot waren16 Bots noch im effektiven Intent groupformation, +obwohl für07:00 Dungeon/Raid vorgesehen war; ein weiterer hatte noch leisure. +Acht meldeten „graph handover failed; retrying later“. Fünf warteten auf einen +sicheren Spieler-/Transportzustand, zwei auf ein lokal nicht verfügbares +Gruppenziel und einer auf den Graphknoten für den Intentwechsel. Diese17 Fälle +zeigen, dass einige Übergänge selbst bis kurz vor08:00 nicht abgeschlossen waren. +26 Action-Datensätze standen auf Failed. Diese Zahl überschneidet sich mit den +anderen Zuständen und kann auch einen früheren, gespeicherten Fehler darstellen. + +Treffpunkt-Fallbacks wählten unter anderem andere Gasthäuser und andere Städte. +Eine Ankunft an tatsächlich anderen Orten wurde nicht als Erfolg für Goldene Rose/Broken Tusk gezählt. +Ein gesetzter fester Treffpunkt wird somit nicht durchgängig als unveränderliches +Ziel eingehalten. Die dadurch verstreuten Gruppen erschweren das sichtbare +Zusammenfinden zusätzlich. + +## Einordnung und nächste Untersuchungen + +Der Hauptausfall liegt vor dem Instanzeintritt: unvollständige Gruppen, +fehlgeschlagene Reise-Recovery und nicht abgeschlossene Intentübergaben. Es gibt +keinen Beleg, dass eine native Einlassverweigerung der hauptsächliche Engpass war. + +Für den nächsten reproduzierbaren Test sollte zunächst die Rollenverteilung je +Fraktion und Levelband passend besetzt werden. Danach sind lokale Bodenweg- +Ablehnungen, sichere Wiederaufnahme fehlgeschlagener Reisen und die Einhaltung +der vorgegebenen Treffpunkte vorrangig. Wiederholte Gruppenmanager-Transfers +sollten getrennt und ohne manuelle Managerläufe reproduziert werden. Ein +Erfolgsnachweis der Kostenvariation oder einer vollständigen Gruppenreise lässt +sich aus diesem Lauf nicht ableiten. Keine dieser Maßnahmen wurde allein durch +diese Auswertung umgesetzt. + +## Datengrundlage und Grenzen + +22 Statusstichproben, erste06:13:38, letzte07:59:25 MEZ. Die Einrichtungszeit und +die ersten Minuten vor der Beobachtung sind nicht vollständig erfasst. In keiner +Stichprobe fehlte der Testserver. Das spätere Serverende und der anschließend +installierte Höhenanflug-Fix liegen außerhalb dieses Testzeitraums. Insbesondere +ist Renrens späterer erfolgreicher Plattformanflug kein Erfolg dieses Tests. + +Der Logsammler hat wiederholte Logzeilen erfasst. Identische normalisierte +Meldungen werden deshalb dedupliziert; Rohhäufigkeiten werden nicht als Anzahl +unabhängiger Versuche ausgegeben. Betroffenenmengen und native Snapshots sind +die primären Kennzahlen. Native Logzeitstempel haben Vorrang; bei ungestempelten +Servermeldungen bleibt die Erfassungszeit als Näherung. Fehlende Eintrittslogs +werden zusätzlich durch die beobachteten Zustände gestützt, ersetzen aber keine +visuelle Gameplay-Abnahme. DB-Positionen wurden nicht als Livepositionen ausgegeben. + +Reproduzierbare Auswertung und Ergebnisdaten: +`checkpoints/2026-11-02-0600-group-test/evaluate_without_raid.py` und +`final-without-raid.json`; dort liegen auch Manifest, Rohlogs und Snapshots. diff --git a/doc/clientless/2026-11-04-combat-reference-defense.md b/doc/clientless/2026-11-04-combat-reference-defense.md new file mode 100644 index 0000000000..b45438ed8b --- /dev/null +++ b/doc/clientless/2026-11-04-combat-reference-defense.md @@ -0,0 +1,55 @@ +# Defensive Zielübernahme aus bestehenden Kampfbindungen + +Rixzek73 stand mit vollem Leben und echter PvE-Kampfbindung zu LisaileFireweaver +(Spawn84701) fest. Die Bedrohungsliste zeigte Rixzek als verfügbaren Eintrag mit +0Bedrohung. Der Adventure-Controller wartete auf einen sicheren Zustand; die +Kampfsteuerung übernahm Soloverteidigungsziele nur aus tatsächlichen +DamageTaken-Meldungen. Proaktive Zielwahl war im Kampf gesperrt. + +AdventureCombatMgr::Update ergänzt nach Gruppen-/Heilzielwahl und der bisherigen +Schadenswarteschlange eine defensive Übernahme aus den nativen PvE-/PvP-CombatRefs. +Voraussetzungen: Bot tatsächlich im Kampf, keine aktive Kampf-AI und kein laufender +Cooldown. Native Valid-Zugangsregeln bleiben maßgeblich; unterdrückte Referenzen +auf beiden Seiten und Ziele außerhalb des vorhandenen80Yards-Kampfbereichs werden +ignoriert. Aktive Angreifer werden bevorzugt, dann Nähe und stabile GUID-Reihenfolge. +Andere nahe Gegner ohne bestehende Bindung werden nicht angegriffen. + +Der Start nutzt den vorhandenen Fighter als defending=true einschließlich +Reichweiten-/Zauberprüfung, Flucht, Todesbehandlung und Reiseunterbrechung. Es wird +kein Combatflag gelöscht und keine Bedrohung künstlich erhöht. Aktive Kampfziele, +erster tatsächlicher Damager sowie bereits gewählte Gruppen-/Heilziele haben +weiter Vorrang. Der defensive Fallback gilt auch für Gruppenmitglieder, wenn die +bestehende Gruppenkoordination kein Ziel übernehmen konnte. + +Der erweiterte Produktionstest führt Update/Valid mit nativen Weltadaptern aus: +Null-Schaden-PvE-Bindung, PvP und Berechtigungsablehnung, Gruppenpriorität, +bestehendes Ziel/Schadenspriorität, beide Suppressionsrichtungen, unsichtbare/ +phasenfalsche/evadierende/tote/ungültige Ziele, Dummies, Reichweite, Cooldown, +stabile Zielwahl und keine Aggression gegen ungebundene Nachbarn. Weitere +Regressionen decken Rotation/CombatPreparation und finaleShutdownbereinigung ab. +Vier Tests und Vollbuild-j10 bestanden. + +Die genauen internen AI-/Scriptphasen, wegen denen Lisaile im alten Zustand nicht +angriff, wurden nicht als eigenständiger NPC-Fehler nachgewiesen. Dieser Fix +schließt die bestätigte defensive Zielübernahme-Lücke der Bots. Installation, +Livebeobachtung und abschließender Betrieb werden im Session-Checkpoint und unter +checkpoints/2026-11-04-combat-reference-defense/ dokumentiert. + +## Installations- und Live-Ergebnis + +Normaler Shutdown vor Installation:16Restbotsfinalauf0abgemeldet, GDBexitednormally, +keinDumpundkeinealteBinary-Sicherungangelegt. NachvollständigemProzessendeinstalliert, +passiverGDB-StartPID244387,frischesReadyundPorts8085/8087geprüft. + +Rixzekzuersteingeloggt, ohneTeleport/Zwangsattacke/CombatStop/Intentoverride. +Live-LogbelegtCOMBAT_START defending1 UND COMBAT_DEFEND_REFERENCE aufLisaile +Entry18583. DanachnativeErfolgszauber30482(Selbstbuff),2136undzweimal133gegen +Lisaile, anschließendCOMBAT_END. NativeTelemetrycombat0;Lisaileattackable0und +neuerRespawn-EintragfürSpawn84701aufMap530belegendasKampfende/ToddesGegners. +Rixzekistvollgesund7666/7666undnichtmehrdurchKampfblockiert. + +DeraktuelleTagesplanistRaid,Phase5:waitingforasuitablegroup/nativebrowserqueue. +ErkannalsoweiteramOrtstehen, jetztwegenGruppen-/Browserwartephase. EineReise +oderfertigerInstanzbeitrittwurdennichtbehauptet. DieserZustandwurde nicht durch +einenmanuellenIntentwechselhergestellt. Ursprüngliche118BotsundLoader1wiederhergestellt, +SafeLogoutpending0,shutdownPaused0. KeinCommit/Push. VorherigesWIPerhalten. diff --git a/doc/clientless/2026-11-04-dry-landing-recovery.md b/doc/clientless/2026-11-04-dry-landing-recovery.md new file mode 100644 index 0000000000..910bd56125 --- /dev/null +++ b/doc/clientless/2026-11-04-dry-landing-recovery.md @@ -0,0 +1,56 @@ +# Recovery nach gescheiterter Rückfluglandung + +Braneven121 blieb an (1899.804,-4342.669,22.272) in Orgrimmar hängen. +Die Flugplanung nutzte die Wasseroberfläche, die Landungsbestätigung dagegen +verlangte festen Boden (20.906). Nach der fehlgeschlagenen Rückfluglandung +setzte Fail den Flug terminal auf Failed und Update übersprang ihn anschließend. + +Ein lebender, weiterhin flugfähiger Bot bleibt nun in aktiver LandingRecovery. +Die bestehende Umgebungssuche prüft Punkte in 0/8/16/24/40Yards Entfernung. +Wasser über dem festen Boden wird nach derselben0.7Yards-Toleranz wie bei der +Ziel-Landung ausgeschlossen. Zusätzlich gelten offene Landungssäule, Gebietszugang +und eine vollständig vorgeprüfte, maximal32Segmente lange Flugbahn. Diese wird +über die vorhandene geprüfte Rückflugspur ausgeführt und während der tatsächlichen +Bewegung nochmals auf Kollisionen geprüft. Es gibt keinen ungeprüften direkten +Sinkflug durch Wasser, Boden oder Plattformen. + +Gescheiterte Punkte werden mit3Yards-Umkreis für biszu60Sekunden gemieden. +Findet sich keine geeignete Alternative, hält der Bot kontrolliert in der Luft +und sucht alle10Sekunden erneut. Das bisherige Gesamtzeitlimit beendet diese +Sicherheitslandung nicht terminal; die Flugberechtigung bleibt Voraussetzung. +Erst die tatsächliche Bodenbestätigung entfernt Flugflags und Mount. Eine +Abbruchlandung gilt weiterhin als fehlgeschlagene ursprüngliche Reise, damit der +normale Reiseplaner vom neuen Bodenstandort wieder übernehmen kann. Die +Gruppenroute wird dadurch nicht durch eine eigene Makroroute ersetzt. + +Elf gezielte Regressionstests bestanden, inklusive tatsächlichem Fail, +FindReturnLanding und Advance: Wasser-Rückfluglandung wird abgewiesen, +vollständiger trockener Anflug ausgeführt und danach korrekt abgemountet. +Weitere Fälle: anderer Punkt nach erneutem Fehler, blockierte/gesperrte/fehlende +Flächen, vollständig nasse Umgebung, Zehnsekunden-Retry und entzogene +Flugberechtigung. Die Departure-Fixture führt jetzt wie Begin die lokale +Startvorbereitung vor der Mountzulassung aus; hohe und nahe Bodenabfragen sind +getrennt, damit neue feste-Boden-Prüfungen realistisch abgebildet werden. + +Vollbuild-j10 und Installation erfolgreich. PID222501 frisch ready, +Ports8085/8087 geprüft. Braneven zuerst ohne Teleport/Intentänderung eingeloggt: +Er verließ die alte Stelle und landete um18:53:34 auf einer trockenen Plattform +(1839.670,-4379.120,103.859), Flug33Segmente/171.91Yards, anschließend native +Bodenbewegung auf der weiteren Route. Die konkrete neue Ersatzlandungs-Recovery +musste dabei bisher nicht live ausgelöst werden; sie ist durch den oben +beschriebenen Produktionstest geprüft. Nachfolgende lokale Wege am Zeppelinturm +zeigen teils Gruppen-LOS-Ablehnungen und neue Anreiseversuche; die gesamte +Shattrath-Reise wurde nicht abschließend beobachtet. + +Loader und ursprüngliche Botpopulation wiederhergestellt. Kein Commit/Push. +Beim vorherigen Shutdown endete die CPC-Finalisierung korrekt mit16->0, danach +crashte OutdoorPvPMgr::Die beim OPvPCapturePoint-Destruktor (OutdoorPvPNA). +Coreworldserver-20261104-181845 und passende alte Binary gesichert; dieser +weitere Befund wurde nicht im Rahmen der Landungsänderung korrigiert. +Artefakte checkpoints/2026-11-04-dry-landing-recovery/. + +Weitere Livebeobachtung: Die LOS-Ablehnungen wurden per regulärem Flugfallback +überwunden. Um18:54:19 zweitebestätigteLandung am Zeppelinhaltepunkt +(1839.67,-4391.12,135.23), danach RouteSchritt2/14 TRANSPORT_WAIT +zeppelin_org_under -> zeppelin_under_org (spawn6). Branevenwartet regulär auf +dasVerkehrsmittel; keinStillstandanderWasserstellemehr. diff --git a/doc/clientless/2026-11-04-final-shutdown-fix.md b/doc/clientless/2026-11-04-final-shutdown-fix.md new file mode 100644 index 0000000000..02bca464d1 --- /dev/null +++ b/doc/clientless/2026-11-04-final-shutdown-fix.md @@ -0,0 +1,57 @@ +# Finale Abmeldung vor Mapabbau + +Die Dumps vom 4. November zeigen verbliebene clientlose Sitzungen nach Ablauf +der sicheren Logoutfrist. Der Netzwerk-Shutdown entfernte nur normale Sessions. +CPC-Sitzungen wurden erst in World::~World, nach dem Mapabbau, gelöscht. Das +führte abhängig vom Zustand zu Pet-/Transport-Assertions oder fehlender Map bei +der nativen Logout-Zonenabmeldung. + +Die finale Map-Aufräumroutine in Main ruft nun zuerst +World::ShutdownClientlessSessions auf. Diese sperrt weitere direkte CPC- +Registrierungen, bestätigt native Teleports und finalisiert den generischen +ClientlessPlayerMgr. Controller erhalten ihre bisher fehlende Logout- +Benachrichtigung genau einmal, Reise-/Flugausführung wird bereinigt. Danach +werden sämtliche CPC-Sitzungen, auch nicht über den Manager angelegte, +zerstört. Ihr normaler WorldSession-Destruktor führt LogoutPlayer(true) aus, +solange OutdoorPvP, Instanzspeicher, Battlegrounds und Maps noch verfügbar sind. +Erst danach beginnt deren bisheriger Abbau. + +Ausstehende Initialisierungs-/Login-Rückrufe sind Eigentum ihrer Sitzung und +werden nur durch deren Update ausgeliefert. Die finale Bereinigung führt kein +Update aus und zerstört diese Rückrufe mit der Sitzung. Laufende SQL-Queryholder +bleiben separat per shared_ptr durch ihre DB-Aufgabe gehalten; deren späteres +Ergebnis kann keinen Sitzungscode mehr aufrufen. Damit muss der Shutdown auch +bei DB-Ausfall nicht auf einen noch nicht begonnenen Botlogin warten. + +Die sichere 60-Sekunden-Phase wird nicht verändert. Ihr Recovery-Audit wird +beibehalten. Bei erzwungenem/abgekürztem Shutdown erstellt die finale Phase eine +zusätzliche Sicherung, wenn die normale Fristsicherung noch nicht geschrieben +wurde. Wiederholter Aufruf der finalen World-Bereinigung ist wirkungslos. + +Zwölf gezielte Regressionstests bestanden. Der neue Test führt tatsächliche +Produktionsmethoden von World/ClientlessPlayerMgr und den Main-Abbaublock aus: +Petbesitzer, Passagiere, Teleports, bereits benachrichtigte Controller, +controllerlose Sitzungen, Nullslots, noch laufende/bereite Login-Rückrufe, +Anmeldesperre, Wiederholung und Audit. Vorherige Instanz-/Unterstadt-WIP bleibt +erhalten. Der alte test_safe_logout.py verweist bereits am unveränderten HEAD +auf den entfernten Pending-Container im Script; seine aktuellen Prüfungen sind +im bestehenden test_shutdown_lifecycle.py enthalten. + +Build-/Installations- und Live-Shutdown-Ergebnis stehen im Session-Checkpoint +und unter checkpoints/2026-11-04-final-shutdown-fix/. + +## Live-Ergebnis + +Vollbuild -j10 und Installation erfolgreich. Erster StartPID216135 erreichte +frisches Ready und Ports8085/8087. Mit118 ursprünglichen Bots normaler +server shutdown10: nach60246ms blieben8Bots, davon7imKampf und1tot. +CLIENTLESS_SHUTDOWN_FINALIZE_BEGIN remaining=8 und END remaining=0. +GDB bestätigt: Inferior exited normally; kein neuer Dump. Damit wurde der +bisherige konkrete Restbot-Shutdownfall erfolgreich durchlaufen. + +Danach erneut passiv unterGDB gestartet: PID216676, frisches Ready und beide +Ports geprüft. Ursprünglicher Loaderbetrieb und118Bots wiederhergestellt. +DB-Ausfall und Shutdown mitten im Login wurden nicht zusätzlich live injiziert; +der Rückrufabbruch wird durch die Regressionstests abgedeckt. Der vor der +Installation noch abgestürzte alte Build ist mit Core175333 und passender +Binary gesichert. Keine Änderungen an Tagesplänen, kein Commit/Push. diff --git a/doc/clientless/2026-11-04-ground-departure-retry.md b/doc/clientless/2026-11-04-ground-departure-retry.md new file mode 100644 index 0000000000..c27499508d --- /dev/null +++ b/doc/clientless/2026-11-04-ground-departure-retry.md @@ -0,0 +1,27 @@ +# Blockiertes erstes Flugsegment: anderen Abflugplatz zu Fuß erreichen + +`ClientlessFlightMgr::Advance` ruft bei einem ungültigen Korridor vor dem ersten +Segment jetzt `RetryDeparture` auf. Dieser Pfad verlangt tatsächlichen +Bodenkontakt und schließt laufende Rückflüge, bereits erfolgten Abflug, +Flugsegmente, Fallen, Wasser, Transport, Taxi, Teleport und Kampf aus. +Erst danach wird das Flugmount entfernt und die vorhandene MMAP-gestützte +Abflugplatzsuche ausgeführt. Drei Ortswechsel pro Flugversuch sind zulässig. +Bereits fliegende Bots behalten die bisherigen Ausweich- und Landepfade. + +Die Suche prüft eine 40 Yards hohe Säule: den maximal 35 Yards hohen senkrechten +Abflug zuzüglich der oberen Körper-Strahlen. Die normale Bodennavigation, +Gebietszugangsprüfung und vollständige MMAP-Ankunft bleiben erforderlich. +Ein nur 15 Yards hoher freier Abschnitt reicht für Ersatzplätze nicht mehr aus. +Nach dem Hinlaufen wird der Flug regulär neu begonnen; Versuchszähler und +ActionOwned werden dabei erhalten. Das eigentliche Ziel bleibt unverändert. +Ein kopierter Record verhindert eine Selbstverschiebung des Map-Eintrags beim +Publizieren des Ersatzstarts. + +Regressionen prüfen den Produktionspfad einschließlich Record aus derselben +Map, Zielerhalt, Absatteln nur auf sicherem Boden, Ausschluss bereits erfolgter +Flüge und Rückflüge, Versuchsgrenze sowie Erhalt von Budget und Aktionszuordnung +über Begin. Ein Dach bei 38 Yards lässt kurze Säulen zu, blockiert aber korrekt +die Ersatzplatzsuche mit vollständiger Körperhöhe. + +Build-, Server- und Liveergebnis: SESSION-RESUME.md sowie +checkpoints/2026-11-04-ground-departure-retry/. diff --git a/doc/clientless/2026-11-04-halaa-owner-fix.md b/doc/clientless/2026-11-04-halaa-owner-fix.md new file mode 100644 index 0000000000..62f80748aa --- /dev/null +++ b/doc/clientless/2026-11-04-halaa-owner-fix.md @@ -0,0 +1,56 @@ +# Halaa-Eroberungspunkt: eindeutiger Besitzer + +Der Halaa-Controller hielt einen OPvPCapturePointNA im eigenen unique_ptr und +registrierte denselben Zeiger über get() zugleich in der besitzenden Basisklassen- +Map. Beim Shutdown wurde das Objekt zuerst als abgeleitetes Member, dann erneut +als Basisklassen-Eintrag gelöscht. Der gesicherte Dump185220 trat noch mit der +alten Binary auf; sein Vorgänger181845 wurde zuvor gezielt diagnostiziert. + +Der Fix beschränkt sich auf OutdoorPvPNA.cpp/.h. m_unregisteredCapturePoint +besitzt das Objekt bis zum ersten Laden des echten Banner-Spawns. Bei der +Registrierung wird dieser Besitzer einmalig an AddCapturePoint übertragen. +m_obj ist eine nichtbesitzende Referenz und bleibt für Update/Faktionsbuffs +sowohl vor als auch nach der Registrierung gültig. Ein nie geladenes Banner +verursacht kein Leck. Entladen und erneutes Laden desselben Spawns binden nur +das neue GameObject an das vorhandene Ziel; Eigentum und Zustand bleiben erhalten. +Eine zweite Registrierung unter einer anderen Spawn-ID wird mit Fehlermeldung +abgewiesen. Wiederholtes Setup ist wirkungslos. Die generische OutdoorPvP-API +und die Besitzübergaben der anderen PvP-Skripte wurden nicht geändert. + +Vier gezielte Tests bestanden. Der neue Produktionstest führt Setup, +OnGameObjectCreate, Basisregistrierung/-bindung/-entbindung, Update und Buff- +Zugriff mit ASan/UBSan aus. Fälle: nie geladenes Banner, einmalige Übergabe, +20 Lade-/Entladezyklen, unerwartete zweite Spawn-ID, wiederholtes Setup, +mehrere Map-Lebensdauern und genau eine Freigabe je Allokation. Zusätzlich +OutdoorPvP-Shutdown-Lifetime sowie CPC-Shutdown-/Grace-Regressionen bestanden. + +Vollbuild -j10 und Installation erfolgreich. Buildbeginn mit Loaderdatei/nativ0 +und safe logout all; normaler Shutdown erst nach Build. AlterPID222501 crashte +noch im alten Halaa-Code, Coreworldserver-20261104-185220 und passende Binary +worldserver-before-halaa-owner-fix bewahrt. Erst nach vollem Prozessende installiert. + +## Liveprüfung + +NeuerPID226712 frisch ready und Ports8085/8087 geprüft. Dedizierter Testcharakter +Morvath150 (Lv80, kein regulärer AdventureBot) über nativen Testbot-Controller +angemeldet. Teleport ausschließlich dieses Testcharakters zu Halaa. Der Test +bestätigte das echte sichtbare Banner gleichzeitig über Spawn23273 und Entry182210, +auf Karte530/Zone3518/Area3628. Anschließend native Rückkehr nach Orgrimmar an die +ursprünglichen Koordinaten (1569,-4397,16.1,Orientierung0) und normaler Testjob- +Logout. Gesundheit9531 unverändert, kein Kampf, keine Gegenstands-/Questbenutzung. +Keine manuellen DB-Änderungen oder Teleports regulärer AdventureBots. + +Danach normaler server shutdown10 mit registriertem Halaa-Eroberungspunkt: +GDB bestätigt Inferior exited normally; kein Signal und kein neuer Dump. Damit +wurde genau der vorher abstürzende registrierte-Banner-Abbau live durchlaufen. +Die eigentliche PvP-Eroberung wurde nicht als Gameplay-Abnahme getestet. + +Finaler Neustart und wiederhergestellter Botbetrieb stehen im Session-Checkpoint +und in checkpoints/2026-11-04-halaa-owner-fix/result.json. Dort liegen auch +Build-/Installlogs, Sanitizer-/Regressionstests, native Test-Lua, Halaa-Ergebnis, +Shutdownnachweis und ursprünglicher Betriebs-/Testbot-Snapshot. Vorheriges +Unterstadt-/Instanz-/Shutdown-/Landungs-WIP erhalten. Kein Commit/Push. + +Finaler ServerPID227005 ist frisch ready, beidePorts lauschen. Ursprünglicher +Loaderbetrieb und118Bots wiederhergestellt; Testcharakter150offline an seiner +Originalposition. KeinCommit/Push. diff --git a/doc/clientless/2026-11-04-halaa-shutdown-crash.md b/doc/clientless/2026-11-04-halaa-shutdown-crash.md new file mode 100644 index 0000000000..386f79ac88 --- /dev/null +++ b/doc/clientless/2026-11-04-halaa-shutdown-crash.md @@ -0,0 +1,64 @@ +# Weiterer Shutdown-Absturz: doppelte Besitzverhältnisse beim Halaa-Eroberungspunkt + +Coreworldserver-20261104-181845, abgestürzterPID216676. Der Dateiname bezeichnet +den Start des Diagnoseprozesses; der Crash trat beim Shutdown gegen18:50 auf. +Passende Binary: dry-landing-recovery/worldserver-before-dry-landing-recovery. +GDB nur lesend am Dump, keine Verbindung zum laufenden Serverprozess. + +## Ursache + +OutdoorPvPNA::SetupOutdoorPvP erstellt in OutdoorPvPNA.cpp148 einen +OPvPCapturePointNA als unique_ptr im Memberm_obj (Header175). +Beim Laden des Halaa-Banners (GO182210) ruft OnGameObjectCreate in cpp56 +AddCapturePoint(m_obj.get()) auf. OutdoorPvP::AddCapturePoint übernimmt denselben +rohen Zeiger in einen weiteren unique_ptr der Basisklassen-Map (cpp426). +Damit existieren zwei getrennte Besitzer derselben Allokation. + +Beim Shutdown wird zuerst der Member der abgeleiteten Klasse zerstört und das +Objekt gelöscht. Danach zerstört der Basisklassen-Destruktor m_capturePoints +und versucht dieselbe Allokation erneut zu löschen. Der virtuelle Aufruf greift +bereits auf den Destruktionszustand des zuvor zerstörten Objekts zu: SIGSEGV0. + +## Dump-Belege + +Controller0x7fff842af300, CapturePoint0x7fff831fca80. Der abgeleitete Owner m_obj +ist im Dump schon null; die Basisklassen-Map besitzt weiterhin genau diesen +Zeiger unter Spawn23273. Dessen vptr verweist auf vtableforOPvPCapturePoint+16; +beide virtuellen Destruktoreinträge sind0. Stack: unique_ptr +->OutdoorPvP::~OutdoorPvP ->OutdoorPvPNA::~OutdoorPvPNA ->OutdoorPvPMgr::Die +->Main-Map-Aufräumroutine. Kein GetMap-Assert wie bei den früheren CPC-Dumps. + +Datenbank bestätigt Spawn23273, Entry182210 HalaaBanner, Karte530, +(-1572.57,7945.3,-22.475), Halaa/Nagrand. Die doppelte Besitzerkonstruktion ist +in den OutdoorPvP-Scripts nur bei NA in dieser unique_ptr/get-Kombination +vorhanden. Andere AddCapturePoint-Aufrufer müssen bei einem späteren Fix ihre +bisherige Besitzübergabe behalten. + +## Verhältnis zum Bot-Shutdownfix + +pre-install-shutdown.log belegt CLIENTLESS_SHUTDOWN_FINALIZE_BEGIN16 und END0. +Im Dump sind sowohl m_clientlessSessions als auch m_sessions leer. Die finale +Bot-Abmeldung war abgeschlossen; dieser Crash entsteht danach in einem anderen +Besitzfehler. Das Laden des Halaa-Banners aktiviert den zweiten Besitzer; +welcher Spieler/Bot das Banner zuvor geladen hat, wurde hier nicht festgestellt. + +## Korrekturrichtung, noch nicht umgesetzt + +Genau einen Besitzer festlegen. Registrierung muss Eigentum tatsächlich +übertragen; direkte Halaa-Zugriffe erhalten nur eine nichtbesitzende Referenz. +Die Zeit vor dem erstmaligen Laden des Banners sowie wiederholtes Laden/Entladen +berücksichtigen: kein Leck bei nie geladenem Banner, kein zweiter Besitzer bei +erneuter Registrierung und kein Nullzeiger in Update/HandlePlayerEnterZone. +Ein bloßes Ersetzen von get durch release würde m_obj leeren und seine übrigen +Benutzer beschädigen. Die anderen nativen OutdoorPvP-Mechaniken bewahren. + +Untersuchung abgeschlossen ohne Code-/Konfigurationsänderung, Build oder +Serverneustart. LaufenderPID222501 unberührt. Zielgerichtete GDB-Skripte/Logs und +JSON-Befund: checkpoints/2026-11-04-outdoor-halaa-crash/. + +## Anschließende Korrektur + +Auf Nutzerauftrag implementiert und getestet; Details in +[Halaa-Ownershipfix](2026-11-04-halaa-owner-fix.md). Echter Banner-Spawn23273 +wurde mit einem dedizierten Testcharakter geladen. Der folgende Shutdown +endete regulär ohne Crash. Vier Regressionstests inklusiveASan/UBSan bestanden. diff --git a/doc/clientless/2026-11-04-instance-rendezvous.md b/doc/clientless/2026-11-04-instance-rendezvous.md new file mode 100644 index 0000000000..294458c879 --- /dev/null +++ b/doc/clientless/2026-11-04-instance-rendezvous.md @@ -0,0 +1,36 @@ +# Warten innerhalb der Zielinstanz + +Nach Instanzeintritt verwerfen AdventureBot-Gruppenmitglieder übrig gebliebene +Weltreiseaktionen. Sie warten innerhalb der Instanz, bis der lebende, fertig +transferierte Anführer in derselben Karten-, Instanz- und Phasenkopie höchstens +100 Yards entfernt ist. Diese Reichweite war bereits die Grenze des lokalen +Gruppenfolgens. Der Anführer kann ein Spieler oder ein Bot sein. + +Innerhalb von Dungeons und Raids werden weder aufgezeichnete Weltübergänge +wiederholt noch neue Travelgraph-Verfolgungsrouten zum Anführer gestartet. +Reine Bot-Instanzbesuche nutzen bei zusammengeführter Gruppe das vorhandene +lokale Formationsfolgen. Tod, Kampf, Charme sowie native Taxi-/Transport- +Übergänge behalten ihre eigenen Zuständigkeiten. + +Ein menschlicher Mitspieler außerhalb oder während des Ladebildschirms löst +keinen vorzeitigen Instanzaustritt der bereits eingetretenen Bots mehr aus. +Außerhalb verbliebene Begleiter gemischter Dungeonbrowser-Gruppen verwenden für +den Wiedereintritt den nativen LFG-Teleport einschließlich Gruppenbindung, +Zugangsprüfungen und Clientless-Teleportbestätigung. Fehlgeschlagene Versuche +werden frühestens nach drei Sekunden wiederholt. Keine administrativen +Teleports und keine Änderungen an Instanzbindungen. + +Elf gezielte Regressionstests bestanden, einschließlich tatsächlicher +Produktionsmethoden mit Weltadaptern: unterschiedliche Instanzkopien, ladender/ +offline/entfernter Anführer, Mensch/Bot, Phasen, lokale Wiederaufnahme, +Welteintrittsbereinigung, Weltreiseverbot im Dungeon, Kampf/Tod, nativer +Browser-Wiedereintritt/Ablehnung und Wiederholungsfrist. Der bestehende +Gruppenschutz-Test extrahiert jetzt ausschließlich seine Zielmethode statt +versehentlich benachbarte Logout-Methoden mitzukompilieren. + +Vollbuild mit -j10 erfolgreich. Installation/Neustart werden im +Session-Checkpoint dokumentiert. Die abschließende sichtbare Abnahme des +Beitritts mit dem Benutzercharakter steht noch aus. Bestehendes Unterstadt-WIP +bleibt erhalten. Der getrennt diagnostizierte Shutdownfehler wurde nicht geändert. + +Artefakte: `checkpoints/2026-11-04-instance-rendezvous/`. diff --git a/doc/clientless/2026-11-04-shutdown-crash-analysis.md b/doc/clientless/2026-11-04-shutdown-crash-analysis.md new file mode 100644 index 0000000000..3c34b9361b --- /dev/null +++ b/doc/clientless/2026-11-04-shutdown-crash-analysis.md @@ -0,0 +1,107 @@ +# Shutdown-Abstürze vom 4. November 2026 + +## Ergebnis + +Beide Dumps belegen dieselbe unvollständige finale Sitzungssperrung/-bereinigung. +Die sichere Logoutphase mit 60s-Frist und JSONL-Sicherung lief durch. Verbleibende +clientlose Sitzungen wurden jedoch vor dem Entladen der Maps nicht tatsächlich +abgemeldet. Die beiden Assertions schützen konsistente Besitzverhältnisse; +SIGSEGV wird hier vom Core über Trinity::Abort/Assert gezielt ausgelöst. +Es handelt sich in diesen Dumps nicht um einen neuen DB-Verbindungsverlust. + +## Pet-Fall + +Core `worldserver-20261104-124032.core`, passende Binärdatei +`checkpoints/2026-11-04-browser-wait/worldserver-before-browser-wait`. +GDB: 17 clientlose Sitzungen, null Netzwerksitzungen. Brynda (GUID165) bleibt +mit `m_playerLogout=false` und `m_inWorld=true` angemeldet. Ihr Wolf (Entry299, +Pointer0x7fff2db9b000) befindet sich weiterhin in ihrer m_Controlled-Menge. +Beide gehören zur Map0x7fffbba98480. Die Fristsicherung erfasst Brynda noch im +Kampf. Beim Mapdestruktor wird der Wolf direkt entfernt, während sein Besitzer +noch lebt: Unit::RemoveFromWorld9519 -> ABORT. Besitzer-Entry0 ist normal für +einen Spieler und bedeutet keinen Nullbesitzer. + +## Transport-Fall + +Core `worldserver-20261104-135325.core`, passende Binärdatei +`checkpoints/2026-11-04-pvp-wait/worldserver-before-pvp-wait`. +GDB:16 clientlose Sitzungen, null Netzwerksitzungen. Transport The Zephyr +(Entry190549, Pointer0x7fff7bd3f000) hat genau einen dynamischen Passagier: +Faelirin (GUID105), weiterhin inWorld, `m_playerLogout=false`, echte beidseitige +Transportbindung vorhanden. Fristsicherung bestätigt ihren Passagierzustand. +Map::UnloadAll entfernt den Transport vor ihrem Logout; Transport::~Transport94 +fordert `_passengers.empty()` und löst die Assertion aus. + +## Fehler im Lebenszyklus + +1. ClientlessPlayerMgr::ShutdownReady erlaubt nach60s plus Audit das Beenden + der Worldschleife. Das ist die vereinbarte Frist, kein Fehler an sich. +2. Der Netzwerk-Shutdownhandle in worldserver/Main.cpp ruft KickAll und einmal + UpdateSessions auf, bevor mapManagementHandle die Maps entlädt. +3. World::KickAll (World.cpp3048) berücksichtigt ausschließlich m_sessions, + nicht den getrennten Speicher m_clientlessSessions. Die17/16 Bots bleiben + daher angemeldet; GDB bestätigt, dass alle Netzwerksitzungen bereits weg sind. +4. Ein einfaches zusätzliches KickPlayer auf clientlose Sitzungen wäre ebenfalls + unzureichend: WorldSession::KickPlayer (720) arbeitet ausschließlich mit + vorhandenen Sockets; forceExit wird nur innerhalb dieses Socketzweigs gesetzt. +5. World::~World (179) löscht den clientlosen Sitzungsspeicher zwar, wird aber + erst nach den lokalen Main-Aufräumhandles erreicht. Für Pets/Transporte ist + das nach der Mapzerstörung zu spät. + +## Abgeleitete Korrektur, noch nicht implementiert + +Die finale Shutdownphase muss nach Frist/Audit und vor Map-/OutdoorPvP-Abbau +alle clientlosen Sitzungen über ihren vorhandenen nativen Logout-/Lifecyclepfad +beenden. Beim Laden befindliche Sitzungen und ausstehende Callbacks brauchen +weiterhin sichere Behandlung. Erst nach tatsächlich abgeschlossener Bereinigung +dürfen die Maps entfernt werden. Der native Logout entfernt Pets +(WorldSession::LogoutPlayer, RemovePet) und löst Transportbindungen über +WorldObject::CleanupsBeforeDelete/RemovePassenger. + +Weder Assertions entfernen noch einfach Kontroll-/Passagiermengen leeren. +Keine längere Frist als Ersatz; kein zusätzlicher Teleport vor dem Shutdown +notwendig. Die normale sichere Logoutphase bleibt erhalten, nach ihrem Ende +ist reguläres finales Abmelden bei noch lebenden Maps erforderlich. + +Untersuchung nur lesend an Quellcode und gesicherten Dumps. Keine Quellcode- +oder Konfigurationsänderung, kein Build, kein Serverneustart. TestserverPID187536 +blieb aktiv. Zielgerichtete GDB-Ausgaben ohne Verbindungsdaten: +`checkpoints/2026-11-04-shutdown-crash-diagnosis/`. + +## Weiterer Dump: Startkennung 16:16:45, Shutdown gegen 17:52 + +Untersucht: `worldserver-20261104-161645.core`, PID200134; passende gesicherte +Binary `checkpoints/2026-11-04-instance-rendezvous/worldserver-before-instance-rendezvous`. +Die Uhrzeit im Dateinamen ist die Startzeit des Diagnoseprozesses, nicht die +Absturzzeit. Noch der alte Build vor Installation des Instanz-Rendezvous. + +GDB belegt wieder dieselbe Reihenfolge: `World::~World` (World.cpp192) löscht +clientlose Sitzungen erst nach dem Mapabbau. `WorldSession::~WorldSession` +ruft `LogoutPlayer(save=true)` auf; dessen OutdoorPvP-Abmeldung (601) greift +über `OutdoorPvPMgr::HandlePlayerLeaveZone` (180) auf `player->GetMap()` zu. +`m_currMap==nullptr` löst die Assertion in Object.h448 aus. SIGSEGV ist hier +der gezielt ausgelöste Assert-Crash, kein Beleg für zufällige Speicherkorruption. + +Betroffen: Drakzug, GUID186, Zone3537. Im Dump `m_inWorld=false`, `m_currMap=0`, +`m_playerLogout=true` (Logout wurde bereits begonnen), Session hält noch den +Spieler. Noch24 clientlose Sitzungen, null Netzwerksitzungen. Die Fristsicherung +`clientless-recovery/shutdown-1793811079-5668125.jsonl` bestätigt24 Restbots nach +60Sekunden, DB verfügbar; Drakzug auf Map571 fliegend. Ein Flug ist hier der +Grund, ihn während der sicheren Phase noch angemeldet zu lassen, nicht der +unmittelbare Crash: die finale native Abmeldung erfolgt schlicht zu spät. + +Korrekturrichtung bleibt: finale CPC-Abmeldung nach Grace/Audit, aber VOR +OutdoorPvP-/Mapabbau; alle Restzustände und noch ladenden Sitzungen geordnet +beenden. Kein Nullcheck nur an der Absturzstelle und kein Entfernen der Assertion +als Ersatz für die korrekte Lebensdauer. Keine Code-/Konfigurationsänderung, +kein Build/Restart bei dieser Untersuchung; laufender ServerPID211336 unberührt. +Gezielte GDB-Ausgaben und maschinenlesbarer Befund: +`checkpoints/2026-11-04-shutdown-161645/`. + +## Korrektur umgesetzt und im Live-Shutdown bestätigt + +Auf folgenden Nutzerauftrag implementiert; siehe +[Finaler Shutdownfix](2026-11-04-final-shutdown-fix.md). Bei der Prüfung des +neuen Builds wurden nach der60s-Frist8Restbots vor Mapabbau vollständig +abgemeldet, anschließend reguläres Prozessende ohne Crash. Build/zwölf +Regressionstests bestanden, Server danach wieder gestartet. diff --git a/doc/clientless/2026-11-04-undercity-arrival.md b/doc/clientless/2026-11-04-undercity-arrival.md new file mode 100644 index 0000000000..3458a58a71 --- /dev/null +++ b/doc/clientless/2026-11-04-undercity-arrival.md @@ -0,0 +1,49 @@ +# Unterstadt-Anreise über den Fledermausmeister + +Explizite Ortsausnahme auf Nutzerwunsch: Ziele auf Karte0 mit gespeicherter Katalog-Zielzone oder +Katalog-Zielgebiet1497 (Unterstadt) erhalten den verbindlichen Anreiseknoten taxi_11. +Andere Städte/Gebiete werden durch diese Regel nicht verändert. + +Datenbankabgleich: taxi_11 hat Typ taxi und Position +(1568.62,267.97,-43.10). Der Katalogpunkt poi_guard_321 heißt +„Undercity Bat Handler“, gehört zu Zone/Gebiet1497 und liegt bei +(1565.90,271.435,-43.1024), knapp4.41Yards daneben. + +Initialplanung und Wiederherstellung planen auf den konkreten Graphenknoten +statt auf einen kostenoptimierten Ausstieg nahe der Endlocation. Direkte lokale +Abkürzung vor dem Anreiseknoten und ein Fallback auf das eigentliche Stadtziel +werden dabei verhindert. Ein fehlender erforderlicher Knoten führt zu einem +Fehler, nicht zum Ausweichen auf einen oberirdischen Ersatzanker. + +Erst nach tatsächlichem Erreichen des Knotens (dreidimensional<=5Yards) beginnt +die finale lokale Strecke zum unveränderten Ziel. Für Braneven bleibt das +Kriegsviertel bei (1724.8,368.623,-60.4843) das Ziel. Der Ankunftsstatus bleibt +während lokaler Folgeschritte erhalten; neue Reisen setzen ihn zurück. +Gruppenmitglieder übernehmen Vorgabe und Reiseweg ihres Anführers, müssen ihre +physische Ankunft am Knoten aber selbst nachweisen. + +Sieben gezielte Tests bestanden: neue Unterstadt-Policy/3D-Ankunftssperre, +Gruppenroutenvererbung, Journeykosten, Graphhandover, Flughandover, +Bodenroutenfortsetzung und Flugannäherung. Der zusätzliche Rescue-Executor-Test +scheitert unverändert auch auf HEAD: sein Testausschnitt enthält inzwischen +TryRouteApproachFlight, der Adapter deklariert diese Methode jedoch nicht. +Dieser vorbestehende Testfehler wurde nicht verändert. + +Build mit bestehender CMake-Konfiguration und -j10. Bekannte Shutdownfehler +wurden in diesem Änderungssatz nicht verändert. Artefakte: +checkpoints/2026-11-04-undercity-arrival/. + +Die AdventureBot-Schicht übergibt dafür den generischen Actionparameter +arrival_node. Eine erste Geländeabfrage wurde nach negativem Live-Test ersetzt: +Gebietsabfragen ferner unterirdischer Punkte waren unzuverlässig. Der generische +Reiseexecutor verwendet keine Unterstadt-Geländeheuristik. Auch Flächenziele in +Unterstadt erhalten taxi_11. + +Finale Version installiert, frisches Ready/Ports8085/8087 geprüft. Live-Test mit +Actor150: WarQuarter, anchor taxi_11 und routeComplete1/reachedtaxi11. Braneven121 +bestätigte physisch REQUIRED_ARRIVAL_REACHED taxi_11 um16:19:36 in seiner regulären +Reise. Actor150 sicher ausgeloggt und ursprüngliche Position restauriert. +Alle118Originalbots online, Loader aktiv, SafeLogoutpending0. +Die alten Prozesse zeigten erneut Folgen des bekannten zu späten CPC-Logouts +(GetMap-null-Assert); Dumps141428 und160739 plus passende Binaries gesichert. +Keine Änderung am Shutdowncode. diff --git a/doc/clientless/2026-11-04-vertical-takeoff.md b/doc/clientless/2026-11-04-vertical-takeoff.md new file mode 100644 index 0000000000..5177ba50d9 --- /dev/null +++ b/doc/clientless/2026-11-04-vertical-takeoff.md @@ -0,0 +1,34 @@ +# Senkrechten Start vor dem Anschlussflug abschließen + +Die vorzeitige Planung des nächsten Flugsegments (`handoff` bei höchstens +1,5 Sekunden Restlaufzeit) konnte einen geprüften senkrechten Start bereits +auf halber Höhe abbrechen. `Advance` kennzeichnet jetzt ein tatsächlich +gewähltes senkrechtes Startsegment. `Update` wartet für dieses Segment auf das +native Spline-Ende und plant erst anschließend den Anschlussflug. + +Die laufenden Kollisionsabfragen auf dem verbleibenden Korridor bleiben aktiv. +Auch die positionsbasierte Stillstandserkennung mit 60 Sekunden pro Segment, +lokale Wiederherstellung, Berechtigungsprüfung, reguläre Flugsegmentübergänge +und vorhandene Gruppenrouten bleiben erhalten. Neue Hindernisse dürfen den +Aufstieg weiterhin unterbrechen. + +Der Produktionstest der Update-Schleife prüft den fehlenden vorzeitigen Wechsel, +die Unterbrechung bei geänderter Geometrie, den Anschluss nach Spline-Ende und +den weiterhin wirksamen 60-Sekunden-Timer während eines senkrechten Starts. +Acht ausgewählte Flug-/Gruppenflugtests bestanden. Build- und Liveergebnis siehe +SESSION-RESUME.md und checkpoints/2026-11-04-vertical-takeoff/. + +Beim ersten Livecheck erreichte Rixzek mit dieser Änderung die volle Starthöhe +Z=36,26 und plante weitere Segmente. Eine weitere Ablehnung in der Luft zeigte, +dass nach erschöpften seitlichen Kandidaten kein senkrechtes Entkommen mehr +versucht wurde. `Next` erhält dafür eine ausdrückliche Freigabe vom Executor +für den Hinflug. Eine vollständig geprüfte senkrechte Säule bis maximal zur +Gebietsobergrenze kann in Schritten nach oben genutzt werden. Der Einstieg aus +einem knappen Bodenabstand verwendet die geprüfte Aufstiegsfreigabe; eine obere +Plattform darf weiterhin nicht durchflogen werden. Rückflug-/Landesimulationen +und gewöhnliche Flugkandidaten behalten ihr bisheriges Verhalten. Auch diese +senkrechten Ausweichsegmente werden vor der Anschlussplanung beendet. + +`test_vertical_flight_escape` prüft zusätzlich die Freigabe, den Aufstieg aus +einem engen Sicherheitsabstand, blockierende Dächer, die Höhengrenze, unbekannte +Geometrie und unveränderte normale Routen. Neun ausgewählte Flugtests bestanden. diff --git a/doc/clientless/2026-11-04-waiting-capital.md b/doc/clientless/2026-11-04-waiting-capital.md new file mode 100644 index 0000000000..6de68a3546 --- /dev/null +++ b/doc/clientless/2026-11-04-waiting-capital.md @@ -0,0 +1,35 @@ +# Hauptstadt als Warteziel für Instanz- und PvP-Intents + +Bots in `WaitingGroup` behalten ihren Raid-/Dungeon-/PvP-Intent einschließlich +Instanzziel. Ein separates `WaitingLocation` erhält jetzt eine tatsächliche +`travel_to_location`-Anreise über den normalen ActionExecutor/TravelGraph. +Die Auswahl verwendet vorhandene Hauptstadt-, Fraktions-, Karten- und +Gebietszugangsregeln sowie ausdrücklich `LevelEligible(..., true)`. +Neutrale Hauptstädte sind zulässig, sofern alle Zugangsprüfungen bestehen. +Eine bereits besuchte erlaubte Hauptstadt wird behalten, sonst zunächst die +Hauptstadt gleichverteilt und anschließend ein erlaubter Treffpunkt gewählt. + +Unvollständige Botgruppen übernehmen das Warteziel des Anführers und warten +gegebenenfalls dessen Auswahl ab. Queue setzt weiterhin `route_source` und den +verpflichtenden Unterstadt-Ankunftsknoten. Kein aktives Suchen nach neuen +Gruppen; vorhandene Einladungs-/Browserregeln bleiben zuständig. + +Der Browser bleibt während der Anreise aktiv. Kartenwechsel einer Graphreise +verwerfen das Warteziel nicht. Innerhalb eines Dungeons und im Schlachtfeld +setzt die bisherige native Bewegungsübernahme den Wartezustand aus. Eine nun +vollständige physisch reisende Gruppe wechselt über den vorhandenen sicheren +Intentwechsel (Stop am Graphknoten), statt eine laufende Hauptstadtfahrt zu +überlagern. Performative Aktivitäten beginnen erst nach tatsächlicher Ankunft +und dürfen sich danach innerhalb der Hauptstadt vom Ankunftspunkt entfernen. +Fehlgeschlagene Anreisen werden nach 30 Sekunden neu ausgewählt; fehlende +zulässige Ziele werden nach 60 Sekunden erneut geprüft. Der eigentliche +Aktivitätsintent wird dabei nicht durch einen Ersatzintent überschrieben. + +Regressionen: `test_waiting_capital`, `test_browser_wait_activities`, +`test_adventure_intent_state`, `test_shared_group_route`, +`test_undercity_arrival`, `test_adventure_group_departure`, +`test_instance_group_protection` bestanden. Die veralteten Testdoubles im +Intent-State-Test wurden an vorhandene Ankunfts-/Aktivitätsfunktionen und die +neue Wartezielverwaltung angepasst; zusätzliche Kartenwechsel-/Eintrittstests. +Build-/Installations- und Liveergebnis siehe Session-Checkpoint und +`checkpoints/2026-11-04-waiting-capital/`. diff --git a/doc/clientless/2026-11-05-adaptive-start-radius.md b/doc/clientless/2026-11-05-adaptive-start-radius.md new file mode 100644 index 0000000000..1874c6e389 --- /dev/null +++ b/doc/clientless/2026-11-05-adaptive-start-radius.md @@ -0,0 +1,36 @@ +# Erweiterter TravelGraph-Einstieg für flugfähige Bots + +PlanAdaptiveRoute sucht zunächst wie bisher nach lokalen Knoten und nach +Einstiegen auf aufgezeichneten Bodenkanten. Gibt es keinen nutzbaren Einstieg +und beherrscht der Bot freies Fliegen, werden anschließend zusätzliche Ringe +in 500-Yards-Schritten bis einschließlich 10.000 Yards geprüft. Der erste Ring +mit einem zulässigen Einstieg liefert die Startkandidaten für die Route. + +Die Knoten werden nach Entfernung und Schlüssel geordnet. Erweiterte Einstiege +benötigen eine zulässige Flugverbindung; die lokale MMAP-Suche wird nicht auf +10.000 Yards ausgedehnt. Bodenbots behalten ihren bisherigen Radius. Gesperrte +Abflugorte, gesperrte Knoten, Gebiets- und Fraktionsregeln, native Flugfreigaben +sowie die bestehenden Reisekosten gelten weiterhin. Gemeinsame Gruppenrouten +verwenden denselben Executor wie zuvor. + +Pro Ring bleiben maximal vier neue Flugprüfungen vorgesehen. Die Erweiterung +hat ein separates Budget für bis zu 20 solcher Ringe und verdrängt nicht die +16 übrigen Flugprüfungen der eigentlichen Graphsuche. Prüfungen werden innerhalb +desselben Plans einschließlich der zweiten Suchstufe wiederverwendet. + +Der Produktionstest prüft einen Einstieg bei 952 Yards, die inklusive Grenze +von 10.000, Ausschluss bei 10.001, Bodenbots, gesperrte Abflugorte und Gebiete +sowie erfolglose innere Ringe vor einem erlaubten äußeren Einstieg. Der bisherige +Test einer einzelnen gesperrten Flugverbindung berücksichtigt jetzt einen +zulässigen alternativen Einstieg außerhalb des alten Radius. Zusätzlich bestehen +die Tests für gemeinsame Gruppenrouten, den lokalen Flugadapter und den +Graph-Probe-Cache. + +Build-/Installations- und Liveergebnisse stehen in SESSION-RESUME.md und +checkpoints/2026-11-05-adaptive-start-radius/. + +Liveprüfung: Brynli fand im Ring 1.000 Yards den Knoten taxi_122 (952,4 Yards), +verließ den Innenbereich zu Fuß und flog anschließend dorthin. Native Flugposition +und 434,7 zurückgelegte Yards bestätigen Bewegung; eine spätere Hauptstadtankunft +wurde nicht abgewartet. Build und Installation erfolgreich, alle 118 ursprünglichen +Bots und Loaderbetrieb wiederhergestellt. diff --git a/doc/clientless/SESSION-RESUME.md b/doc/clientless/SESSION-RESUME.md index 9d2509016b..114da0cd3a 100644 --- a/doc/clientless/SESSION-RESUME.md +++ b/doc/clientless/SESSION-RESUME.md @@ -1,3 +1,913 @@ +# 2026-11-05 – Gesamter verbleibender Entwicklungsstand für lokalen Commit zusammengeführt + +Nutzer hat ausdrücklich den gesamten noch offenen Stand zum Commit freigegeben. +Dieser gemeinsame Stand enthält alle zuvor noch uncommitteten Änderungen an +Navigation, Hauptstadt-Wartezielen, CombatRefs, Instanzgruppen, finalem Logout, +Halaa-Besitzverwaltung sowie deren Regressionstests, Berichte und wiederverwendbare +Diagnose-/Testskripte. Der Alchemistenstein-Fix wurde bereits separat als +2403edcf18 committed. Frühere Hinweise auf bewusst uncommittetes Entwicklungs-WIP +sind durch diesen Sammelcommit überholt. Kein Push. + +Generierte Laufzeitdaten bleiben lokal: Logs, JSON-/TSV-/Text-Snapshots, Core-Dumps, +gesicherte Worldserver-Binaries, Python-Caches und der Datenbank-Fault-Marker. +Neue .gitignore-Regeln verhindern ihre versehentliche Aufnahme. Es wurde nichts +von diesen Belegen gelöscht. Schon versionierte historische Checkpoints bleiben +versioniert. Bestehende Build-/Installations-/Gameplay-Belege stehen unten bei +den jeweiligen Änderungen; der Commit selbst erfordert keinen Serverneustart. + +--- + +# 2026-11-05 – Alchemistenstein-Fix lokal committed + +NutzerauftragletzteÄnderungencommitten: lokalerCommit2403edcf18 +"Fix Alchemist Stone procs for healing potion events" auf feature/clientless-player-core. +Enthält ausschließlichspell_generic.cppAlchStonefix, test_alchemist_stone_proc.py +und2026-11-05-alchemist-stone-crash.md (3Dateien). KeineLogs/Dumps/Binariesundkein +Push. ÄlteresbekanntesWIPindenanderenDateienundSessioncheckpointbewusstnicht +inAlchStonecommitgemischt. Build/Install/ASanUBSanundServerzustandwieunten. + +--- + +# 2026-11-05 10:27 – Alchemistenstein-Crash FERTIG korrigiert und installiert + +NutzerauftragAlchstonefixerledigt:spell_generic.cppCheckProc+HandleProcverwenden +nativeProcEventInfoGetSpellInfo+Null/Familyguard, HeilbonusHealInfoGetHeal40%, +keinpflichtDamageInfofürHeiltreffer. Fehlende/NullpayloadsnoBonus. Bestehende +ManaDamageInfoPayloadweiterwievorher(keinegenerischeManabetragserfassung). +SavedCorebelegtDrakzug43185heil3728DamageInfo0; Bonus1491Regressionexaktgetestet. +ProductionHandler+nativeMetadataMethodtest_alchemist_stone_procmitASan/UBSanPASS, +Build-j10PASS/InstallPASS. ServerwarbeiBuildstartschonoffline(keinWorld/GDB/tmux), +Loaderconf0; keinRuntimeLogoutmöglich/nötig. DanachpassiverGDBStartPID4477, +freshReady+Ports8085/8087PASS, prozessweiterlive. 118Botsonline/Loaderconf+nativ1, +SafeLogoutpending0. AlteRosterreferenz183fehlte,weilCharacterinDBnichtmehr +existiert; nichtreproduziert/neuangelegt. RegulärePopulation118wiederhergestellt. + +GameplayHeiltrankbonusnichtunabhängigingamegeprüft; Produktionsrepro+ASan/UBSan +undBuild/Startverified, nichtnurcompilationalsgameplayausgeben. Dump7.8GBund +matchingBinary1.16GBedwin-crash/worldserver-crashedbewahrt. KeinCommit/Push. +ArbeitssourceeinzigAlchStoneAuraScript+Regression/Docs, altesWIPbewahrt. +Belegealchemist-stone/result.json/native-final.log/crash-event.log/build/install/ +startup-ready/test.log/roster-final.json; stdoutstartup.log. Bericht +2026-11-05-alchemist-stone-crash.md. Edwin36aktuellerIntentnachRestart10Uhr +Gruppenbildungpoi_guard_346, imTravel; beiweitererNavfrageerneutnativabfragen. + +--- + +# 2026-11-05 10:22 – Alchemistenstein-Crash korrigiert, neuer Start läuft + +NutzerfragtzuerstEdwin36: letzteNativeDatenSocialpoi_npc_299015(Map1Salista), +OutlandlokalerFußwegzumAbflugplatz; währendAbfrageWorld265301bereitsSIGSEGV +00:30:24(keineaktuellenAntworten). Crashcoreworldserver-20261105-002730.core7.8GB +undmatchingBinaryedwin-crash/worldserver-crashed1.16GBgesichert. PassiveGDB +hingbeimfullbtPrettyprint; dedicatedGDB265259+world265301mitSIGTERMbeendet, +RecoveryPID267338Ready+118Login00:34. SpäteruserneuerFixauftragamVormittag: +ServerbereitsbeendetkeinWorld/GDB/tmux(Startupedwin-crashlogendet01:05TC), +keinenneuenCrashdafürbehaupten/keineZusammenhangsvermutung. + +NutzerbeauftragtAlchStonefix. CoreofflineleanBt+eventbestätigtDrakzugPotion43185, +DamageInfo0,HealInfoheal3728. spell_generic.cppnurCheckProc/HandleProcgeändert: +nativeGetSpellInfo+Null/Familyguardbeides, HealInfoGetHeal40%(1491), bestehender +ManaDamageInfozweignurwennvorhanden+Betragpositiv, fehlende/NullpayloadskeinBonus. +KeinCoreProcumbau/keinClientlessDependency. test_alchemist_stone_procProduction- +Handler+nativeMetadatamethodeASan/UBSanPASSinklkonkretem3728Fall/Overheal/ +Rundung/Nullmeta/Nullpayload/Nullbeträge/NichtPotion/alteManaPayloads. +BekanntManaEnergizeCorelieferttypischNODAMAGE0; generischeManabonus-Neuerfassung +wurdeNICHTinCrashfixausgeweitet, dasistkeinneuerRegressionserfolg. + +Build-j10+InstallPASS, ServerwarvorBuildschonoffline,nativeSafeLogoutnichtmöglich, +Loaderconf0,vorInstallStartProzess+tmuxAbwesenheitgeprüft. FrischerStartmit +passivemGDBläuft, stdouter2026-11-05-alchemist-stone/startup.log. Ready/Ports +8085/8087nochprüfen, ursprünglichenLoader1+Botbetriebwiederherstellen. Referenz +118Rosteredwin-crash/roster-before-restart.json(ggfaktuellebestehendePopulation +beachten). KeineTP/GMcharacter/Intentänderungbeauftragt. DanachStatusEdwinwenn +sinnvollweiterprüfen; NutzeraktuellwillCrashfix. Belegealchemist-stone/ +crash-event.log/build/install/test.log/result.json. KeinCommit/Push. +VorherigesWIPbewahrt; Bericht2026-11-05-alchemist-stone-crash.md. + +--- + +# 2026-11-05 00:29 – Adaptiver Graph-Einstiegsradius FERTIG und Brynli LIVE unterwegs + +Nutzerauftrag500->+500bis10000fürFlugfähigeimplementiert/gebaut/installiert. +PlanAdaptiveRoutebisherigeNah-/RecordedEdge-Einstiegezuerst; nurwennkeineakzeptiert +undFlightEnabled:sortierteKandidatennachEntfernung, Ringe500bis10000inklusive, +StopbeimerstenbrauchbarenRing. FerneEinstiegenurFlug, lokaleMMAPweiter500. +4FlugprobesproRing,max80fürExpansionseparatvon16normalenGraphflugprobes; +CacheüberSuchtierswiederverwenden; Originsperren/Area/Faction/Kostenweitergelten. + +4RegressionenPASS:free_flight_planner(inklneue952/10000/10001/Bodenbot/ +BudgetüberalleRinge/Originsperren/Areasperren),shared_group_route, +route_approach_flight,graph_probe_cache. Build-j10PASS/InstallPASS. +AltPID260775normalexitedFINALIZEremaining0keinDump. NeuerPID265301passiverGDB, +freshReady+Ports8085/8087PASS. stdoutadaptive-start-radius/startup.log. + +Brynli84zuerstalleinbeiLoader0ohneTP/Intentoverrideeingeloggt. LIVE: +START_RADIUS_EXPAND map530radius1000accepted1flightCalls1 (2Tiersreused); +Route8stepswartendeHauptstadtpoi_npc_82940anchorTaxi627, ersterFlightRootTaxi122 +952.4yd. Indoors->ApproachDeparture->ausGebäudegelaufen->FlightSTART32240. +NativeStatusFlyingtravelled434.7, tatsächlichePosition(3569.782,3834.480,135.585), +mounted1/canFly1/flying1. Instanzintent4320SchwarzerMorastbeibehalten. Nichtschon +Hauptstadtankunftbehaupten; Bewegung/erweiterterEinstiegdagegenLIVEbelegt. +Original118BotsallewiederOnline, Loaderconf+nativ1, SafeLogoutpending0. +Artefakteadaptive-start-radius/result.json/native-final.log/brynli-navigation.log, +brynli-first-live.log/roster-final.json/BuildInstallShutdownStartupTests. +Bericht2026-11-05-adaptive-start-radius.md. VorherigesWIPbewahrt, keinCommit/Push. + +--- + +# 2026-11-05 00:25 – Adaptiver Graph-Einstiegsradius implementiert, Build läuft + +NutzerbeauftragtflugfähigeBots:keinEinstieg500->+500bis10000. +ClientlessTravelGraphMgr::PlanAdaptiveRoute: sortedallowedstartNodesproMap; +seedStartNodesLambda, initial500unverändert, vorhandeneRecordedGroundEntry +vorErweiterungbevorzugt. NurwennkeinStartRoot/keinvirtualgroundentry+flightEnabled, +Ringe+500bis10000inklusive; erstenRingmitStartRootnutzen. FerneRoots ausschließlich +Flight, keineMMAP-Langwege; ground-onlyRadiusbleibt. ViernearestFlugprobesproRing, +separatesmax80(20*4)ExpandedBudget, vorhandene16FlugprüfungenfürrestlicheSuche +nichtdurchErweiterungverbrauchen. CacheinnerhalbPlanundDeferredTierweiterbenutzen. +Blockedstart/flightOrigin/area/faction/nativeFlight/costProfileallesweiterprüfen. + +Productiontest_free_flight_plannerPASSinkl952/10000/10001/ground-only/ +innerenFehlernüberalleRinge/budget/gesperrteOriginsundAreas; alterTesteinzelne +RootAsperrejetztzulässigerAlternativrootBanstattTotalfehler. DreiweitereTests +shared_group_route/route_approach_flight/graph_probe_cachePASS. Build-j10läuft +(adaptive-start-radius/build.log), Loaderconf+nativ0+safeallbeiBuildbeginn. +Operation.jsonOriginalroster118Loader1. AktuellPID260775stdoutvertical-takeoff/ +startup-final.log. NachBuildnormalshutdown10/warten/install/passiverGDBBash, +neuesstdoutadaptive-start-radius/startup.log,ReadyPorts8085/8087. Brynli84zuerst +amOriginalortohneTP/Intentoverridebeobachten, dannOriginal118+Loader1restore. +OriginalBrynliMap530ManaschmiedeAra3722(3926.615,4034.016,192.818),indoors, +level70/flightenabled1, nächsterNode952.4ydtaxi122, bisherPlanfehlerkeinStart. +InnenraumkannweitereFlugfreigabevordemExecutorverhindern; nichtungeprüftErfolg +behaupten. KeinCommit/Push. VorherigesWIPbewahren. + +--- + +# 2026-11-05 00:03 – Senkrechtstart und vertikales Luft-Ausweichen FERTIG installiert + +Nutzerauftragabgeschlossen. SegmentVerticalClimbhältgeprüftesvertikalesAufstiegs- +segmentbisNativeSplineEnde;Collision+positionsbasierte60sStallerkennungweiteraktiv. +ZusätzlichNextopt-inverticalEscapefürHinflugwennSeitkandidatenerschöpft:voller +geprüfterAufstiegauchausengerSafetyband, bodyrays/Deckebindenweiter;nichtdurch +Slab. Return-/LandingSimulationenohneOpt-inbleibenunverändert. 9Regressionen +PASS(inklneuemtest_vertical_flight_escapeundproductionUpdateHandoff/Collision/ +SplineEnde/60sStall). Finalsourcevollmit-j10gebaut(build-final.log13/13),Install +PASS, AltPID258409normalexitedFINALIZEremaining0keinDump; neuerPID260775 unter +passivemGDB, freshReady+Ports8085/8087PASS. Finalstdoutvertical-takeoff/startup-final.log. + +LiveersterTeil: VorEscape-ErweiterungRixzek73erreichteStarthöheZ36.26dannweitere +Segmente. NachzusätzlichenVersuchenNativeFLIGHT_LANDED23:55:47 travelled2373.76 +undSaveMap530(-2183.82,5402.48,51.8825)inShattrath. KeineTeleport-/Intentänderung; +vorhergeplantermanuellerTestwurdeNICHTausgeführt. ZusätzlicherVerticalEscape- +ZweigbishernurRegressiongetestet; nichtalsneuenLiveBranch-Erfolgausgeben. +Original118RosterwiederOnline, Loaderconf+nativ1wiederhergestellt, safepending0. +ErsteRosterquerynachNeustartwarfrüh72/118loading, danach118/118ohneMissing +bestätigt. KeinCode-/Buildfehler. Artefaktevertical-takeoff/result.json, +roster-final.json, native-final.log/rixzek-native-flight.log, alleBuild/Install/ +Shutdown-/Start-/Testlogs. Bericht2026-11-04-vertical-takeoff.md. AllefrüherenWIP +bewahrt, KEINCommit/Pushbeauftragtoderausgeführt. + +--- + +# 2026-11-04 23:59 – Rixzek inzwischen OHNE Teleport in Shattrath gelandet + +Alt-LivebuildPID258409mitStartlatch: Nach2weiterenLuftkorridorabbrüchenhatRixzek +selbstständigShattratherreicht. Native FLIGHT_LANDED23:55:47 travelled2373.76, +SavedCharacterposition(-2183.82,5402.48,51.8825)Map530offline0(safeLogout). +Neuesrixzek-before-final.jsondokumentiertOnline0+Position. KEINTeleport und +KEINIntentoverride. VorhergeplantermanuellerAnreisetestentfällt, nichtausführen. +DamitursprünglicherStartfehlerlivebelegtbehoben; zusätzlicheEscape-Erweiterung +wirdnochverlinkt(build-final.log12/13),nachBuildnormalshutdown/install/passive +startup-final.log/Ready/Ports, Original118undLoader1restore. NeuerTagbald, +RixzeknormalLogin+Schedulebelassen. 9TestsPASS inklvertical_flight_escape. +VorherigesWIPbewahren;keinCommit/Push. Finalresultpartiellaktualisiert. + +--- + +# 2026-11-04 23:56 – Senkrechtstart live bestätigt; vertikales Luft-Ausweichen ergänzt + +LIVE ersterFixunterPID258409: Rixzek73n1jetztvollZ36.26, danchn2/3/4 seitlich, +beiZ41.77erneutKorridorabgelehnt->Return. ZuerstvorzeitigesHandoffdamitbehoben, +aberLuftausweichpfadfehlte. NutzerimCommentaryinformiert; Taskweitergeführt. +ClientlessFlightPath::Next zusätzlicheropt-in verticalEscape=false; Advancefür +Hinflugaktiviert, Return/FindReturnSimulationunverändert. WennalleSeitkandidaten +scheitern, vollständigeSäuleaufwärtsprüfen (inklBodyrays/Gebietsdecke),ausenger +SafetybandmitdepartureCorridor(true)entkommen,nichtdurchüberliegendenSlab. +SegmentVerticalClimb(umbenannt)hältjedesgeprüftevertikaleAufstiegssegmentbis +SplineEnde;Collision+60sStallweiteraktiv. NeuesProduction-Next-Test PASS inkl +unterSafetyband5yd/Dach38/Decke34/unknown/slab/opt-in/normalRoutegleich. +9RegressionenPASS. Sourceheaderwährendbuild-escapeangepasst, deshalbnachdessen +ErfolgUNBEDINGTfinalen-j10Buildmitloaderreload+safeallerneutstarten! +Loaderconfig+nativ0+safeallbeiEscapeBuildbeginn. PID258409weiteralterStand, +stdoutvertical-takeoff/startup.log. NachFinalBuildshutdown/install/passiveGDB, +neuerstdoutvertical-takeoff/startup-final.log. Originalroster118/Loader1restore. +DaMitternachtbevorsteht,kanngezielterkurzerAnreisetestohneTeleportpernative +adventureintent73groupformationpoi_guard_242120nötigwerden; nachCheck +adventureschedule73onwiederherstellen(sichererGraphhandover). Vorherstatus +aufzeichnen, temporäreIntentänderungtransparentberichten. KeinCommit/Push. + +--- + +# 2026-11-04 23:52 – Senkrechtstart-Fix installiert, Liveflug noch abzuwarten + +Build-j10+InstallPASS,8FlugtestsPASS. AltPID254830exitednormallyFINALIZEremaining0, +keinDump. NeuerPID258409passiverGDBReady+8085/8087. Rixzek73loginamOriginalplatz +(-2931.27,3430.67,1.26) ohneTP/Intentoverride, nativeRaid-Wartezielwahlzuerst +ThunderBluffpoi_npc_296156, GraphstartfehlernochvorFlug, automRetry. +Originalroster118Loginwiederangefordert, Loaderconf1/reloadconfig/populationreload; +Onlinebestand/Loadernochabschließendprüfen. stdoutvertical-takeoff/startup.log, +result.jsonaktualisiert. KeineCommits. NeuesFlagSegmentVerticalTakeoffunterdrückt +nurfrühesHandoff, Kollision/60sStallvorherweitergeprüft. NativeErfolgnochNICHT +behaupten. UserwurdeüberInstallation/ausstehendenFlugcheckinformiert. + +--- + +# 2026-11-04 23:48 – Senkrechtstart-Handoff korrigiert, Build läuft + +NutzerbeauftragtfixvorzeitigerAbbruchimsenkrechtenStart. Recordbool +SegmentVerticalTakeoff wirdbeimLaunchausnoch-nicht-Departed+vertikal+Aufstieg +ermittelt. UpdatefrühesHandoff<=1500mswirdfürdiesesSegmentausgelassen; +SplineEndeplantregulärweiter. Kollisionenund60sWatchdogstehenweiterdavor. +KeineKollisionslockerung/Timerabschaltung. AchtRegressionenPASS, productionUpdate +neu:VerticalflaguP+Rest1000keinAdvance; geänderteCollisionreplantsofort; native +EndeöffnetAnschluss;60sStillstandauchmitVerticalFlaggeprüft. + +Build-j10läuft(logvertical-takeoff/build.log); Loaderconf0+nativpopulationreload0, +safeallbeiBuildbeginnangefordert. Operation.jsonOriginalroster118+Loader1. +NachErfolgnormalshutdown10/wartenprocess+tmuxend/install/passiverGDBviaBash; +neuerstdoutvertical-takeoff/startup.log. AktuellerPID254830,oldstdout +ground-departure-retry/startup.log. Rixzek73zuerstohneTP/Intentwechselanmelden, +vertikalenAufstiegZ36.26jetztabschließenlassenundAnschlussprüfen. +Originalroster+Loader1danachrestore,Ready/8085/8087prüfen,keinCommit/Push. +VorherigesWIPunverändertbewahren. Bericht2026-11-04-vertical-takeoff.md. + +--- + +# 2026-11-04 23:45 – Boden-Ausweichstart fertig; Luftproblem separat sichtbar + +NutzerauftragFußwegfallbackeingebaut+installiert. 9FlugtestsPASS; letzterDeparture- +TestauchproductionNext-Dach38-Repro. Finalbuild-j10/InstallPASS, alterPID249148 +exitednormally/remaining0, keinDump. AktuellPID254830passiverGDBReady+8085/8087; +Loader1/allOriginal118BotswiederOnline. KeinCommit/Push. VorherigesWIPbewahrt. + +LiveRixzek73ohneTeleport/Intentänderung: automatischeRaid-Wartezielwahl nach +vierGraphstartfehlern endlichShattrathpoi_guard_261(23:44:42). ApproachDeparture +findetanderenAbflugplatz(-2931.265,3430.665,1.258), Fußweg8.01ydNAV_COMPLETE +23:44:44. SenkrechterTakeoffn1 ZielZ36.26; tatsächlichePositionZ15.8021um23:44:47, +mounted1/canFly1. DortAdvancebeihandofferneutNextkeingültigerKorridor->Return, +landetwiederunten. Danach5genericRecoveriesgleichesSteigen/Abbrechen. +DamitjetztLIVEbewiesen: Fußweg+Abhebengehen, LuftteilhatweiteresProblem +(SenkrechtstartwirdwährendSegmentneugeplant/abgebrochen, stattvolleHöheerst +zuerreichen). NutzerimCommentarydarüberinformiert; NICHTAnkunftbehaupten. +NeueRetryDeparture-VerzweigungselbstnurProductionRegressionbestätigt, hier +initialerApproachDeparturegerufen(keinFLIGHT_DEPARTURE_RETRYimLog). +FixändertbewusstkeinebestehendeAirbornevermeidung. FallsNutzerLuftkorrektur +beauftragt, Next/UpdateHandoffMemory.DepartedwährendsenkrechtemTakeoffprüfen. +Artefakteground-departure-retry/result.json/native-final/rixzek-navigation.log, +roster-final.json,Build-/Install-/Shutdown-/Startup-Testlogs; Bericht +2026-11-04-ground-departure-retry.md. Hauptstadtankunftweiterunbestätigt. + +--- + +# 2026-11-04 23:43 – Boden-Ausweichstart installiert, Liveprüfung läuft + +Finalbuild-j10/InstallPASS; alterPID249148normalexited/FINALIZEremaining0keinDump. +NeuerPID254830passiverGDB, frischesReady+Ports8085/8087PASS; stdout/checkpoints +2026-11-04-ground-departure-retry/startup.log. Rixzek73LoginohneTP/Intentoverride, +NPC16810Abwehrgewonnen. Raid409MoltenCoreLevel68Eligibilitycontent/own/invitation1. +NativeWaitingGroup->Capitaltravelpoi_npc_326726scheitertvorFlightbekannter +Graphstartfehler; automatischerRetryläuft. NeuesRetryDeparturebisherNICHTlive +bestätigt. Operationrosterwirdzurückeingeloggt, Loader1reloadangefordert, finalen +Onlinebestand/Loadernochprüfen. NeueColumn40yd(35+Körper),Dach38RegressionPASS. +9RegressioneninsgesamtPASS. VorherigesWIPbewahrt,keinCommit. + +--- + +# 2026-11-04 23:37 – Boden-Ausweichstart implementiert, Build läuft + +NutzerbeauftragtFixblockiertesErstsegment->zuFußanderenAbflugplatzsuchen. +ClientlessFlightMgr::RetryDeparture vor Advance-Fail: nurvorSegment1/MemoryDeparted0, +Returning0, flight/fall/water/transport/taxi/teleport/combat0,alive, tatsächlicher +Support<=3yd. Dismountnurdanach; kopierterRecordfürApproachDeparture (keine +Selfmove-Zuweisung). max3DepartureAttempts, UpdateApproach->Begin erhält Budget+ +ActionOwned. SucheprüftOpenColumn40statt15 (35ydvertikalerFallback+Körperstrahlen). +KeinEingriffinbestehendeAirbornevermeidung/Landung. + +8RegressionenPASS:flight_departure/handoff/dry_landing_recovery/elevated_flight/ +free_flight/flight_takeoff_handoff/shared_flight_landing/shared_flight_rejoin. +Zusatzflight_policyPASS. NeueTestsproductionRetryDepartureguards/recordmapalias/ +roof38(clear15/35aberkeinStart40), Budget/OwnernachBegin. Build-j10läuft; +checkpoint ground-departure-retry/operation.json Originalroster/Loader1, config0+ +reloadconfig+clientlesspopulationreload+safeallbeiBuildbeginn. NochkeineInstallation. +DanachnormalShutdown10, komplettEndeprüfen/install/passiverGDBStartviaBash, +Ready/8085/8087, Rixzek73ohneTPwiederLoginundbeobachten; Scheduleist23Raid3528, +vorherIdlefallbacknoRemainingMatchingLocationkannweitereAnreisefortsetzung +verhindern, nichtblindIntentändern. Originalroster+Loaderrestore. +AktuellerPID249148,stdoutwaiting-capital/startup.log. Neuerstdoutground-departure-retry/startup.log. +KeineCommitsbeauftragt, vorherigesWIPbewahren. + +--- + +# 2026-11-04 23:30 – Rixzek-Abflug read-only untersucht + +NutzerfragtwarumRixzeknichtwegkommt, KEINCodefixbeauftragt. NativeLogsbeweisen: +Mountfunktioniert, Flying->NextscheitertvorSegment1, lokaleLandungsteps0 und +5ActionRecoveryversuchegleicherStart. LetzterStatusFlightFailed "safely returned +after abort: no safe bounded flight corridor; holding position mounted"; +BehaviorinzwischenIdle "no remaining matching location; waiting for next schedule". +Position(-2923.814,3433.576,0.949),keinCombat/Falling/Water, Map530. +Read-onlyFlightprobes:hierMAXsurface0.938; y+2surface31.078; y+5surface34.539; +x+5surface34.469; +/-15teils30..38. Überbau/Randinder2ydFlugbreite. +OpenColumnStandard15ydlässtStartzu, NextsenkrechterFallback35ydvolumetrisch +kollidiertmitobererGeometrie31+; diagonaleKandidateneinengleichenBereich. +ApproachDeparturewirdnurvorMountbeiStartvorprüfungsfehlergerufen,nichtbei +Next/AdvanceErstsegmentfehler. DaherkeinseitenversetztesBoden-Ausweichmanöver. +Fixvorschlag:vollständigerersterAbflugvolumentest/aufBodenbeiErstsegmentfehler +vorhandenesApproachDeparturenutzen, keineKollisionlockern. AktuellerIdle- +Fallback-Endzustandseparatprüfen,wennFixbeauftragt. KeineTP/Intent/Runtimeänderung; +nurDiagnosebefehle, Artefakte rixzek-departure/native-probes.log/diagnosis.json. + +--- + +# Nachtrag 22:58 – Livegrenzen Hauptstadtfallback + +WAIT_CAPITAL_ARRIVED bestätigt49(PvP,Darnassuspoi_guard_93)und100(PvP,Stormwind +poi_guard_548). Rixzek nachweiterenFehlern erneutShattrathpoi_npc_68760, +ActionMoving, FlightMounting. LetzterProbe22:57:40bei(-2923.814,3433.576,0.949), +mounted0/canFly0/flying0; wiederholte32243/32244/32245-Aufsitzzustände. +NICHTbehauptenerfliegebereitsoderseiangekommen. DerneueFallback/start/retry +istbestätigt, RixzekhatseparatesnochungeklärtesMount/Navigationproblem. +NativeAuszügeundresult.json aktualisiert; gesamterServerbetriebwiederhergestellt. + +--- + +# 2026-11-04 22:57 – Hauptstadtfallback LIVE installiert + +Aktueller Nutzerauftrag umgesetzt, Details 2026-11-04-waiting-capital.md. +Beide Builds-j10+Install PASS. Sieben ausgewählte Regressionen PASS; gitdiffcheck +sauber. Loader korrekt clientless population reload; ursprünglicher Betrieb +Loader1/alle118Bots wiederhergestellt. NormalerShutdown alterPID244387 endet +FINALIZE_END remaining0/exitednormally, keinDump/Backup. Neuer PID249148 unter +passivemGDB, frisches Ready (startup-ready.log, später auch stdout), Ports8085/8087. + +Live Rixzek73 ohne TP/Intentoverride: bleibtRaid am MoltenCore-Ziel3528, wählt +separat Shattrath3703 poi_npc_68760, queued nativeTravelLocation/2-stepFreeFlight. +NPC18583 bindet unmittelbar vorAbflug erneutCombat; Defensive gewinnt Kampf. +Retry greift automatisch, würfeltThunderBluff poi_npc_296156, dannSilvermoon +poi_npc_63362. DiesebeidenTravelActions scheitern mit vorhandenem Startproblem +"no reachable travel node or recorded ground edge near current position". +Neue Hauptstadtanreise + Retry LIVE bestätigt; erfolgreicheAnkunft NICHTbestätigt. +Nicht als vollständigen Ankunftsnachweis ausgeben; aktuelle Navigation beiRixzek +kann separatweitere Diagnose brauchen. Tests prüfen echteAnkunftsvoraussetzung. +Artefakte waiting-capital/result.json, roster-current.json, rixzek-first-live.log, +server-after-restore.log, startup.log, nativeDiagnosen imstdout. KeinCommit/Push. + +--- + +# 2026-11-04 – Hauptstadt-Warteziel implementiert, Installation folgt + +Aktueller Nutzerauftrag: Bots ohne passende Gruppe im Raid/Dungeon/PvP-Intent +sollen eine erlaubte Hauptstadt anreisen (zufällig, Fraktion/Level/Gebietszugang). +AdventureBehaviorMgr UpdateWaitingCapital + separate WaitingTravel/WaitingArrived/ +WaitingRetryAt-Felder implementiert. Eigentliches Aktivitätsziel bleibt bestehen. +Uniforme Hauptstadtwahl, dort POI; vorhandene erlaubte Hauptstadt behalten. +Partial-Botgruppen warten Anführerwahl ab und übernehmen erlaubtes Warteziel; +Queue unverändert mit route_source/Undercity-taxi_11. Physische vollständige +Gruppe wechselt sicher via SetWanted statt direkt laufende Fahrt zu überschreiben. +Graphkartenwechsel löschen Ziel nicht mehr, Dungeon/BG übernehmen weiter nativ. +Erst echte Ankunft öffnet Aktivitäten, danach kein Rückruf an POI bei jedem Tick. +Fehler 30s retry, ohne zulässige Ziele 60s retry. Sieben Tests PASS, darunter neuer +Production-Helper-Test und aktualisierter Intent-State-Harness/Kartenwechseltest. + +Artefakte waiting-capital/: operation.json mit Loader1+Originalroster118; +Build-j10 initial PASS, finaler Build für nachgezogenen Leader-Tick-Fix läuft/prüfen. +Loaderconf0; anfangs falsches Kommando adventure loader reload (Syntaxfehler), +inzwischen korrekt clientless population reload bestätigt enabled0; safeall aktiv, +10 Restbots natürlich abwarten. Nicht forceausloggen. Nach finalem Build normalen +shutdown10, Prozess/tmux komplett beenden, install, passiver Launcher via bash, +fresh Ready/Ports8085+8087 prüfen, Rixzek73 zuerst ohne Teleport/Intentoverride +beobachten, Originalroster118 und Loader1 wiederherstellen. Aktueller PID244387, +stdout combat-reference-defense/startup.log. Neues stdout waiting-capital/startup.log +verwenden. Kein Commit/Push beauftragt. Vorheriges WIP komplett bewahrt. + +--- + +# 2026-11-04 22:23 – CombatRef-Defensive fertig und LIVE bestätigt + +NutzerFixauftragumgesetzt:AdventureCombatMgr.cpp(nur)übernimmtbestehendePvE/PvP- +CombatRefsalsDefensivzielewennimKampf/keineAI/keinCooldown. NachGruppenheilung/ +GruppenzielundDamagerQueue; nurValidnativeZiele<=80yd, beidseitigeSuppression +überspringen. DirekteAngreiferbevorzugt,dannNähe/GUIDstabil. KeinAngriffaufbloß +naheungebundeneNPCs, keinCombatflagReset/Threat-Hack. BestehendeFighter/Flucht/ +Tod/Phaseweiterverwenden. GruppeneigeneFallbackdefensewennGroupTargetleerbleibt. + +VierRegressionstestsPASSinklProduktionsUpdate/ValidfürSchaden0Ref/PvP/Gruppe/ +Suppression/Sicht/Phase/ungültig/Evade/Tod/Dummy/Distanz/Cooldown/Priorität. +Vollbuild-j10/InstallPASS; Loaderconfig+nativ0+safeallbeiBuildbeginn, normaler +ShutdownerstnachBuild. AlterServer16Rest->0,exitednormally,keinDump/Binarybackup. +NeustartPID244387freshReadyundPorts8085/8087. Rixzek73 zuerstohneTeleport/ +Zwangsattacke/CombatStop/Intentoverrideeingeloggt. LiveCOMBAT_DEFEND_REFERENCEauf +Lisaile18583/Spawn84701,zaubert30482/2136/133/133, COMBAT_END. Danachcombat0, +health7666/7666;NPCattackable0+Respawnrecord. Korrekturdamitliveausgelöst, nicht +nurkompiliert. AktuellerTagesplanRaidPhase5wartetGruppe/nativeBrowser; Stillstand +jetztgegebenenfallsdieseWartephase, nichtmehrKampfsperre. KeineInstanzankunftbehaupten. +Original118Bots/Loader1/SafeLogout0wiederhergestellt. NPC-AI-Scriptphaseursprünglich +nichtseparatbewiesen/angepasst. KeinegenerischenCoreCombatManagerÄnderungen. + +Artefaktecheckpoints/2026-11-04-combat-reference-defense/:operation/result/tests.json, +Build/Install/Testlogs,rixzek-first-live/rixzek-combat-live.log,lisaile-respawn.json, +native-final.log,roster-final.json,startup-ready.log/startup.log,shutdown-before-install.log. +Bericht2026-11-04-combat-reference-defense.md. VorherigesWIPbewahrt, keinCommit/Push. + +--- + +# 2026-11-04 – Kampfziel aus CombatRefs implementiert, Build startet + +NutzerhatFixautorisiert. AdventureCombatMgr::Update übernimmt bestehende native +PvE/PvP-Kampfgegner wennimKampf, keineaktiveAI undkeinCooldown. NachGruppen- +ZielwahlundDamageQueue, bestehendeZiele/ersterDamagerbehaltenVorrang. Nurgültige +nativeZielein80yd, keineSuppressionaufbeidenSeiten; direkteAngreiferbevorzugt, +dannNähe/GUIDstabil. StartdefendingtrueübervorhandenenFighter/Fluchtpfad, kein +CombatStop/ForceAttackFlag. GruppenfallbacknurwennGroupTargetkeinZielübernahm. +NeuesLogCOMBAT_DEFEND_REFERENCE. Source.cppnur, Headerunverändert. +VierTestsundBuild-j10PASS. RegulärerShutdown16Restbots->0undexitednormally; +keinneuerDump/Binarybackup. InstallPASS, neuerStartstartup.logläuft, +Ready/LiveprüfungRixzeknochabzuschließen. + +Build-j10parallelzumsafeall angefordert, LoaderDatei+nativ0. Testsnochanzupassen/ +auszuführen. NochNICHTinstalliert. NachErfolg normalShutdown10/vollesEnde/install/ +passiverGDBStart/Ready/Ports, Rixzek73zuerstbeobachten, dannOriginalbetriebrestore. +VorherigesWIPbewahren, keinepauschalenBinaryBackupsmehr; nurfallsneuerCrash +matchingBinarynotwendig. Artefaktecombat-reference-defense/operation.json. + +--- + +# 2026-11-04 – Foto der Lisaile-Bedrohungsliste ausgewertet + +Nutzerfoto:.debugthreatLisaileFireweaverRuntime2528Spawn84701zeigtRixzekGUID73 +threat0.000000, keineOFFLINE/SUPPRESSED-Markierung. NormalerThreatlist-Header +belegtEngaged; erstesCombatstate0/Manager0imChatnichtLisailezuordnen, ältere +GM-Selbstabfrage. NativeReselectVictim nimmtauch0ThreatalsZielwennverfügbar, +NullbedrohungalleinistalsoKEINEErklärungfürNPC-Inaktivität. GenaueNPC-AI- +Victim/ReactState/Scriptphasebislangnichtgemessen; darübernichtalsbewiesenreden. +UnabhängigbestätigtbleibtSoloBot-DefensivzielwahlfehltohneDamageTaken bei +bestehendenCombatRefs. KeineCode/Runtimeänderung/Fixbeauftragungbisjetzt. +JSONrixzek-combat-stall/diagnosis.jsonmitFoto-Befundaktualisiert. + +--- + +# 2026-11-04 22:01 – Nutzer bestätigt Rixzek-Kampfbindung + +.debugcombat: Combatstate1/Managerstate1, PvELisaileFireweaverspawn84701. +BeidestehenKampfhaltungaberkeineAktionen. Bot-SeiteCode-Lückebestätigt: +Solo-Defensivzielüberr.Damagers nur nachDamageTaken(damage>0). Proaktive +Zielwahlverlangt!IsInCombat. KeineÜbernahmebestehenderCombatRefs beiAIleer. +DamitkannBotaufErstschadenwartenwährendReisewegenCombatblockiertist. +GezielterFixvorschlag: aktivevalidierteCombatRefs/AngreiferalsdefensivenFallback +übernehmen, DamagePriorität/aktiveZieleerhalten, niemalsCombatflagblindlöschen. +NochNICHTimplementiert; NutzerhatbishernurBefundgeliefert. + +NeueAsyncfrage: Lisaileauswählen.debugthreatTextschickenfürtatsächliche +NPC-Threat/Engaged/OfflineSuppressed-Ziele. NPC-Ursachebislangnichtbelegt; +SmartAI/CreatureAIbenötigtIsEngaged+gültigesVictim fürAngriffe. KeineCode- +oderRuntimeMutation. BefundJSONunterrixzek-combat-stallaktualisiert. + +--- + +# 2026-11-04 21:55 – Rixzek73 Stillstand read-only untersucht + +RixzekLv68Mage stehtMap530Terokkarzone3519area3685(-2916.474,3430.395,1.742) +seitStundenunverändert; NativeTelemetrycombat1alive1flying0keinTransport. +AdventurephaseIdleinstance->instance_visit_at_4406, Detailwaitingsafeplayerstate. +ActionIdle, keineTravel/Flight/Transport-Ausführung. HP7666/7666, keinerlei +COMBAT_START/CAST/END für73imaktuellenBootlog, alsoBlockadeKampfsperre, nicht +aktuelleRoutenberechnung. LisaileFireweaver18583spawn84701in11.08yd, lvl64, +attackable/visible/LOS1, Flags559104 inklIN_COMBAT/PET_IN_COMBAT; Bonechewer16810 +spawn57869in19.45ydauchattackable/LOS1. DasbelegtNICHTwelcherGegnerwirklich +anRixzekgebundenist. SourceCombatSolo beginntDefendüberDamageNotices; +proaktivStartnurbei!IsInCombat. KeineZwangsattacke/CombatStop/Teleport/Build. + +OffeneAsyncfrageanNutzer: RixzekimSpielexamswählenund.debugcombatTextausgabe +schicken. VorhandenerDebugbefehlcs_debug.cpp1015zeigtPvE/PvPCombatRefs, jedoch +nurPlayerSession/selectedUnit, nichtConsole. AusstehendeAntwortwirdfürgenauen +Gegner/fehlerhafteBindung benötigt. Runtimezustanderhalten. Artefakt +checkpoints/2026-11-04-rixzek-combat-stall/diagnosis.json. Server227005unverändert. + +--- + +# 2026-11-04 – Alte Debug-Binary-Sicherungen auf Nutzerwunsch gelöscht + +Die zuvor identifizierten24historischenworldserver-ELF-Sicherungen in +checkpoints wurden ausdrücklich autorisiert gelöscht (ca25.9GiB). Manifest: +checkpoints/2026-11-04-binary-cleanup/deleted.json. Frühere Referenzen auf diese +matchingBinaries sind historisch und nicht mehr verfügbar; Dumps bereits gelöscht. +Aktuelle installierte Binary, Builddateien, Quellcode und Diagnose-/Testlogs +unverändert. LaufenderServer nicht gestoppt. Zukünftige Crashaufzeichnung aktiv. + +--- + +# 2026-11-04 – Gesicherte Dumps auf ausdrücklichen Nutzerwunsch gelöscht + +Alle noch vorhandenen Core-Dumps im Testserverbestand gelöscht. Bei Inventur +war nur noch worldserver-20261104-185220.core vorhanden (andere zuvor genannte +Dumps bereits nicht mehr vorhanden). Backtrace-/Diagnoselogs und passende +Binärdateien erhalten. Künftige Crashaufzeichnung bleibt aktiv, Server unverändert. +Löschmanifest checkpoints/2026-11-04-dump-cleanup/deleted.json. Frühere Referenzen +auf Core-Dateien sind historisch; diese Dumps können nicht mehr geöffnet werden. + +--- + +# 2026-11-04 19:30 – Halaa-Ownershipfix fertig und LIVE bestätigt + +NutzerhatFixautorisiert. NurOutdoorPvPNA.cpp/.h geändert: PendinguniqueOwner +m_unregisteredCapturePoint biserstesBanner, dannreleaseanBase-CapturePointMap; +m_objnichtbesitzenderZeigerbleibtfürUpdates/Buffs gültig. ReloadgleicherSpawn +bindetnurGameObjectneu, keinneuerBesitz. AndereBannerSpawnIDabgelehntmitLog; +Setupwiederholungsicher. KeinegenerischeOutdoorPvPAPIoderanderePvPScriptsgeändert. + +NeuerProduktionstestASan/UBSanPASS(Niegeladen/Erstübergabe/20Reloads/Buff+Update/ +Konflikt/SetupWiederholung/mehrereMapzyklen); weitere3TestsPASS (OutdoorLife, +CPCfinalShutdown,60sGrace). Vollbuild-j10PASS, PolicyLoaderDatei+nativ0+safeall +beiBuildbeginnbestätigt; ShutdownerstnachBuild. AlterPID222501crashtebekannt +(Core185220 +matchingworldserver-before-halaa-owner-fixgesichert). Vollständig +beendet,dannInstallPASS. NeuerPID226712ready+Ports8085/8087. + +Livejobhalaa_owner_20261104_1925PASSED: dedizierterMorvath150Lv80(keinGeneratedbot) +realesHalaaBannerSpawn23273/Entry182210aufMap530Zone3518sichtbargeladen. Anschließend +nativeRückkehrzurursprünglichenMap1-Position(1569,-4397,16.1,o0), gesund9531wievorher, +keinKampf. RegulärerTestjobLogout, Actoroffline; keineDB-Schreibkorrektur nötig. +DanachnormalShutdown10mitregistriertemEroberungspunkt:GDBexitednormally! Kein +Signal/neuerDump. DamitvorherabstürzenderHalaa-Abbaukonkretliveverifiziert. +PvPEroberungalsGameplaynichtdurchgespielt. FinalerNeustartPID227005freshReady+ +Ports, Original118Bots/Loader1wiederhergestellt, Actor150bleibtausgeloggt. + +Artefaktecheckpoints/2026-11-04-halaa-owner-fix/: operation/result/tests.json, +Build/Install/Testlogs, halaa_load.lua, halaa-live-result.json, shutdown-validation.log, +startup-final-ready.log/startup-final.log, testbot-before/after.json, roster-final.json. +Bericht2026-11-04-halaa-owner-fix.md. NativeLua/ResultauchimRuntime-testbots-Verzeichnis. +VorherigesUnterstadt/Instanz/Shutdown/Landungs-WIPbewahrt, keinCommit/Push. +DatenträgernachweiteremgesichertemDump+matchingBinarynurca3.8GiBfrei, nichts gelöscht. + +--- + +# 2026-11-04 – Halaa-Ownershipfix implementiert, Build startet + +NutzerhatFixautorisiert. NA-Script PendinguniqueOwner biserstesBannerGO, danach +releaseanbestehendeBase-CapturePointMap; m_objnichtbesitzenderZeigerfürUpdate/ +Buffs. ReloadgleicherSpawnübernimmtkeinenneuenBesitz; andererBannerSpawnwird +mitFehlermeldungabgelehnt; Setupwiederholungidempotent. KeinegenerischenAPI- +Änderungen. NeuerProduktions-TestASan/UBSan PASSinklNiegeladen/Transfer/20Reloads/ +Konflikt/Buffs/mehrfacherMaplebensdauer. Build-j10startetnachLoaderconfig+nativ0 +undsafeallparallel. NochNICHTinstalliert. VorbetriebundActor150Snapshotunter +checkpoints/2026-11-04-halaa-owner-fix/. Actor150MorvathLv80dedizierterTestbot, +offlineMap1, keinGeneratedAdventurebot. GeplanteLiveprüfung: nachInstall/start +HalaaBannermitActor150realLadenundregulärenShutdownprüfen, erneutstarten, +ActorofflineOriginalpositionwiederherstellenundOriginalPopulation/Loaderrestore. +VorherigesWIPbewahren, keinCommitangefordert. + +--- + +# 2026-11-04 – Outdoor-Halaa-Shutdown-Dump vollständig untersucht + +NurDiagnosebeauftragt, keinCodefix. Core181845/matchingBinary ausdry-landing- +recovery. UrsacheeindeutigdoppelterOwnerOPvPCapturePointNA: Setupcpp148m_obj +unique_ptr; OnGameObjectCreatecpp56AddCapturePoint(m_obj.get()); basecpp426 +m_capturePoints[spawn].reset(cp)zweiterunique_ptr. DerivedDestruktorlöschtzuerst, +BaseMaplöschtnochmal->nullvirtuellerDestruktoraufruf. GDBcontroller0x7fff842af300, +cp0x7fff831fca80, derivedm_obj0, baseMap23273nochcp; vtableOPvPCapturePoint+16 +mitbeidenDtorSlots0. DBSpawn23273/GO182210HalaaBanner, Nagrandmap530 +(-1572.57,7945.3,-22.475). SitzungenCPC0/network0; CPCFinalization16->0korrekt. +NeueCPCLogoutreihenfolgefunktionierte, andererOwnershipfehlerbeimPvP-Abbau. + +Korrekturrichtung: eindeutigerOwnerplusnichtbesitzenderHalaa-Zugriff, vorerster +GO-RegistrierungundReloads berücksichtigen. Nichtbloßget->release (Updatewürde +nullzugreifen). AnderePvPScriptsAddCapturePoint-Ownershipbewahren. +Bericht2026-11-04-halaa-shutdown-crash.md, Artefaktecheckpoints/2026-11-04-outdoor- +halaa-crash/targeted/identity.gdb+logs,diagnosis.json. KeineCode/Config/Build/ +Restartänderung; Server222501 bliebaktiv. VorherigesWIPbewahrt. + +--- + +# 2026-11-04 18:55 – Trockene Ersatzlandung implementiert/installiert + +Auf Nutzerauftrag: FlightFail bei gescheiterter Rückkehr bleibt für lebenden +flugfähigen Bot aktiveLandingRecovery statt terminalFailed. Bestehende Suchedry +0/8/16/24/40yd, solid<=0.7Wasserabstand, OpenColumn/Katalog, volleNext-Planung +max32Segmente vorab prüfen und alsBreadcrumb-Replay ausführen. GescheitertePunkte +3yd/60smeiden; erfolgloseSuchealle10s wiederholen. KeinDismountohneBodenbeweis, +AbbruchlandungweiterFailedOriginalreise nachsicheremAufsetzen. Gruppenmakroroute +unverändert. ElfTestsPASSinklrealFail/FindReturn/Advance/10sGate. Build-j10 und +InstallPASS, PolicyLoader0+safelogout beiBuildstart; ShutdownerstnachBuild. + +PID222501 ready+8085/8087, ursprünglicheBotpopulation/Loaderwiederhergestellt. +Braneven121zuerstohneTeleport/Intentänderungangemeldet; hatWasserstelleverlassen, +18:53:34FLIGHT_LANDED(1839.670,-4379.120,103.859),33Segmente/171.91yd, danach +BodenbewegungzumZeppelinturm.18:54:19LOSviaFlugfallbacküberwunden, zweite +bestätigteLandung(1839.67,-4391.12,135.23); Schritt2/14TRANSPORT_WAIT +zeppelin_org_under->zeppelin_under_orgspawn6, alsoerfolgreicherReisefortschritt. +KonkreteneueErsatzlandungs-Branchbislangnichtlive +benötigt, Produktionstestprüftsie. FolgendeZeppelin-Turm-BodenwegezeigenLOS- +AblehnungenmitneuenVersuchen; gesamteShattrathreiseNICHTfertigverifiziert. + +BeimShutdownaltenPID216676:CPC_FINALIZE_BEGIN16->END0 korrekt, DANACHweiterer +CrashOutdoorPvPMgr::Die->OutdoorPvPNA::~->OPvPCapturePoint-unique_ptrDestruktor, +nullvirtuellerAufruf. Coreworldserver-20261104-181845 +matchingBinary +worldserver-before-dry-landing-recovery bewahrt. DiesenBefundNOCHNICHTdetailliert +untersucht/korrigiert. pre-install-shutdown.logbelegtCPC-Erfolg. KeinCodefixanPvP. + +Artefakte checkpoints/2026-11-04-dry-landing-recovery/: operation/result/tests.json, +Build/Install/Testlogs,startup-ready/startup.log,braneven-first-live.log, +braneven-navigation-live.log,roster-final.json,shutdown-extra-crash.txt. +Bericht2026-11-04-dry-landing-recovery.md. VorherigesWIPbewahrt, keinCommit/Push. + +--- + +# 2026-11-04 – Braneven trockene Recovery-Landung implementiert, Build startet + +Nutzerautorisiert. FlugFailednachRückkehr wird bei lebendem flugfähigem Bot nun +aktiveLandingRecovery: trockeneZiele8/16/24/40yd suchen, echteBodenbestätigung, +vollebegrenzteNext-Flugplanung undReplayübergeprüfteBreadcrumbs. Wasserpunkte +undgescheiterteZiele3ydUmkreis ablehnen(temporär60s), fehlschlagendeSuchealle10s +wiederholen. NormaleLandungkeineZielankunftbeimAbbruch, echteLandungdanachFailed +mitentferntenFlugflagsfürgewöhnlicheBoden-Recovery. Testsnochanzupassen/auszuführen. +Build-j10parallelzumsafelogoutall,LoaderDatei+nativ0. Vorbetriebimoperation.json +checkpoints/2026-11-04-dry-landing-recovery. NochNICHTinstalliert. NachErfolg +normalShutdown10/Prozessende/Install/passiverGDBStart/Ready/Ports,Originalbetrieb +restore; Braneven121 gezieltbeobachten. KeinmanuellerTeleportdesOriginalbots. +VorherigesWIPbewahren. KeinCommitangefordert. + +--- + +# 2026-11-04 18:40 – Braneven121 read-only Navigation untersucht + +Braneven groupformation -> poi_npc_68760 (Innkeeper Haelthol/Shattrath map530). +Lebend online Map1, Istposition (1899.804,-4342.669,22.272) inOrgrimmar. +NativeActionFailed travel_to_location node:virtual_ground_entry, keineTravelstate, +FlightFailed touchdown ground not confirmed, bleibtmounted/canFly/flying1. +Probes bestätigt nearFeet20.906/20.907, rawWater/liquidLevel22.272, liquidStatus2; +Sample() nutztmax(GetHeight,GetWaterLevel)alsGround, Touchdownverlangtgleichzeitig +Abstand zu echtemGetHeight<=0.7. Wasseroberfläche1.365ydüberBoden => widersprüchlich. +RotiertesNavlog18-31-08: erster Flug18:20:29 zu(1881.18,-4334.92,29.29) scheitert +18:20:32 ohneKorridor; Bodenrecoveries GROUP_GROUND_REJECT HeightError2.277>1.75. +Fallback18:20:41zuorgrimmar_the_drag(1805.31,-4346.40,-10.71) scheitertKorridor, +RückflugzumStart18:20:44; touchdown18:20:46Failed. SeitdembleibtgleicheIstposition +(Probes18:39+18:40). FailreturnfinalbewahrtFlugflagsabsichtlich =>Aktionsfehlerund +keineRecoveryblockierenweitereBodenreise. Wasser/Festbodenlandungsemantik korrigieren, +nichtnurToleranzvergrößern; fehlgeschlagenerFlugbrauchtweiteraktiveSicherheitsrecovery. +KeineCode/Config/Teleport/Intentänderung, keinRestart/Build bei dieserDiagnose. + +--- + +# 2026-11-04 18:20 – Finaler Shutdownfix fertig und LIVE bestätigt + +Nutzer hat Fix ausdrücklich autorisiert. World::ShutdownClientlessSessions im +Main-Map-Abbau VOR Factory/BGs/InstanceSave/OutdoorPvP/Maps. Weitere direkte +CPC-Anmeldungen gesperrt; native Teleports abschließen; generischer Manager +benachrichtigt Controller einmalig und entfernt Aktionen/Flugstatus/Records; +alle CPC-Sitzungen (auch unmanaged/loading) werden über nativen Destructor/ +LogoutPlayer(true) bei noch lebenden Maps gelöscht. Session-eigene Rückrufe +werden nicht mehr gepumpt, mit Sitzung verworfen; kein zusätzlicher DB-Wait. +60sGrace/Audit unverändert; finaler Snapshot auch bei Forced ohne Deadline. + +Build-j10 und12Regressionstests PASS. Policy eingehalten: Loader config+nativ0 +und safeall bei Buildbeginn; Shutdown erst nach Build. AlterPID211336 crashte +nochbekannt(Core175333/matchingworldserver-before-final-shutdown-fix); nach +vollemEnde installiert. NeuesPID216135 ready+Ports,118Bots für Liveprüfung. +NormalShutdown10: nach60246ms8Restbots(7Kampf,1tot), FINALIZE_BEGIN8 -> END0, +GDB Inferior exited normally! Kein neuer Dump. Anschließend finaler Neustart +PID216676 ready+8085/8087, Original118Bots online, Loader wieder1, keinSafeLogout. +DB-Ausfall/Loginabbruch nichtliveinjiziert, durch neuen Rückruf-Test abgedeckt. + +Artefakte checkpoints/2026-11-04-final-shutdown-fix/: operation/result/tests.json, +Build/Install/Testlogs, startup-validation.log/final.log, shutdown-validation.log, +deadline-validation.json, roster-final.json. Bericht2026-11-04-final-shutdown-fix.md, +Crashdiagnosereport ergänzt. Legacytest_safe_logout.py schonHEADveraltet (entfernter +PendingContainer); aktuelle Eligibility/Grace-Tests in test_shutdown_lifecycle PASS. +Vorheriges autorisiertes Unterstadt-/Instanz-WIP erhalten. Noch kein Commit/Push. + +--- + +# 2026-11-04 – Finalen Shutdownfix implementiert, Build startet + +Jetzt ausdrücklich vom Nutzer autorisiert. World::ShutdownClientlessSessions vor +Factory/BG/InstanceSave/OutdoorPvP/Map-Abbau im Main-Map-Handle. Neue Anmeldungen +blockiert, native Teleports abschließen, generischen Manager finalisieren und +Sitzungen per nativem Destructor/Logout bei lebenden Maps löschen. Session-eigene +DB-Rückrufe verwerfen ohne weiteren Update/DB-Wartezyklus. Controller-Logout nur +einmal; Recovery-Audit bleibt, finaler Snapshot auch bei Forced-Shutdown. +Build-j10 und zwölf Tests PASS; alter Prozess211336 normalShutdown10, bekannter +Crash/Coreworldserver-20261104-175333 +matchingBinarybewahrt. Nach Prozessende +installiert, neuer Start startup-validation.log läuft; Livefinaltest noch offen. +Build-j10 startete parallel zum safe logout all; +Loader config+nativ0. Vorbetrieb in final-shutdown-fix/operation.json. +Erst Build/Test, normal Shutdown10, vollständiges Prozessende, Install, passiver +GDB-Start/Ready/Ports. Danach gezielt neuen Shutdown mit Restbots verifizieren, +nochmal starten und Originalbetrieb herstellen. Vorheriges WIP bewahren. + +--- + +# 2026-11-04 – Letzten Shutdown-Dump zusätzlich untersucht + +Nutzerauftrag nur Diagnose. Coreworldserver-20261104-161645, matching alte Binary +instance-rendezvous/worldserver-before-instance-rendezvous, bestätigt denselben +Lifecyclefehler: World::~World löscht24CPC-Sitzungen nach Mapabbau, null normale +Sessions. Drakzug186 m_currMap0/inWorldfalse, Logoutbereitsgestartet. Native +OutdoorPvP-Zonenabmeldung verlangt GetMap -> Object.h448Assert ->SIGSEGV. +Auditdeadline24Restbots/DBverfügbar/DrakzugfliegendMap571. Kein neuer Instanz- +Rendezvousfehler, alterBuild. GDB/JSONincheckpoints/2026-11-04-shutdown-161645/, +Report2026-11-04-shutdown-crash-analysis.md ergänzt. KeinCode/Config/Build/Restart. +Server211336 blieb aktiv. Korrektur noch NICHT beauftragt/implementiert. + +--- + +# 2026-11-04 17:55 – Instanz-Rendezvous fertig installiert + +Nutzerauftrag umgesetzt für reine und gemischte Botgruppen. Nach Instanzeintritt +wartet Mitglied drinnen auf lebenden, fertig transferierten Anführer in derselben +Map/Instanz-ID/Phase und <=100yd (vorhandene lokale Follow-Grenze). Alte Welt- +Queues/Aktionen bei Eintritt entfernt; kein Graphrouting/Journey-Replay im Dungeon. +Reine Bot-Instanzbesuche folgen lokal, sobald Leader nahe. Kampf/Tod/Transport +behalten eigene Handler. humanOutside-Frühauswurf entfernt; draußen verbliebene +LFG-Begleiter verwenden nativen Browser-Teleport mit Bindung/Prüfungen, ACK, 3s Retry. + +Elf Regressionstests PASS, Vollbuild-j10/Install PASS. Vor Build Loader Datei+nativ0 +und safe logout all angefordert; nach erfolgreichem Build normal Shutdown10. +Alte Binary abgestürzt beim Abbau SIGSEGV (noch OHNE neuen Instanzcode), Dump +server/debug/crash/worldserver-20261104-161645.core, matching Binary im +instance-rendezvous/worldserver-before-instance-rendezvous. Shutdowncode unverändert. +Nach vollständigem Prozessende installiert, passiver GDB-Start: frisches Ready +und Ports8085/8087, PID211336. Original118Bots wieder online, LoaderDatei+nativ1, +SafeLogoutpending0. Kein Benutzercharakter automatisch eingeloggt oder teleportiert, +keine Instanzbindungen editiert. Sichtbare Browser-/Raidabnahme durch Nutzer offen. + +Artefakte checkpoints/2026-11-04-instance-rendezvous/: operation/result/tests.json, +Build/Install/Testlogs, startup-ready.log/startup.log, roster-final.json. +Bericht doc/clientless/2026-11-04-instance-rendezvous.md. Vorheriges autorisiertes +Unterstadt-WIP bewahrt; alles noch uncommitted, kein Push. Gruppenschutz-Testadapter +extrahiert jetzt nur Zielmethode statt versehentlich benachbarte Logoutfunktionen. + +--- + +2026-11-04 – Low-Pop-Beitritt Test/Braneven/Volakmar untersucht + +Nutzer: Test ist im Dungeon, Bots fehlen; Verdacht eigene Instanzkopien. +Read-only belegt: Test6 waronlineinmap645/instance3, Braneven121undVolakmar128 +beide map0/instance0 bei(-7571.14,-1324.96,245.538), exaktGoBackTrigger6011 +Außenposition. Alle inGruppe9, Gruppenbindung9->Instanz3. NurInstanz3fürmap645 +inDB vorhanden. SpäterTestoffline, nichtautomatischeingeloggt. Bots weiteraußen, +BranevenbewegtzumPortal,VolakmarnachOfflinewiederunterwegs. KeinBelegfürfremde +Instanzkopien. Schwierigkeit0normal, Gruppentyp12LFG, AnführerTest6. + +PassenderCodefehler inAdventureEncounterMgr::Update: humanOutside wirdbereits +währendpendingTeleportdesMenschen gesetzt, wennseinMapnochnichtDungeonist; +führtBotsdiebereitsdrinsinddirektüberr.Outsidehinaus. NativeMakeNewGroupportiert +Botszuerst+sofortACK, MenschbenötigtClientACK, daherZeitfenster. Isolierten +PRODUKTIONSblockalsReproausgeführt: PendingHumanTransfer -> Botexit, humaninside +->keinexit. KonkreteralterLiveablaufnichtlückenlosgeloggt; Ausgangspositionpasst +exaktdazu, abernichtalsvollständigbeobachtetenTransferbehaupten. + +FollowIntoInstance nutztbeiLFGnurphysischeEntrancetrigger statt gezielten +nativenLFGWiedereintritt; derzeitigerPortalzugangkannzusätzlichhängen. Präzise +Ablehnung/BeitrittszeitfolgeNOCHNICHTliveinstrumentiert. KeinCode/Configgeändert, +keinBuild/Restart/Teleport/Bindreset. FrüheresNav-WIPunverändertbeibehalten. +Artefakte checkpoints/2026-11-04-lowpop-instance-entry/: diagnosis.json, +repro_transfer.py, repro_transfer.log. NutzerüberZwischenbefundeinformiert. + +--- + +# 2026-11-04 – Jägerausrüstung für Benutzercharakter Test geliefert + +Nutzer möchte Low-Pop-Browser mit Braneven prüfen, Test(GUID6) ist aktiver Orc- +Jäger85 mit schwacher Ausrüstung. Vollständige16teilige Beweglichkeitsausrüstung +über native send items in zwei Ingame-Briefen84/85 geliefert; alle20Anhänge +DB-verifiziert. FünfheroischeT13Teile410, passendeMail-Offpieces410, +Kiril78473/Vishanka78471heroisch416, WrathofUnchaining77994(416), +VialofShadows77999(410), sinnvolleAgiringe78413(410)/77111(397), +Umhang77095(397). VierGlutseidenstofftaschen54443mitje22Plätzen. +Noch nicht automatisch angelegt; Nutzer soll aus Post abholen/anlegen. +Kein Logout/Restart und keine bestehende Ausrüstung entfernt. IDs/Anlegbarkeit +gegen lokale4.3.4ItemDB2undHotfixoverride geprüft, keinPvP/Expertisegear. + +Artefakte checkpoints/2026-11-04-test-hunter-gear/: catalog.py (lokalerDB2- +Leser), loadout.json, before-inventory.json, commands.json, delivered.json, +result.json. KeineCodeänderung/Build/Commit/PushfürdieseAusrüstungsaktion. +VorherigeUnterstadt-Source-WIPbewahrt, nochuncommitted. Keineautomatische +Gruppen-/BrowseranmeldungfürdenBenutzer ausgelöst. + +--- + +# 2026-11-04 16:23 – Unterstadt-Anreiseknoten fertig und live bestätigt + +Unterstadt-Katalogziele(map0,Zone/Area1497) reisen über taxi_11 bei +(1568.62,267.97,-43.10), DB-BatHandler4.4ydentfernt. RegelinAdventureBehaviorMgr +nutztgespeicherteLocation-Metadaten: Queueübergibtarrival_node, AreaNodeauchTaxi. +GenericExecutor speichertRequiredArrivalNode/Reached, plantinitialundRecovery +aufkonkretenKnoten(Goalnullptr), Fallback/ContinuationbisAnkunftauchTaxinode. +KeinlokalerKurzschlussvorher; finaleLokaletappeerstphysisch<=5yd3D, Originalziel +bleibterhalten. NachAnkunftlokaleRecoveryerlaubt, neueReisesetztFlagszurück. +GruppenerbeübernimmtNode/Journey/Route, nieAnkunftsflagdesAnführers. +ErsteTerrainklassifizierungwurdeimLive-Testalsunzuverlässigerkanntundersetzt. + +FinalerBuild-j10/Installbestanden, frischesReady/Ports8085/8087geprüft. +PID200134unterpassivemGDB. Original118Bots online, LoaderDatei+nativ1, +shutdownPaused0, SafeLogoutpending0. Actor150WarQuarter-TestanchorTaxi11complete; +Braneven121physischREQUIRED_ARRIVAL_REACHEDTaxi11um16:19:36inregulärer16UhrReise. +KeinenOriginaltagesplangeändert. Actor150safeausgeloggt(Elevatordurftebeenden), +ursprünglichePosition/Health/TransportwerteausTestSnapshotrestauriert. +SiebenTestsPASSinklQueue-Metadatenintegration,3DGatter,Anführervererbung. +ZusätzlicherRescue-TestauchHEADdefekt(TryRouteApproachFlightAdapter),unverändert. +KeineweitereaktiveBeobachtungnötig. KeinCommit/Pushangefordert. + +Artefakte checkpoints/2026-11-04-undercity-arrival/: result.json,operation.json, +bat-handler-db.json,required-arrival-live.log,native-final.log,build*.log, +install*.log,Tests,testbot-before.json,startup-catalog.log,roster-final.json. +NativeArrival-Logrotiertezuadventurebot-navigation.log.2026-11-04_16-20-36, +relevanteZeilengespiegeltimrequired-arrival-live.log. Doc2026-11-04-undercity- +arrival.md. SourceWIP enthältauchfrühereShutdowndiagnosedokumentation. + +ShutdowncodeweiterNICHTgeändert. AlteProzessebeimAbbaufolgendesverspäteten +CPCLogoutsGetMap-nullAssert:141428Core+matchingworldserver-before-undercity- +arrival,160739Core+matchingworldserver-before-catalog-fixbewahrt. Erstnach +vollständigemProzessendeinstalliert. UrsacheReport2026-11-04-shutdown-crash- +analysis.md; bisherkeinFixauftragfürdiesenShutdowncode. + +--- + +# 2026-11-04 16:10 – Liveprüfung deckt Terrain-Metadatenlücke auf, Korrektur läuft + +Unterbrechungbis16:07, Serverstandoffline; installiertersterVersion+Neustart +PID197254ready/ports,Original118Bots/Loader1wiederhergestellt. Branevenplan14 +verstrichen; dedizierterActor150fürPlanprobe angemeldet, Snapshot +undercity-arrival/testbot-before.json. Testintentleisurepoi_tele_1307 zeigte +nochanchorportal_go_203457_source! TerrainabfragebeifernemUntergrundziel +liefertnichtzuverlässig1497. Nichtalsfertigmelden. + +Korrigiert: expliziteRegeljetztAdventureBehaviorMgr anhand gespeicherter +AdventureLocation.ZoneId/AreaId/Point.MapId; Queue setzt generic arrival_node +Parameter, AreaNode liefertfürUnterstadtauchdirekttaxi_11. GenericExecutor +interpretiertParamstattTerrainheuristik. PhysischeAnkunftssperre/Recovery/ +Gruppenvererbungbleiben. IntegrationstestQueue->ArrivalParamergänzen. +ZweiterBuild-j10läuft, Loaderwieder0/safeall inklActor150angefordert. +NachBuildnormalShutdown10/Prozessende/Install/Restart/Ready, gezieltePlanprobe +Actor150wiederholen understbeianchorTaxi11erfolgreichmelden. Actor150sicher +abmelden,Originalpositionofflinewiederherstellen; keineOriginalpläneändern. +OriginalRosterimoperation.json. MatchingBinaryvorletztemShutdownliegtvor; +vorzweitemInstallneuesMatchingBinary sichern. + +LetzterShutdown141428 warGetMap-nullAssert inWorldSession::LogoutPlayer beim +World::~World: exaktzuspäteCPC-BereinigungwieDiagnose. Core+matchingBinary +worldserver-before-undercity-arrivalbewahrt. ShutdowncodeNICHTändern. +KeinCommit/Pushangefordert. SiebenTestsbislangPASS; Rescue-TestauchHEADdefekt. + +--- + +# 2026-11-04 – Unterstadt-Anreise über Fledermausmeister in Umsetzung + +User beauftragt explizite Ausnahme: ZielgebietUnterstadt1497 muss Anreisebis +Fledermausmeisterplanen, nichtoberirdischennahenAnkerbenutzen. DBtaxi_11 +bestätigt(1568.62,267.97,-43.10), BatHandler-Katalog1497. Quellevorhertracked +nurDiagnosecheckpointdirty, NavSourcejetztneu. Record.RequiredArrivalNode / +Reached flag, Terrainzielflächezone/area1497 map0 -> taxi_11. Initialplanung +undRecovery pinnenNode (Goalnullptr); finaleBodenstrecke erstnachwirklichem +Erreichen(<=5yd), GetTravelGoal/FallbackvorhernurTaxinode. Gruppenerbenübernimmt +Node aberkeinfremdesReachedflag. HardDestination fürdieseAnreiseerzwingen. +LoaderbeiBuildbeginnmanuell0/safeall, -j10parallel, TestsnochinArbeit. +NacherfolgreichemBuildnormalShutdown10,Prozessende/ggfDumpabwarten,Install, +passiverGDBStart/Ready/Ports,Originalbotbetrieb+Loader1wiederherstellen. +BekannteShutdowncrashesnurdiagnostiziert,keinenShutdowncodeändern; letzter +Report2026-11-04-shutdown-crash-analysis.md. AktuelleArtefakte +checkpoints/2026-11-04-undercity-arrival/. Braneven121hatleisureWarQuarter +poi_tele_1307(Z-60.4843), alteLoopüberoberirdischeNode/Kante1942hin/zurück. +KeinCommit/Pushangefordert. + +--- + +# 2026-11-04 – Beide letzten Shutdowncrashes untersucht, kein Code geändert + +User fordert ausdrücklich nur Diagnose. Gemeinsame Ursache belegt: normaler +finaler Main-Shutdown ruft World::KickAll nur für Netzwerksitzungen auf. Die +separaten clientlosen Sitzungen überleben bis zur World-Singletonzerstörung, +also über den vorherigen Mapabbau hinaus. KickPlayer ist zudem rein socket- +basiert und allein kein geeigneter CPC-Logout. Die60s-Sicherung selbst lief. + +Core124032:17CPC/0Netzwerksitzungen, Brynda165 nochinWorld/logoutfalse mitWolf299 +inm_Controlled; MapdestruktorentferntPetvorBesitzerlogout -> Unit9519ABORT. +Fristaudit:BryndaimKampf. Core135325:16CPC/0Netzwerksitzungen, TheZephyr190549 +nochgenau1PassagierFaelirin105 (inWorld/logoutfalse/echtangehängt) -> Transport94 +_passengers.empty ASSERT. GDBmitjeweilsbewahrtemmatchingBinarybelegt. + +AbgeleiteterFixNOCHNICHTimplementiert: nach60sAudit finaleCPCAbmeldung über +nativenLifecycle VOR Map-/OutdoorPvP-Abbau; ausstehendeLoginCallbacks sicher +behandeln; tatsächlicheBereinigungprüfen. KeineAssertionsabschalten/Container +blindleeren/Fristverlängern. UserhatNOCHKEINENFixbeauftragt. +Report doc/clientless/2026-11-04-shutdown-crash-analysis.md; Artefakte +checkpoints/2026-11-04-shutdown-crash-diagnosis/{pet.gdb,transport.gdb, +pet-targeted.log,transport-targeted.log,diagnosis.json}. Source/Configunverändert, +keinBuild/Restart/Commit/Push. Server187536 liefwährendUntersuchungweiter. +FrühereNotizShutdownbefundeoffen durchdieseDiagnosekonkretisiert. + +--- + # 2026-11-04 – Lokaler Commit der Browser- und PvP-Erweiterungen Nutzer hat den Commit der letzten Änderungen angefordert. Umfang: performative diff --git a/doc/clientless/checkpoints/2026-10-31-coredump-restart/pending-bots.md b/doc/clientless/checkpoints/2026-10-31-coredump-restart/pending-bots.md new file mode 100644 index 0000000000..0ea92dbd4a --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-31-coredump-restart/pending-bots.md @@ -0,0 +1,32 @@ +# Pending bots before core-dump restart + +- Safe-logout requested: 2026-10-31 approximately 22:44 CET +- Five-minute cutoff status: `pending=5 sessions=5 combat=0 transport=0 flight=5 loading=0` +- Character DB state immediately before shutdown: + - GUID 68, Sylenriel, Alliance (race 4), map 530, `-3013.510 -13204.100 59.628`, flight + - GUID 73, Rixzek, Horde (race 9), map 1, `1495.897 -4264.104 147.547`, flight + - GUID 97, Talzul, Horde (race 8), map 1, `-2772.743 -4030.782 97.119`, flight + - GUID 153, Ragnli, Alliance (race 3), map 1, `9552.060 992.021 1366.619`, flight +- One fifth clientless session had not persisted `characters.online=1`; its GUID + was not exposed by the existing aggregate safe-logout status command. +- No bot was individually force-logged-out or detached from flight. +- After restart, verify login/movement state and recover to Stormwind/Orgrimmar + only if a retained invalid state requires it. + +## Restart outcome + +- Worldserver restarted through `start-worldserver-with-coredumps.sh` as PID + 70328 and reached the authoritative ready message. +- `/proc/70328/limits` confirms core size soft/hard `unlimited`. +- Ports 8085 and 8087 are listening. +- The loader remained paused during startup verification and was then restored. + +## Final GDB-wrapper restart cutoff + +- A second safe logout was required after the isolated test proved that Apport + discards custom unpackaged executables even with an unlimited limit. +- Five-minute cutoff: `pending=2 sessions=2 combat=0 transport=0 flight=1 loading=0`. +- GUID 40 Drakagmok, Horde, map 1, `269.917 -4766.760 11.495`. +- GUID 153 Ragnli, Alliance, map 1, `9552.060 992.021 1366.619`. +- Neither bot was individually interrupted; the server is shut down cleanly as + permitted by the five-minute cutoff and both states must be checked after login. diff --git a/doc/clientless/checkpoints/2026-10-31-flight-recovery/build-recovery.md b/doc/clientless/checkpoints/2026-10-31-flight-recovery/build-recovery.md new file mode 100644 index 0000000000..945a04d348 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-31-flight-recovery/build-recovery.md @@ -0,0 +1,30 @@ +# Flight recovery build: remaining safe-logout sessions + +- Safe logout requested: 2026-10-31 23:47:58 CET +- Five-minute exception reached: 2026-10-31 23:53:05 CET +- Loader and TestBot admission: disabled before the request +- Remaining native summary: pending=3, sessions=3, combat=0, transport=0, + flight=3, loading=0 +- `saveall` was issued before the position snapshot and clean shutdown. + +| GUID | Name | Faction | Saved position | Available state evidence | +|---:|---|---|---|---| +| 146 | Loraria | Horde | map 530, 6210.21 -6922.80 144.403, zone 3433 | Adventure instance state waiting for safe player/transport state; no ClientlessFlightMgr record; native safe-logout classified flight; combat=0, transport=0, loading=0. | +| 167 | Ragnorli | Alliance | map 0, -8723.84 612.132 151.179, zone 1519 | ClientlessFlightMgr Failed, mount 32240, 156 segments, 88 avoidances, 79 handoffs, 2551.1 travelled; `no safe bounded flight corridor; holding position mounted`; adventure waiting for safe player/transport state; combat=0, transport=0, loading=0. | +| 176 | Ashaelri | Alliance | map 1, 7990.23 1296.75 59.6283, zone 141 | Adventure rest state waiting for safe player/transport state; no ClientlessFlightMgr record; native safe-logout classified flight; combat=0, transport=0, loading=0. | + +After the new server starts, inspect all three before restoring the loader. Reset +Loraria to Orgrimmar and Ragnorli/Ashaelri to Stormwind if their native movement +or flight state did not clear during restart, then record the result here. + +## Recovery outcome + +- Installed build started ready under GDB at 2026-10-31 23:54 CET; ports 8085 + and 8087 listened and the process remained active. +- All three logged in without an old ClientlessFlightMgr record. Ashaelri's + incomplete native taxi list was detected by Player::LoadFromDB and reset to + its original taxi node. +- Loraria was reset to Orgrimmar (map 1, area 5170, allowed=1). +- Ragnorli and Ashaelri were reset to Stormwind (map 0, area 5148, allowed=1). +- All three schedules were restored. Loader and TestBot admission were restored + to enabled=1; population status reported schema=1, busy=0 and pools 50/5/50/5. diff --git a/doc/clientless/checkpoints/2026-11-01-route-alternatives/README.md b/doc/clientless/checkpoints/2026-11-01-route-alternatives/README.md new file mode 100644 index 0000000000..419c194853 --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-01-route-alternatives/README.md @@ -0,0 +1,57 @@ +# Routenalternativen und fluggebundene Portale, 01.11.2026 + +## Änderungen + +ClientlessTravelGraphMgr: Portalquellen werden auch beiNeuplanung alsStart +alsPortal behandelt. NurwennPortal gesperrt/nichtzulässig ist, bleiben normale +Straßenfortsetzungen zulässig. Nahe unerreichbareStartanker dürfen höchstens4 +Flugproben verbrauchen. Vor spekulativenStraßenexits werden vollständige +Portal-Flugverbindungen aus vorhandenenRoots geprüft: Einflug zurQuelle bis +1500yd, nativePortal-Kante, Weiterflug zumZiel. Gesamtkosten konkurrieren mit +normalenGraphrouten. Max8Kandidaten/6neueSchätzungen innerhalb des bestehenden +16erPlanlimits; keine festeBodenkante/Orts-Sonderregel hinzugefügt. + +ClientlessActionExecutorMgr: Eine vomGruppen-Sicherheitsadapter abgelehnte +aufgezeichneteStraße sperrt die betroffeneKante für die aktuelleRecovery und +ruft RecoverTravel statt sofortigemEnde auf. GeometrischeSicherheitsprüfungen +bleiben aktiv. + +## Validierung + +Vier gezielteTests PASS: free_flight_planner (erweitert), ground_exit_continuation +(erweitert), route_portal_prerequisites, portal_use. NeuerBudgetfall ohne den +Portalverbindungs-Pass fällt absichtlich bei budgetPortal durch; mitPass PASS. +Zwei CMakeDebugBuilds ausschließlich-j10 erfolgreich, finalinstalliert. + +ErsterLivecheck: Renren159 benutztIFPortal217855 nativ23:32:04; EDGE_COMPLETE +23:32:06, arrivalError0.00. Daswar ein gezielterExecutorauftrag zumPortalziel, +keineBehauptung einererfolgreichenRaidanreise. +FinalerLivecheck vonbestätigterSWPosition(-8833.070,622.778,93.932): +23:42:21 PORTAL_FLIGHT_CONNECTION source200627/target200627/goal6354, +unteranderem rootstormwind_gate_inner, entry1311.7/finish1055.8. Dennoch gewinnt +weiterhinSW->Taxi2->Taxi6->IFPortalroute. KonkreterweitererGrund: importierte +Taxikante15 hatcost1 (KommentarTaxiPath13 price50), Boden1885cost478.308; +Flugkosten beruhen dagegen aufStreckenlänge. DiePortalverbindung wirdjetzt +berücksichtigt, aber einebevorzugteAuswahl desSWPortals istNICHTlivebestätigt. +EineinheitlichesZeit-/Kostenmodell fürMakrotransport bleibt offen. + +## Betrieb + +Loader1->0; safeLogout23:24:15. Nach5Minuten22Restbots mitPosition/Faktion/ +TaxiPath undverfügbarenFlags/nativeStatus gesichert, sauberShutdown10, +Prozessende VORInstallation. FreshreadyPID195658, Ports8085/8087. +22Restbots geladen, alterFlight/Transportstatefehlte; fürRecovery mitgezieltem +Logout/nativenAdminteleports inSW/ORGzurückgesetzt undwiedereingeloggt. + +ZweiterSafeLogout23:35:20, Loaderweiter0. Nach5Minuten3Restbots46/73/132, +Recovery-before-final-install undnativeStatus gesichert; Shutdown23:40:28, +Prozessende vorfinalerInstallation. FreshreadyPID200529, Ports8085/8087. +DreiRestbots geladen/geprüft undnachSW/ORG-Vorgabe (hieralleHorde:ORG) geborgen, +neu eingeloggt. NativeTaxiflüge danach sindneueSchedule-Aktionen. Renrenfür +Livecheck mehrfach kontrolliertneuaufgesetzt; letzterSchedulemodusonrestauriert. +Loaderkonfiguration1 undnativepopulationreload abschließendrestauriert. + +DiefrühereZehnergruppewarbereitsbeimerstenSafeLogout imaltenRuntime inTeilgruppen +zerfallen. Kein erneuter vollständigerGruppenraidabgenommen. KeinCommit/Push. +Kolonnenintegration undgenaueGroundPathValidator-Ablehnung inGildedRose weiterhin +offen. KeineAnkunft/Instanzgameplaybehaupten. diff --git a/doc/clientless/checkpoints/2026-11-02-0600-group-test/evaluate_without_raid.py b/doc/clientless/checkpoints/2026-11-02-0600-group-test/evaluate_without_raid.py new file mode 100644 index 0000000000..be096e1f9f --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-02-0600-group-test/evaluate_without_raid.py @@ -0,0 +1,62 @@ +"""Final read-only offline analysis; excludes the ten specified Bastion bots.""" +from pathlib import Path +import json,re,collections,datetime +BASE=Path(__file__).resolve().parent +ROOT=BASE.parents[3] +m=json.loads((BASE/'manifest.json').read_text()) +ids=set(m['eligible_guids'])-set(m['raid_guids']) +by={r['guid']:r for r in m['roster']} +start=datetime.datetime.fromisoformat('2026-11-02T06:00:00+01:00');end=start.replace(hour=8);seven=start.replace(hour=7) +snapshots=[json.loads(l) for l in (BASE/'snapshots.jsonl').open()] +snapshots=[s for s in snapshots if start<=datetime.datetime.fromisoformat(s['time'])=next_snapshot:snapshot();next_snapshot=time.monotonic()+120 + except Exception as e:append('errors.jsonl',{'time':d.now().isoformat(),'error':str(e)}) + time.sleep(2) + for t in tails:t.poll() + append('lifecycle.jsonl',{'time':d.now().isoformat(),'event':'finished'}) diff --git a/doc/clientless/checkpoints/2026-11-02-crash-110519/README.md b/doc/clientless/checkpoints/2026-11-02-crash-110519/README.md new file mode 100644 index 0000000000..e4046fcd31 --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-02-crash-110519/README.md @@ -0,0 +1,52 @@ +# Crash investigation checkpoint — waiting for recurrence + +User explicitly requested pausing active investigation until the next crash. +Do not restart the raid test. Resume crash diagnosis when user returns. + +Original crash: 02.11.2026 11:05:19 CET, worldserver process258518, crashing +thread258566. Kernel IP offset0x308157c resolves to Trinity::AbortHandler(int), +Errors.cpp152. Preserved console stderr establishes the preceding error: +std::vector::operator[] const, stl_vector.h1282, +assertion __n < this->size(), followed by Caught signal6. The handler deliberately +writes to NULL, producing the kernel SIGSEGV. The ORIGINAL invalid access caller +is not yet known. Do not attribute the crash to the handler or the VMAP warnings. + +No fresh original core: inherited core limit0. The Nov1 core is unrelated and +must not be mistaken for this crash. Original Server/navigation logs, journal, +110 saved character states and original loader setting1 preserved here. Original +stdout/stderr including assertion remains in ../2026-11-02-shared-flight/startup.log. + +Controlled reproduction is RUNNING under existing GDB wrapper: +- worldserver PID266615, GDB PID266570, tmux vcore-worldserver. +- fresh ready verified, ports8085/8087 listening; /proc/266615/limits core unlimited. +- launched through bin/start-worldserver-with-coredumps.sh. +- expected automatic capture paths: + /home/wow_admin/wow_cata_server/server/debug/crash/worldserver-20261102-114738.core + /home/wow_admin/wow_cata_server/server/debug/crash/worldserver-20261102-114738.backtrace.log +- GDB stops on fatal signal, generates core, prints thread apply all bt full. + A stopped process/remaining listening port during dump is not healthy readiness. +- reproduction-console.log captures complete output including stderr. +- loader configuration0 for fixed-roster reproduction; same110 bots manually loaded. + Their normal autonomous behavior continues. No new raid schedule/reset requested. +- No source change, no build, no speculative guard added for this investigation. +- Keep this exact binary and sources until core analysis. Source commits through + ae0b7513d9; no pushes. User has explicitly allowed waiting for hours. + +Offline evidence: +- check_bih.py structurally validated140 .vmtree,9910 .vmo,27646 BIHs: zero invalid + indices. Parser handles legitimate empty-side infinite clips. Initial naïve + parser false positives corrected; bih-check.json is final authoritative result. +- check_spawns.py scanned4123 tiles/1034378 spawn references: one anomaly and3466 + missing model names. spawn-check.json retains the exact record; NOT yet diagnosed. +- installed binary const uint-vector call sites enumerated/demangled into + uint-vector-const-callers*.txt (66 sites). Includes BIH traversal, Battlefield + data and other native getters; no caller has yet been confirmed by a stacktrace. +- Kernel also shows earlier abort-handler-like crash04:03:45 (not proof of same + vector error), runtime libstdc++ crash08:14:33 and distinct null-call shutdown + crashes. Keep those separate from this runtime assertion. + +Next: inspect NEW backtrace/core when user reports recurrence. Identify vector +index/size and actual caller, affected map/object/bot; then reproduce/fix the +root cause and test. Do not claim investigation complete or a patch verified. +Restore original loader1 via reload config AND clientless population reload when +controlled investigation finishes; preserve/recover bot states under AGENTS rules. diff --git a/doc/clientless/checkpoints/2026-11-02-crash-110519/check_bih.py b/doc/clientless/checkpoints/2026-11-02-crash-110519/check_bih.py new file mode 100644 index 0000000000..55a7b31ce6 --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-02-crash-110519/check_bih.py @@ -0,0 +1,52 @@ +"""Read-only structural check of VMAP map/model BIH index arrays.""" +from pathlib import Path +import struct,json,collections +class Reader: + def __init__(self,data):self.data=data;self.p=0;self.trees=0 + def take(self,n): + if self.p+n>len(self.data):raise ValueError('truncated at '+str(self.p)) + v=self.data[self.p:self.p+n];self.p+=n;return v + def u(self):return struct.unpack('=n:raise ValueError(f'BIH tree[{node}..{node+2}] out of {n}') + if depth>64:raise ValueError('BIH stack depth >64') + word=tree[node];axis=word>>30;offset=word&0x1fffffff + if word&(1<<29): + if axis>2:raise ValueError('invalid BVH2 axis') + pending.append((offset,depth+1)) + elif axis<3: + # Empty-side links are intentional placeholders, excluded by infinite clips. + if tree[node+1]!=0xff800000:pending.append((offset,depth+1)) + if tree[node+2]!=0x7f800000:pending.append((offset+3,depth+1)) + elif offset+tree[node+1]>k:raise ValueError(f'BIH objects[{offset}..{offset+tree[node+1]}] out of {k}') + if objects and max(objects)>=k:raise ValueError('BIH primitive index out of range') + def model(self): + self.take(8);self.tag(b'WMOD');self.u();self.u() + if self.p==len(self.data):return + self.tag(b'GMOD');count=self.u() + for _ in range(count): + self.take(32);self.tag(b'VERT');self.u();nv=self.u() + if not nv:continue + self.take(nv*12);self.tag(b'TRIM');self.u();nt=self.u();self.take(nt*12) + self.tag(b'MBIH');self.bih();self.tag(b'LIQU');self.take(self.u()) + self.tag(b'GBIH');self.bih() +root=Path('/home/wow_admin/wow_cata_server/data/vmaps');counts=collections.Counter();errors=[] +for p in sorted(root.iterdir()): + if p.suffix not in ['.vmtree','.vmo']:continue + try: + r=Reader(p.read_bytes()) + if p.suffix=='.vmtree':r.take(8);r.tag(b'NODE');r.bih() + else:r.model() + counts[p.suffix]+=1;counts['trees']+=r.trees + except Exception as e:errors.append({'file':p.name,'error':str(e)}) +result={'checked':dict(counts),'errors':errors};Path(__file__).with_name('bih-check.json').write_text(json.dumps(result,indent=2));print(json.dumps(result)) diff --git a/doc/clientless/checkpoints/2026-11-02-crash-110519/check_spawns.py b/doc/clientless/checkpoints/2026-11-02-crash-110519/check_spawns.py new file mode 100644 index 0000000000..3660a12816 --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-02-crash-110519/check_spawns.py @@ -0,0 +1,23 @@ +"""Read-only VMAP spawn transform validation, matching ModelSpawn::readFromFile.""" +from pathlib import Path +import struct,math,json,collections +root=Path('/home/wow_admin/wow_cata_server/data/vmaps');counts=collections.Counter();errors=[];missing=collections.Counter() +for path in sorted(root.glob('*.vmtile')): + try: + data=path.read_bytes();pos=8 + def get(fmt): + global pos + size=struct.calcsize(fmt);v=struct.unpack_from(fmt,data,pos);pos+=size;return v + n=get('500 or pos+length>len(data):raise ValueError('bad name length') + name=data[pos:pos+length].decode(errors='replace');pos+=length;counts['spawns']+=1 + if not all(math.isfinite(v) for v in vals+bound) or vals[-1]<=0: + errors.append({'file':path.name,'spawn':spawn,'name':name,'transform':vals,'bounds':bound}) + if not (root/(name+'.vmo')).exists():missing[name]+=1 + if pos!=len(data):raise ValueError('trailing bytes '+str(len(data)-pos)) + except Exception as e:errors.append({'file':path.name,'error':str(e)}) +result={'checked':dict(counts),'invalid_transforms_or_parse_errors':errors,'missing_unique_models':len(missing),'missing_spawn_references':sum(missing.values()),'missing_examples':missing.most_common(12)} +Path(__file__).with_name('spawn-check.json').write_text(json.dumps(result,indent=2));print(json.dumps(result)) diff --git a/doc/clientless/checkpoints/2026-11-02-crash-140558/README.md b/doc/clientless/checkpoints/2026-11-02-crash-140558/README.md new file mode 100644 index 0000000000..deed6a48bc --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-02-crash-140558/README.md @@ -0,0 +1,43 @@ +# Crash 2026-11-02 14:05:58 CET + +GDB-controlled worldserver 266615 exited with SIGSEGV while unloading a grid. +Private core: `/home/wow_admin/wow_cata_server/server/debug/crash/worldserver-20261102-114738.core`. +Matching executable preserved as `worldserver.crashed` before any replacement. +Full original thread stacks are beside the core; scoped analysis in core-details.log, +model-cache.log and lifetime.log. No compilation or gameplay source changes. + +Main-thread path: std::_Hash_bytes -> VMapManager2::releaseModelInstance -> +GameObjectModel::~GameObjectModel -> GameObject::~GameObject -> Map::UnloadGrid. +GameObject spawn59235, entry190590 Unstable Explosives, display8181, +map571 position5988.04,4473.78,-87.3617. Expected model Sc_Plaguebomb_Orange.m2. +GameObjectModel0x7ffe25ec1c40 retains iModel0x7ffe138288c0; owner points back to +correct GameObject0x7ffe132b0800. Model name pointer NULL, length4503599630732544. +That address now has Movement::Spline vtable (confirmed ELF symbol), +consistent with reused allocation / stale pointer, not simply a misspelled filename. +Cache has same model at0x7fff24244340 with refcount5; cache8416 entries. +Exact premature release or pointer corruption source NOT proven. + +Distinct immediate signature from11:05 const uint vector assertion; relationship +not established. Model refcount/cache lifecycle is next diagnostic target. +Static same-map spawn identity scan1034378 references, ZERO conflicting model +names per spawnID. All140 SIDX tables checked: ZERO index aliases. +Earlier BIH structural checks also found no index error. These checks do not +exclude runtime lifetime/concurrency corruption. Do not present speculative +MapTree refcount hypotheses as established causes or patch them without evidence. + +Restart uses start-diagnostic.sh in this directory, existing unchanged binary, +GDB core capture plus nonstopping breakpoint VMapManager2.cpp356: log model free +stack for Sc_Plaguebomb_Orange.m2 (with/without trailing NUL). No user code invoked +by debugger logpoint. Debugger tracing may affect timing. Original loader value1 +should be restored via reload config AND clientless population reload afterready. +No repeat raid or dayplan changes authorized by this investigation. + +## Restart result +Final worldserver PID284716, fresh ready message confirmed in restart-final-console.log; +ports8085/8087 LISTEN and live process verified. Original Loader.Enabled=1 +restored; reload config and clientless population reload executed. Native status +confirms enabled1/schema1, pools50/5/50/5, error empty; initial asynchronous load +was still busy at first status. No gameplay acceptance or crash fix claimed. +First diagnostic startup was stopped normally before loading bots to correct +the model-name length filter; final filter23/24 matches actual name length23. +No build/install; no individual bot forced logout; no dayplan/position resets. diff --git a/doc/clientless/checkpoints/2026-11-02-crash-140558/check_spawn_identity.py b/doc/clientless/checkpoints/2026-11-02-crash-140558/check_spawn_identity.py new file mode 100644 index 0000000000..4d915c0228 --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-02-crash-140558/check_spawn_identity.py @@ -0,0 +1,17 @@ +from pathlib import Path +import struct,json,collections +root=Path('/home/wow_admin/wow_cata_server/data/vmaps');seen={};conflicts=[];target=[];count=0 +for path in sorted(root.glob('*.vmtile')): + data=path.read_bytes();pos=8;mapid=int(path.name.split('_')[0]) + def get(fmt): + global pos + n=struct.calcsize(fmt);v=struct.unpack_from(fmt,data,pos);pos+=n;return v + for _ in range(get('100ms, without rebuilding. Other logger settings +unchanged. Low-volume logger may remain for diagnosis; remove exact added block +and reloadconfig after diagnosis if not needed. + +Observed UpdateMapMgr143,1808,1817,420,835ms; UpdateTerrainMgr206,166ms. +CPC ten-bot controller time ~340-400ms total per30sec, maxsingle8.5ms in recent +windows. Earlier near population drain35sec/194ticks and54sec/3602ticks indicate +large stalls; phases were not logged then. CPU/memory:7.5GBavailable, vmstat +no current swap activity/steal, hostmostlyidle. Swap historical2.7GB not proof +of current swapping. No hardware performance profiling privilege(perf denied, +sudo requires authentication); do not change host security settings. + +Thread sample20sec: Mapworkers2404..2407 used~25 CPU-seconds combined, +mainthread1.1 CPU-seconds. Only1/1236 samples showed debugger stop, so no evidence +for continuous GDB all-stop. Cannot exclude short or intermittent debug overhead +or grid teardown pauses. Do not claim definite root cause or fix. + +Prepared start-passive-diagnostic.sh preserves GDB core+all-thread backtraces, +omits additional model free breakpoint. NOT STARTED. Controlled comparison may +be run only after user recording is safely saved; normal10bot drain/restart +rules apply. Preserve current crash binary/core. Current crash tracing active. diff --git a/doc/clientless/checkpoints/2026-11-02-lag-diagnostics/start-passive-diagnostic.sh b/doc/clientless/checkpoints/2026-11-02-lag-diagnostics/start-passive-diagnostic.sh new file mode 100644 index 0000000000..a2b14642fc --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-02-lag-diagnostics/start-passive-diagnostic.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e +umask 077 +ulimit -c unlimited +root=/home/wow_admin/wow_cata_server +stamp=$(date +%Y%m%d-%H%M%S) +crash_dir="$root/server/debug/crash" +exec gdb -q -nx -batch \ + -ex "set logging file $crash_dir/worldserver-$stamp.backtrace.log" \ + -ex 'set logging overwrite on' -ex 'set logging enabled on' \ + -ex run -ex "generate-core-file $crash_dir/worldserver-$stamp.core" \ + -ex 'thread apply all bt full' --args ./worldserver diff --git a/doc/clientless/checkpoints/2026-11-03-crash-0608/README.md b/doc/clientless/checkpoints/2026-11-03-crash-0608/README.md new file mode 100644 index 0000000000..65ae9643f8 --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-03-crash-0608/README.md @@ -0,0 +1,54 @@ +# Crash 2026-11-03 06:08:42 CET: VMAP handle leak and unowned model release + +Core: server/debug/crash/worldserver-20261103-011325.core, birth06:08:42, +writecomplete06:08:59. Matching ELF saved as worldserver.crashed before install. +Main thread15926: std::_Hash_bytes(NULL,137445607484) -> +VMapManager2::releaseModelInstance -> GameObjectModel destructor -> grid unload. +Object Shards, spawn210760, entry138492, display1667, map0/zone45ArathiHighlands, +position-942.842/-3116.75/48.4207. Expected Floatingpurplecrystal01.m2. +Its retained model address0x7fff27c2c080 now contains Movement::Spline vtable; +cache has no matching model. Same immediate stale-model failure as earlier +Sc_Plaguebomb_Orange case, different object/location. Not an elevator call path. + +Concrete evidence in core:4075 live stdio streams,4069 flags0xfbad2488; +4070 unread/unbuffered files, highestFD4095 (launch limit4096). StaticMapTree:: +CanLoadMap opens a tile via OpenMapTileFile, then shadows/reopens tf and never +closes the original unread stream. One leak per successful existence probe. +Several existing/unchanged/valid model files then fail to open in runtime logs. +The old LoadMapTile records spawns even when acquireModelInstance returnedNULL. +UnloadMapTile re-reads filenames and releases them unconditionally. If a dynamic +GameObject successfully acquires that model after the temporary FD shortage, +unloading the failed tile can release its reference and delete the live model. + +Production-method regression reproduces BOTH defects on revision6f8f59ec6d: +--baseline6f8f59ec6d fd and lifetime cases abort at their safety assertions. +Fixed-source fd/lifetime/shared cases PASS. Tests cover bounded FD count over128 +real probes, parent fallback, invalid magic, transient acquire failure followed +by dynamic ownership, shared two-tile ownership, duplicate load, whole-map clear, +partial/truncated files and unloading after tile-file deletion, missing tiles and +unindexed parent overlaps. Tests use actual production lifecycle methods with a +controlled model cache/temporary tile files; parser/collision integration remains +native-server verification. + +Fix: close and validate the already-open tile, including parent fallback. Each +loaded tile stores its exact successful model references (filename/tree index), +unloading releases only that ledger without reopening files. Failed acquisitions +are not registered; rejected indices are not acquired; conflicting slot models +are balanced immediately. Duplicate load returns stored result without acquiring +again. Whole-map unload uses the same ownership ledger. Core architecture and +bot code unchanged. Sources MapTree.h/.cpp plus new test_vmap_tile_lifetime.py. + +Build-j10 and existing configured install successful. Server initially already +crashed/offline, no live sessions to request safe logout from; loader paused0 +before tests/build/install. No running worldserver/authserver overwritten. +Original loader1 and population50/5/50/5 to restore after ready verification. +Passive GDB core+all-thread capture used on restart, without software logpoints. +Original core/binary retained. No commit or push requested for this new fix. + +## Restart verified +Worldserver33318/GDB33307, fresh ready+ports8085/8087 and unlimitedCore checked. +Original loader1 restored with config+population reload. Native final counts +50/5/50/5 all online, pending0, total110, loadererror empty. FD samples remained +26 during0/39/77/107 bots and final count is26, with no open vmtile files. +New crash baseworldserver-20261103-121738. Passive GDB capture stays active. +Long-duration recurrence has not been ruled out by this short verification. diff --git a/doc/clientless/checkpoints/2026-11-03-elevator-exit/README.md b/doc/clientless/checkpoints/2026-11-03-elevator-exit/README.md new file mode 100644 index 0000000000..3e85cbe8de --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-03-elevator-exit/README.md @@ -0,0 +1,56 @@ +# Elevator boarding, exit and five-minute recovery + +Final source: ClientlessTransportMgr.h/.cpp and ClientlessTransportDeckMgr.cpp. +Type11 boarding begins only at a matching stopped dock with >=500ms remaining; +exiting requires stopped dock but no remaining-time budget. Ship, zeppelin and +tram budget checks unchanged. An elevator that departs during a transition keeps +its passenger and retries without accidentally cancelling the journey. Once on +land, final exit movement can finish after the platform departs. + +After300000ms as an actual elevator passenger, native TeleportTo moves to the +original recorded destination node. Aborted attempts cannot replace that target. +Native ACK completes before arrival is published. Unknown, incompatible or +forbidden targets do not trigger rescue. Dead characters retain normal death +handling. A restored passenger without original boarding history uses the nearest +recorded floor and its unique opposite as recovery direction; direction is not +persisted across restarts. Source guards and documentation make this explicit. + +Validation: test_transport.py (test-transport-3.log) PASS; actual production +methods exercised with deterministic world doubles:499/500ms boundary, stopped +vs moving/wrong dock, both directions, exactly300000ms, no rescue when on land +or riding ships, unchanged destination after cancellation, teleport rejection, +short-stop interrupted boarding and final land exit. test_transport_deck_return.py +and test_transport_grid_loading.py PASS. Build3-j10 and final install PASS. + +Dedicated registered testbot Morvath150, no Adventure profile, used in live run +'elevator-final-20261103'. Both physical and native Arrived states confirmed: +01:14:52 upper node,01:16:16 lower node. Boarding diagnostics show500ms. +On downward exit, departure interrupted disembarking; passenger retained and +next stop completed successfully (no cancelled result). Test passed in128192ms; +no five-minute rescue used. Final testbot automatically logged out. Five-minute +rescue was tested deterministically, not by intentionally stranding a live bot. +First live run succeeded physically but exposed cancelled outcomes; final +source fixes that case and was rebuilt/reinstalled before the final live run. + +Build safety: first safe drain00:55:25;10remaining after>5min documented before +shutdown. Native shutdown again hit preexisting OPvPCapturePoint NULLcall; core +003512 kept, matching old binary worldserver.crashed in previous checkpoint. +First build installed, tested, then continuous second safe drain from01:06:36; +8remaining after311sec documented in shutdown-exception-2.json. No individual +forced logout/transport detach to compile. Shutdown core010220 kept, its matching +first-fix binary worldserver-first-elevator preserved here. Final process15926 +under passiveGDB15915, ready+ports8085/8087+unlimitedcore verified. Crash base +worldserver-20261103-011325. Additional model-release debugger breakpoint absent. +Loader0 throughout builds/install/restart; restored1 after startup/recovery, +original50/5/50/5 pools loading normally. Recovery probes/actions in final startup +log: eight recorded bots relogged; taxi48/52 progressing,72 reached node,81 moved +from tower to ground;43/61/88/94 native states preserved. Earlier35/121 were +natively recovered to faction capitals with post-teleport probes verifying result. +No character deletion, dayplan/profile replacement, commit or push. + +## Final state +110 AdventureBots confirmed online, pools50/5/50/5, pending0, loaderenabled1, +error empty. Dedicated test150 is logged out. Mokrak117 naturally used the other +Orgrimmar elevator219022 and reached lower node normally at01:19:05, with native +Arrived and no forced administrative test/teleport. See mokrak-natural-trip.log. +Crash capture remains active under passive GDB. No lag/crash fix claimed. diff --git a/doc/clientless/checkpoints/2026-11-03-elevator-exit/live-roundtrip.lua b/doc/clientless/checkpoints/2026-11-03-elevator-exit/live-roundtrip.lua new file mode 100644 index 0000000000..1a3c0c86b5 --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-03-elevator-exit/live-roundtrip.lua @@ -0,0 +1,23 @@ +local bot = testbot.login(150) +assert(testbot.teleport(bot, 1, 1750.16, -4389.2, 42.508, 5.31253)) +testbot.await(function() return testbot.online(bot) end, 10000, "initial teleport") +testbot.record("ready_bottom", testbot.state(bot)) +testbot.await(function() return testbot.online(bot) and testbot.state(bot).transport == 1 end, 90000, "board upward") +testbot.record("boarded_upward", testbot.state(bot)) +testbot.await(function() + if not testbot.online(bot) then return false end + local s=testbot.state(bot) + return s.transport == 0 and math.abs(s.z-109.633)<2 and math.abs(s.x-1759.33)<3 and math.abs(s.y+4402.45)<3 +end, 100000, "leave at upper node") +testbot.wait(1000) +testbot.record("arrived_top", testbot.state(bot)) +testbot.await(function() return testbot.online(bot) and testbot.state(bot).transport == 1 end, 90000, "board downward") +testbot.record("boarded_downward", testbot.state(bot)) +testbot.await(function() + if not testbot.online(bot) then return false end + local s=testbot.state(bot) + return s.transport == 0 and math.abs(s.z-42.508)<2 and math.abs(s.x-1750.16)<3 and math.abs(s.y+4389.2)<3 +end, 100000, "leave at lower node") +testbot.wait(1000) +testbot.record("arrived_bottom", testbot.state(bot)) +testbot.check(true, "real elevator round trip completed without administrative rescue") diff --git a/doc/clientless/checkpoints/2026-11-03-ground-validator/README.md b/doc/clientless/checkpoints/2026-11-03-ground-validator/README.md new file mode 100644 index 0000000000..c9df97923c --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-03-ground-validator/README.md @@ -0,0 +1,44 @@ +# Ground-safety rejection at Stormwind gryphon master + +Native first rejection after diagnostic deployment is identical for176/166/164: +14:49:09..14 segment9, LOS from(-8856.588,517.633,109.881) to +(-8858.637,521.069,109.340). These Z values are already feet+1, matching the +validator's actual ray. Ground/step samples on preceding segments passed. + +Independent reconstruction from the actual map0 vmtile placements and .vmo +triangle meshes reproduces intersection with Sw_Tradedistrict.wmo, spawn46365, +WMO group13/id27435, triangles37 and43. First hit approximately +(-8856.671,517.772,109.859). These are two perpendicular vertical model faces; +we have not established which visible architectural piece they represent. +Nearby barrel46585's mesh does NOT intersect this ray; do not blame the barrel. +See check_los_mesh.py, los-static-candidates.json and los-triangle-hits.json. + +The MMAP-complete path's straight segment conflicts with the actual VMAP geometry. +This is a specific LOS rejection, not the height-tolerance/steepness limit. Later +alternative paths also generated ground-height errors (e.g1.778 vs1.750 yards, +and some5-8yard drops); those are separate subsequent failures, not this first +rejection. Recovery reuses the same synthetic local_bridge with ID0, which cannot +be blacklisted by its current edge-ID-only logic, and exhausts five retries. + +Only diagnostic logging was added, plus the log macro stub in the existing +production-method group-travel test. No movement/safety/planning behavior changed. +Existing group-travel test and CMake-j10 build passed, installed. User explicitly +allowed interruption of the current raid test and ONE-TIME60sec shutdown delay +instead of300sec; requesttime14:41:37, actual>202sec before shutdown. Original +five-minute rule remains for future unrelated builds. Loader0 and safe logout +requested before compilation.22remaining with GUID/name/faction/position/available +flight/action/transport status persisted before shutdown. No individual force +logout/detach to compile. Native shutdown reproduced known null-call failure; +core121738 retained, matching ELF worldserver-before-diagnostics saved here. + +New process40815 under passive GDB40804, ready+ports8085/8087+unlimitedCore checked. +Affected3 logged in at their original saved gryphon-master position, no admin +teleport/reset of those3. Their normal schedules replanned and they subsequently +walked elsewhere; no claim that a navigation correction unblocked them. +Previous119sessions restored via native logins (core event list missed72, union +with initial start list/ready inventory restores all119). Loader1+config/population +reload restored. Native119online/pending0. Recorded restart residuals checked; +failed unsupported flights42/99/118 recovered natively to faction capitals per +build policy and post-teleport movement verified. Others retain native taxi/flight +owners (e.g165 still in an active flight). No ongoing observer/evaluator created. +Crash capture continues, baseworldserver-20261103-144731. No commit/push requested. diff --git a/doc/clientless/checkpoints/2026-11-03-ground-validator/check_los_mesh.py b/doc/clientless/checkpoints/2026-11-03-ground-validator/check_los_mesh.py new file mode 100644 index 0000000000..82a655a9be --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-03-ground-validator/check_los_mesh.py @@ -0,0 +1,51 @@ +from pathlib import Path +import struct,json,math +ROOT=Path('/home/wow_admin/wow_cata_server/data/vmaps') +BASE=Path(__file__).parent +A=(-8856.588,517.633,109.881);B=(-8858.637,521.069,109.340) +mid=0.5*64*533.33333333 +world=lambda p:(mid-p[0],mid-p[1],p[2]) +def sub(a,b):return tuple(x-y for x,y in zip(a,b)) +def add(a,b):return tuple(x+y for x,y in zip(a,b)) +def mul(a,t):return tuple(x*t for x in a) +def dot(a,b):return sum(x*y for x,y in zip(a,b)) +def cross(a,b):return(a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]) +def matmul(a,b):return [[sum(a[i][k]*b[k][j] for k in range(3)) for j in range(3)] for i in range(3)] +def mv(m,p):return tuple(dot(row,p) for row in m) +def rotation(pose): + x,y,z=(math.radians(v) for v in pose[3:6]);c,s=math.cos,math.sin + rz=[[c(y),-s(y),0],[s(y),c(y),0],[0,0,1]];ry=[[c(x),0,s(x)],[0,1,0],[-s(x),0,c(x)]];rx=[[1,0,0],[0,c(z),-s(z)],[0,s(z),c(z)]] + return matmul(rz,matmul(ry,rx)) +def triangle_hit(a,b,p,q,r): + d=sub(b,a);e1=sub(q,p);e2=sub(r,p);h=cross(d,e2);det=dot(e1,h) + if abs(det)<1e-9:return None + inv=1/det;tvec=sub(a,p);u=dot(tvec,h)*inv + if u<0 or u>1:return None + v=dot(d,cross(tvec,e1))*inv + if v<0 or u+v>1:return None + t=dot(e2,cross(tvec,e1))*inv + return t if 0<=t<=1 else None +hits=[] +for model in json.loads((BASE/'los-static-candidates.json').read_text()): + name=model['name'];path=ROOT/(name.split('\x00')[0]+('' if '\x00' in name else '.vmo'));data=path.read_bytes();pos=28;count=struct.unpack_from(' +using namespace ClientlessFlightPath; +int main(){Settings c;Memory m;auto sample=[](float,float){return Surface{94.314f,550,true};};auto ray=[](Point const&,Point const&){return true;};Point goal{-8186,419,94.314f};auto n=Next({-8829,624,94.314f},goal,c,m,sample,ray);assert(n.Valid&&!m.Departed);n=Next({-8826.7f,622.306f,96.4122f},goal,c,m,sample,ray);assert(n.Valid&&!m.Departed);n=Next({-8800,610,104},goal,c,m,sample,ray);assert(n.Valid&&m.Departed);} diff --git a/doc/clientless/checkpoints/2026-11-03-wg-client-permission/automatic_permission.lua b/doc/clientless/checkpoints/2026-11-03-wg-client-permission/automatic_permission.lua new file mode 100644 index 0000000000..07cdb9f7a1 --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-03-wg-client-permission/automatic_permission.lua @@ -0,0 +1,17 @@ +local bot = testbot.login(150) +testbot.level(bot, 80) +testbot.stats(bot, 1000000) +testbot.learn(bot, 34091) +testbot.learn(bot, 54197) +testbot.learn(bot, 107844) +assert(testbot.skill(bot, 762, 375, 375)) +assert(testbot.teleport(bot, 571, 4059.27, 2747.79, 354.88, 0)) +testbot.wait(4000) +testbot.request_cast(bot, 107844) +testbot.wait(7000) +testbot.record('after_native_mount_request', testbot.state(bot)) +testbot.wait(25000) +assert(testbot.teleport(bot, 0, -8833.07, 622.778, 93.932, 0)) +testbot.wait(4000) +testbot.record('after_leaving_wintergrasp', testbot.state(bot)) +testbot.wait(25000) diff --git a/doc/clientless/checkpoints/2026-11-03-wg-client-permission/automatic_permission_verified.lua b/doc/clientless/checkpoints/2026-11-03-wg-client-permission/automatic_permission_verified.lua new file mode 100644 index 0000000000..e12e818bb9 --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-03-wg-client-permission/automatic_permission_verified.lua @@ -0,0 +1,23 @@ +local bot = testbot.login(150) +testbot.level(bot, 80) +testbot.stats(bot, 1000000) +testbot.learn(bot, 34091) +testbot.learn(bot, 54197) +testbot.learn(bot, 107844) +assert(testbot.skill(bot, 762, 375, 375)) +for _, aura in ipairs({32243, 32235, 107844, 91262}) do + testbot.remove_aura(bot, aura) +end +assert(not testbot.has_aura(bot, 91262)) +assert(not testbot.has_aura(bot, 107844)) +assert(testbot.teleport(bot, 571, 4059.27, 2747.79, 354.88, 0)) +testbot.wait(4000) +testbot.check(testbot.has_aura(bot, 91262), 'permission automatically applied in Wintergrasp') +testbot.request_cast(bot, 107844) +testbot.wait(7000) +testbot.check(testbot.has_aura(bot, 107844), 'Twilight Harbinger freshly summoned via native request') +testbot.record('in_wintergrasp', testbot.state(bot)) +assert(testbot.teleport(bot, 1, 1569, -4397, 16.1, 0)) +testbot.wait(4000) +testbot.check(not testbot.has_aura(bot, 91262), 'permission removed after leaving Wintergrasp') +testbot.record('after_leaving_wintergrasp', testbot.state(bot)) diff --git a/doc/clientless/checkpoints/2026-11-03-wg-mount-cast/wg_mount_cast.lua b/doc/clientless/checkpoints/2026-11-03-wg-mount-cast/wg_mount_cast.lua new file mode 100644 index 0000000000..28a664055b --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-03-wg-mount-cast/wg_mount_cast.lua @@ -0,0 +1,13 @@ +local bot = testbot.login(150) +testbot.level(bot, 80) +testbot.stats(bot, 1000000) +testbot.learn(bot, 34090) +testbot.learn(bot, 54197) +testbot.learn(bot, 32243) +assert(testbot.skill(bot, 762, 225, 225)) +assert(testbot.teleport(bot, 571, 4059.27, 2747.79, 354.88, 0)) +testbot.wait(4000) +testbot.request_cast(bot, 32243) +testbot.wait(7000) +testbot.record('after_native_mount_request', testbot.state(bot)) +testbot.wait(30000) diff --git a/doc/clientless/checkpoints/2026-11-04-0200-raid-test/setup.py b/doc/clientless/checkpoints/2026-11-04-0200-raid-test/setup.py new file mode 100644 index 0000000000..0b44165c44 --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-04-0200-raid-test/setup.py @@ -0,0 +1,44 @@ +from pathlib import Path +import sys,json,re,time,datetime +sys.path.insert(0,'doc/clientless/checkpoints/2026-11-02-0600-group-test') +sys.path.insert(0,'doc/clientless/checkpoints/2026-11-02-elevated-flight') +import test_driver as d +import live_support as w +B=Path(__file__).resolve().parent +G=[159,160,161,162,163,164,165,166,167,176];ids=','.join(map(str,G));today=int(d.now().strftime('%Y%m%d')) +assert d.now().hour==1, 'Formation hour no longer current' +def save(name,data): + p=B/(name+'.json') + if p.exists():return # Preserve snapshots from the read-only attempt before any mutation. + p.write_text(json.dumps(data,indent=2,ensure_ascii=False)) +conf=d.CONF.read_text();loader=int(re.search(r'^Clientless.Adventure.Loader.Enabled\s*=\s*(\d+)',conf,re.M)[1]) +save('operation',{'requested_at':d.now().isoformat(),'date':today,'formation_hour':1,'raid_hour':2,'meeting':'the_gilded_rose','raid':'instance_visit_at_6354_d0','guids':G,'loader_before':loader,'observation':'user observes; no background observer'}) +save('before-online',d.roster()) +save('before-plans',d.query('World',f'SELECT * FROM vcore_adventure_day WHERE character_guid IN ({ids}) AND local_date={today}')) +save('before-handoff',d.query('World',f'SELECT * FROM vcore_adventure_group_handoff WHERE character_guid IN ({ids})')) +save('before-visits',d.query('Character',f'SELECT * FROM vcore_adventure_instance_visit WHERE character_guid IN ({ids})')) +save('before-characters',d.query('Character',f'SELECT guid,name,map,instance_id,zone,position_x,position_y,position_z,orientation,transguid,taxi_path,health FROM characters WHERE guid IN ({ids})')) +d.CONF.write_text(re.sub(r'^(Clientless.Adventure.Loader.Enabled\s*=\s*)\d+',lambda m:m[1]+'0',conf,flags=re.M));d.command('clientless population reload') +for g in G:d.command(f'clientless logout {g}') +for _ in range(40): + if not d.query('Character',f'SELECT guid FROM characters WHERE guid IN ({ids}) AND online=1'):break + time.sleep(1) +else:raise RuntimeError('Test group still online; no position writes performed; loader remains paused') +# Persist new intents before teleport. No running record can overwrite them. +values=','.join(f"({g},{today},{h},'{intent}','{location}')" for g in G for h,intent,location in [(1,'groupformation','the_gilded_rose'),(2,'raid','instance_visit_at_6354_d0')]) +w.write('World',f"REPLACE INTO vcore_adventure_day (character_guid,local_date,hour_slot,intent,location_key) VALUES {values}; DELETE FROM vcore_adventure_group_handoff WHERE character_guid IN ({ids});") +plans=d.query('World',f'SELECT character_guid,hour_slot,intent,location_key FROM vcore_adventure_day WHERE character_guid IN ({ids}) AND local_date={today} AND hour_slot IN (1,2) ORDER BY character_guid,hour_slot') +assert len(plans)==20 +save('verified-plans-before-teleport',plans) +w.write('Character',f"DELETE FROM vcore_adventure_instance_visit WHERE character_guid IN ({ids}); UPDATE characters SET map=0,instance_id=0,zone=1519,position_x=-8864.73,position_y=669.015,position_z=97.9031,orientation=0,transguid=0,trans_x=0,trans_y=0,trans_z=0,trans_o=0,taxi_path='' WHERE guid IN ({ids}) AND online=0;") +save('offline-rose',d.query('Character',f'SELECT guid,name,map,instance_id,position_x,position_y,position_z,health FROM characters WHERE guid IN ({ids})')) +for g in G:d.command(f'clientless login {g}') +for _ in range(40): + online=d.query('Character',f'SELECT guid FROM characters WHERE guid IN ({ids}) AND online=1') + if len(online)==10:break + time.sleep(1) +else:raise RuntimeError('Not all test bots logged in; loader remains paused') +# Stop briefly outside the instance so stale native membership can be left safely. +for g in G:d.command(f'clientless adventure stop {g}');d.command(f'clientless interaction {g} leave') +time.sleep(2) +print('Plans persisted, all ten relocated and online; native group leave requested.',flush=True) diff --git a/doc/clientless/checkpoints/2026-11-04-crash-0634/diagnosis.md b/doc/clientless/checkpoints/2026-11-04-crash-0634/diagnosis.md new file mode 100644 index 0000000000..52c5c40d77 --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-04-crash-0634/diagnosis.md @@ -0,0 +1,23 @@ +Der Absturz wurde durch einen Fehler in der Datenbank-Wiederverbindung ausgelöst. + +Ablauf am 4. November 2026 (MEZ): +- 06:33:21 startet apt-daily-upgrade.service. Das automatische Update installiert libpcre2-8-0 und libpcre2-posix3. +- 06:33:26 wird die lokale MariaDB regulär gestoppt; 06:33:28 ist sie wieder verbindungsbereit. +- Der Worldserver protokolliert Fehler 2013 (Verbindung während Query verloren), danach einen fehlgeschlagenen Verbindungsaufbau nach 127.0.0.1 und "Unhandled MySQL errno 2002". +- MySQLConnection::_HandleMySQLErrno schließt den alten Handle und setzt ihn auf null. Open() scheitert vorübergehend. Der rekursive Fehlerpfad kennt CR_CONN_HOST_ERROR=2003, aber nicht CR_CONNECTION_ERROR=2002. Daher bricht er nach der ersten fehlgeschlagenen Wiederverbindung ab, obwohl noch Versuche übrig wären. +- Der asynchrone Worker bleibt mit null-Handle und m_reconnecting=true bestehen. Ping() prüft weder den Handle noch den Rückgabewert von mysql_ping. +- Eine nachfolgende PingOperation ruft mysql_ping(nullptr) auf und verursacht SIGSEGV. Der Dump wurde um 06:34 geschrieben; dieser Dateizeitpunkt ist nicht als sekundengenauer Signalzeitpunkt zu verstehen. + +Beweise im Dump: +Ursprünglicher Crashthread: Thread 5 / LWP139811; im neu geöffneten Core GDB-Thread1. +MySQLConnection this=0x7ffff6b23500; m_Mysql=0x0; m_reconnecting=true; +m_connectionFlags=CONNECTION_ASYNC; m_connectionInfo.database="cata_characters". +RDI=0 und RBX=0 in mysql_ping+31. Die vollständige Callchain ist im beim Crash am Liveprozess gesicherten Backtrace erhalten. Die erneute Coreauswertung konnte den Stack nicht zuverlässig abwickeln (Stackdaten dort null), die gezielten Handle-/Flag-/Datenbankwerte und Register sind aber lesbar und stimmen mit Quellcode und Fehlerlogs überein. + +Erforderliche Korrektur: +- CR_CONNECTION_ERROR=2002 in die begrenzte Wiederverbindungsstrategie aufnehmen; vorübergehende Verbindungsfehler dürfen den Pool nicht still mit null-Handle zurücklassen. +- Ping gegen null-Handles absichern und fehlgeschlagene Pings der Wiederverbindungsbehandlung zuführen. +- Wiederholte Ausfälle weiterhin begrenzen und bei dauerhaftem Fehlschlag kontrolliert abbrechen, statt unbrauchbare Verbindungen oder still verworfene Aufgaben weiterzubetreiben. +- Regression für Verbindungsverlust -> Open meldet2002 -> erneuter Versuch erfolgreich sowie Ping bei leerem Handle. + +Keine Änderung an Datenbankkern, Serverbetrieb oder Systemupdates während dieser Diagnose. Vorhandener Transportfix-WIP bleibt erhalten. Der Transport-/Navigationscode liegt nicht im Absturzpfad; der auslösende Dienstneustart stammt aus dem Systemupdate-Zeitfenster. diff --git a/doc/clientless/checkpoints/2026-11-04-crash-0634/inspect-handle.gdb b/doc/clientless/checkpoints/2026-11-04-crash-0634/inspect-handle.gdb new file mode 100644 index 0000000000..6f6aaf2a01 --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-04-crash-0634/inspect-handle.gdb @@ -0,0 +1,10 @@ +set pagination off +set confirm off +set debuginfod enabled off +thread 1 +info registers rip rdi rsi rax rbx r12 r13 rsp rbp +p/x ((MySQLConnection*)0x7ffff6b23500)->m_Mysql +p ((MySQLConnection*)0x7ffff6b23500)->m_reconnecting +p ((MySQLConnection*)0x7ffff6b23500)->m_connectionFlags +p ((MySQLConnection*)0x7ffff6b23500)->m_connectionInfo.database +x/8gx $rsp diff --git a/doc/clientless/checkpoints/2026-11-04-crash-0634/inspect.gdb b/doc/clientless/checkpoints/2026-11-04-crash-0634/inspect.gdb new file mode 100644 index 0000000000..f9acff9185 --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-04-crash-0634/inspect.gdb @@ -0,0 +1,14 @@ +set pagination off +set confirm off +set debuginfod enabled off +thread 1 +bt 8 +frame 1 +p/x this +p/x m_Mysql +p m_reconnecting +p m_connectionFlags +p m_connectionInfo.database +frame 0 +info registers rip rdi rax rbx r12 r13 +x/8i $pc-16 diff --git a/doc/clientless/checkpoints/2026-11-04-halaa-owner-fix/halaa_load.lua b/doc/clientless/checkpoints/2026-11-04-halaa-owner-fix/halaa_load.lua new file mode 100644 index 0000000000..dfb62b1e94 --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-04-halaa-owner-fix/halaa_load.lua @@ -0,0 +1,14 @@ +local g=testbot.login(150) +local before=testbot.state(g) +testbot.record('before',before) +assert(testbot.teleport(g,530,-1572.57,7945.30,-22.475,0)) +testbot.await(function() return testbot.online(g) end,15000,'Halaa teleport incomplete') +testbot.await(function() return testbot.object(g,23273)~=nil end,10000,'Halaa banner spawn not visible') +local by_spawn=testbot.object(g,23273) +local by_entry=testbot.nearest_object(g,182210,40) +testbot.check(by_spawn~=nil and by_spawn==by_entry,'real Halaa banner loaded by spawn and entry') +testbot.record('banner_guid',by_spawn) +testbot.record('at_halaa',testbot.state(g)) +assert(testbot.teleport(g,before.map,before.x,before.y,before.z,0)) +testbot.await(function() return testbot.online(g) end,15000,'return teleport incomplete') +testbot.record('returned',testbot.state(g)) diff --git a/doc/clientless/checkpoints/2026-11-04-lifecycle/db_fault_proxy.py b/doc/clientless/checkpoints/2026-11-04-lifecycle/db_fault_proxy.py new file mode 100644 index 0000000000..e447bc3742 --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-04-lifecycle/db_fault_proxy.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Loopback-only fault injection. Never logs authentication or SQL payloads.""" +import asyncio +from pathlib import Path +base=Path(__file__).resolve().parent +marker=base/'drop-database-connections' +writers=set() +async def client(reader,writer): + writers.add(writer) + upstream=None + try: + incoming,upstream=await asyncio.open_connection('127.0.0.1',3306) + writers.add(upstream) + async def pipe(src,dst): + while data:=await src.read(65536): + dst.write(data) + await dst.drain() + jobs=[asyncio.create_task(pipe(reader,upstream)),asyncio.create_task(pipe(incoming,writer))] + await asyncio.wait(jobs,return_when=asyncio.FIRST_COMPLETED) + for task in jobs:task.cancel() + await asyncio.gather(*jobs,return_exceptions=True) + except (ConnectionError,OSError):pass + finally: + for stream in (writer,upstream): + if stream: + writers.discard(stream) + stream.close() +async def main(): + server=await asyncio.start_server(client,'127.0.0.1',33307) + print('Loopback proxy ready on 33307',flush=True) + while not marker.exists():await asyncio.sleep(.2) + server.close() + for stream in list(writers):stream.close() + await server.wait_closed() + print('Fault injected: listener and existing connections closed',flush=True) + await asyncio.sleep(2) +asyncio.run(main()) diff --git a/doc/clientless/checkpoints/2026-11-04-lowpop-instance-entry/repro_transfer.py b/doc/clientless/checkpoints/2026-11-04-lowpop-instance-entry/repro_transfer.py new file mode 100644 index 0000000000..b24c2c5c48 --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-04-lowpop-instance-entry/repro_transfer.py @@ -0,0 +1,36 @@ +"""Read-only reproduction of the existing encounter exit decision.""" +from pathlib import Path +import subprocess,tempfile +root=Path(__file__).resolve().parents[4] +s=(root/'src/server/game/World/AdventureEncounterMgr.cpp').read_text() +a=s.index(' bool humanPresent=false,offlineMember=false,humanOutside=false;');b=s.index(' if(it->second.Returning)',a) +code=r''' +#include +#include +struct Position{float GetPositionX()const{return 0;}float GetPositionY()const{return 0;}float GetPositionZ()const{return 0;}float GetOrientation()const{return 0;}}; +struct Map{bool dungeon=false;bool IsDungeon(){return dungeon;}}; +struct Session{bool bot=false;bool IsClientless(){return bot;}}; +struct Player; +struct Ref{Player*p;Ref*n=nullptr;Player*GetSource(){return p;}Ref*next(){return n;}}; +struct Group{Ref*first;Ref*GetFirstMember(){return first;}}; +struct Player{bool transfer=false,combat=false;unsigned exits=0;Map map;Session session;Group*group; + bool IsAlive(){return true;}bool IsInCombat(){return combat;}Map*GetMap(){return ↦}Session*GetSession(){return &session;}Group*GetGroup(){return group;} + bool TeleportTo(unsigned,float,float,float,float){++exits;return true;} +}; +struct Record{unsigned OutsideMap=0;Position Outside;}; +bool decision(Player*p){std::maprecords{{1,{}}};auto it=records.begin(); +'''+s[a:b]+r''' +return false;} +int main(){Player bot,human;bot.session.bot=true;bot.map.dungeon=true; + Ref second{&human},first{&bot,&second};Group g{&first};bot.group=human.group=&g; + human.transfer=true;human.map.dungeon=false; + assert(decision(&bot)&&bot.exits==1); // Existing code ejects despite pending human transfer. + human.map.dungeon=true;assert(!decision(&bot)&&bot.exits==1); + human.transfer=false;human.map.dungeon=false;assert(decision(&bot)&&bot.exits==2); +} +''' +with tempfile.TemporaryDirectory() as folder: + p=Path(folder);(p/'test.cpp').write_text(code) + subprocess.run(['c++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) +print('Existing production exit decision reproduces premature bot exit while human transfer is pending; human already inside does not trigger it.') diff --git a/doc/clientless/checkpoints/2026-11-04-outdoor-halaa-crash/identity.gdb b/doc/clientless/checkpoints/2026-11-04-outdoor-halaa-crash/identity.gdb new file mode 100644 index 0000000000..81b0ebfc4b --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-04-outdoor-halaa-crash/identity.gdb @@ -0,0 +1,7 @@ +set pagination off +set confirm off +set debuginfod enabled off +info symbol 0x55555a1f6388 +x/4gx 0x55555a1f6388 +p ((OPvPCapturePoint*)0x7fff831fca80)->m_capturePointSpawnId +p ((Map*)0x7fff839764c0)->i_mapEntry->ID diff --git a/doc/clientless/checkpoints/2026-11-04-outdoor-halaa-crash/targeted.gdb b/doc/clientless/checkpoints/2026-11-04-outdoor-halaa-crash/targeted.gdb new file mode 100644 index 0000000000..9ecc34d4ea --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-04-outdoor-halaa-crash/targeted.gdb @@ -0,0 +1,15 @@ +set pagination off +set confirm off +set debuginfod enabled off +bt 16 +p ((OutdoorPvpScript::NA::OutdoorPvPNA*)0x7fff842af300)->m_obj +p ((OutdoorPvpScript::NA::OutdoorPvPNA*)0x7fff842af300)->m_capturePoints +p ((OutdoorPvpScript::NA::OutdoorPvPNA*)0x7fff842af300)->m_map +p ((OutdoorPvpScript::NA::OutdoorPvPNA*)0x7fff842af300)->m_TypeId +x/4gx 0x7fff831fca80 +python +import gdb +for field in ['m_clientlessSessions','m_sessions']: + visual=gdb.default_visualizer(gdb.parse_and_eval("'World::instance()::instance'."+field)) + print('SESSION_COUNT',field,len(list(visual.children()))//2) +end diff --git a/doc/clientless/checkpoints/2026-11-04-shutdown-161645/targeted.gdb b/doc/clientless/checkpoints/2026-11-04-shutdown-161645/targeted.gdb new file mode 100644 index 0000000000..a112424f6c --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-04-shutdown-161645/targeted.gdb @@ -0,0 +1,19 @@ +set pagination off +set confirm off +set debuginfod enabled off +bt 12 +p ((Player*)0x7fff440ee000)->m_name +p ((Player*)0x7fff440ee000)->m_currMap +p ((Player*)0x7fff440ee000)->m_inWorld +p ((Player*)0x7fff440ee000)->m_zoneUpdateId +p ((WorldSession*)0x7fffad7ca000)->m_GUIDLow +p ((WorldSession*)0x7fffad7ca000)->m_playerLogout +p ((WorldSession*)0x7fffad7ca000)->_player +python +import gdb +for field in ['m_clientlessSessions','m_sessions']: + value=gdb.parse_and_eval("'World::instance()::instance'."+field) + visual=gdb.default_visualizer(value) + children=list(visual.children()) + print('SESSION_CONTAINER',field,'entries',len(children)//2) +end diff --git a/doc/clientless/checkpoints/2026-11-04-shutdown-crash-diagnosis/pet.gdb b/doc/clientless/checkpoints/2026-11-04-shutdown-crash-diagnosis/pet.gdb new file mode 100644 index 0000000000..ac966caae8 --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-04-shutdown-crash-diagnosis/pet.gdb @@ -0,0 +1,15 @@ +set pagination off +set confirm off +set debuginfod enabled off +p 'World::instance()::instance'.m_clientlessSessions +p 'World::instance()::instance'.m_sessions +p ((Player*)0x7fffbb740000)->m_name +p ((Player*)0x7fffbb740000)->m_session->m_GUIDLow +p ((Player*)0x7fffbb740000)->m_session->m_playerLogout +p ((Player*)0x7fffbb740000)->m_inWorld +p ((Player*)0x7fffbb740000)->m_Controlled +p ((Pet*)0x7fff2db9b000)->m_name +p ((Pet*)0x7fff2db9b000)->m_inWorld +p ((Pet*)0x7fff2db9b000)->m_currMap +p ((Player*)0x7fffbb740000)->m_currMap +p ((Player*)0x7fffbb740000)->m_transport diff --git a/doc/clientless/checkpoints/2026-11-04-shutdown-crash-diagnosis/transport.gdb b/doc/clientless/checkpoints/2026-11-04-shutdown-crash-diagnosis/transport.gdb new file mode 100644 index 0000000000..91a927bd78 --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-04-shutdown-crash-diagnosis/transport.gdb @@ -0,0 +1,20 @@ +set pagination off +set confirm off +set debuginfod enabled off +p 'World::instance()::instance'.m_clientlessSessions +p 'World::instance()::instance'.m_sessions +p ((Transport*)0x7fff7bd3f000)->m_name +p ((Transport*)0x7fff7bd3f000)->_passengers +p ((Transport*)0x7fff7bd3f000)->m_uint32Values[OBJECT_FIELD_ENTRY] +python +import gdb +value=gdb.parse_and_eval('((Transport*)0x7fff7bd3f000)->_passengers') +visual=gdb.default_visualizer(value) +for key,ptr in visual.children(): + obj=ptr.cast(gdb.lookup_type('WorldObject').pointer()).dereference() + print('PASSENGER',key,'pointer',ptr,'name',obj['m_name'],'type',obj['m_objectTypeId'],'in_world',obj['m_inWorld'],'transport',obj['m_transport']) + if int(obj['m_objectTypeId'])==4: + player=ptr.cast(gdb.lookup_type('Player').pointer()).dereference() + sess=player['m_session'].dereference() + print('PLAYER guid',sess['m_GUIDLow'],'logout',sess['m_playerLogout'],'map',player['m_currMap']) +end diff --git a/doc/clientless/checkpoints/2026-11-04-sylaria-navigation/read_mesh.py b/doc/clientless/checkpoints/2026-11-04-sylaria-navigation/read_mesh.py new file mode 100644 index 0000000000..0e352e8f60 --- /dev/null +++ b/doc/clientless/checkpoints/2026-11-04-sylaria-navigation/read_mesh.py @@ -0,0 +1,44 @@ +"""Read-only inspection of stored Detour triangles; no server code/config changes.""" +import struct,json,math +from pathlib import Path +P=(1780.166,-4333.466,110.187) +path=Path('/home/wow_admin/wow_cata_server/data/mmaps/0012840.mmtile') +b=path.read_bytes();fh=struct.unpack_from('<4I4B',b) +assert fh[0]==0x4d4d4150 and fh[2]==14 and fh[3]==len(b)-20 +h=struct.unpack_from('<15i10f',b,20) +pc,vc,lc,dc,dvc,dtc,bvc,oc=h[6:14] +vo=120;po=vo+vc*12;lo=po+pc*32;do=lo+lc*16;dvo=do+dc*12;dto=dvo+dvc*12 +assert dto+dtc*4+bvc*16+oc*36==len(b) +def vert(offset,i): + y,z,x=struct.unpack_from('<3f',b,offset+12*i) + return x,y,z +def project(a,c,d): + x,y=P[:2];u=(c[0]-a[0],c[1]-a[1]);v=(d[0]-a[0],d[1]-a[1]);w=(x-a[0],y-a[1]) + det=u[0]*v[1]-u[1]*v[0] + if abs(det)>1e-10: + s=(w[0]*v[1]-w[1]*v[0])/det;t=(u[0]*w[1]-u[1]*w[0])/det + if s>=-1e-6 and t>=-1e-6 and s+t<=1+1e-6:return (x,y,a[2]+s*(c[2]-a[2])+t*(d[2]-a[2])) + points=[] + for e,f in [(a,c),(c,d),(d,a)]: + dx=f[0]-e[0];dy=f[1]-e[1];den=dx*dx+dy*dy + t=max(0,min(1,((x-e[0])*dx+(y-e[1])*dy)/den)) if den else 0 + points.append(tuple(e[k]+t*(f[k]-e[k]) for k in range(3))) + return min(points,key=lambda q:(q[0]-x)**2+(q[1]-y)**2) +found=[] +for i in range(min(pc,dc)): + poly=struct.unpack_from('>6 + if kind or n<3:continue + pv=[vert(vo,j) for j in poly[1:1+n]] + if min(v[0] for v in pv)>P[0]+3 or max(v[0] for v in pv)P[1]+3 or max(v[1] for v in pv)=410 and r['quality']==4 and r['classes']&4 and r['rep']==0 and ((r['class']==4 and r['subclass'] in(0,3) and (3 in r['stats'] or r['slot']==12)) or (r['class']==2 and (3 in r['stats'] or r['subclass'] in(2,3,18))))] + for r in sorted(out,key=lambda v:(v['slot'],v['name'])):print(json.dumps(r)) diff --git a/src/server/game/World/AdventureBehaviorMgr.cpp b/src/server/game/World/AdventureBehaviorMgr.cpp index 55250b5ac6..54b2a93b36 100644 --- a/src/server/game/World/AdventureBehaviorMgr.cpp +++ b/src/server/game/World/AdventureBehaviorMgr.cpp @@ -8,6 +8,7 @@ #include "AdventureCombatMgr.h" #include "AdventureEncounterMgr.h" #include "AdventureGroupPolicy.h" +#include "AdventureGroupTravelMgr.h" #include "Group.h" #include "AdventureLocationMgr.h" #include "AdventureEconomyPolicy.h" @@ -41,12 +42,19 @@ #include "Opcodes.h" #include "WorldPacket.h" #include +#include #include #include #include namespace { using namespace AdventureBehaviorPolicy; + std::string RequiredLocationArrivalNode(AdventureLocation const& location) + { + // Explicit Undercity exception, using its authoritative catalog area. + return location.Point.MapId==0 && (location.ZoneId==1497 || location.AreaId==1497) ? "taxi_11" : ""; + } + AdventureMicroAction ResolvedAction(AdventureMicroAction action) { if(action.Kind=="work") @@ -420,6 +428,10 @@ void AdventureBehaviorMgr::Queue(Player * player, Record & r, std::string const a.Name = action; a.Parameters[key] = value; a.Parameters["hard_destination"] = hard ? "true" : "false"; + if (action=="travel_to_location") + if (auto location=sAdventureLocationMgr->Find(value)) + if (auto arrival=RequiredLocationArrivalNode(*location); !arrival.empty()) + a.Parameters["arrival_node"]=arrival; if(auto group=player->GetGroup())if(group->GetLeaderGUID()!=player->GetGUID() && sAdventureGroupMgr->BotLed(player)) a.Parameters["route_source"]=std::to_string(group->GetLeaderGUID().GetCounter()); r.Queued = sClientlessPlayerMgr->QueueAction(player->GetGUID().GetCounter(), std::move(a)); @@ -427,6 +439,7 @@ void AdventureBehaviorMgr::Queue(Player * player, Record & r, std::string const std::string AdventureBehaviorMgr::AreaNode(Player * player, AdventureLocation const & l) { + if (auto arrival=RequiredLocationArrivalNode(l); !arrival.empty()) return arrival; std::vector < std::pair < float, std::string >> candidates; auto terrain = sTerrainMgr.LoadTerrain(l.Point.MapId); if (! terrain) return @@ -468,6 +481,94 @@ void AdventureBehaviorMgr::LatchGroupTravel(Player* player,Record& r) } } +// The waiting destination is independent of the actual instance/PvP intent. +// Keep it stable across graph transitions and let Queue preserve leader routes. +bool AdventureBehaviorMgr::UpdateWaitingCapital(Player* player, Record& r, time_t now) +{ + uint32 guid=player->GetGUID().GetCounter(); + auto permitted=[&](AdventureLocation const& site) + { + return !site.IsArea && AdventureGroupPolicy::Capital(site.ZoneId?site.ZoneId:site.AreaId) && + Allowed(player,site,"groupformation") && + sAdventureLocationMgr->LevelEligible(player,site,"groupformation",true); + }; + auto site=sAdventureLocationMgr->Find(r.WaitingLocation); + // An in-flight action owns movement until it finishes, even across maps. + if(r.WaitingTravel) + { + if(r.Queued && sClientlessPlayerMgr->GetQueuedActionCount(guid))return false; + r.Queued=false; + if(Busy(guid))return false; + r.WaitingTravel=false; + auto status=sClientlessActionExecutorMgr->GetStatus(guid); + if(!site || !permitted(*site) || !status || status->State!=ClientlessActionExecutionState::Completed || + player->GetMapId()!=site->Point.MapId || player->GetExactDist(site->Point.X,site->Point.Y,site->Point.Z)>6 || + !AdventureGroupPolicy::Capital(player->GetZoneId())) + { + TC_LOG_WARN("server.adventurebot.navigation","WAIT_CAPITAL_FAILED guid=%u location='%s'",guid,r.WaitingLocation.c_str()); + r.WaitingLocation.clear();r.WaitingArrived=false;r.WaitingRetryAt=now+30; + r.Detail="capital travel failed; retrying without changing activity intent"; + return false; + } + r.Anchor.Relocate(player);r.AnchorMap=player->GetMapId(); + r.WaitingArrived=true; + r.Detail="waiting in capital for a suitable group / native browser queue"; + TC_LOG_INFO("server.adventurebot","WAIT_CAPITAL_ARRIVED guid=%u intent='%s' location='%s'",guid,r.Intent.c_str(),r.WaitingLocation.c_str()); + } + if(nowGetGroup())if(sAdventureGroupMgr->BotLed(player) && group->GetLeaderGUID()!=player->GetGUID()) + { + auto leader=_records.find(group->GetLeaderGUID().GetCounter()); + if(leader!=_records.end()) + { + // The leader may be updated later in this tick. Do not commit + // followers to unrelated cities before its destination exists. + if(leader->second.State==Phase::WaitingGroup && leader->second.WaitingLocation.empty()) + {r.WaitingRetryAt=now+1;return false;} + if(auto target=sAdventureLocationMgr->Find(leader->second.WaitingLocation);target && permitted(*target))site=target; + } + } + if(!site) + { + // Pick cities uniformly, rather than favouring cities with more POIs. + std::map> cities; + for(auto const& candidate:sAdventureLocationMgr->Locations())if(permitted(candidate)) + cities[candidate.ZoneId?candidate.ZoneId:candidate.AreaId].push_back(&candidate); + if(cities.empty()) + {r.WaitingRetryAt=now+60;r.Detail="no permitted capital waiting location";return false;} + auto city=cities.find(player->GetZoneId()); + // Already in a permitted capital: stay there instead of city hopping. + if(city==cities.end()) + {city=cities.begin();std::advance(city,std::uniform_int_distribution(0,cities.size()-1)(r.Random));} + auto const& choices=city->second; + site=choices[std::uniform_int_distribution(0,choices.size()-1)(r.Random)]; + } + ResetMicro(player,r);r.WaitingLocation=site->Point.Id;r.WaitingArrived=false; + } + if(player->GetMapId()==site->Point.MapId && AdventureGroupPolicy::Capital(player->GetZoneId()) && + player->GetExactDist(site->Point.X,site->Point.Y,site->Point.Z)<=6) + { + r.Anchor.Relocate(player);r.AnchorMap=player->GetMapId();r.WaitingArrived=true; + return true; + } + // Activities may roam around the arrival anchor; do not recall them to the POI. + if(r.WaitingArrived && player->GetMapId()==r.AnchorMap && + player->GetZoneId()==(site->ZoneId?site->ZoneId:site->AreaId))return true; + if(Busy(guid) || sClientlessPlayerMgr->GetQueuedActionCount(guid))return false; + ResetMicro(player,r); + Queue(player,r,"travel_to_location","id",r.WaitingLocation,true); + r.WaitingTravel=r.Queued; + if(!r.Queued)r.WaitingRetryAt=now+30; + r.Detail="travelling to capital while waiting for group / native browser queue: "+r.WaitingLocation; + TC_LOG_INFO("server.adventurebot","WAIT_CAPITAL_TRAVEL guid=%u intent='%s' activityLocation='%s' waitingLocation='%s' queued=%u", + guid,r.Intent.c_str(),r.Location.c_str(),r.WaitingLocation.c_str(),r.Queued?1u:0u); + return false; +} + void AdventureBehaviorMgr::Begin(Player * player, Record & r) { r.Dirty = false; @@ -504,13 +605,7 @@ void AdventureBehaviorMgr::Begin(Player * player, Record & r) { r.State=Phase::WaitingGroup;r.Detail="waiting for a suitable group / native browser queue"; r.Anchor.Relocate(player);r.AnchorMap=player->GetMapId();r.WaitingLocation.clear(); - float nearest=std::numeric_limits::max(); - for (auto const& site:sAdventureLocationMgr->Locations()) - if (site.Point.MapId==r.AnchorMap && Allowed(player,site,"groupformation")) - { - float distance=player->GetExactDist(site.Point.X,site.Point.Y,site.Point.Z); - if (distanceQueueBrowser(player,r.Intent); return; } @@ -1492,12 +1587,11 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff) if(r.State==Phase::WaitingGroup)ResetMicro(player,r); return; } - if (r.State==Phase::WaitingGroup && (player->GetMap()->IsDungeon() || player->GetMapId()!=r.AnchorMap)) + if (r.State==Phase::WaitingGroup && player->GetMap()->IsDungeon()) { // Native transfer owns movement; discard the old city activity. - ResetMicro(player,r);r.WaitingLocation.clear(); - if (player->GetMap()->IsDungeon()) return; - r.Anchor.Relocate(player);r.AnchorMap=player->GetMapId(); + ResetMicro(player,r);r.WaitingLocation.clear();r.WaitingTravel=false;r.WaitingArrived=false; + return; } if(UpdateNavigationRescue(player,r))return; if(r.AreaRevision!=sAdventureAreaMgr->Revision()) @@ -1631,12 +1725,11 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff) // Once a native browser owns this group, never race it with physical travel. auto group=player->GetGroup(); if(world && (r.ExpeditionStarted || sAdventureGroupMgr->Complete(player)) && (!group || !group->isLFGGroup()) && !player->isUsingLfg() && !player->InBattlegroundQueue()) - {ResetMicro(player,r);r.Dirty=true;r.State=Phase::Idle;} + {SetWanted(player,r,r.Intent,r.Location,r.Deadline);} else { sAdventureGroupMgr->QueueBrowser(player,r.Intent); - if (AdventureGroupPolicy::Capital(player->GetZoneId()) && !r.WaitingLocation.empty() && - (r.Intent=="instance" || r.Intent=="raid" || r.Intent=="pvp")) UpdateActivities(player,r); + if (UpdateWaitingCapital(player,r,now)) UpdateActivities(player,r); } return; } @@ -2132,7 +2225,15 @@ bool AdventureBehaviorMgr::UpdateInstance(Player* player, Record& r, time_t now) { if (!v.Abort && nowGetMapId()==v.Map && player->GetInstanceId()==v.InstanceId) - {r.Detail="waiting just inside private instance entrance";return true;} + { + if(auto group=player->GetGroup();group && group->GetLeaderGUID()!=player->GetGUID()) + { + sAdventureGroupTravelMgr->Follow(player); + r.Detail="local instance follow or waiting for group leader"; + } + else r.Detail="waiting just inside private instance entrance"; + return true; + } v.Stage=Stage::Returning; if (!SaveInstance(player,r)) {v.Stage=Stage::Inside;return true;} } diff --git a/src/server/game/World/AdventureBehaviorMgr.h b/src/server/game/World/AdventureBehaviorMgr.h index ec35200046..5cb4936f2d 100644 --- a/src/server/game/World/AdventureBehaviorMgr.h +++ b/src/server/game/World/AdventureBehaviorMgr.h @@ -65,6 +65,8 @@ private: std::string Intent, Location, WantedIntent, WantedLocation, Detail, Micro, ManualNextIntent, ManualNextLocation; std::string PersonalIntent,PersonalLocation,WaitingLocation; time_t PersonalDeadline=0; + time_t WaitingRetryAt=0; + bool WaitingTravel=false, WaitingArrived=false; time_t Deadline=0; AdventureGroupPolicy::Departure Departure; bool Gathering=false, ExpeditionStarted=false; @@ -114,6 +116,7 @@ private: void FinishInstance(Player* player, Record& record); void Generate(Player* player, Record& record, uint32 date); void SetWanted(Player* player, Record& record, std::string const& intent, std::string const& location, time_t deadline); + bool UpdateWaitingCapital(Player* player, Record& record, time_t now); void Begin(Player* player, Record& record); void LatchGroupTravel(Player* player,Record& record); void Fallback(Player* player, Record& record); diff --git a/src/server/game/World/AdventureCombatMgr.cpp b/src/server/game/World/AdventureCombatMgr.cpp index 8fa47aa563..857d2770ca 100644 --- a/src/server/game/World/AdventureCombatMgr.cpp +++ b/src/server/game/World/AdventureCombatMgr.cpp @@ -8,6 +8,7 @@ #include "AdventureEncounterMgr.h" #include "Pet.h" #include "CreatureAI.h" +#include "CombatManager.h" #include "ObjectMgr.h" #include "AdventureBehaviorMgr.h" #include "ClientlessLuaEngine.h" @@ -812,6 +813,34 @@ bool AdventureCombatMgr::Update(Player* p,uint32 diff) if(r.AI)Finish(p,r); Start(p,r,c,false,true,0);break; } + // Combat can begin through aggro before any damage is dealt. Preserve an + // existing/group-selected target and the damage queue's higher priority. + if(!r.AI && !r.Cooldown && p->IsInCombat()) + { + Unit* target=nullptr; + auto consider=[&](auto const& refs) + { + for(auto const& entry:refs) + { + auto ref=entry.second; + auto enemy=ref->GetOther(p); + if(ref->IsSuppressedFor(p) || ref->IsSuppressedFor(enemy) || + !Valid(p,enemy,false) || p->GetExactDist(enemy)>80)continue; + bool attacking=enemy->GetVictim()==p; + bool currentAttacking=target && target->GetVictim()==p; + if(!target || (attacking && !currentAttacking) || + (attacking==currentAttacking && + (p->GetExactDist(enemy)GetExactDist(target) || + (p->GetExactDist(enemy)==p->GetExactDist(target) && enemy->GetGUID()GetGUID())))) + target=enemy; + } + }; + consider(p->GetCombatManager().GetPvECombatRefs()); + consider(p->GetCombatManager().GetPvPCombatRefs()); + if(target && Start(p,r,target,false,true,0)) + TC_LOG_INFO("server.adventurebot","COMBAT_DEFEND_REFERENCE guid=%u target='%s'", + p->GetGUID().GetCounter(),target->GetGUID().ToString().c_str()); + } if(Flee(p,r))return true; if(!r.AI && (!p->GetGroup() || p->GetGroup()->GetLeaderGUID()==p->GetGUID()) && !p->IsInCombat() && !r.Cooldown) { diff --git a/src/server/game/World/AdventureEncounterMgr.cpp b/src/server/game/World/AdventureEncounterMgr.cpp index f405b43f80..3c3f59a496 100644 --- a/src/server/game/World/AdventureEncounterMgr.cpp +++ b/src/server/game/World/AdventureEncounterMgr.cpp @@ -1,5 +1,8 @@ #include "AdventureEncounterMgr.h" #include "AdventureBehaviorMgr.h" +#include "AdventureGroupTravelMgr.h" +#include "LFGMgr.h" +#include "GameTime.h" #include "ClientlessTravelMgr.h" #include "ClientlessTravelGraphMgr.h" #include "ClientlessPlayerMgr.h" @@ -19,6 +22,7 @@ #include "CellImpl.h" #include "GridNotifiersImpl.h" #include "MotionMaster.h" +#include "Log.h" #include #include #include @@ -37,7 +41,7 @@ bool AdventureEncounterMgr::Active(Player* p) const (MixedGroup(p) || (it!=_records.end() && it->second.Map==p->GetMapId())); } void AdventureEncounterMgr::Register(uint32 map,Behavior behavior){if(behavior)_behaviors[map]=std::move(behavior);else _behaviors.erase(map);} -void AdventureEncounterMgr::Offline(Player* p){_records.erase(p->GetGUID().GetCounter());} +void AdventureEncounterMgr::Offline(Player* p){_records.erase(p->GetGUID().GetCounter());_entryAttempts.erase(p->GetGUID().GetCounter());} bool AdventureEncounterMgr::Update(Player* p,uint32 diff) { auto guid=p->GetGUID().GetCounter();auto it=_records.find(guid); @@ -66,18 +70,9 @@ bool AdventureEncounterMgr::Update(Player* p,uint32 diff) it=_records.insert_or_assign(guid,r).first; } if(!p->IsAlive())return false; // Death() owns wipe/release/re-entry. - bool humanPresent=false,offlineMember=false,humanOutside=false; - for(auto ref=p->GetGroup()->GetFirstMember();ref;ref=ref->next()) - { - auto member=ref->GetSource();if(!member){offlineMember=true;continue;} - if(!member->GetSession()->IsClientless()) - {humanPresent=true;if(member->IsAlive() && !member->GetMap()->IsDungeon())humanOutside=true;} - } - if(!p->IsInCombat() && (humanOutside || (!humanPresent && !offlineMember))) - { - auto const& r=it->second; - return p->TeleportTo(r.OutsideMap,r.Outside.GetPositionX(),r.Outside.GetPositionY(),r.Outside.GetPositionZ(),r.Outside.GetOrientation()); - } + // A human may still be loading, and pure bot groups can arrive in any + // order. Remaining group members wait inside instead of exiting early. + if(sAdventureGroupTravelMgr->HoldForInstanceLeader(p))return true; if(it->second.Returning) { bool gathering=false;Player* livingHuman=nullptr; @@ -166,6 +161,19 @@ bool AdventureEncounterMgr::FollowIntoInstance(Player* p) if(member->IsAlive() && !member->GetSession()->IsClientless() && member->GetMap()->IsDungeon()) {human=member;break;} if(!human)return false; + if(auto group=p->GetGroup();group && group->isLFGGroup()) + { + auto dungeon=sLFGMgr->GetLFGDungeon(sLFGMgr->GetDungeon(group->GetGUID())); + if(!dungeon || uint32(dungeon->map)!=human->GetMapId())return true; + auto now=GameTime::GetGameTime();auto& next=_entryAttempts[p->GetGUID().GetCounter()]; + if(nowTeleportPlayer(p,false); + if(p->IsBeingTeleported())p->GetSession()->CompleteClientlessTeleport(); + TC_LOG_INFO("server.adventurebot.navigation","LFG_INSTANCE_REENTRY guid=%u targetMap=%u actualMap=%u instance=%u", + p->GetGUID().GetCounter(),uint32(dungeon->map),p->GetMapId(),p->GetInstanceId()); + return true; + } AreaTriggerEntry const* entrance=nullptr;float distance=1e30f; for(auto area:sAreaTriggerStore) { diff --git a/src/server/game/World/AdventureEncounterMgr.h b/src/server/game/World/AdventureEncounterMgr.h index f17f9cd8da..4cfd26c513 100644 --- a/src/server/game/World/AdventureEncounterMgr.h +++ b/src/server/game/World/AdventureEncounterMgr.h @@ -4,6 +4,7 @@ #include "Position.h" #include "ObjectGuid.h" #include +#include #include class Player; // Adventure policy only. DungeonBot CreatureAI and generic CPC stay independent. @@ -31,6 +32,7 @@ private: bool AvoidGroundDamage(Player* player); bool FollowIntoInstance(Player* player); std::unordered_map _records; + std::unordered_map _entryAttempts; std::unordered_map _behaviors; }; #define sAdventureEncounterMgr AdventureEncounterMgr::instance() diff --git a/src/server/game/World/AdventureGroupTravelMgr.cpp b/src/server/game/World/AdventureGroupTravelMgr.cpp index 51bb11a12a..c2ca8e6c21 100644 --- a/src/server/game/World/AdventureGroupTravelMgr.cpp +++ b/src/server/game/World/AdventureGroupTravelMgr.cpp @@ -148,6 +148,11 @@ void AdventureGroupTravelMgr::Observe(Player* player,uint32 diff) 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; + if(player->GetMap()->IsDungeon()) + { + record.Pending.clear();record.SeparationMs=0;record.ProtectedTravel=true; + return; + } record.Timer+=std::min(diff,30000u);if(record.TimerGetLeaderGUID());if(!leader)return; @@ -260,7 +265,7 @@ Position AdventureGroupTravelMgr::Formation(Player* player,Player* leader,bool f } bool AdventureGroupTravelMgr::Route(Player* player,Record& record,std::string const& node) { - if(node.empty())return false; + if(node.empty() || player->GetMap()->IsDungeon())return false; uint32 guid=player->GetGUID().GetCounter();auto now=GameTime::GetGameTime(); if(Executing(guid))return true; if(sClientlessPlayerMgr->GetQueuedActionCount(guid))return record.RouteOwned; @@ -271,6 +276,7 @@ bool AdventureGroupTravelMgr::Route(Player* player,Record& record,std::string co } bool AdventureGroupTravelMgr::JourneyFollow(Player* player,Record& record,Leader const& leader) { + if(player->GetMap()->IsDungeon())return false; auto now=GameTime::GetGameTime();if(leader.Kind==Journey::None || now-leader.Began>=1800)return false; // Goal is the native fixed portal destination recorded by SampleLeader, // not the player's later position. Following must not bypass area rules. @@ -322,8 +328,54 @@ bool AdventureGroupTravelMgr::JourneyFollow(Player* player,Record& record,Leader {if(portal->IsInPhase(player)){portal->Use(player);return true;}} return false; } +bool AdventureGroupTravelMgr::HoldForInstanceLeader(Player* player) +{ + auto group=player->GetGroup(); + if(!player->GetMap()->IsDungeon() || !group || group->GetLeaderGUID()==player->GetGUID()) + { + auto found=_records.find(player->GetGUID().GetCounter()); + if(found!=_records.end()) + {found->second.EntryMap=0;found->second.EntryInstance=0;found->second.EntryLeader.Clear();found->second.WaitingInstanceLeader=false;} + return false; + } + if( + !player->IsAlive() || player->IsInCombat() || player->IsBeingTeleported() || + player->GetTransport() || player->IsInFlight() || player->IsCharmed()) return false; + auto guid=player->GetGUID().GetCounter();auto& record=_records[guid]; + bool entered=record.EntryMap!=player->GetMapId() || record.EntryInstance!=player->GetInstanceId() || + record.EntryLeader!=group->GetLeaderGUID(); + if(entered) + { + // Discard travel from before the transfer, while preserving the native + // instance/group binding. Never navigate back through a world portal. + sClientlessPlayerMgr->ClearQueuedActions(guid); + sClientlessActionExecutorMgr->Clear(guid); + sClientlessTravelMgr->CancelTravel(guid); + player->StopMoving(); + record.RouteOwned=false;record.FlightOwned=false;record.RouteGoal.clear(); + record.EntryMap=player->GetMapId();record.EntryInstance=player->GetInstanceId(); + record.EntryLeader=group->GetLeaderGUID(); + } + record.Pending.clear();record.SeparationMs=0;record.ProtectedTravel=true; + auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID()); + bool ready=leader && leader->IsAlive() && !leader->IsBeingTeleported() && + player->IsInMap(leader) && player->GetInstanceId()==leader->GetInstanceId() && + player->IsInPhase(leader) && player->GetExactDist(leader)<=AdventureGroupTravelPolicy::LocalFollowRange; + bool waiting=!ready; + if(entered || waiting!=record.WaitingInstanceLeader) + TC_LOG_INFO("server.adventurebot.navigation","INSTANCE_RENDEZVOUS guid=%u map=%u instance=%u leader=%u waiting=%u", + guid,player->GetMapId(),player->GetInstanceId(),group->GetLeaderGUID().GetCounter(),waiting?1u:0u); + record.WaitingInstanceLeader=waiting; + if(!waiting)return false; + sClientlessTravelMgr->CancelTravel(guid); + if(player->IsSplineEnabled())player->StopMoving(); + record.Detail="waiting inside instance for group leader in local range"; + return true; +} + bool AdventureGroupTravelMgr::Follow(Player* player) { + if(HoldForInstanceLeader(player))return true; uint32 guid=player->GetGUID().GetCounter();auto group=player->GetGroup();auto found=_records.find(guid); if(!group || group->GetLeaderGUID()==player->GetGUID()) { @@ -389,7 +441,7 @@ bool AdventureGroupTravelMgr::Follow(Player* player) auto target=Formation(player,leader,false); if(!GroundPoint(player,target) || (leader->IsFlying() && std::fabs(leader->GetPositionZ()-target.GetPositionZ())>AdventureGroupTravelPolicy::GroundLeaderHeight)) {sClientlessTravelMgr->CancelTravel(guid);record.Detail="leader above reachable ground; waiting";return true;} - if(player->GetExactDist(leader)>100) + if(player->GetExactDist(leader)>AdventureGroupTravelPolicy::LocalFollowRange) { std::string node;float distance=0; if(sClientlessTravelGraphMgr->FindNearestNodeToPosition(leader->GetMapId(),target.GetPositionX(),target.GetPositionY(),target.GetPositionZ(),node,distance) && distance<1000)Route(player,record,node); diff --git a/src/server/game/World/AdventureGroupTravelMgr.h b/src/server/game/World/AdventureGroupTravelMgr.h index 1f2d13364b..bc7f25d5c4 100644 --- a/src/server/game/World/AdventureGroupTravelMgr.h +++ b/src/server/game/World/AdventureGroupTravelMgr.h @@ -16,6 +16,7 @@ public: static AdventureGroupTravelMgr* instance(); void Observe(Player* player, uint32 diff); bool Follow(Player* player); + bool HoldForInstanceLeader(Player* player); bool AdaptRoad(Player* player, std::vector& path); bool ValidateGroundPath(Player* player, std::vector const& path) const; bool Protected(Player* player) const; @@ -46,6 +47,9 @@ private: uint32 Timer = 0, SeparationMs = 0, Generation = 0, BotMap = 0; Position ProgressPosition; time_t ProgressAt = 0, NextRoute = 0, NextMount = 0, LastPortalUse = 0, NativeTransitAt = 0; + uint32 EntryMap=0,EntryInstance=0; + ObjectGuid EntryLeader; + bool WaitingInstanceLeader=false; bool RouteOwned = false, FlightOwned = false, ProtectedTravel = false; std::string RouteGoal, Detail; std::deque Pending; diff --git a/src/server/game/World/AdventureGroupTravelPolicy.h b/src/server/game/World/AdventureGroupTravelPolicy.h index 7f00cffb34..44c72a4ba1 100644 --- a/src/server/game/World/AdventureGroupTravelPolicy.h +++ b/src/server/game/World/AdventureGroupTravelPolicy.h @@ -9,6 +9,7 @@ namespace AdventureGroupTravelPolicy constexpr float SeparationDistance = 500; constexpr unsigned SeparationTimeoutMs = 600000; constexpr float GroundLeaderHeight = 12; + constexpr float LocalFollowRange = 100; constexpr float MaxGroundStep = 1.75f; constexpr float MaxGroundSlope = 0.8f; struct Offset { float Behind, Side, Height; }; diff --git a/src/server/game/World/ClientlessActionExecutorMgr.cpp b/src/server/game/World/ClientlessActionExecutorMgr.cpp index 3fbc3269b4..f64afd3cad 100644 --- a/src/server/game/World/ClientlessActionExecutorMgr.cpp +++ b/src/server/game/World/ClientlessActionExecutorMgr.cpp @@ -288,6 +288,7 @@ bool ClientlessActionExecutorMgr::StartAction(Player* player, ClientlessAction c record.PlannedStartRadius = 500.0f; record.PlannedAnchorRadius = 1000.0f; record.PlannedDestinationNode.clear(); + record.RequiredArrivalNode.clear();record.RequiredArrivalReached=false; record.FailedBridgeKeys.clear(); record.DeferredStartActive = false; // VCORE_C819_DEFERRED_START_NODES_V2 record.DeferredStartAttempts = 0; @@ -650,6 +651,7 @@ bool ClientlessActionExecutorMgr::StartTravelToNode( if (std::string const* hard = find("hard_destination")) record.HardDestination = (*hard == "1" || *hard == "true" || *hard == "yes"); + if (!record.RequiredArrivalNode.empty()) record.HardDestination=true; if (std::string const* radius = find("bridge_radius")) record.LocalBridgeRadius = ParseFloat(*radius, record.LocalBridgeRadius); if (std::string const* pathMax = find("bridge_path_max")) @@ -770,6 +772,18 @@ bool ClientlessActionExecutorMgr::StartTravelToLocation( record.Status.State=ClientlessActionExecutionState::Failed; record.Status.Detail="location denied by area catalog";record.Active=false;return false; } + if (auto arrival=find("arrival_node")) record.RequiredArrivalNode=*arrival; + record.RequiredArrivalReached=false; + if (!record.RequiredArrivalNode.empty() && !sClientlessTravelGraphMgr->FindNode(record.RequiredArrivalNode)) + { + sClientlessTravelGraphMgr->EnsureLoaded(); + if (!sClientlessTravelGraphMgr->FindNode(record.RequiredArrivalNode)) + { + record.Status.State=ClientlessActionExecutionState::Failed; + record.Status.Detail="required arrival node unavailable: "+record.RequiredArrivalNode; + record.Active=false;return false; + } + } record.FinalMapId = point->MapId; record.FinalX = point->X; record.FinalY = point->Y; @@ -805,7 +819,7 @@ bool ClientlessActionExecutorMgr::StartTravelToLocation( Position goalPosition;goalPosition.Relocate(point->X,point->Y,point->Z,point->O); bool const preferredFlight=sClientlessFlightMgr->Enabled(player) && sClientlessFlightMgr->Preferred(player,point->MapId,goalPosition); - if (directDistance <= directRadius && !preferredFlight) + if (record.RequiredArrivalNode.empty() && directDistance <= directRadius && !preferredFlight) { // C8.20: do not bypass the macro graph when the apparent direct // distance is short but the actual local MMAP route is blocked. @@ -836,7 +850,8 @@ bool ClientlessActionExecutorMgr::StartTravelToLocation( std::string error; float proxyDistance = std::numeric_limits::infinity(); sClientlessTravelGraphMgr->EnsureLoaded(); - if (!sClientlessTravelGraphMgr->FindNearestNodeToPosition( + anchorNode=record.RequiredArrivalNode; + if (anchorNode.empty() && !sClientlessTravelGraphMgr->FindNearestNodeToPosition( point->MapId, point->X, point->Y, point->Z, anchorNode, proxyDistance)) { if (TryFallbackFlight(player, characterGuid, record)) @@ -860,7 +875,8 @@ bool ClientlessActionExecutorMgr::StartTravelToLocation( if (!sClientlessTravelGraphMgr->PlanAdaptiveRoute( player, anchorNode, startRadius, record.LocalBridgeRadius, record.Route, record.RouteComplete, record.AdaptiveReachedNode, error, - &record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys, &goal, &record.JourneyCosts)) + &record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys, + record.RequiredArrivalNode.empty()?&goal:nullptr, &record.JourneyCosts)) { TC_LOG_WARN("server.adventurebot.navigation", "C820_LOCATION_PLAN_FAILED location='%s' reason='%s'", @@ -908,6 +924,16 @@ bool ClientlessActionExecutorMgr::StartFinalLocalLeg( return false; } + if (!record.RequiredArrivalNode.empty() && !record.RequiredArrivalReached) + { + auto node=sClientlessTravelGraphMgr->FindNode(record.RequiredArrivalNode); + if (!node || player->GetMapId()!=node->MapId || player->GetExactDist(node->X,node->Y,node->Z)>5.0f) + return RecoverTravel(player,characterGuid,record,0,"required-arrival-node-not-reached"); + record.RequiredArrivalReached=true; + TC_LOG_INFO("server.adventurebot.navigation","REQUIRED_ARRIVAL_REACHED guid=%u node='%s' location='%s'", + characterGuid,record.RequiredArrivalNode.c_str(),record.FinalLocationId.c_str()); + } + Position destination; destination.Relocate( record.FinalX, @@ -1943,6 +1969,12 @@ bool ClientlessActionExecutorMgr::StartRecordedEdgePath( bool ClientlessActionExecutorMgr::GetTravelGoal(Record const& record, ClientlessTravelNode& goal) const { + if (!record.RequiredArrivalNode.empty() && !record.RequiredArrivalReached) + { + auto node=sClientlessTravelGraphMgr->FindNode(record.RequiredArrivalNode); + if (!node) return false; + goal=*node;return true; + } if (record.Status.ActionName == "travel_to_location") { goal.MapId = record.FinalMapId; @@ -2405,7 +2437,9 @@ bool ClientlessActionExecutorMgr::RecoverTravel( std::string anchorNode; float proxyDistance = std::numeric_limits::infinity(); sClientlessTravelGraphMgr->EnsureLoaded(); - if (sClientlessTravelGraphMgr->FindNearestNodeToPosition( + bool const required=!record.RequiredArrivalNode.empty() && !record.RequiredArrivalReached; + if (required) anchorNode=record.RequiredArrivalNode; + if (required || sClientlessTravelGraphMgr->FindNearestNodeToPosition( record.FinalMapId, record.FinalX, record.FinalY, record.FinalZ, anchorNode, proxyDistance)) { @@ -2419,7 +2453,7 @@ bool ClientlessActionExecutorMgr::RecoverTravel( planned = sClientlessTravelGraphMgr->PlanAdaptiveRoute( player, anchorNode, radius, record.LocalBridgeRadius, record.Route, record.RouteComplete, record.AdaptiveReachedNode, error, - &record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys, &goal, &record.JourneyCosts); + &record.FailedEdgeDatabaseIds, &record.FailedBridgeKeys, required?nullptr:&goal, &record.JourneyCosts); } else error = "no graph node exists on destination map"; @@ -3140,6 +3174,9 @@ bool ClientlessActionExecutorMgr::AdoptRoute(Player* player,ClientlessAction con record.FinalMapId=snapshot.FinalMapId;record.FinalX=snapshot.FinalX;record.FinalY=snapshot.FinalY; record.FinalZ=snapshot.FinalZ;record.FinalO=snapshot.FinalO;record.FinalLocationId=snapshot.FinalLocationId; record.FinalDatabaseId=snapshot.FinalDatabaseId;record.PlannedDestinationNode=snapshot.PlannedDestinationNode; + record.RequiredArrivalNode=snapshot.RequiredArrivalNode; + // Every follower must actually reach the city entrance anchor itself. + record.RequiredArrivalReached=false; record.RouteComplete=snapshot.RouteComplete;record.AdaptiveReachedNode=snapshot.AdaptiveReachedNode; record.HardDestination=snapshot.HardDestination;record.Active=true; TC_LOG_INFO("server.adventurebot.navigation","TRAVEL_SHARED_ROUTE guid=%u leader=%u steps=%u journeySeed=%u",player->GetGUID().GetCounter(),sourceGuid,uint32(record.Route.size()),record.JourneyCosts.Seed); diff --git a/src/server/game/World/ClientlessActionExecutorMgr.h b/src/server/game/World/ClientlessActionExecutorMgr.h index 62ad01fd80..f5f1c49317 100644 --- a/src/server/game/World/ClientlessActionExecutorMgr.h +++ b/src/server/game/World/ClientlessActionExecutorMgr.h @@ -117,6 +117,8 @@ private: float PlannedStartRadius = 500.0f; float PlannedAnchorRadius = 1000.0f; std::string PlannedDestinationNode; + std::string RequiredArrivalNode; + bool RequiredArrivalReached = false; // C8.13b adaptive graph bridging / best-effort travel. std::unordered_set FailedBridgeKeys; diff --git a/src/server/game/World/ClientlessFlightMgr.cpp b/src/server/game/World/ClientlessFlightMgr.cpp index 62146ba35a..5bfd2d8fa6 100644 --- a/src/server/game/World/ClientlessFlightMgr.cpp +++ b/src/server/game/World/ClientlessFlightMgr.cpp @@ -577,17 +577,52 @@ bool ClientlessFlightMgr::FindReturnLanding(Player* player,Record& record) auto sample=[&](float x,float y){return Sample(player,record,x,y);}; auto ray=[&](Point const& a,Point const& b){return ClearRay(player,a,b);}; Point current=PointOf(*player); + if(record.LandingRecovery && record.Elapsed>=record.LandingFailuresResetAt) + { + record.RejectedLandings.clear(); + record.LandingFailuresResetAt=record.Elapsed+60000; + } for(float radius:{0.0f,8.0f,16.0f,24.0f,40.0f})for(unsigned i=0;i<(radius==0?1u:8u);++i) { float angle=float(i)*0.785398163f; float x=current.X+radius*std::cos(angle),y=current.Y+radius*std::sin(angle); auto surface=sample(x,y);Point goal{x,y,surface.Ground}; - if(!surface.Valid || Distance(current,goal)>80 || !OpenColumn(player,x,y,surface))continue; - Position candidate;candidate.Relocate(x,y,surface.Ground,player->GetOrientation()); - if(!sAdventureAreaMgr->AllowsSegment(player,player->GetMapId(),*player,candidate) || - !Corridor(current,goal,goal,record.Settings,sample,ray,false))continue; + if(!surface.Valid || Distance(current,goal)>80)continue; + // Water clearance is useful in flight, but is not solid support for a + // touchdown. Use the same support tolerance as final confirmation. + float solid=player->GetMap()->GetHeight(player->GetPhaseShift(),x,y,MAX_HEIGHT,true,MAX_HEIGHT); + if(!std::isfinite(solid) || solid<=INVALID_HEIGHT+1 || std::fabs(surface.Ground-solid)>0.7f || + !OpenColumn(player,x,y,surface))continue; + Position candidate;candidate.Relocate(x,y,solid,player->GetOrientation()); + if(std::any_of(record.RejectedLandings.begin(),record.RejectedLandings.end(), + [&](Position const& rejected){return candidate.GetExactDist(rejected)<3;}))continue; + if(!sAdventureAreaMgr->AllowsSegment(player,player->GetMapId(),*player,candidate))continue; + std::vector approach; + if(record.LandingRecovery) + { + // Validate a complete bounded approach, including takeoff from a + // water surface. Replay these checked segments, rather than trying + // a direct diagonal to shore through the water/terrain boundary. + Point at=current;Memory memory; + for(unsigned step=0;step<32 && Distance(at,goal)>0.1f;++step) + { + auto next=Next(at,goal,record.Settings,memory,sample,ray); + if(!next.Valid || next.UnderOverhang || Distance(at,next.Next)<0.05f)break; + Position from,to;from.Relocate(at.X,at.Y,at.Z,0);to.Relocate(next.Next.X,next.Next.Y,next.Next.Z,0); + if(!sAdventureAreaMgr->AllowsSegment(player,player->GetMapId(),from,to))break; + approach.push_back(to);at=next.Next; + } + if(Distance(at,goal)>0.1f)continue; + } + else if(!Corridor(current,goal,goal,record.Settings,sample,ray,false))continue; record.Start=candidate; - TC_LOG_INFO("server.adventurebot.navigation","FLIGHT_RETURN_LANDING guid=%u target=(%.3f %.3f %.3f)",player->GetGUID().GetCounter(),x,y,surface.Ground); + if(record.LandingRecovery) + { + record.Breadcrumbs.assign(approach.rbegin(),approach.rend()); + record.NextLandingSearch=0; + } + TC_LOG_INFO("server.adventurebot.navigation","FLIGHT_RETURN_LANDING guid=%u target=(%.3f %.3f %.3f) localRecovery=%u steps=%u", + player->GetGUID().GetCounter(),x,y,solid,record.LandingRecovery?1u:0u,uint32(approach.size())); return true; } return false; @@ -615,7 +650,8 @@ bool ClientlessFlightMgr::ApproachDeparture(Player* player, Record& record, bool if(!terrain.outdoors){++rejected[1];continue;} Position point;point.Relocate(x,y,surface.Ground,player->GetOrientation()); if(!sAdventureAreaMgr->AllowsSegment(player,player->GetMapId(),*player,point)){++rejected[2];continue;} - if(!OpenColumn(player,x,y,surface)){++rejected[3];continue;} + // Cover the 35-yard vertical fallback plus its upper body rays. + if(!OpenColumn(player,x,y,surface,40)){++rejected[3];continue;} ClientlessPathDiagnostics path; if(!sClientlessTravelMgr->AnalyzePath(player,point,path) || !path.Calculated || !path.Complete || path.Suspicious || path.FinalDistance>1.5f || path.PathLength>600 || path.MaxSegmentLength>8 || path.MaxZDelta>4){++rejected[4];continue;} @@ -631,6 +667,28 @@ bool ClientlessFlightMgr::ApproachDeparture(Player* player, Record& record, bool summary();return false; } +bool ClientlessFlightMgr::RetryDeparture(Player* player, Record& record) +{ + // This is a ground recovery only. Never dismount an airborne player or + // replace the existing airborne avoidance / checked landing recovery. + if(record.Returning || record.Memory.Departed || record.Status.Segments || + record.DepartureAttempts>=3 || !player->IsAlive() || player->IsInCombat() || + player->IsFlying() || player->IsFalling() || player->IsInWater() || + player->GetTransport() || player->IsInFlight() || player->IsBeingTeleported())return false; + float support=player->GetMap()->GetHeight(player->GetPhaseShift(),player->GetPositionX(), + player->GetPositionY(),player->GetPositionZ()+2,true,6); + if(!std::isfinite(support) || support<=INVALID_HEIGHT+1 || + std::fabs(support-player->GetPositionZ())>3)return false; + // ApproachDeparture publishes a replacement record. Use a copy rather + // than moving the map entry onto itself, retaining goal and route ownership. + Record retry=record; + ++retry.DepartureAttempts;retry.Elapsed=0;retry.SegmentElapsed=0;retry.Moving=false; + player->RemoveAurasByType(SPELL_AURA_MOUNTED); + TC_LOG_INFO("server.adventurebot.navigation","FLIGHT_DEPARTURE_RETRY guid=%u attempt=%u", + player->GetGUID().GetCounter(),retry.DepartureAttempts); + return ApproachDeparture(player,retry,retry.Status.FollowingPlayer); +} + void ClientlessFlightMgr::Fail(Player * player, Record & record, std::string const & reason) { ClientlessTravelTelemetry::Note(player,"flight-failed",reason+" "+ClientlessTravelTelemetry::State(player)); @@ -653,6 +711,27 @@ void ClientlessFlightMgr::Fail(Player * player, Record & record, std::string con TC_LOG_WARN("server.adventurebot.navigation", "FLIGHT_RETURN guid=%u reason='%s'", player->GetGUID().GetCounter(), reason.c_str()); return; } + if(record.Status.State==ClientlessFlightState::Flying && player->CanFly() && player->IsAlive() && + player->GetMapId()==record.Status.MapId) + { + // A failed return is not a reason to abandon an airborne player. + Position rejected=record.Returning?record.Start:record.Status.Landing; + if(record.RejectedLandings.size()<64)record.RejectedLandings.push_back(rejected); + if(!record.LandingFailuresResetAt)record.LandingFailuresResetAt=record.Elapsed+60000; + record.LandingRecovery=true;record.Returning=true;record.Following.reset(); + record.Breadcrumbs.clear();record.Memory={}; + if(record.AbortReason.empty())record.AbortReason=reason; + if(FindReturnLanding(player,record)) + record.Status.Detail="recovering to nearby verified dry landing"; + else + { + record.NextLandingSearch=record.Elapsed+10000; + record.Status.Detail="holding airborne; retrying local dry landing search"; + } + TC_LOG_WARN("server.adventurebot.navigation","FLIGHT_LANDING_RECOVERY guid=%u retryMs=%u reason='%s'", + player->GetGUID().GetCounter(),record.NextLandingSearch?10000u:0u,reason.c_str()); + return; + } record.Status.State = ClientlessFlightState::Failed; record.Status.Detail = reason; // If even the reverse corridor is blocked, remain mounted and stopped; @@ -743,7 +822,7 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff else { auto const memory = record.Memory; - next = Next(current, goal, record.Settings, record.Memory, sample, ray); + next = Next(current, goal, record.Settings, record.Memory, sample, ray, false, !record.Returning); // Reissuing the same endpoint would only restart its final milliseconds. // Let that spline finish instead; keep planning memory unchanged. if (handoff && next.Valid && Distance(next.Next, PointOf(record.Segment)) < 1) @@ -754,6 +833,7 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff } if (! next.Valid ||(! record.Returning && record.Status.Travelled > Distance(PointOf(record.Start), goal) * 2 + 500)) { + if(!next.Valid && RetryDeparture(player,record))return true; // A detached/pathological flight gets one fresh airborne plan. If the // new corridor is still impossible, finish with a checked local // touchdown so the ordinary behavior controller can plan from ground. @@ -774,6 +854,8 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff {Fail(player,record,"flight segment denied by area catalog");return false;} if (! record.Returning) record.Breadcrumbs.push_back(* player); record.Segment.Relocate(next.Next.X, next.Next.Y, next.Next.Z, player->GetOrientation()); + record.SegmentVerticalClimb=!record.Returning && + Horizontal(current,next.Next)<1.0f && next.Next.Z>current.Z+2.0f; player->AddUnitMovementFlag(MOVEMENTFLAG_FLYING); Movement::MoveSplineInit init(player); init.MoveTo(next.Next.X, next.Next.Y, next.Next.Z, false); @@ -799,6 +881,7 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff record.Status.Detail = next.UnderOverhang ? "elevated approach lateral escape" : record.Memory.Elevated==Memory::ElevatedPhase::Climb ? "elevated approach vertical climb" : record.Memory.Elevated==Memory::ElevatedPhase::Cross ? "elevated approach above platform" : + next.Avoiding && vertical && next.Next.Z>current.Z ? "vertical obstacle avoidance" : next.Avoiding ? "obstacle avoidance" : next.Landing && vertical ? "vertical landing fallback" : next.Landing ? "landing approach" : vertical && next.Next.Z>current.Z ? "vertical takeoff fallback" : vertical ? "vertical descent fallback" : next.Next.Z < current.Z - 0.25f ? "diagonal descent" : @@ -832,8 +915,11 @@ void ClientlessFlightMgr::Update(uint32 diff) if(player->GetExactDist(record.Departure)>2) {Fail(player,record,"departure-unavailable: takeoff point not reached");continue;} auto target=record.Status.Target;bool following=record.Status.FollowingPlayer;std::string error; - // Begin replaces this record on success; do not access old fields afterwards. + uint32 attempts=record.DepartureAttempts;bool owned=record.ActionOwned; + // Begin replaces this record; preserve the bounded ground recovery + // budget and action ownership across walking and remounting. if(!Begin(player,target,error,following))Fail(player,record,error); + else {auto& resumed=_records.at(guid);resumed.DepartureAttempts=attempts;resumed.ActionOwned=owned;} continue; } // Count actual motion, not overlapping issued spline lengths. @@ -851,11 +937,18 @@ void ClientlessFlightMgr::Update(uint32 diff) } } if(record.Following && record.ElapsedCanFly() || record.Elapsed >(record.Returning ? 2100000u : 1800000u)) + if (! player->CanFly() || (!record.LandingRecovery && record.Elapsed >(record.Returning ? 2100000u : 1800000u))) { Fail(player, record, "flight permission lost or total timeout"); continue; } + if(record.LandingRecovery && record.NextLandingSearch) + { + if(record.ElapsedOwnsActiveFlight(guid); bool const pathological=record.Status.Segments>=12 && record.Status.Avoidances*2>=record.Status.Segments; if(!record.Returning && !record.Following && record.DetachedRecoveryCount==0 && (ownerLost || pathological)) @@ -924,9 +1017,12 @@ void ClientlessFlightMgr::Update(uint32 diff) record.Moving = false; Advance(player, record); } - else if (!record.Returning && player->movespline->timeRemaining() <= 1500 && + else if (!record.Returning && !record.SegmentVerticalClimb && player->movespline->timeRemaining() <= 1500 && Distance(PointOf(record.Segment), PointOf(record.Status.Landing)) > 0.1f) { + // A checked vertical climb must reach its clearance height + // before nominal cruise planning takes over. Collision/stall + // checks above still interrupt a blocked or stalled climb. // Replace a still-running spline from its native current position. // No StopMoving/arrival packet between ordinary flight segments. // Landing and conservative breadcrumb return finish their endpoint. diff --git a/src/server/game/World/ClientlessFlightMgr.h b/src/server/game/World/ClientlessFlightMgr.h index f0b21c1b0a..0816e18051 100644 --- a/src/server/game/World/ClientlessFlightMgr.h +++ b/src/server/game/World/ClientlessFlightMgr.h @@ -52,9 +52,14 @@ class TC_GAME_API ClientlessFlightMgr ClientlessFlightPath::Memory Memory; Position Start, Segment, LastPosition; bool SegmentUnderOverhang = false; + bool SegmentVerticalClimb = false; uint32 Elapsed = 0, SegmentElapsed = 0, CheckTimer = 0; bool Moving = false, Returning = false; + bool LandingRecovery = false; + uint32 NextLandingSearch = 0, LandingFailuresResetAt = 0; + std::vector RejectedLandings; bool ApproachingDeparture = false; + uint32 DepartureAttempts = 0; Position Departure; uint32 TargetUpdated = 0, FormationDelay = 0; uint32 DetachedRecoveryCount = 0; @@ -70,6 +75,7 @@ class TC_GAME_API ClientlessFlightMgr void Store(uint32 guid, Record&& record); bool Begin(Player* player, Position const& target, std::string& error, bool following); bool ApproachDeparture(Player* player, Record& record, bool following); + bool RetryDeparture(Player* player, Record& record); bool FindReturnLanding(Player* player, Record& record); bool AdvanceFollowing(Player* player, Record& record); void HoldFollowing(Player* player, Record& record); diff --git a/src/server/game/World/ClientlessFlightPath.h b/src/server/game/World/ClientlessFlightPath.h index 4223538e35..83c1150f1a 100644 --- a/src/server/game/World/ClientlessFlightPath.h +++ b/src/server/game/World/ClientlessFlightPath.h @@ -118,7 +118,7 @@ namespace ClientlessFlightPath } return true; } - inline Result Next(Point const & from, Point const & goal, Settings const & cfg, Memory & memory, Sample const & sample, Ray const & ray, bool airborneGoal = false) + inline Result Next(Point const & from, Point const & goal, Settings const & cfg, Memory & memory, Sample const & sample, Ray const & ray, bool airborneGoal = false, bool verticalEscape = false) { float const distance = Horizontal(from, goal); Surface const here = sample(from.X, from.Y), end = sample(goal.X, goal.Y); @@ -291,15 +291,17 @@ namespace ClientlessFlightPath } // Diagonal departure candidates were exhausted. Tight settlements // can still have a completely open vertical column, so use it before - // asking the caller to walk to a different takeoff point. - if (!best.Valid && departure) + // asking the caller to walk to a different takeoff point. In flight, + // climb out of a tight clearance band using the same checked column; + // never require an already-achieved cruise margin at its lower end. + if (!best.Valid && (departure || verticalEscape)) { float const top = std::min(here.Ceiling - 6.0f, std::max(from.Z + 20.0f, here.Ground + std::min(cfg.Clearance, 35.0f))); Point vertical{from.X,from.Y,top}; if (top > from.Z + 2.0f && Corridor(from,vertical,goal,cfg,sample,ray,true)) { - return {vertical,true,false,false}; + return {vertical,true,!departure,false}; } } return best; diff --git a/src/server/game/World/ClientlessLuaEngine.cpp b/src/server/game/World/ClientlessLuaEngine.cpp index f11c3c6d53..cd796b7835 100644 --- a/src/server/game/World/ClientlessLuaEngine.cpp +++ b/src/server/game/World/ClientlessLuaEngine.cpp @@ -261,6 +261,7 @@ public: // Browser responses remain independent of combat and supply animations. sAdventureGroupMgr->UpdateBrowser(player); sAdventureGroupTravelMgr->Observe(player,diff); + if(sAdventureGroupTravelMgr->HoldForInstanceLeader(player))return; sAdventurePopulationMgr->Maintain(player,diff); // Restart restores native ship attachment, but CPC ride execution is // transient. Own its safe recorded exit before starting another action. diff --git a/src/server/game/World/ClientlessPlayerMgr.cpp b/src/server/game/World/ClientlessPlayerMgr.cpp index 64bd7001c8..0eaceec8ba 100644 --- a/src/server/game/World/ClientlessPlayerMgr.cpp +++ b/src/server/game/World/ClientlessPlayerMgr.cpp @@ -471,6 +471,33 @@ bool ClientlessPlayerMgr::ShutdownReady() return true; } +void ClientlessPlayerMgr::FinalizeShutdown() +{ + BeginShutdown(); // Also blocks new controllers on forced/early shutdown. + if (!_shutdownSnapshotDone) + { + WriteShutdownSnapshot("finalize"); + _shutdownSnapshotDone = true; + } + // Notify controllers once, while their Players and maps still exist. + // Do not run Update: it could deliver an unfinished login or start actions. + for (auto& entry : _records) + { + auto session = sWorld->FindClientlessSession(entry.first); + auto player = session ? session->GetPlayer() : nullptr; + auto& record = entry.second; + if (!record.LogoutNotified && record.Controller && player) + { + record.LogoutNotified = true; + record.Controller->OnPlayerLogout(player); + } + sClientlessActionExecutorMgr->Clear(entry.first); + sClientlessFlightMgr->Clear(entry.first); + } + _records.clear(); + _safeLogouts.clear(); +} + void ClientlessPlayerMgr::WriteShutdownSnapshot(char const* event) { std::ofstream out(_shutdownAuditFile,std::ios::app); diff --git a/src/server/game/World/ClientlessPlayerMgr.h b/src/server/game/World/ClientlessPlayerMgr.h index 721ab82525..e917bb69ec 100644 --- a/src/server/game/World/ClientlessPlayerMgr.h +++ b/src/server/game/World/ClientlessPlayerMgr.h @@ -88,6 +88,7 @@ public: void BeginShutdown(); void CancelShutdown(); bool ShutdownReady(); + void FinalizeShutdown(); bool IsShutdownDraining() const { return _shutdownDraining; } bool IsSafeLogoutPending(uint32 guid) const { return _safeLogouts.count(guid) != 0; } static bool ReadyForSafeLogout(Player const* player); diff --git a/src/server/game/World/ClientlessTravelGraphMgr.cpp b/src/server/game/World/ClientlessTravelGraphMgr.cpp index e622f98c56..ee1f4b719b 100644 --- a/src/server/game/World/ClientlessTravelGraphMgr.cpp +++ b/src/server/game/World/ClientlessTravelGraphMgr.cpp @@ -656,6 +656,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( // call, including its two search tiers. Never cache live Player paths globally. std::map startProbes; std::map, float> flightEstimates; + std::uint64_t expandedStartFlightCalls=0; auto analyzeStart=[&](Position const& target,ClientlessPathDiagnostics& result) { auto key=ClientlessGraphProbeCache::MakeKey(player->GetMapId(),player->GetInstanceId(),player->GetPositionX(),player->GetPositionY(),player->GetPositionZ(),target.GetPositionX(),target.GetPositionY(),target.GetPositionZ()); @@ -713,7 +714,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( // search. In particular, a flight to a portal source must still leave // enough probes for the flight from its destination map to the goal. constexpr std::uint64_t MaxFlightEstimatesPerOrigin = 6; - auto flightCost = [&](ClientlessTravelNode const& from, uint32 map, float x, float y, float z, bool access = false) + auto flightCost = [&](ClientlessTravelNode const& from, uint32 map, float x, float y, float z, bool access = false, bool expandedStart = false) { if (!flightEnabled || from.MapId != map || (blockedBridgeKeys && blockedBridgeKeys->count("flight_origin:"+from.Key))) @@ -730,8 +731,12 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( // constructing an implicit complete air graph. Cached estimates remain // usable after the cap; unexamined links simply stay unavailable in // this plan and ordinary graph travel can still win. - if (trace.FlightCalls >= MaxFlightEstimatesPerPlan) + // Adaptive start rings have their own bounded budget, so an exhausted + // inner ring cannot suppress outer rings or the subsequent graph search. + if (expandedStart ? expandedStartFlightCalls>=80 : + trace.FlightCalls-expandedStartFlightCalls>=MaxFlightEstimatesPerPlan) return std::numeric_limits::infinity(); + if(expandedStart)++expandedStartFlightCalls; auto begin=std::chrono::steady_clock::now();++trace.FlightCalls; float result=sClientlessFlightMgr->Estimate(player,map,a,b,access); if(std::isfinite(result)) @@ -870,19 +875,25 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( // flight budget before graph search can consider flight-only connections. std::uint64_t startFlightProbes = 0; constexpr std::uint64_t MaxStartFlightProbes = 4; - for (auto const& pair : _nodes) + std::vector> startNodes; + for(auto const& pair:_nodes) { - ClientlessTravelNode const& node = pair.second; - if (node.MapId != player->GetMapId()) - continue; - if ((!RescueNodeAllowed(node) || !sAdventureAreaMgr->AllowsNode(player,node))) continue; - - float const dx = node.X - player->GetPositionX(); - float const dy = node.Y - player->GetPositionY(); - float const dz = node.Z - player->GetPositionZ(); - float const localDistance = std::sqrt(dx * dx + dy * dy + dz * dz); - if (localDistance > startRadius) - continue; + auto const& node=pair.second; + if(node.MapId!=player->GetMapId() || !RescueNodeAllowed(node) || + !sAdventureAreaMgr->AllowsNode(player,node))continue; + float dx=node.X-player->GetPositionX(),dy=node.Y-player->GetPositionY(),dz=node.Z-player->GetPositionZ(); + startNodes.emplace_back(std::sqrt(dx*dx+dy*dy+dz*dz),pair.first); + } + std::sort(startNodes.begin(),startNodes.end()); + auto seedStartNodes=[&](float inner,float outer,bool expanded) + { + startFlightProbes=0; + for(auto const& ranked:startNodes) + { + float const localDistance=ranked.first; + if(localDistance>outer)break; + if(localDistance<=inner)continue; + auto const& node=_nodes.at(ranked.second); float startCost = groundCost(localDistance); // VCORE_VIRTUAL_EDGE_V11_PORTAL_APPROACH: never seed an unreachable node. @@ -893,8 +904,9 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( Position entryTarget; entryTarget.Relocate(node.X, node.Y, node.Z, node.O); ClientlessPathDiagnostics entryNav; - analyzeStart(entryTarget,entryNav); - bool const reachable = entryNav.Calculated && entryNav.Complete && + bool const groundCandidate=localDistance<=startRadius; + if(groundCandidate)analyzeStart(entryTarget,entryNav); + bool const reachable = groundCandidate && entryNav.Calculated && entryNav.Complete && entryNav.FinalDistance <= 1.5f && entryNav.MaxSegmentLength <= 8.0f && entryNav.MaxZDelta <= 5.0f && entryNav.PathLength <= 500.0f; ClientlessTravelNode departure; @@ -909,7 +921,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( blockedBridgeKeys->count("flight:flight_departure->"+node.Key))) { ++startFlightProbes; - air=flightCost(departure,node.MapId,node.X,node.Y,node.Z,true); + air=flightCost(departure,node.MapId,node.X,node.Y,node.Z,true,expanded); } if(std::isfinite(air) && !reachable) {flightRoots.insert(node.Key);startCost=air;} @@ -917,7 +929,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( { // VCORE_C819_DEFERRED_START_NODES: incomplete is not the same as unsafe. // The endpoint is handled by the executor, not counted as arrival at node. - bool const partial = entryNav.Calculated && + bool const partial = groundCandidate && entryNav.Calculated && (entryNav.PathType & uint32(PATHFIND_INCOMPLETE)) != 0 && (entryNav.PathType & uint32(PATHFIND_NOPATH)) == 0 && entryNav.PointCount >= 2 && entryNav.PathLength >= 2.0f && @@ -954,6 +966,9 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( ++startCount; } + }; + seedStartNodes(-1.0f,startRadius,false); + // Use the nearest ten nodes merely to shortlist real recorded ground edges. // All distances below are on the player's current map; no database mutation. std::vector> nearestNodes; @@ -1075,6 +1090,20 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( "VIRTUAL_EDGE_CANDIDATES map=%u tested=%u accepted=%u", player->GetMapId(), validated, accepted); + // Keep nearby recorded ground entries ahead of long flight-only entries. + // A ground-only bot never widens its local navigation limit. + if(!startCount && !accepted && flightEnabled) + { + for(float inner=startRadius;inner<10000.0f && !startCount;) + { + float outer=std::min(10000.0f,inner+500.0f); + seedStartNodes(inner,outer,true); + TC_LOG_INFO("server.adventurebot.navigation", + "START_RADIUS_EXPAND map=%u radius=%.0f accepted=%u flightCalls=%llu", + player->GetMapId(),outer,startCount,static_cast(expandedStartFlightCalls)); + inner=outer; + } + } if (!startCount && !accepted) { if(std::isfinite(directFlightCost))return directFlightRoute(); @@ -1615,7 +1644,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( // Dynamic flights connect real nodes; never the synthetic entry waypoints. // Include them before the local-bridge guard: a flight is a separate // macro travel mode with its own native departure and landing checks. - if (flightEnabled && !portalArrival && trace.FlightCalls < MaxFlightEstimatesPerPlan) + if (flightEnabled && !portalArrival && trace.FlightCalls-expandedStartFlightCalls < MaxFlightEstimatesPerPlan) { auto const& origin=_nodes.at(current.Value.Node); std::vector> macroFlightCandidates; @@ -1677,7 +1706,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( std::uint64_t const callsAtOrigin = trace.FlightCalls; for (auto const& ranked : macroFlightCandidates) { - if (trace.FlightCalls >= MaxFlightEstimatesPerPlan || + if (trace.FlightCalls-expandedStartFlightCalls >= MaxFlightEstimatesPerPlan || trace.FlightCalls - callsAtOrigin >= MaxFlightEstimatesPerOrigin) break; auto const& target = _nodes.at(ranked.second); diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index 7a743c032a..af4b762ae2 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -342,7 +342,7 @@ ClientlessPlayerState World::GetClientlessPlayerState(uint32 characterGuid) cons bool World::AddClientlessSession(uint32 characterGuid, WorldSession* session) { - if (!characterGuid || !session) + if (m_clientlessSessionsFinalized || !characterGuid || !session) return false; if (!session->IsClientless()) @@ -384,6 +384,36 @@ bool World::AddClientlessSession(uint32 characterGuid, WorldSession* session) return true; } +void World::ShutdownClientlessSessions() +{ + if (m_clientlessSessionsFinalized) + return; + m_clientlessSessionsFinalized = true; + + TC_LOG_INFO("server.worldserver", "CLIENTLESS_SHUTDOWN_FINALIZE_BEGIN remaining=%u", GetClientlessSessionCount()); + // No further WorldSession updates/callback processing are allowed here. + // Finish native transfers while both source and destination maps exist. + for (auto const& entry : m_clientlessSessions) + if (entry.second) + entry.second->CompleteClientlessTeleport(); + + sClientlessPlayerMgr->FinalizeShutdown(); + while (!m_clientlessSessions.empty()) + { + auto entry = m_clientlessSessions.begin(); + uint32 guid = entry->first; + // The session destructor runs the native LogoutPlayer(true): pets, + // passengers, groups and persistence are cleaned up before map unload. + // Pending login callbacks are session-owned and only invoked by Update; + // destroying them cancels delivery without waiting for a DB response. + delete entry->second; + m_clientlessSessions.erase(entry); + m_clientlessPlayerStates.erase(guid); + } + m_clientlessPlayerStates.clear(); + TC_LOG_INFO("server.worldserver", "CLIENTLESS_SHUTDOWN_FINALIZE_END remaining=%u", GetClientlessSessionCount()); +} + bool World::RemoveClientlessSession(uint32 characterGuid) { SessionMap::iterator itr = m_clientlessSessions.find(characterGuid); @@ -403,6 +433,9 @@ bool World::RemoveClientlessSession(uint32 characterGuid) bool World::CreateClientlessPlayerSession(uint32 accountId, uint32 characterGuid) { + if (m_clientlessSessionsFinalized) + return false; + if (!accountId || !characterGuid) { TC_LOG_ERROR("server.worldserver", diff --git a/src/server/game/World/World.h b/src/server/game/World/World.h index f015d8ec0a..f894c4e74a 100644 --- a/src/server/game/World/World.h +++ b/src/server/game/World/World.h @@ -620,6 +620,7 @@ class TC_GAME_API World WorldSession* FindClientlessSession(uint32 characterGuid) const; bool AddClientlessSession(uint32 characterGuid, WorldSession* session); bool RemoveClientlessSession(uint32 characterGuid); + void ShutdownClientlessSessions(); bool CreateClientlessPlayerSession(uint32 accountId, uint32 characterGuid); ClientlessPlayerState GetClientlessPlayerState(uint32 characterGuid) const; SessionMap const& GetAllClientlessSessions() const { return m_clientlessSessions; } @@ -877,6 +878,7 @@ class TC_GAME_API World time_t mail_timer_expires; SessionMap m_sessions; // Network sessions, keyed by account id + bool m_clientlessSessionsFinalized = false; SessionMap m_clientlessSessions; // Clientless sessions, keyed by character low GUID std::unordered_map m_clientlessPlayerStates; typedef std::unordered_map DisconnectMap; diff --git a/src/server/scripts/OutdoorPvP/OutdoorPvPNA.cpp b/src/server/scripts/OutdoorPvP/OutdoorPvPNA.cpp index 6a9a5a5a52..8c78775d93 100644 --- a/src/server/scripts/OutdoorPvP/OutdoorPvPNA.cpp +++ b/src/server/scripts/OutdoorPvP/OutdoorPvPNA.cpp @@ -21,6 +21,7 @@ #include "GameObject.h" #include "GridNotifiersImpl.h" #include "MapManager.h" +#include "Log.h" #include "ObjectMgr.h" #include "OutdoorPvPMgr.h" #include "Player.h" @@ -52,8 +53,18 @@ void OutdoorPvPNA::OnGameObjectCreate(GameObject* go) switch (go->GetEntry()) { case 182210: - m_obj->m_capturePointSpawnId = go->GetSpawnId(); - AddCapturePoint(m_obj.get()); + if (m_unregisteredCapturePoint) + { + m_obj->m_capturePointSpawnId = go->GetSpawnId(); + AddCapturePoint(m_unregisteredCapturePoint.release()); + } + else if (m_obj->m_capturePointSpawnId != go->GetSpawnId()) + { + // Never register the same allocation under a second spawn ID. + TC_LOG_ERROR("outdoorpvp", "Halaa banner spawn %u conflicts with registered spawn %u.", + go->GetSpawnId(), m_obj->m_capturePointSpawnId); + return; + } break; } @@ -140,12 +151,16 @@ OPvPCapturePointNA::~OPvPCapturePointNA() = default; bool OutdoorPvPNA::SetupOutdoorPvP() { + if (m_obj) + return true; + // m_TypeId = OUTDOOR_PVP_NA; _MUST_ be set in ctor, because of spawns cleanup // add the zones affected by the pvp buff RegisterZone(NA_BUFF_ZONE); // halaa - m_obj = std::make_unique(this); + m_unregisteredCapturePoint = std::make_unique(this); + m_obj = m_unregisteredCapturePoint.get(); return true; } diff --git a/src/server/scripts/OutdoorPvP/OutdoorPvPNA.h b/src/server/scripts/OutdoorPvP/OutdoorPvPNA.h index 745e80a839..5229d4126e 100644 --- a/src/server/scripts/OutdoorPvP/OutdoorPvPNA.h +++ b/src/server/scripts/OutdoorPvP/OutdoorPvPNA.h @@ -172,7 +172,10 @@ class OutdoorPvPNA : public OutdoorPvP void HandleKillImpl(Player* player, Unit* killed) override; private: - std::unique_ptr m_obj; + // Own the objective until its real banner spawn can be registered. + // Afterwards OutdoorPvP::m_capturePoints is the sole owner. + std::unique_ptr m_unregisteredCapturePoint; + OPvPCapturePointNA* m_obj = nullptr; }; } diff --git a/src/server/worldserver/Main.cpp b/src/server/worldserver/Main.cpp index e5bff2daca..6eb431ba09 100644 --- a/src/server/worldserver/Main.cpp +++ b/src/server/worldserver/Main.cpp @@ -307,6 +307,8 @@ extern int main(int argc, char** argv) std::shared_ptr mapManagementHandle(nullptr, [](void*) { + // Final native bot logout must precede OutdoorPvP and map destruction. + sWorld->ShutdownClientlessSessions(); ClientlessCharacterFactory::Shutdown(); // unload battleground templates before different singletons destroyed sBattlegroundMgr->DeleteAllBattlegrounds(); diff --git a/tests/clientless/test_adventure_combat_reactions.py b/tests/clientless/test_adventure_combat_reactions.py index f9196f01dc..124c42a728 100644 --- a/tests/clientless/test_adventure_combat_reactions.py +++ b/tests/clientless/test_adventure_combat_reactions.py @@ -29,7 +29,7 @@ using uint32=unsigned;using uint8=unsigned char; #define TC_LOG_WARN(...) ((void)0) constexpr int PLAYER_FLAGS=0,PLAYER_FLAGS_GHOST=1,CMSG_REPOP_REQUEST=2,CMSG_RECLAIM_CORPSE=3,CORPSE_RESURRECTABLE_PVP=1; constexpr float CORPSE_RECLAIM_RADIUS=39,INVALID_HEIGHT=-100000; -struct ObjectGuid{unsigned id=0;unsigned GetCounter()const{return id;}void Clear(){id=0;}std::string ToString()const{return std::to_string(id);}bool operator==(ObjectGuid b)const{return id==b.id;}bool operator!=(ObjectGuid b)const{return id!=b.id;}}; +struct ObjectGuid{unsigned id=0;unsigned GetCounter()const{return id;}void Clear(){id=0;}std::string ToString()const{return std::to_string(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 escapeCreatures; struct Group{ObjectGuid leader{26};ObjectGuid GetLeaderGUID(){return leader;}}group; +struct CombatRef{Unit*first;Unit*second;bool suppressedFirst=false,suppressedSecond=false; + Unit*GetOther(Unit*p){return p==first?second:first;}bool IsSuppressedFor(Unit*p){return p==first?suppressedFirst:suppressedSecond;}}; +struct Combat{std::unordered_mappve,pvp;auto const&GetPvECombatRefs(){return pve;}auto const&GetPvPCombatRefs(){return pvp;}}; struct Player:Unit{ + Combat combatManager;Combat&GetCombatManager(){return combatManager;} bool IsResurrectRequested(){return false;}void ResurrectUsingRequestData(){}void ClearResurrectRequestData(){} bool IsWithinLOSInMap(Unit*){return los;}bool IsFlying(){return false;}bool IsFalling(){return false;}bool HasUnitState(int){return false;}void GetPartyMembers(std::list&){}bool IsInMap(Unit*){return true;} bool grouped=false;float health=100;std::set attackers; @@ -114,7 +118,8 @@ code+=header+r''' class AdventureCombatMgr::Fighter{public:bool Ranged(){return false;}int ticks=0;void Tick(Unit*,uint32){++ticks;}}; bool AdventureCombatMgr::GroupHeal(Player*,Record&){return false;} -bool AdventureCombatMgr::GroupTarget(Player*,Record&){return false;} +Unit* groupChoice=nullptr; +bool AdventureCombatMgr::GroupTarget(Player*p,Record&r){return groupChoice?Start(p,r,groupChoice,false,true,0):false;} bool AdventureCombatMgr::Reachable(Player*p,Unit*)const{return p->los;} Unit* AdventureCombatMgr::PvPTarget(Player*)const{return nullptr;} Creature* AdventureCombatMgr::Proactive(Player*)const{return nullptr;} @@ -161,6 +166,41 @@ int main(){ mgr.Finish(&p,r);r.Damagers.clear();mgr.DamageTaken(&p,&enemy,1);mgr.Update(&p,500); assert(r.Target.id==99 && r.Defending);mgr.DamageTaken(&p,&c,1);mgr.Update(&p,500);assert(r.Target.id==99); enemy.valid=false;mgr.Update(&p,500);assert(!r.AI);r.Damagers.clear(); + + // Native combat bindings alone must start defence before the first hit. + // This reproduces Rixzek/Lisaile's available zero-threat/no-damage deadlock. + p.combat=true;p.health=100;p.melee=true;p.Relocate(0,0,0);a.alive=a.valid=true;a.level=60;a.Relocate(11,0,0); + CombatRef refA{&p,&a},refB{&p,&b},refPlayer{&p,&enemy};p.combatManager.pve={{1,&refA}}; + r={};mgr.Update(&p,500);assert(r.AI&&r.Defending&&!r.Training&&r.Target.id==1&&r.AI->ticks==1); + // A running target and the first actual damager retain priority over refs. + r={};mgr.Start(&p,r,&b,false,true,0);mgr.Update(&p,500);assert(r.Target.id==2); + r={};mgr.DamageTaken(&p,&b,1);mgr.Update(&p,500);assert(r.Target.id==2&&r.Defending); + // Suppression on either side, native invalidity, phase/visibility, evade, + // death and training dummies must not be turned into a fresh attack. + for(unsigned mode=0;mode<8;++mode){ + r={};refA.suppressedFirst=mode==0;refA.suppressedSecond=mode==1; + a.valid=mode!=2;a.phase=mode!=3;a.visible=mode!=4;a.evading=mode==5;a.alive=mode!=6;a.script=mode==7?"npc_training_dummy":"mob"; + mgr.Update(&p,500);assert(!r.AI); + } + refA.suppressedFirst=refA.suppressedSecond=false;a.valid=a.phase=a.visible=a.alive=true;a.evading=false;a.script="mob"; + r={};a.Relocate(81,0,0);mgr.Update(&p,500);assert(!r.AI);a.Relocate(11,0,0); + // Failed-target/death cooldowns remain effective for the no-damage fallback. + r={};r.Cooldown=1000;mgr.Update(&p,500);assert(!r.AI);mgr.Update(&p,500);assert(r.AI); + // Prefer somebody actually attacking us; ties use a stable GUID order. + a.Relocate(2,0,0);b.Relocate(15,0,0);b.victim=&p;p.combatManager.pve[2]=&refB; + r={};mgr.Update(&p,500);assert(r.Target.id==2);b.victim=nullptr;a.Relocate(11,0,0);b.Relocate(-11,0,0); + r={};mgr.Update(&p,500);assert(r.Target.id==1); + // PvP uses the same native permission check, without creating new combat. + p.combatManager.pve.clear();p.combatManager.pvp={{99,&refPlayer}};enemy.valid=true; + r={};mgr.Update(&p,500);assert(r.Target.id==99&&r.Defending); + enemy.valid=false;r={};mgr.Update(&p,500);assert(!r.AI);p.combatManager.pvp.clear(); + // Group coordination/healing runs first; if it finds no target, a grouped + // bot can still defend against its own established combat opponent. + p.grouped=true;p.combatManager.pve={{1,&refA}};r={};mgr.Update(&p,500);assert(r.Target.id==1); + groupChoice=&b;r={};mgr.Update(&p,500);assert(r.Target.id==2);groupChoice=nullptr;p.grouped=false; + p.combatManager.pve.clear();r={};mgr.Update(&p,500);assert(!r.AI); // no attacking arbitrary nearby creatures + p.combat=false;p.combatManager.pve={{1,&refA}};r={};mgr.Update(&p,500);assert(!r.AI); + p.combatManager.pve.clear();r={}; // Release through native handler and save ghost plus corpse timestamp. mgr.Finish(&p,r);p.alive=false;p.ghost=false;r.Dead=false;mgr.Death(&p,r); assert(p.ghost && p.session.releases==1 && p.saves==1 && corpse.saves==1); @@ -228,4 +268,4 @@ 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 combat focus, damage ordering, risk filters, corpse radius/delay, flight/group override, blocked escape, danger avoidance and malus-free persisted fallback passed.') +print('Production combat bindings (PvE/PvP/group), suppression/permission/distance/cooldown guards, focus/damage priority, risk filters, corpse radius/delay, flight/group override, blocked escape, danger avoidance and malus-free persisted fallback passed.') diff --git a/tests/clientless/test_adventure_encounter.py b/tests/clientless/test_adventure_encounter.py index 02a01c6ab5..3cf723da2f 100644 --- a/tests/clientless/test_adventure_encounter.py +++ b/tests/clientless/test_adventure_encounter.py @@ -11,6 +11,7 @@ header=(root/'src/server/game/World/AdventureEncounterMgr.h').read_text() header='\n'.join(l for l in header.splitlines() if not l.startswith('#include')).replace('private:','public:') code=r''' #include +#include #include #include #include @@ -49,6 +50,7 @@ enum class ClientlessTravelState{Moving,Arrived,Stuck,PathRejected};struct Statu struct ClientlessPathDiagnostics{bool Complete=true,Suspicious=false;}; struct Travel{bool present=false,path=true,accept=true;unsigned starts=0;Status status;Status*GetStatus(unsigned){return present?&status:nullptr;}void CancelTravel(unsigned){present=false;}bool AnalyzePath(Player*,Position const&,ClientlessPathDiagnostics&){return path;}bool StartTravel(Player*,Position const&p){++starts;if(accept){present=true;status.State=ClientlessTravelState::Moving;status.Target=p;}return accept;}}travel;auto sClientlessTravelMgr=&travel; struct Behavior{unsigned actual=0;void EnterPlayerInstance(Player*){++actual;}}behavior;auto sAdventureBehaviorMgr=&behavior; +struct GroupTravel{bool hold=false;bool HoldForInstanceLeader(Player*){return hold;}}groupTravel;auto sAdventureGroupTravelMgr=&groupTravel; '''+header+r''' AdventureEncounterMgr* AdventureEncounterMgr::instance(){static AdventureEncounterMgr m;return &m;} bool AdventureEncounterMgr::AvoidGroundDamage(Player*){return false;} @@ -61,6 +63,13 @@ int main(){ Ref a{&bot},b{&human},c{&other};a.nextRef=&b;b.nextRef=&c;group.first=&a; assert(mgr.MixedGroup(&bot) && mgr.Active(&bot));human.playerSession.clientless=true;assert(!mgr.Active(&bot));assert(!mgr.Update(&bot,500));assert(mgr._records.empty());human.playerSession.clientless=false; assert(!mgr.Update(&bot,500));assert(mgr._records.count(1));assert(behavior.actual==1); + // A pending human transfer or a temporarily bot-only group cannot eject + // members who already entered. The shared rendezvous gate owns waiting. + human.map=0;human.teleporting=true;groupTravel.hold=true; + assert(mgr.Update(&bot,500)&&bot.map==100&&bot.teleports==0); + human.playerSession.clientless=true; + assert(mgr.Update(&bot,500)&&bot.map==100&&bot.teleports==0); + human.playerSession.clientless=false;human.map=100;human.teleporting=false;groupTravel.hold=false; unsigned hook=0;mgr.Register(100,[&](Player*,unsigned){++hook;return true;});assert(mgr.Update(&bot,500) && hook==1);mgr.Register(100,{});mgr._behaviors.clear(); bot.alive=false;bot.Relocate(35,0,0);bot.companions={&human};assert(mgr.Death(&bot));assert(!bot.ghost && bot.teleports==0); // waits while ally lives human.alive=false;other.alive=false;bot.companions.clear();assert(mgr.Death(&bot));assert(bot.ghost && bot.map==0); diff --git a/tests/clientless/test_adventure_group_travel.py b/tests/clientless/test_adventure_group_travel.py index 7ef434cdc7..5fd566ece9 100644 --- a/tests/clientless/test_adventure_group_travel.py +++ b/tests/clientless/test_adventure_group_travel.py @@ -24,6 +24,7 @@ code=r''' using uint32=unsigned; #define TC_GAME_API #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; namespace VMAP{enum class ModelIgnoreFlags{None};} @@ -37,15 +38,15 @@ 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;bool IsDungeon(){return false;} +struct Map{bool cliff=false,los=true,narrow=false,dungeon=false;bool IsDungeon(){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 Group{ObjectGuid leader{21};struct Slot{ObjectGuid guid;};std::vectorslots{{{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;Group*g=&group;Session session; - Player(unsigned id):guid{id}{}ObjectGuid GetGUID(){return guid;}Group*GetGroup(){return g;}Map*GetMap(){return ↦}unsigned GetMapId(){return mapid;}unsigned GetPhaseShift(){return 169;} + 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; + 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 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;} @@ -82,7 +83,7 @@ AdventureGroupTravelMgr* AdventureGroupTravelMgr::instance(){static AdventureGro 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::Follow(', 'bool AdventureGroupTravelMgr::AdaptRoad(', 'bool AdventureGroupTravelMgr::ValidateGroundPath(', 'void AdventureGroupTravelMgr::Clear(']:code+=method(sig)+'\n' +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' code+=r''' int main(){ using namespace AdventureGroupTravelPolicy; @@ -156,6 +157,38 @@ int main(){ leader.taxi=false;executor.status.State=ClientlessActionExecutionState::Completed; // Recorded roads are bounded and validated, never a spline through a cliff. std::vectorroad;for(unsigned x=0;x<=100;x+=10)road.emplace_back(x,0,0);leader.moving=false;map.cliff=true;assert(!mgr.AdaptRoad(&a,road));map.cliff=false;road.clear();for(unsigned x=0;x<=100;x+=10)road.emplace_back(x,0,0);assert(mgr.AdaptRoad(&a,road) && road.back().GetPositionX()<=60); + + // Entered followers discard world travel and wait in the actual instance, + // regardless of whether their leader is human or clientless. + map.dungeon=true;a.mapid=645;a.instance=3;a.moving=true; + leader.mapid=0;leader.instance=0;core.queued=2; + auto beforeClear=executor.clears;auto beforeStops=a.stops; + mgr._records.at(26).RouteOwned=true;mgr._records.at(26).Pending.push_back(state); + assert(mgr.HoldForInstanceLeader(&a));assert(core.queued==0&&executor.clears==beforeClear+1&&a.stops==beforeStops+1); + assert(mgr._records.at(26).Pending.empty()&&!mgr._records.at(26).RouteOwned); + auto routes=core.nodes.size();assert(mgr.Follow(&a)&&core.nodes.size()==routes); + mgr.Observe(&a,500);assert(mgr._records.at(26).Pending.empty()&&mgr.Protected(&a)); + leader.mapid=645;leader.instance=4;assert(mgr.HoldForInstanceLeader(&a)); // different instance, same map + leader.instance=3;leader.teleport=true;assert(mgr.HoldForInstanceLeader(&a)); + leader.teleport=false;leader.m_positionX=200;assert(mgr.HoldForInstanceLeader(&a)); + leader.m_positionX=20;leader.phase=false;assert(mgr.HoldForInstanceLeader(&a)); + leader.phase=true;ObjectAccessor::players.erase({21});assert(mgr.HoldForInstanceLeader(&a)); + ObjectAccessor::players[{21}]=&leader; + for(bool clientless:{false,true}){ + leader.session.clientless=clientless;assert(!mgr.HoldForInstanceLeader(&a)); + travel.status.State=ClientlessTravelState::Arrived;auto local=travel.starts; + assert(mgr.Follow(&a)&&travel.starts==local+1&&core.nodes.size()==routes); + } + assert(!mgr.Route(&a,mgr._records.at(26),"world-node")); + assert(!mgr.JourneyFollow(&a,mgr._records.at(26),state)); + // Combat/death stay with their native owners. Re-entering the same copy + // still clears fresh actions left over from the intervening world travel. + leader.mapid=0;a.combat=true;beforeClear=executor.clears; + assert(!mgr.HoldForInstanceLeader(&a)&&executor.clears==beforeClear);a.combat=false; + a.alive=false;assert(!mgr.HoldForInstanceLeader(&a));a.alive=true; + map.dungeon=false;assert(!mgr.HoldForInstanceLeader(&a)); + map.dungeon=true;core.queued=1;assert(mgr.HoldForInstanceLeader(&a));assert(core.queued==0&&executor.clears==beforeClear+1); + map.dungeon=false;a.mapid=leader.mapid=0;a.instance=leader.instance=0; // Leaving a group clears formation-owned adapters and validators before the // next autonomous route. This prevents valid MMAP paths being rejected by a // stale group formation safety check. diff --git a/tests/clientless/test_adventure_instance.py b/tests/clientless/test_adventure_instance.py index 630874607c..8d18971eef 100644 --- a/tests/clientless/test_adventure_instance.py +++ b/tests/clientless/test_adventure_instance.py @@ -54,6 +54,7 @@ struct Manager{void ClearQueuedActions(uint32){}}manager;auto sClientlessPlayerM struct Executor{void Clear(uint32){}}executor;auto sClientlessActionExecutorMgr=&executor; struct Travel{void CancelTravel(uint32){}}travel;auto sClientlessTravelMgr=&travel; struct Lua{void DismountGroundMount(Player*,char const*){}}lua;auto sClientlessLuaEngine=&lua; +struct GroupTravel{unsigned follows=0;bool Follow(Player*){++follows;return true;}}groupTravel;auto sAdventureGroupTravelMgr=&groupTravel; struct AdventureBehaviorMgr{ enum class Phase{Idle,Travel,OnSite}; struct Record{AdventureInstancePolicy::Visit Instance;Phase State=Phase::OnSite;bool Dirty=false;std::string Detail;}; @@ -78,6 +79,8 @@ int main(){ assert(mgr.UpdateInstance(&p,r,2409)&&p.entryCalls==0); assert(mgr.UpdateInstance(&p,r,2410)&&p.entryCalls==1&&p.map==36); assert(mgr.UpdateInstance(&p,r,2411)&&r.Instance.Stage==Stage::Inside&&r.Instance.InstanceId==7); + // A bot-led instance visitor uses local group follow once inside. + p.group=true;assert(mgr.UpdateInstance(&p,r,2412)&&groupTravel.follows==1&&p.exitCalls==0);p.group=false; int transfers=p.teleports; assert(mgr.UpdateInstance(&p,r,3299)&&p.teleports==transfers&&p.exitCalls==0); assert(mgr.UpdateInstance(&p,r,3300)&&r.Instance.Stage==Stage::Rest&&p.map==0&&p.x==500); diff --git a/tests/clientless/test_adventure_intent_state.py b/tests/clientless/test_adventure_intent_state.py index b6e1c8f68a..805847acb4 100644 --- a/tests/clientless/test_adventure_intent_state.py +++ b/tests/clientless/test_adventure_intent_state.py @@ -3,7 +3,7 @@ from pathlib import Path import subprocess,tempfile root=Path(__file__).resolve().parents[2] s=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text() -a=s.index('void AdventureBehaviorMgr::Update(');b=s.index('bool AdventureBehaviorMgr::Command(',a) +a=s.index('void AdventureBehaviorMgr::Update(');b=s.index('void AdventureBehaviorMgr::UpdateActivities(',a) code=r''' #include "AdventureBehaviorPolicy.h" #include "AdventureGroupPolicy.h" @@ -14,12 +14,13 @@ code=r''' #include using uint32=uint32_t;using uint64=uint64_t; #define TC_LOG_INFO(...) ((void)0) +#define TC_LOG_WARN(...) ((void)0) constexpr int SPELL_VISUAL_KIT_DRINK=438,UNIT_NPC_EMOTESTATE=0; struct Position {float x=0;void Relocate(void const*){}}; struct Guid{uint32 GetCounter(){return 26;}bool operator!=(Guid)const{return false;}}; struct Group{bool lfg=false;Guid GetLeaderGUID(){return {};}bool isLFGGroup(){return lfg;}}; struct Object{} object; -struct Map{Object* GetGameObject(int){return &object;}} map; +struct Map{bool dungeon=false;bool IsDungeon(){return dungeon;}Object* GetGameObject(int){return &object;}} map; struct Player { bool combat=false,battleground=false;uint32 area=87,zone=12,mapId=0; Group*GetGroup(){return nullptr;}bool InBattleground(){return battleground;}bool isUsingLfg(){return false;}bool InBattlegroundQueue(){return false;}Guid GetGUID(){return {};}bool IsFlying(){return false;}bool IsInWorld(){return true;}bool IsInCombat(){return combat;} @@ -28,6 +29,7 @@ struct Player { float GetExactDist(Position const&){return 0;}float GetExactDist(float,float,float){return 0;}float GetExactDist(Object*){return 0;} void SendPlaySpellVisualKit(int,int,int){}void SetUInt32Value(int,uint32){} bool IsWithinLOSInMap(Object*){return true;}void SetFacingToObject(Object*){} + templatebool IsInAreaTriggerRadius(T*){return true;} Map* GetMap(){return ↦} } player; namespace ObjectAccessor{Player*FindPlayer(Guid){return nullptr;}} @@ -49,6 +51,7 @@ struct Graph{ } graph;auto sClientlessTravelGraphMgr=&graph; struct AdventureLocation{struct{uint32 MapId=0;float X=0,Y=0,Z=0;}Point;bool IsArea=false;uint32 AreaId=12,ZoneId=12;} location; struct Locations{auto Find(std::string const&){return &location;}} locations;auto sAdventureLocationMgr=&locations; +struct TriggerStore{struct Trigger{int ContinentID=0;}t;Trigger*LookupEntry(unsigned){return &t;}}sAreaTriggerStore; struct Areas{unsigned Revision(){return 0;}} areas;auto sAdventureAreaMgr=&areas; enum class ClientlessTravelState{Moving,Arrived}; struct Travel{struct{ClientlessTravelState State=ClientlessTravelState::Arrived;}status;auto GetStatus(uint32){return &status;}}travel;auto sClientlessTravelMgr=&travel; @@ -56,13 +59,13 @@ class AdventureBehaviorMgr { public: enum class Phase{Idle,Travel,OnSite,Switching,Handover,WaitingGroup}; struct Record{ - bool Enabled=true,Schedule=false,Queued=false,Dirty=false,ExpeditionStarted=false;AdventureGroupPolicy::Departure Departure; + bool WaitingTravel=false,WaitingArrived=false;bool Enabled=true,Schedule=false,Queued=false,Dirty=false,ExpeditionStarted=false;AdventureGroupPolicy::Departure Departure; Phase State=Phase::Travel; - std::string Intent="social",Location="tavern",WantedIntent="social",WantedLocation="tavern",Detail,Micro,ManualNextIntent,ManualNextLocation; + std::string Intent="social",Location="tavern",WantedIntent="social",WantedLocation="tavern",Detail,Micro,WaitingLocation,ManualNextIntent,ManualNextLocation; time_t Deadline=1000;uint32 AreaRevision=0,Date=20261026,Timer=0,MicroTimer=0,GestureTimer=0,RetryTimer=0;int Hour=5; struct{bool Active=false;}Social; struct{bool Active=false;}Recovery; - struct{bool Active=false;}Instance; + struct{bool Active=false;unsigned Entry=0;}Instance; struct{bool Active=false,Flying=false,Progressive=false;std::vector Route;size_t Step=0;}Leisure; AdventureBehaviorPolicy::Day Plan;Position Anchor;uint32 AnchorMap=0;int WorkObject=0;uint32 WorkEmote=0;bool WorkPending=false; }; @@ -73,6 +76,8 @@ public: bool UpdateNavigationRescue(Player*,Record&){return false;} bool UpdateGathering(Player*,Record&,time_t){return false;} bool UpdateDeparture(Player*,Record&,time_t){return false;} + unsigned capitalChecks=0;bool UpdateWaitingCapital(Player*,Record&,time_t){++capitalChecks;return false;} + void UpdateActivities(Player*p,Record&r){Micro(p,r);} void Update(Player*,uint32); void EnterPlayerInstance(Player*); void Generate(Player*,Record&,uint32){} @@ -102,6 +107,16 @@ int main(){ // A latched departure survives a vacancy while another member catches up. botGroups.full=false;r.State=AdventureBehaviorMgr::Phase::WaitingGroup;r.Dirty=false;r.ExpeditionStarted=true; mgr.Update(&player,1000);assert(r.State==AdventureBehaviorMgr::Phase::Idle&&r.Dirty&&botGroups.queued==1); + mgr.switches=0; + // A graph continent transition is not mistaken for native dungeon admission. + r.State=AdventureBehaviorMgr::Phase::WaitingGroup;r.ExpeditionStarted=false; + r.WaitingLocation="capital";r.WaitingTravel=true;player.mapId=530; + auto checked=mgr.capitalChecks;mgr.Update(&player,1000); + assert(mgr.capitalChecks==checked+1&&r.WaitingLocation=="capital"&&r.WaitingTravel); + // Native dungeon admission suspends capital movement/activities immediately. + map.dungeon=true;checked=mgr.capitalChecks;mgr.Update(&player,1000); + assert(mgr.capitalChecks==checked&&r.WaitingLocation.empty()&&!r.WaitingTravel); + map.dungeon=false;player.mapId=0; // An active battleground suspends the day planner, including hour transitions. player.battleground=true;r.Schedule=true;r.Deadline=1;r.Plan[6]={"profession","mine"};hour=6; mgr.Update(&player,1000);assert(mgr.switches==0&&r.Hour==5); diff --git a/tests/clientless/test_dry_landing_recovery.py b/tests/clientless/test_dry_landing_recovery.py new file mode 100644 index 0000000000..ac249283a1 --- /dev/null +++ b/tests/clientless/test_dry_landing_recovery.py @@ -0,0 +1,89 @@ +"""Replay production landing recovery from a water surface onto verified dry ground.""" +from pathlib import Path +import subprocess,tempfile +root=Path(__file__).resolve().parents[2] +s=(root/'src/server/game/World/ClientlessFlightMgr.cpp').read_text() +def method(sig): + a=s.index(sig);i=s.index('{',a)+1;n=1 + while n:n+=(s[i]=='{')-(s[i]=='}');i+=1 + return s[a:i] +h=(root/'src/server/game/World/ClientlessFlightMgr.h').read_text() +h='\n'.join(x for x in h.splitlines() if not x.startswith('#include')).replace(' private :','public:') +code=r''' +#include "ClientlessFlightPath.h" +#include "ClientlessFlightProgress.h" +#include +#include +#include +#include +#include +using uint32=unsigned;using uint8=unsigned char;using int32=int;using namespace ClientlessFlightPath; +#define TC_GAME_API +#define TC_LOG_INFO(...) ((void)0) +#define TC_LOG_WARN(...) ((void)0) +constexpr float INVALID_HEIGHT=-100000,MAX_HEIGHT=100000; +constexpr int MOVEMENTFLAG_FLYING=1,MOVEMENTFLAG_ASCENDING=2,MOVEMENTFLAG_DESCENDING=4,SPELL_AURA_MOUNTED=0,MOVE_FLIGHT=0; +namespace AnimTier{constexpr int Ground=0;} +struct Position{float x=0,y=0,z=0,o=0;float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}float GetOrientation()const{return o;} + void Relocate(float a,float b,float c,float d=0){x=a;y=b;z=c;o=d;}void Relocate(Position const*p){*this=*p;} + float GetExactDist(Position const&p)const{return std::hypot(std::hypot(x-p.x,y-p.y),z-p.z);}}; +struct Map{float floor=20.906f;bool valid=true;float GetHeight(int,float,float,float,bool,float){return valid?floor:INVALID_HEIGHT;}}map; +struct Player:Position{bool alive=true,canFly=true,flying=true,mounted=true;unsigned stops=0;struct Guid{unsigned GetCounter(){return 121;}};Guid GetGUID(){return {};} + bool CanFly(){return canFly;}bool IsAlive(){return alive;}unsigned GetMapId(){return 1;}Map*GetMap(){return ↦}int GetPhaseShift(){return 0;}float GetSpeed(int){return 30;} + void StopMoving(){++stops;}void InterruptNonMeleeSpells(bool){}void AddUnitMovementFlag(int){flying=true;}void RemoveUnitMovementFlag(int){flying=false;} + void SetAnimTier(int){}void RemoveAurasByType(int){mounted=false;} +}; +namespace Movement{struct MoveSplineInit{Player*p;Position target;MoveSplineInit(Player*who):p(who){}void MoveTo(float x,float y,float z,bool){target.Relocate(x,y,z);}void SetFly(){}void SetWalk(bool){}void SetVelocity(float){}int Launch(){p->Relocate(&target);return 500;}};} +namespace ClientlessTravelTelemetry{void Note(Player*,char const*,std::string const&){}std::string State(Player*){return {};}} +struct Travel{void CancelTravel(unsigned){}}travel;auto sClientlessTravelMgr=&travel; +struct Areas{bool allowed=true;bool AllowsSegment(Player*,unsigned,Position const&,Position const&){return allowed;}}areas;auto sAdventureAreaMgr=&areas; +bool blocked=false,allWater=false;bool ClearRay(Player*,Point const&,Point const&){return !blocked;} +Point PointOf(Position const&p){return {p.x,p.y,p.z};} +'''+h+r''' +Surface ClientlessFlightMgr::Sample(Player*,Record&,float x,float y)const{return {allWater||std::hypot(x,y)<4?22.272f:map.floor,450,map.valid};} +bool ClientlessFlightMgr::RetryDeparture(Player*,Record&){return false;} +bool ClientlessFlightMgr::LandFollowing(Player*){return false;} +'''+method(' bool OpenColumn(')+method('bool ClientlessFlightMgr::FindReturnLanding(')+method('void ClientlessFlightMgr::Fail(')+method('bool ClientlessFlightMgr::Advance(')+r''' +int main(){ + ClientlessFlightMgr mgr;Player p;p.z=22.272f; + ClientlessFlightMgr::Record r;r.Status.MapId=1;r.Status.State=ClientlessFlightState::Flying;r.Returning=true;r.Start=p;r.Status.Landing=p; + // Real touchdown confirmation rejects the water surface, then selects a dry + // neighbour with a complete checked takeoff/approach instead of terminal fail. + assert(!mgr.Advance(&p,r));assert(r.Status.State==ClientlessFlightState::Flying&&r.LandingRecovery); + assert(std::hypot(r.Start.x,r.Start.y)>=4&&r.Start.z==map.floor&&!r.Breadcrumbs.empty()); + for(unsigned i=0;i<64&&r.Status.State==ClientlessFlightState::Flying;++i)mgr.Advance(&p,r); + assert(r.Status.State==ClientlessFlightState::Failed&&!p.flying&&!p.mounted&&std::fabs(p.z-map.floor)<.1f); + assert(r.Status.Detail.find("safely returned after abort")!=std::string::npos); // never requested-destination arrival + // A failed alternative is not selected again on the next local search. + p=Player{};p.z=22.272f;r={};r.Status.MapId=1;r.Status.State=ClientlessFlightState::Flying;r.Returning=true;r.Start=p; + mgr.Fail(&p,r,"touchdown ground not confirmed");auto first=r.Start; + mgr.Fail(&p,r,"approach blocked");assert(r.Start.GetExactDist(first)>=3&&r.Status.State==ClientlessFlightState::Flying); + // Unknown terrain, blocked volumes, catalog restrictions and all-water + // surroundings hold safely with a bounded retry instead of dismounting. + for(unsigned mode=0;mode<4;++mode){ + p=Player{};p.z=22.272f;r={};r.Status.MapId=1;r.Status.State=ClientlessFlightState::Flying;r.Returning=true;r.Start=p; + blocked=mode==0;allWater=mode==1;areas.allowed=mode!=2;map.valid=mode!=3; + mgr.Fail(&p,r,"return blocked");assert(r.Status.State==ClientlessFlightState::Flying&&r.LandingRecovery&&r.NextLandingSearch==10000&&p.mounted&&p.flying); + } + blocked=allWater=false;areas.allowed=map.valid=true; + r.Elapsed=10000;assert(mgr.FindReturnLanding(&p,r)&&r.NextLandingSearch==0); // obstacle gone: search resumes + // Retry pacing is the exact production Update block. +} +''' +# The full production Update is already covered by test_flight_handoff; retain +# explicit timer/no-search assertions here with its actual gate. +a=s.index(' if(record.LandingRecovery && record.NextLandingSearch)');b=s.index(' bool const ownerLost=',a) +block=s[a:b].replace('continue;','return;') +code=code.replace(' // Retry pacing is the exact production Update block.',r''' + auto gate=[&](ClientlessFlightMgr::Record& record){Player* player=&p;auto FindReturnLanding=[&](Player*p,ClientlessFlightMgr::Record&r){return mgr.FindReturnLanding(p,r);}; +'''+block+r'''}; + r.NextLandingSearch=20000;r.Elapsed=19999;gate(r);assert(r.NextLandingSearch==20000); + r.Elapsed=20000;gate(r);assert(r.NextLandingSearch==0); + // Lost flight permission remains terminal; no unsafe recovery or dismount. + p.canFly=false;r.Status.State=ClientlessFlightState::Flying;mgr.Fail(&p,r,"permission lost");assert(r.Status.State==ClientlessFlightState::Failed); + // Retry pacing is the exact production Update block.''') +with tempfile.TemporaryDirectory(prefix='vcore-dry-landing-')as folder: + p=Path(folder);(p/'test.cpp').write_text(code) + subprocess.run(['c++','-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('Dry landing recovery: real failed touchdown -> complete checked dry approach -> dismount; rejected points, blocked/wet/unknown/forbidden terrain, 10s retry and lost permission PASS') diff --git a/tests/clientless/test_final_shutdown.py b/tests/clientless/test_final_shutdown.py new file mode 100644 index 0000000000..930fb36ca7 --- /dev/null +++ b/tests/clientless/test_final_shutdown.py @@ -0,0 +1,105 @@ +"""Execute production final CPC cleanup before actual Main teardown sequence.""" +from pathlib import Path +import subprocess, tempfile +root = Path(__file__).resolve().parents[2] +world = (root/'src/server/game/World/World.cpp').read_text() +mgr = (root/'src/server/game/World/ClientlessPlayerMgr.cpp').read_text() +main = (root/'src/server/worldserver/Main.cpp').read_text() +def function(source, signature): + a=source.index(signature);i=source.index('{',a)+1;depth=1 + while depth: + depth+=(source[i]=='{')-(source[i]=='}');i+=1 + return source[a:i] +a=main.index('std::shared_ptr mapManagementHandle(') +a=main.index('{',a)+1;b=main.index('\n });',a) +teardown=main[a:b] +processor=(root/'src/common/Utilities/AsyncCallbackProcessor.h').read_text() +processor='\n'.join(l for l in processor.splitlines() if not l.startswith('#include')) +code=r''' +#include +#include +#include +#include +#include +#include +#include +#include +using uint32=unsigned; +#define TC_LOG_INFO(...) ((void)0) +#define TC_LOG_ERROR(...) ((void)0) +'''+processor+r''' +bool mapsAlive=true;unsigned players=0,pets=0,passengers=0,deleted=0,acks=0,notifications=0,delivered=0; +struct Player{unsigned guid;bool pet=false,transport=false,pending=false;Player(unsigned g):guid(g){++players;}~Player(){assert(mapsAlive);--players;}}; +struct Callback{std::futurefuture;std::functiondone;bool InvokeIfReady(){if(future.wait_for(std::chrono::seconds(0))!=std::future_status::ready)return false;done();return true;}}; +struct Session{Player*p=nullptr;bool clientless=true;AsyncCallbackProcessorcallbacks; + Player*GetPlayer(){return p;}bool IsClientless(){return clientless;}unsigned GetGUIDLow(){return p?p->guid:0;}unsigned GetAccountId(){return 8;} + void CompleteClientlessTeleport(){if(p&&p->pending){assert(mapsAlive);p->pending=false;++acks;}} + ~Session(){if(p){assert(mapsAlive);if(p->pet)--pets;if(p->transport)--passengers;delete p;}++deleted;} +}; +using WorldSession=Session; +enum class ClientlessPlayerState{Loading,Online,Removing}; +struct World{using SessionMap=std::map; + bool m_clientlessSessionsFinalized=false;SessionMap m_clientlessSessions,m_sessions; + std::mapm_clientlessPlayerStates; + unsigned GetClientlessSessionCount(){return m_clientlessSessions.size();} + Session*FindClientlessSession(unsigned g){auto i=m_clientlessSessions.find(g);return i==m_clientlessSessions.end()?nullptr:i->second;} + bool AddClientlessSession(unsigned,Session*);void ShutdownClientlessSessions(); +}world;auto sWorld=&world; +struct Controller{void OnPlayerLogout(Player*p){assert(mapsAlive&&p);assert(!p->pending);++notifications; + Session late;assert(!world.AddClientlessSession(999,&late)); // no reentrant login during teardown +}}controller; +struct Executor{unsigned clears=0;void Clear(unsigned){++clears;}}executor;auto sClientlessActionExecutorMgr=&executor; +struct Flight{unsigned clears=0;void Clear(unsigned){++clears;}}flight;auto sClientlessFlightMgr=&flight; +struct ClientlessPlayerMgr{ + bool _shutdownDraining=false,_shutdownSnapshotDone=false;unsigned snapshots=0; + struct Record{struct Controller*Controller=nullptr;bool LogoutNotified=false;}; + std::map_records;std::map_safeLogouts; + void BeginShutdown(){_shutdownDraining=true;}void WriteShutdownSnapshot(char const*event){assert(std::string(event)=="finalize");++snapshots;} + void FinalizeShutdown(); +}manager;auto sClientlessPlayerMgr=&manager; +struct Factory{static void Shutdown(){assert(world.m_clientlessSessions.empty());}};using ClientlessCharacterFactory=Factory; +struct BG{void DeleteAllBattlegrounds(){assert(players==0&&pets==0&&passengers==0);}}bg;auto sBattlegroundMgr=&bg; +struct Saves{void Unload(){assert(players==0);}}saves;auto sInstanceSaveMgr=&saves; +struct Outdoor{void Die(){assert(players==0);}}outdoor;auto sOutdoorPvPMgr=&outdoor; +struct Maps{void UnloadAll(){assert(players==0&&pets==0&&passengers==0);mapsAlive=false;}}maps;auto sMapMgr=&maps; +struct Terrain{void UnloadAll(){assert(!mapsAlive);}}sTerrainMgr; +'''+function(world,'bool World::AddClientlessSession(')+function(world,'void World::ShutdownClientlessSessions(')+function(mgr,'void ClientlessPlayerMgr::FinalizeShutdown(')+r''' +void teardown(){ +'''+teardown+r''' +} +int main(){ + // Pet owner, transport passenger and a pending native teleport survive grace. + for(unsigned i=1;i<=3;++i){auto s=new Session;s->p=new Player(i);assert(world.AddClientlessSession(i,s));manager._records[i]={&controller,false};manager._safeLogouts[i]=true;} + world.FindClientlessSession(1)->p->pet=true;++pets; + world.FindClientlessSession(2)->p->transport=true;++passengers; + world.FindClientlessSession(3)->p->pending=true; + // Already notified Removing sessions must not notify a second time. + manager._records[2].LogoutNotified=true; + // Both ready and still-running login callbacks are discarded, not delivered. + std::promiseready,slow;ready.set_value(); + auto loading=new Session;loading->callbacks.AddCallback({ready.get_future(),[]{++delivered;}}); + loading->callbacks.AddCallback({slow.get_future(),[]{++delivered;}}); + assert(world.AddClientlessSession(4,loading));manager._records[4]={&controller,false}; + // Low-level clientless sessions without a managed controller also log out. + auto unmanaged=new Session;unmanaged->p=new Player(5);assert(world.AddClientlessSession(5,unmanaged)); + world.m_clientlessSessions[6]=nullptr;world.m_clientlessPlayerStates[6]=ClientlessPlayerState::Loading; + Session network;network.clientless=false;world.m_sessions[8]=&network; + teardown();slow.set_value(); + assert(!mapsAlive&&players==0&&pets==0&&passengers==0&&acks==1&¬ifications==2&&delivered==0); + assert(world.m_clientlessSessions.empty()&&world.m_clientlessPlayerStates.empty()); + assert(manager._records.empty()&&manager._safeLogouts.empty()&&manager._shutdownDraining&&manager.snapshots==1); + assert(world.m_sessions.size()==1&&executor.clears==4&&flight.clears==4); + auto before=deleted;world.ShutdownClientlessSessions();assert(deleted==before&&manager.snapshots==1); + Session late;assert(!world.AddClientlessSession(6,&late)); + // A normal shutdown that already wrote the deadline audit retains that audit. + manager.FinalizeShutdown();assert(manager.snapshots==1); +} +''' +# The native session destructor owns LogoutPlayer; no custom pet/passenger deletion. +session=(root/'src/server/game/Server/WorldSession.cpp').read_text() +assert 'LogoutPlayer(true)' in function(session,'WorldSession::~WorldSession(') +with tempfile.TemporaryDirectory(prefix='vcore-final-shutdown-') as directory: + p=Path(directory);(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('Final shutdown: real World/manager/Main sequence; pet/transport/teleport cleanup before maps, pending callbacks cancelled, no new sessions, exactly-once notifications, audit and idempotence PASS') diff --git a/tests/clientless/test_flight_departure.py b/tests/clientless/test_flight_departure.py index 634d42556d..c3e2c8fc9a 100644 --- a/tests/clientless/test_flight_departure.py +++ b/tests/clientless/test_flight_departure.py @@ -28,20 +28,23 @@ code=r''' using uint32=unsigned;using namespace ClientlessFlightPath; #define TC_LOG_WARN(...) ((void)0) #define TC_LOG_INFO(...) ((void)0) -constexpr float INVALID_HEIGHT=-100000;constexpr unsigned SPELL_AURA_MOUNTED=1; +constexpr float MAX_HEIGHT=100000;constexpr float INVALID_HEIGHT=-100000;constexpr unsigned SPELL_AURA_MOUNTED=1; struct Position{float x=0,y=0,z=0,o=0; float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}float GetOrientation()const{return o;} + float GetExactDist(Position const&p)const{return std::hypot(std::hypot(x-p.x,y-p.y),z-p.z);} void Relocate(float a,float b,float c,float d){x=a;y=b;z=c;o=d;}}; struct Guid{unsigned GetCounter()const{return 84;}}; struct PositionFullTerrainStatus{bool outdoors=true;}; -struct Map{bool outdoors=true;void GetFullTerrainStatusForPosition(int,float,float,float,PositionFullTerrainStatus&t){t.outdoors=outdoors;}float feet=0;float GetHeight(int,float,float,float,bool,float){return feet;}}map; +struct Map{bool outdoors=true;void GetFullTerrainStatusForPosition(int,float,float,float,PositionFullTerrainStatus&t){t.outdoors=outdoors;}float feet=0;float GetHeight(int,float,float,float z,bool,float){return z==MAX_HEIGHT?0:feet;}}map; struct Player:Position{bool mounted=false,fly=false,aura=false;int removals=0; + bool alive=true,combat=false,air=false,falling=false;bool IsAlive(){return alive;}bool IsInCombat(){return combat;}bool IsFlying(){return air;}bool IsFalling(){return falling;}bool IsInWater(){return false;}bool GetTransport(){return false;}bool IsInFlight(){return false;}bool IsBeingTeleported(){return false;} Guid GetGUID(){return {};}unsigned GetMapId(){return 530;}int GetPhaseShift(){return 0;}Map* GetMap(){return ↦} bool outdoor=true;bool IsOutdoors(){return outdoor;}bool IsMounted(){return mounted;}bool CanFly(){return fly;}bool HasAura(unsigned){return aura;} void RemoveAurasByType(unsigned){++removals;mounted=fly=false;}}; Point PointOf(Position const&p){return {p.x,p.y,p.z};} enum class ClientlessFlightState{Mounting,Flying,Arrived,Failed}; -struct Record{struct{Position Target;ClientlessFlightState State=ClientlessFlightState::Failed;bool FollowingPlayer=false;std::string Detail;}Status; +struct Record{struct{Position Target;ClientlessFlightState State=ClientlessFlightState::Failed;unsigned Segments=0;bool FollowingPlayer=false;std::string Detail;}Status; + ClientlessFlightPath::Memory Memory;bool Returning=false,Moving=false;unsigned DepartureAttempts=0,SegmentElapsed=0;bool LandingRecovery=false;uint32 Elapsed=0,LandingFailuresResetAt=0,NextLandingSearch=0;std::vectorRejectedLandings,Breadcrumbs; bool ApproachingDeparture=false;Position Departure,Start;ClientlessFlightPath::Settings Settings;}; struct ClientlessPathDiagnostics{bool Calculated=true,Complete=true,Suspicious=false;float FinalDistance=0,PathLength=15,MaxSegmentLength=4,MaxZDelta=1;}; struct Travel{ClientlessPathDiagnostics nav;bool start=true;int starts=0,probes=0; @@ -52,8 +55,9 @@ struct Areas{bool allowed=true;bool AllowsSegment(Player*,unsigned,Position cons auto sAdventureAreaMgr=&areas; struct Lua{int calls=0;void DismountGroundMount(Player*,char const*){++calls;}}lua; auto sClientlessLuaEngine=&lua; -bool clear=true,narrow=false,columnBlocked=false; +bool clear=true,narrow=false,columnBlocked=false,roofAt38=false; bool ClearRay(Player*,Point const&a,Point const&b){ + if(roofAt38&&a.Z<38&&b.Z>=38)return false; if(!clear||(narrow&&std::fabs(a.Y)>=0.01f))return false; if(columnBlocked)for(int i=0;i<=80;++i){auto p=Mix(a,b,i/80.f);if(std::fabs(p.X)<1&&std::fabs(p.Y)<1&&p.Z>3&&p.Z<5)return false;} return true; @@ -63,13 +67,13 @@ struct ClientlessFlightMgr{ std::unordered_map_records;Surface surface{0,300,true};int approached=0;bool seek=false; void Store(unsigned guid,Record&& record){_records[guid]=std::move(record);} Surface Sample(Player*,Record&,float,float){return surface;} - bool ApproachDeparture(Player*,Record&,bool);bool FindReturnLanding(Player*,Record&); + bool RetryDeparture(Player*,Record&);bool ApproachDeparture(Player*,Record&,bool);bool FindReturnLanding(Player*,Record&); bool Prepare(Player*player,Record&record,std::string&error){auto origin=surface;bool following=false; '''+preparation+r''' return false;} bool Admit(Player* player,Record&record,std::string&error,bool following=false){ auto origin=surface;std::vector>candidates{{123,1}}; -'''+admission+r''' +'''+preparation+admission+r''' return true; } void ReturnAnchor(Player*player,Record&record){auto origin=surface; @@ -124,8 +128,9 @@ int main(){ travel.nav.MaxZDelta=20;assert(!mgr.Admit(&p,r,error));travel.nav.MaxZDelta=1; travel.nav.PathLength=601;assert(!mgr.Admit(&p,r,error));travel.nav.PathLength=15; travel.start=false;assert(!mgr.Admit(&p,r,error));travel.start=true; - map.feet=INVALID_HEIGHT;assert(!mgr.Admit(&p,r,error)); - mgr.surface.Ground=0;map.feet=NAN;assert(!mgr.Admit(&p,r,error));map.feet=0;mgr.surface.Ground=12; + // Missing support plus a blocked local column cannot authorize takeoff. + columnBlocked=true;map.feet=INVALID_HEIGHT;assert(!mgr.Admit(&p,r,error)); + mgr.surface.Ground=0;map.feet=NAN;assert(!mgr.Admit(&p,r,error));map.feet=0;mgr.surface.Ground=12;columnBlocked=false; // A touchdown may use the actual lower floor, never a roof above it. map.feet=0;p.z=0;mgr.surface.Ground=12; assert(OpenColumn(&p,0,0,{0,300,true}));narrow=true;assert(!OpenColumn(&p,0,0,{0,300,true}));narrow=false; @@ -135,6 +140,28 @@ int main(){ map.feet=INVALID_HEIGHT;assert(!mgr.Touchdown(&p));map.feet=0; clear=false;assert(!mgr.Touchdown(&p));clear=true; mgr.KeepMount(&p,false);assert(p.removals==1&&lua.calls==1); + // A 15-yard gap is insufficient if the full vertical takeoff meets a roof. + roofAt38=true;assert(OpenColumn(&p,0,0,{0,300,true})); + assert(OpenColumn(&p,0,0,{0,300,true},35)); // upper body clearance is still missing + ClientlessFlightPath::Memory memory;ClientlessFlightPath::Settings settings; + auto sample=[](float,float){return Surface{0,300,true};}; + auto ray=[&](Point const&a,Point const&b){return ClearRay(&p,a,b);}; + assert(!Next({0,0,0},{100,0,0},settings,memory,sample,ray).Valid); + auto startsBefore=travel.starts;Record roof;roof.Status.Target={100,0,0}; + assert(!mgr.ApproachDeparture(&p,roof,false)&&travel.starts==startsBefore);roofAt38=false; + // Blocked first segment: grounded mount is removed and a real MMAP walk is published. + p.z=0;p.mounted=p.fly=true;map.feet=0;mgr.surface={0,300,true};Record retry;retry.Status.Target={100,0,0}; + mgr._records[84]=retry;assert(mgr.RetryDeparture(&p,mgr._records.at(84))); + assert(mgr._records.at(84).ApproachingDeparture&&mgr._records.at(84).DepartureAttempts==1&&!p.mounted); + assert(mgr._records.at(84).Status.Target.x==100); + // Airborne/unsupported/returning/progressed/exhausted flights never dismount or walk. + auto reject=[&](){auto n=travel.starts;auto removals=p.removals;assert(!mgr.RetryDeparture(&p,retry));assert(travel.starts==n&&p.removals==removals);}; + p.air=true;reject();p.air=false;p.falling=true;reject();p.falling=false; + map.feet=INVALID_HEIGHT;reject();map.feet=0; + retry.Returning=true;reject();retry.Returning=false;retry.Memory.Departed=true;reject();retry.Memory.Departed=false; + retry.Status.Segments=1;reject();retry.Status.Segments=0;retry.DepartureAttempts=3;reject();retry.DepartureAttempts=0; + p.combat=true;reject();p.combat=false; + } ''' with tempfile.TemporaryDirectory(prefix='vcore-flight-departure-') as d: diff --git a/tests/clientless/test_flight_handoff.py b/tests/clientless/test_flight_handoff.py index 042ea19841..038e98d5dc 100644 --- a/tests/clientless/test_flight_handoff.py +++ b/tests/clientless/test_flight_handoff.py @@ -48,7 +48,8 @@ struct Record { ClientlessFlightProgress::Watch Progress;uint32 StallRecoveries=0;SharedJoin Join; Position LastPosition,Start,Segment{25,0,60};Settings Settings; std::shared_ptr Following;uint32 FormationDelay=0,TargetUpdated=0,Elapsed=0,SegmentElapsed=0,CheckTimer=0; - bool Moving=true,Returning=false,ApproachingDeparture=false,ActionOwned=false,SegmentUnderOverhang=false;uint32 DetachedRecoveryCount=0;Position Departure;Memory Memory;std::shared_ptr Published=std::make_shared();std::vector Breadcrumbs; + bool LandingRecovery=false;uint32 NextLandingSearch=0; + bool Moving=true,Returning=false,ApproachingDeparture=false,ActionOwned=false,SegmentUnderOverhang=false,SegmentVerticalClimb=false;uint32 DetachedRecoveryCount=0,DepartureAttempts=0;Position Departure;Memory Memory;std::shared_ptr Published=std::make_shared();std::vector Breadcrumbs; }; enum class ClientlessTravelState {Moving,Arrived,Stuck}; struct Travel {struct{ClientlessTravelState State=ClientlessTravelState::Moving;}status;auto GetStatus(uint32){return &status;}}travel; @@ -62,10 +63,11 @@ class ClientlessFlightMgr { public: std::unordered_map _records; int advances=0,handoffs=0,failures=0,begins=0; - bool Begin(Player*,Position const&,std::string&,bool){++begins;return true;} + bool Begin(Player*,Position const&,std::string&,bool){++begins;auto&r=_records.at(26);r.DepartureAttempts=0;r.ActionOwned=false;r.ApproachingDeparture=false;return true;} Surface Sample(Player*,Record&,float,float){return {0,300,true};} void Fail(Player*,Record& r,std::string const&){++failures;r.Status.State=ClientlessFlightState::Failed;} bool Advance(Player*,Record&,bool handoff=false){++advances;handoffs+=handoff;return true;} + bool FindReturnLanding(Player*,Record&){return false;} bool LandFollowing(Player*){return true;}bool AdvanceFollowing(Player*,Record&){return true;} void Update(uint32 diff); }; @@ -108,17 +110,29 @@ int main(){ assert(mgr.failures==0&&r.Progress.Overdue()&&r.Progress.IdleMs==0); // A genuinely stuck spline survives 59s, then replans locally at 60s. r.Progress.Begin(Distance(PointOf(player),PointOf(r.Segment)),20000);r.StallRecoveries=0; + r.SegmentVerticalClimb=true; // vertical takeoff retains the same 60s watchdog int before=mgr.advances; for(int i=0;i<59;++i){r.CheckTimer=0;mgr.Update(1000);} assert(mgr.advances==before&&mgr.failures==0); r.CheckTimer=0;mgr.Update(1000); assert(mgr.advances==before+1&&r.StallRecoveries==1&&mgr.failures==0); player.fly=false;mgr.Update(50);assert(mgr.failures==1); + // Preserve the validated vertical climb even in the early handoff window. + r.Status.State=ClientlessFlightState::Flying;r.Returning=false;r.Moving=true; + player.fly=true;player.running=true;player.spline.remaining=1000;r.SegmentVerticalClimb=true; + r.CheckTimer=0;r.Progress.Begin(100,10000);before=mgr.advances; + mgr.Update(200);assert(mgr.advances==before); + // Geometry changes still interrupt that same vertical segment. + clear=false;r.CheckTimer=0;mgr.Update(200);assert(mgr.advances==before+1);clear=true; + // Native spline completion opens the next segment normally. + r.Moving=true;player.running=false;r.CheckTimer=0;before=mgr.advances; + mgr.Update(200);assert(mgr.advances==before+1); + r.SegmentVerticalClimb=false;player.running=true; // A ground departure approach must wait for actual arrival before mounting. r.Status.State=ClientlessFlightState::Mounting;r.ApproachingDeparture=true; r.Elapsed=0;r.Departure=player;travel.status.State=ClientlessTravelState::Moving; mgr.Update(50);assert(mgr.begins==0&&mgr.failures==1); - travel.status.State=ClientlessTravelState::Arrived;mgr.Update(50);assert(mgr.begins==1); + r.DepartureAttempts=2;r.ActionOwned=true;travel.status.State=ClientlessTravelState::Arrived;mgr.Update(50);assert(mgr.begins==1&&r.DepartureAttempts==2&&r.ActionOwned); // A new airborne generation must publish its verified landing immediately // and leave a replacement link for followers already holding the old trail. r.ApproachingDeparture=false;r.Status.State=ClientlessFlightState::Flying; diff --git a/tests/clientless/test_flight_policy.py b/tests/clientless/test_flight_policy.py index 0efba3113f..2c7921633f 100644 --- a/tests/clientless/test_flight_policy.py +++ b/tests/clientless/test_flight_policy.py @@ -62,7 +62,8 @@ struct Area{bool fly=true;uint32 World_pvp_ID=0;Flags GetMountFlags()const{retur struct Areas{bool known=true;auto LookupEntry(uint32){return known?&area:nullptr;}} areas;auto& sAreaTableStore=areas; enum class ClientlessFlightState{Mounting,Flying,Arrived,Failed}; struct Record{std::shared_ptrPublished=std::make_shared(); - struct{bool FollowingPlayer=false;ClientlessFlightState State=ClientlessFlightState::Flying;uint32 MapId=0;std::string Detail;}Status; + struct{bool FollowingPlayer=false;ClientlessFlightState State=ClientlessFlightState::Flying;uint32 MapId=0;Position Landing;std::string Detail;}Status; + ClientlessFlightPath::Memory Memory;Position Start;bool LandingRecovery=false;uint32 NextLandingSearch=0,LandingFailuresResetAt=0,Elapsed=0;std::vectorRejectedLandings;std::shared_ptrFollowing; bool Moving=true,Returning=false,ApproachingDeparture=false;std::vector Breadcrumbs{{}};std::string AbortReason; }; struct Travel{int cancels=0;void CancelTravel(uint32){++cancels;}}travel;auto sClientlessTravelMgr=&travel; @@ -75,6 +76,7 @@ class ClientlessFlightMgr{public: std::pair AreaZone(Player const*,uint32,Position const&)const{return {areaCode,zoneCode};} bool Enabled(Player const*)const;bool Preferred(Player const*,uint32,Position const&)const; float Estimate(Player const*,uint32,Position const&,Position const&,bool recovery=false)const; + bool FindReturnLanding(Player*,Record&){return false;} void Fail(Player*,Record&,std::string const&); }; ''' @@ -111,7 +113,7 @@ int main(){ area.fly=false;area.World_pvp_ID=1;assert(std::isfinite(mgr.Estimate(&p,571,a,b)));area.World_pvp_ID=0; // An airborne abort first returns. A blocked return must not dismount/drop. Record r;mgr.Fail(&p,r,"blocked");assert(r.Returning&&r.Status.State==ClientlessFlightState::Flying&&p.stops==1); - mgr.Fail(&p,r,"reverse blocked");assert(r.Status.State==ClientlessFlightState::Failed); + mgr.Fail(&p,r,"reverse blocked");assert(r.Status.State==ClientlessFlightState::Flying&&r.LandingRecovery&&r.NextLandingSearch==10000); Record mounting;mounting.Status.State=ClientlessFlightState::Mounting;mgr.Fail(&p,mounting,"cast rejected"); assert(mounting.Status.State==ClientlessFlightState::Failed&&p.interrupts==1&&!mounting.Returning); // A planning scope snapshots repeated config/profile work, while actual diff --git a/tests/clientless/test_free_flight_planner.py b/tests/clientless/test_free_flight_planner.py index d16c68d888..133b0d5a4e 100644 --- a/tests/clientless/test_free_flight_planner.py +++ b/tests/clientless/test_free_flight_planner.py @@ -35,14 +35,14 @@ struct ClientlessPathDiagnostics {bool Calculated=true, Complete=true,Suspicious struct Travel {bool complete=true;void AnalyzePath(Player* p,Position b,ClientlessPathDiagnostics& n){Position a;a.Relocate(p->x,p->y,p->z);n.PathLength=n.DirectDistance=dist(a,b);n.Complete=complete;}} travel; auto sClientlessTravelMgr=&travel; struct Flight { - float closedFromX=1e9;bool rejectNearbyTargets=false;bool enabled=false;float minimumX=-10000;float forbiddenTargetX=1e9; + float rejectBelow=-1e9;float closedFromX=1e9;bool rejectNearbyTargets=false;bool enabled=false;float minimumX=-10000;float forbiddenTargetX=1e9; float forbiddenPairFromX=1e9,forbiddenPairTargetX=1e9; void BeginPlanning(Player const* = nullptr){} bool Enabled(Player const*){return enabled;} bool Preferred(Player const*,uint32,Position const&){return false;} float Estimate(Player const*,uint32 map,Position a,Position b,bool recovery=false){ bool forbiddenPair=std::fabs(a.x-forbiddenPairFromX)<0.1f&&std::fabs(b.x-forbiddenPairTargetX)<0.1f; - float d=dist(a,b);return enabled&&std::fabs(a.x-closedFromX)>0.1f&&!(rejectNearbyTargets&&b.x>1&&b.x<450)&&map==0&&a.x>=minimumX&&std::fabs(b.x-forbiddenTargetX)>0.1f&&!forbiddenPair&&(recovery||d>=60)?(recovery?d:180+d*1.5f):std::numeric_limits::infinity(); + float d=dist(a,b);return enabled&&b.x>=rejectBelow&&std::fabs(a.x-closedFromX)>0.1f&&!(rejectNearbyTargets&&b.x>1&&b.x<450)&&map==0&&a.x>=minimumX&&std::fabs(b.x-forbiddenTargetX)>0.1f&&!forbiddenPair&&(recovery||d>=60)?(recovery?d:180+d*1.5f):std::numeric_limits::infinity(); } } flight; auto sClientlessFlightMgr=&flight; @@ -156,7 +156,8 @@ assert(accessFlight.PlanAdaptiveRoute(&start,"B",400,200,route,done,endKey,why)& assert(route.size()==3&&route.front().NodeKey=="flight_departure"&&route[1].NodeKey=="A"&&route[1].EdgeType=="free_flight"); std::unordered_set deniedFlightRoots={"flight:flight_departure->A"}; route.clear(); -assert(!accessFlight.PlanAdaptiveRoute(&start,"B",400,200,route,done,endKey,why,nullptr,&deniedFlightRoots)); +assert(accessFlight.PlanAdaptiveRoute(&start,"B",400,200,route,done,endKey,why,nullptr,&deniedFlightRoots)&&done); +assert(route[1].NodeKey=="B"&&route[1].EdgeType=="free_flight"); // Reject all flight targets from a failed physical departure, not just one target. deniedFlightRoots={"flight_origin:flight_departure"};route.clear(); assert(!accessFlight.PlanAdaptiveRoute(&start,"B",400,200,route,done,endKey,why,nullptr,&deniedFlightRoots)); @@ -251,6 +252,35 @@ assert(route.back().NodeKey!="flight_location"); expensiveFlight.Factors[ClientlessTravelCost::Flight]=.25f; route.clear();assert(modal.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why,nullptr,nullptr,&sameMap,&expensiveFlight)&&done); assert(route.back().NodeKey=="flight_location"&&route.back().EdgeType=="free_flight"); +// Adaptive start radius: nearest permitted usable ring wins; no distant MMAP probe. +flight.enabled=true;travel.complete=false;PathGenerator::available=false; +ClientlessTravelGraphMgr expanded;node(expanded,"R",952);node(expanded,"G",12000); +edge(expanded,"R","G",501,100,false,"taxi"); +route.clear();assert(expanded.PlanAdaptiveRoute(&start,"G",500,200,route,done,endKey,why)&&done); +assert(route.front().NodeKey=="flight_departure"&&route[1].NodeKey=="R"&&route[1].EdgeType=="free_flight"); +// Ground-only bots retain the original radius. +flight.enabled=false;route.clear();assert(!expanded.PlanAdaptiveRoute(&start,"G",500,200,route,done,endKey,why)); +flight.enabled=true; +// The exact 10,000-yard boundary is included; farther nodes are not seeded. +ClientlessTravelGraphMgr boundary;node(boundary,"R",10000);node(boundary,"G",12000); +edge(boundary,"R","G",502,100,false,"taxi"); +route.clear();assert(boundary.PlanAdaptiveRoute(&start,"G",500,200,route,done,endKey,why)&&done&&route[1].NodeKey=="R"); +ClientlessTravelGraphMgr beyond;node(beyond,"R",10001);node(beyond,"G",12000); +edge(beyond,"R","G",503,100,false,"taxi"); +route.clear();assert(!beyond.PlanAdaptiveRoute(&start,"G",500,200,route,done,endKey,why)); +// Native-ineligible inner nodes do not prevent inspection of the outermost ring. +ClientlessTravelGraphMgr rings;node(rings,"R",10000);node(rings,"G",12000); +edge(rings,"R","G",504,100,false,"taxi"); +for(unsigned ring=1;ring<20;++ring)for(unsigned n=0;n<4;++n) + node(rings,"bad"+std::to_string(ring)+"_"+std::to_string(n),ring*500.f-20+n); +flight.rejectNearbyTargets=false;flight.forbiddenTargetX=1e9; +flight.rejectBelow=9999; +route.clear();assert(rings.PlanAdaptiveRoute(&start,"G",500,200,route,done,endKey,why)&&done&&route[1].NodeKey=="R"); +flight.rejectBelow=-1e9; +// Rejected origins and area-denied roots remain forbidden throughout expansion. +std::unordered_set failedOrigin{"flight_origin:flight_departure"}; +route.clear();assert(!expanded.PlanAdaptiveRoute(&start,"G",500,200,route,done,endKey,why,nullptr,&failedOrigin)); +access.blocked.insert("R");route.clear();assert(!expanded.PlanAdaptiveRoute(&start,"G",500,200,route,done,endKey,why));access.blocked.clear(); std::cout<<"ground planner regressions and dynamic flight/node/location/virtual-exit integration passed\n"; } ''' diff --git a/tests/clientless/test_halaa_capture_ownership.py b/tests/clientless/test_halaa_capture_ownership.py new file mode 100644 index 0000000000..e931c5a5a6 --- /dev/null +++ b/tests/clientless/test_halaa_capture_ownership.py @@ -0,0 +1,70 @@ +"""Run production Halaa ownership/registration through unload/reload/destruction.""" +from pathlib import Path +import subprocess,tempfile +root=Path(__file__).resolve().parents[2] +na=(root/'src/server/scripts/OutdoorPvP/OutdoorPvPNA.cpp').read_text() +base=(root/'src/server/game/OutdoorPvP/OutdoorPvP.cpp').read_text() +h=(root/'src/server/scripts/OutdoorPvP/OutdoorPvPNA.h').read_text() +def function(s,sig): + a=s.index(sig);i=s.index('{',a)+1;n=1 + while n:n+=(s[i]=='{')-(s[i]=='}');i+=1 + return s[a:i] +a=h.index('class OutdoorPvPNA :');b=h.index('\n};',a)+3 +h=h[a:b].replace(' override','').replace(' private:','public:') +code=r''' +#include +#include +#include +#include +using uint32=unsigned; +unsigned created=0,destroyed=0,errors=0; +#define TC_LOG_ERROR(...) (++errors) +constexpr unsigned OUTDOOR_PVP_NA=2,NA_BUFF_ZONE=3518,NA_CAPTURE_BUFF=1,GAMEOBJECT_TYPE_CAPTURE_POINT=29; +struct ObjectGuid{using LowType=unsigned;}; +struct Map{}; +struct Player{unsigned buffs=0;unsigned GetTeam(){return 1;}void CastSpell(Player*,unsigned,bool){++buffs;}}; +struct Unit{}; +struct GameObject{unsigned entry=182210,spawn=23273;unsigned GetGoType(){return entry==182210?GAMEOBJECT_TYPE_CAPTURE_POINT:0;}unsigned GetEntry(){return entry;}unsigned GetSpawnId(){return spawn;}}; +struct OPvPCapturePoint{unsigned m_capturePointSpawnId=0;GameObject*m_capturePoint=nullptr;virtual ~OPvPCapturePoint(){++destroyed;}}; +struct OutdoorPvP{using OPvPCapturePointMap=std::map>; + OPvPCapturePointMap m_capturePoints;unsigned m_TypeId=0,zones=0,enters=0;OutdoorPvP(Map*){}virtual ~OutdoorPvP()=default; + void AddCapturePoint(OPvPCapturePoint*);void RegisterZone(unsigned id){assert(id==3518);++zones;} + OPvPCapturePoint* GetCapturePoint(unsigned)const; + void OnGameObjectCreate(GameObject*); + void OnGameObjectRemove(GameObject*); + void HandlePlayerEnterZone(Player*,unsigned){++enters;} +}; +namespace OutdoorPvpScript::NA{ +struct OPvPCapturePointNA:OPvPCapturePoint{unsigned updates=0;OPvPCapturePointNA(OutdoorPvP*){++created;}unsigned GetControllingFaction(){return 1;}bool Update(unsigned){++updates;return true;}}; +'''+h+function(na,'OutdoorPvPNA::OutdoorPvPNA(')+function(na,'bool OutdoorPvPNA::SetupOutdoorPvP(')+function(na,'void OutdoorPvPNA::OnGameObjectCreate(')+function(na,'bool OutdoorPvPNA::Update(')+function(na,'void OutdoorPvPNA::HandlePlayerEnterZone(')+r''' +} +'''+function(base,'void OutdoorPvP::AddCapturePoint(')+function(base,'OPvPCapturePoint* OutdoorPvP::GetCapturePoint(')+function(base,'void OutdoorPvP::OnGameObjectCreate(')+function(base,'void OutdoorPvP::OnGameObjectRemove(')+r''' +int main(){ + using namespace OutdoorPvpScript::NA;Map map;Player player; + // Even a map whose Halaa banner is never loaded must free the objective once. + {OutdoorPvPNA zone(&map);assert(zone.SetupOutdoorPvP());assert(zone.m_obj&&zone.m_unregisteredCapturePoint&&zone.m_capturePoints.empty());assert(zone.Update(100));zone.HandlePlayerEnterZone(&player,3518);assert(player.buffs==1&&zone.enters==1);assert(zone.SetupOutdoorPvP()&&created==1&&zone.zones==1);} + assert(created==1&&destroyed==1); + {OutdoorPvPNA zone(&map);zone.SetupOutdoorPvP();auto objective=zone.m_obj; + GameObject unrelated{123,444};zone.OnGameObjectCreate(&unrelated);assert(zone.m_unregisteredCapturePoint&&zone.m_capturePoints.empty()); + GameObject first;zone.OnGameObjectCreate(&first);assert(!zone.m_unregisteredCapturePoint&&zone.m_obj==objective); + assert(zone.m_capturePoints.size()==1&&zone.m_capturePoints.at(23273).get()==objective&&objective->m_capturePoint==&first); + assert(destroyed==1);zone.Update(100);assert(objective->updates==1); + zone.OnGameObjectRemove(&first);assert(!objective->m_capturePoint&&zone.m_obj==objective); + // Recreated world object, same DB spawn. Ownership/state must survive. + for(unsigned i=0;i<20;++i){GameObject reloaded;zone.OnGameObjectCreate(&reloaded);assert(zone.m_obj==objective&&zone.m_capturePoints.size()==1&&objective->m_capturePoint==&reloaded);zone.OnGameObjectRemove(&reloaded);} + assert(errors==0&&created==2&&destroyed==1);assert(zone.SetupOutdoorPvP());assert(zone.m_obj==objective); + GameObject unexpected{182210,999};zone.OnGameObjectCreate(&unexpected); + assert(errors==1&&zone.m_capturePoints.size()==1&&objective->m_capturePointSpawnId==23273&&!objective->m_capturePoint); + zone.OnGameObjectCreate(&first);assert(objective->m_capturePoint==&first);zone.HandlePlayerEnterZone(&player,3518);assert(player.buffs==2); + } + assert(created==2&&destroyed==2); + // A subsequent map lifetime gets a fresh owner and registration. + {OutdoorPvPNA zone(&map);zone.SetupOutdoorPvP();GameObject banner;zone.OnGameObjectCreate(&banner);assert(zone.Update(1));} + assert(created==3&&destroyed==3); +} +''' +with tempfile.TemporaryDirectory(prefix='vcore-halaa-ownership-') as directory: + p=Path(directory);(p/'test.cpp').write_text(code) + subprocess.run(['c++','-std=c++17','-g','-fsanitize=address,undefined','-fno-omit-frame-pointer',str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) +print('Halaa production ownership under ASan/UBSan: never loaded, first transfer, 20 reloads, state/buffs, conflicting spawn, setup repeat and successive map teardown PASS') diff --git a/tests/clientless/test_instance_browser_reentry.py b/tests/clientless/test_instance_browser_reentry.py new file mode 100644 index 0000000000..abe3081fb8 --- /dev/null +++ b/tests/clientless/test_instance_browser_reentry.py @@ -0,0 +1,60 @@ +"""Execute production LFG re-entry path, including native denial and retry pacing.""" +from pathlib import Path +import subprocess, tempfile +root = Path(__file__).resolve().parents[2] +s = (root/'src/server/game/World/AdventureEncounterMgr.cpp').read_text() +a = s.index('bool AdventureEncounterMgr::FollowIntoInstance(') +b = s.index(' AreaTriggerEntry const* entrance=', a) +# The physical-portal fallback is independent: exercise the complete browser +# branch and its real prerequisites without fabricating world/graph geometry. +code = r''' +#include +#include +#include +using uint32=unsigned; +#define TC_LOG_INFO(...) ((void)0) +namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}} +struct Guid{unsigned id;unsigned GetCounter(){return id;}}; +struct Map{bool dungeon=false;bool IsDungeon(){return dungeon;}}; +struct Player; +struct Ref{Player*p;Ref*following=nullptr;Player*GetSource(){return p;}Ref*next(){return following;}}; +struct Group{bool lfg=true;Ref*first=nullptr;bool isLFGGroup(){return lfg;}Ref*GetFirstMember(){return first;}Guid GetGUID(){return {9};}}group; +struct Session{Player*p;bool bot=true;unsigned acks=0;bool IsClientless(){return bot;}void CompleteClientlessTeleport();}; +struct Player{unsigned id,map=0,instance=0;bool combat=false,pending=false,alive=true;Map terrain;Session session{this}; + bool IsInCombat(){return combat;}bool IsBeingTeleported(){return pending;}bool IsAlive(){return alive;} + Group*GetGroup(){return &group;}Guid GetGUID(){return {id};}unsigned GetMapId(){return map;}unsigned GetInstanceId(){return instance;} + Session*GetSession(){return &session;}Map*GetMap(){return &terrain;} +}; +void Session::CompleteClientlessTeleport(){++acks;p->pending=false;p->map=645;p->instance=3;} +struct Dungeon{int map=645;}dungeon; +struct LFG{bool present=true,allowed=true;unsigned attempts=0;unsigned GetDungeon(Guid g){assert(g.id==9);return 123;} + Dungeon*GetLFGDungeon(unsigned id){assert(id==123);return present?&dungeon:nullptr;} + void TeleportPlayer(Player*p,bool out){assert(!out);++attempts;if(allowed)p->pending=true;} +}lfg;auto sLFGMgr=&lfg; +struct AdventureEncounterMgr{std::unordered_map_entryAttempts;bool mixed=true; + bool MixedGroup(Player*){return mixed;}bool FollowIntoInstance(Player*); +}; +''' + s[a:b] + ' return false;\n}\n' + r''' +int main(){ + Player bot{121},human{6};human.session.bot=false;human.map=645;human.terrain.dungeon=true; + Ref h{&human};group.first=&h;AdventureEncounterMgr mgr; + // Native LFG denial is respected; no administrator teleport bypass. + lfg.allowed=false;assert(mgr.FollowIntoInstance(&bot)&&lfg.attempts==1&&bot.map==0&&bot.session.acks==0); + assert(mgr.FollowIntoInstance(&bot)&&lfg.attempts==1);GameTime::now+=2; + assert(mgr.FollowIntoInstance(&bot)&&lfg.attempts==1);++GameTime::now; + lfg.allowed=true;assert(mgr.FollowIntoInstance(&bot)&&lfg.attempts==2&&bot.map==645&&bot.instance==3&&bot.session.acks==1&&!bot.pending); + // Missing/mismatched native dungeon data cannot silently choose a portal. + bot.map=0;GameTime::now+=3;lfg.present=false;assert(mgr.FollowIntoInstance(&bot)&&lfg.attempts==2); + lfg.present=true;human.map=36;assert(mgr.FollowIntoInstance(&bot)&&lfg.attempts==2);human.map=645; + bot.combat=true;assert(!mgr.FollowIntoInstance(&bot));bot.combat=false; + bot.pending=true;assert(!mgr.FollowIntoInstance(&bot));bot.pending=false; + human.terrain.dungeon=false;assert(!mgr.FollowIntoInstance(&bot));human.terrain.dungeon=true; + mgr.mixed=false;assert(!mgr.FollowIntoInstance(&bot));mgr.mixed=true; + group.lfg=false;assert(!mgr.FollowIntoInstance(&bot));assert(lfg.attempts==2); +} +''' +with tempfile.TemporaryDirectory() as directory: + p=Path(directory);(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('Instance browser re-entry: native group selection, denial, 3s retry, clientless ACK and combat/transfer guards PASS') diff --git a/tests/clientless/test_instance_group_protection.py b/tests/clientless/test_instance_group_protection.py index dec899f3c8..cefdccc6f0 100644 --- a/tests/clientless/test_instance_group_protection.py +++ b/tests/clientless/test_instance_group_protection.py @@ -4,7 +4,7 @@ import subprocess,tempfile root=Path(__file__).resolve().parents[2] s=(root/"src/server/game/World/AdventureBehaviorMgr.cpp").read_text() a=s.index("bool AdventureBehaviorMgr::ProtectInstanceGroupLeave(") -b=s.index("void AdventureBehaviorMgr::InterruptForInteraction",a) +b=s.index("bool AdventureBehaviorMgr::PauseForLogout",a) code=r''' #include "AdventureInstancePolicy.h" #include diff --git a/tests/clientless/test_shared_group_route.py b/tests/clientless/test_shared_group_route.py index 4663096940..09ec77d1e1 100644 --- a/tests/clientless/test_shared_group_route.py +++ b/tests/clientless/test_shared_group_route.py @@ -24,7 +24,7 @@ struct ClientlessTravelRouteStep{std::string NodeKey,EdgeType;unsigned EdgeDatab struct Graph{ClientlessTravelCost::Profile observed;bool allowed=true;bool PlanAdaptiveRoute(Player*,std::string const&node,int,int,std::vector&r,bool&complete,std::string&,std::string&,void const* =nullptr,void const* =nullptr,void const* =nullptr,ClientlessTravelCost::Profile const*costs=nullptr){if(costs)observed=*costs;complete=true;r={{node,"start"}};return allowed;}}graph;auto sClientlessTravelGraphMgr=&graph; struct ClientlessActionExecutorMgr{ struct Record{ClientlessTravelCost::Profile JourneyCosts;std::string JourneyCostKey;bool HasJourneyCosts=false;std::vectorRoute;size_t RouteStep=0;bool FinalLocalLegPending=false,RouteComplete=true,HardDestination=false,Active=false; - unsigned FinalMapId=0,FinalDatabaseId=0;float FinalX=0,FinalY=0,FinalZ=0,FinalO=0;std::string FinalLocationId,PlannedDestinationNode,AdaptiveReachedNode;}; + unsigned FinalMapId=0,FinalDatabaseId=0;float FinalX=0,FinalY=0,FinalZ=0,FinalO=0;std::string FinalLocationId,PlannedDestinationNode,AdaptiveReachedNode,RequiredArrivalNode;bool RequiredArrivalReached=false;}; std::unordered_map_records;int starts=0;bool StartRouteStep(Player*,unsigned,Record&){++starts;return true;} bool AdoptRoute(Player*,ClientlessAction const&,Record&); }; @@ -39,6 +39,10 @@ int main(){ assert(peer.FinalLocationId=="destination"&&peer.FinalLocalLegPending&&peer.FinalX==90&&peer.FinalDatabaseId==77); assert(peer.HasJourneyCosts&&peer.JourneyCosts.Seed==12345&&peer.JourneyCostKey=="leader-trip"&&peer.JourneyCosts.Factors==lead.JourneyCosts.Factors); assert(peer.RouteStep==0&&peer.Active&&lead.RouteStep==1);peer.Route[1].NodeKey="changed";assert(lead.Route[1].NodeKey=="taxi_end"); + // Arrival requirements transfer with the route, but a leader's physical arrival is not inherited. + lead.RequiredArrivalNode="taxi_11";lead.RequiredArrivalReached=true; + assert(mgr.AdoptRoute(&p,a,peer)&&peer.RequiredArrivalNode=="taxi_11"&&!peer.RequiredArrivalReached); + lead.RequiredArrivalNode.clear();lead.RequiredArrivalReached=false; // Late arrival joins the active route at its previous reached node. lead.RouteStep=2;assert(mgr.AdoptRoute(&p,a,peer));assert(peer.Route.size()==2&&peer.Route[0].NodeKey=="taxi_end"&&peer.Route[0].EdgeType=="start"&&peer.Route[1].EdgeType=="free_flight"); p.map=1;assert(mgr.AdoptRoute(&p,a,peer));assert(graph.observed.Factors==lead.JourneyCosts.Factors&&graph.observed.Seed==12345);graph.allowed=false;assert(!mgr.AdoptRoute(&p,a,peer));graph.allowed=true;p.map=0; diff --git a/tests/clientless/test_undercity_arrival.py b/tests/clientless/test_undercity_arrival.py new file mode 100644 index 0000000000..67bde965b4 --- /dev/null +++ b/tests/clientless/test_undercity_arrival.py @@ -0,0 +1,72 @@ +"""Run the explicit Undercity policy and production final-arrival/goal guards.""" +from pathlib import Path +import subprocess,tempfile +root=Path(__file__).resolve().parents[2] +s=(root/'src/server/game/World/ClientlessActionExecutorMgr.cpp').read_text() +def function(prefix,source=s): + a=source.index(prefix);b=source.index('{',a);depth=1;z=b+1 + while depth: + depth+=(source[z]=='{')-(source[z]=='}');z+=1 + return source[a:z] +behavior=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text() +policy=function('std::string RequiredLocationArrivalNode(',behavior) +queue=function('void AdventureBehaviorMgr::Queue(',behavior) +queue=queue[:queue.index(' if(auto group=')]+" r.Queued=sClientlessPlayerMgr->QueueAction(player->GetGUID().GetCounter(),std::move(a));\n}" +goal=function('bool ClientlessActionExecutorMgr::GetTravelGoal(') +gate=function('bool ClientlessActionExecutorMgr::StartFinalLocalLeg(') +gate=gate[:gate.index(' Position destination;')]+'return true;\n}' +code=r''' +#include +#include +#include +#include +using uint32=unsigned; +#define TC_LOG_INFO(...) ((void)0) +struct Guid{unsigned GetCounter(){return 121;}}; +struct Player{Guid GetGUID(){return {};}unsigned map=0;float x=0,y=0,z=0;unsigned GetMapId(){return map;}int GetPhaseShift()const{return 0;}float GetExactDist(float a,float b,float c){return std::sqrt((a-x)*(a-x)+(b-y)*(b-y)+(c-z)*(c-z));}}; +struct AdventureLocation{struct{unsigned MapId=0;}Point;unsigned ZoneId=1497,AreaId=1497;}; +struct Catalog{AdventureLocation location;AdventureLocation const*Find(std::string const& key){return key=="war-quarter"?&location:nullptr;}}catalog;auto sAdventureLocationMgr=&catalog; +struct ClientlessAction{std::string Name;std::mapParameters;}; +struct Players{ClientlessAction last;bool QueueAction(unsigned,ClientlessAction action){last=action;return true;}}players;auto sClientlessPlayerMgr=&players; +struct AdventureBehaviorMgr{struct Record{bool Queued=false;};void Queue(Player*,Record&,std::string const&,std::string const&,std::string const&,bool);}; +struct ClientlessTravelNode{unsigned MapId=0;float X=1568.62,Y=267.97,Z=-43.1,O=0;std::string Key="taxi_11";}; +struct Graph{ClientlessTravelNode taxi;bool missing=false;ClientlessTravelNode const*FindNode(std::string const& key){return !missing&&key=="taxi_11"?&taxi:nullptr;}}graph;auto sClientlessTravelGraphMgr=&graph; +enum class ClientlessActionExecutionState{Failed}; +struct ClientlessActionExecutorMgr{ + struct Record{std::string RequiredArrivalNode="taxi_11",FinalLocationId="war-quarter",PlannedDestinationNode="surface";bool RequiredArrivalReached=false,Active=true,FinalLocalLegPending=true;unsigned FinalMapId=0;float FinalX=1724.8,FinalY=368.623,FinalZ=-60.4843,FinalO=0; + struct{std::string ActionName="travel_to_location",Detail;ClientlessActionExecutionState State;}Status;}; + unsigned recoveries=0;bool GetTravelGoal(Record const&,ClientlessTravelNode&)const; + bool StartFinalLocalLeg(Player*,unsigned,Record&); + bool RecoverTravel(Player*,unsigned,Record&,unsigned,std::string reason){assert(reason=="required-arrival-node-not-reached");++recoveries;return false;} +}; +'''+policy+'\n'+queue+'\n'+goal+'\n'+gate+r''' +int main(){ + Player p;AdventureLocation point; + assert(RequiredLocationArrivalNode(point)=="taxi_11"); + point.ZoneId=85;assert(RequiredLocationArrivalNode(point)=="taxi_11"); + point.AreaId=153;assert(RequiredLocationArrivalNode(point).empty()); + point.ZoneId=point.AreaId=1497;point.Point.MapId=1;assert(RequiredLocationArrivalNode(point).empty()); + AdventureBehaviorMgr behavior;AdventureBehaviorMgr::Record activity; + behavior.Queue(&p,activity,"travel_to_location","id","war-quarter",false); + assert(activity.Queued&&players.last.Parameters.at("arrival_node")=="taxi_11"&&players.last.Parameters.at("id")=="war-quarter"); + catalog.location.ZoneId=85;catalog.location.AreaId=153; + behavior.Queue(&p,activity,"travel_to_location","id","war-quarter",false); + assert(!players.last.Parameters.count("arrival_node")); + ClientlessActionExecutorMgr mgr;ClientlessActionExecutorMgr::Record r;ClientlessTravelNode target; + assert(mgr.GetTravelGoal(r,target)&&target.Key=="taxi_11"&&target.Z==-43.1f); + p.x=1568.62;p.y=267.97;p.z=70; // same XY above the underground arrival node is insufficient + assert(!mgr.StartFinalLocalLeg(&p,121,r)&&!r.RequiredArrivalReached&&mgr.recoveries==1); + p.z=-43.1;assert(mgr.StartFinalLocalLeg(&p,121,r)&&r.RequiredArrivalReached); + assert(r.FinalLocationId=="war-quarter"&&r.FinalZ<-60); // original location retained + assert(mgr.GetTravelGoal(r,target)&&target.Key=="location:war-quarter"&&target.Z<-60); + ClientlessActionExecutorMgr::Record unavailable;graph.missing=true; + assert(!mgr.GetTravelGoal(unavailable,target));assert(!mgr.StartFinalLocalLeg(&p,121,unavailable)); + graph.missing=false;ClientlessActionExecutorMgr::Record ordinary;ordinary.RequiredArrivalNode.clear(); + p.z=70;assert(mgr.StartFinalLocalLeg(&p,121,ordinary)); +} +''' +with tempfile.TemporaryDirectory() as folder: + p=Path(folder);(p/'test.cpp').write_text(code) + subprocess.run(['c++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) +print('Undercity arrival: explicit map/area policy, hard taxi goal before entry, 3D arrival gate, missing anchor rejection and original final target preserved.') diff --git a/tests/clientless/test_vertical_flight_escape.py b/tests/clientless/test_vertical_flight_escape.py new file mode 100644 index 0000000000..0085531ba7 --- /dev/null +++ b/tests/clientless/test_vertical_flight_escape.py @@ -0,0 +1,42 @@ +"""Run production Next: lateral blockage may require a checked vertical escape.""" +from pathlib import Path +import subprocess,tempfile +root=Path(__file__).resolve().parents[2] +code=r''' +#include "ClientlessFlightPath.h" +#include +using namespace ClientlessFlightPath; +int main(){ + Settings cfg;Memory m;m.Departed=true;Point from{0,0,20},goal{200,0,0}; + auto surface=[](float,float){return Surface{0,600,true};}; + auto column=[](Point const&a,Point const&b){return Horizontal(a,b)<0.01f;}; + assert(!Next(from,goal,cfg,m,surface,column).Valid); + auto escape=Next(from,goal,cfg,m,surface,column,false,true); + assert(escape.Valid&&escape.Avoiding&&escape.Next.X==0&&escape.Next.Y==0&&escape.Next.Z==40); + // A safe upward escape can start inside the nominal 8-yard clearance band. + auto lowStart=Next({0,0,5},goal,cfg,m,surface,column,false,true); + assert(lowStart.Valid&&lowStart.Next.Z==35); + // The complete body column is checked; a roof cannot be crossed. + auto roof=[](Point const&a,Point const&b){return Horizontal(a,b)<0.01f&&b.Z<38;}; + assert(!Next(from,goal,cfg,m,surface,roof,false,true).Valid); + // Clamp to the allowed ceiling and stop when no room for ascent remains. + auto low=[](float,float){return Surface{0,34,true};}; + escape=Next(from,goal,cfg,m,low,column,false,true);assert(escape.Valid&&escape.Next.Z==28); + assert(!Next({0,0,28},goal,cfg,m,low,column,false,true).Valid); + // Unknown surfaces and an overhead slab at the origin remain forbidden. + auto unknown=[](float,float){return Surface{};}; + auto slab=[](float,float){return Surface{30,600,true};}; + assert(!Next(from,goal,cfg,m,unknown,column,false,true).Valid); + assert(!Next(from,goal,cfg,m,slab,column,false,true).Valid); + // A clear ordinary flight retains its original route and smooth handoffs. + auto clear=[](Point const&,Point const&){return true;}; + Memory first=m,second=m; + auto a=Next(from,goal,cfg,first,surface,clear),b=Next(from,goal,cfg,second,surface,clear,false,true); + assert(a.Valid&&b.Valid&&Distance(a.Next,b.Next)<0.001f&&Horizontal(from,b.Next)>1); +} +''' +with tempfile.TemporaryDirectory() as d: + p=Path(d);(p/'test.cpp').write_text(code) + subprocess.run(['c++','-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('Vertical airborne escape: full volume, roof/ceiling/unknown support rejection, opt-in recovery and unchanged ordinary routes PASS') diff --git a/tests/clientless/test_waiting_capital.py b/tests/clientless/test_waiting_capital.py new file mode 100644 index 0000000000..fadfa3e488 --- /dev/null +++ b/tests/clientless/test_waiting_capital.py @@ -0,0 +1,84 @@ +"""Execute production capital-wait routing, preserving the remote activity intent.""" +from pathlib import Path +import subprocess,tempfile +root=Path(__file__).resolve().parents[2] +s=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text() +a=s.index('bool AdventureBehaviorMgr::UpdateWaitingCapital(');b=s.index('void AdventureBehaviorMgr::Begin(',a) +code=r''' +#include "AdventureGroupPolicy.h" +#include +#include +#include +#include +#include +#include +using uint32=unsigned; +#define TC_LOG_INFO(...) ((void)0) +#define TC_LOG_WARN(...) ((void)0) +struct Guid{unsigned id=1;unsigned GetCounter()const{return id;}bool operator!=(Guid o)const{return id!=o.id;}}; +struct Group{Guid leader{2};Guid GetLeaderGUID(){return leader;}}group; +struct Player{unsigned map=530,zone=3519,level=68,faction=2;float x=0;Group* group=nullptr;Guid GetGUID(){return {};}Group* GetGroup(){return group;}unsigned GetMapId(){return map;}unsigned GetZoneId(){return zone;}float GetExactDist(float v,float,float){return std::abs(v-x);}}; +struct Position{void Relocate(Player*){}}; +struct AdventureLocation{struct{std::string Id;unsigned MapId;float X,Y=0,Z=0;}Point;unsigned ZoneId,AreaId=0;bool IsArea=false;unsigned faction,min;}; +struct Locations{std::vector sites{{{"org",1,10},1637,0,false,2,1},{{"sw",0,20},1519,0,false,1,1},{{"shatt",530,30},3703,0,false,0,58},{{"dalaran",571,40},4395,0,false,0,70}};auto const& Locations(){return sites;}AdventureLocation const* Find(std::string const&key){for(auto const&s:sites)if(s.Point.Id==key)return &s;return nullptr;}bool LevelEligible(Player*p,AdventureLocation const&s,std::string const&,bool strict){assert(strict);return p->level>=s.min;}}locations; +auto sAdventureLocationMgr=&locations; +enum class ClientlessActionExecutionState{Moving,Completed,Failed}; +struct Executor{struct{ClientlessActionExecutionState State=ClientlessActionExecutionState::Moving;}status;auto GetStatus(unsigned){return &status;}}executor;auto sClientlessActionExecutorMgr=&executor; +struct Manager{unsigned queued=0;unsigned GetQueuedActionCount(unsigned){return queued;}}manager;auto sClientlessPlayerMgr=&manager; +struct Groups{bool BotLed(Player*){return true;}}groups;auto sAdventureGroupMgr=&groups; +struct AdventureBehaviorMgr{ + enum class Phase{Idle,WaitingGroup}; + struct Record{Phase State=Phase::WaitingGroup;std::string WaitingLocation,Intent="raid",Location="remote-raid",Micro,Detail;bool WaitingTravel=false,WaitingArrived=false,Queued=false;time_t WaitingRetryAt=0;Position Anchor;unsigned AnchorMap=0;std::mt19937 Random{123};}; + std::unordered_map_records; + bool busy=false,accept=true;unsigned queues=0,reset=0;std::string target; + bool Busy(unsigned){return busy;} + bool Allowed(Player*p,AdventureLocation const&s,std::string const&i){assert(i=="groupformation");return !s.faction||s.faction==p->faction;} + void ResetMicro(Player*,Record&r){++reset;r.Micro.clear();} + void Queue(Player*,Record&r,std::string const&a,std::string const&key,std::string const&value,bool hard){assert(a=="travel_to_location"&&key=="id"&&hard);++queues;target=value;r.Queued=accept;} + bool UpdateWaitingCapital(Player*,Record&,time_t); +}; +'''.replace('struct Locations{','struct Catalog{')+s[a:b]+r''' +int main(){ + AdventureBehaviorMgr m;AdventureBehaviorMgr::Record r;Player p; + assert(!m.UpdateWaitingCapital(&p,r,100));assert(m.queues==1&&(m.target=="org"||m.target=="shatt")); + assert(r.Intent=="raid"&&r.Location=="remote-raid"&&r.WaitingTravel); + auto chosen=r.WaitingLocation;manager.queued=1;m.UpdateWaitingCapital(&p,r,101);assert(m.queues==1&&r.WaitingLocation==chosen); + // Cross-continent transitions leave destination stable while the action owns travel. + manager.queued=0;m.busy=true;p.map=0;m.UpdateWaitingCapital(&p,r,102);assert(m.queues==1&&r.WaitingLocation==chosen); + m.busy=false;executor.status.State=ClientlessActionExecutionState::Failed; + assert(!m.UpdateWaitingCapital(&p,r,103));assert(r.WaitingLocation.empty()&&r.WaitingRetryAt==133); + m.UpdateWaitingCapital(&p,r,132);assert(m.queues==1); + m.UpdateWaitingCapital(&p,r,133);assert(m.queues==2); + // Actual arrival required: completion far away cannot start activities. + executor.status.State=ClientlessActionExecutionState::Completed; + assert(!m.UpdateWaitingCapital(&p,r,134));assert(!r.WaitingArrived); + m.UpdateWaitingCapital(&p,r,164);auto site=locations.Find(r.WaitingLocation);assert(site); + p.map=site->Point.MapId;p.zone=site->ZoneId;p.x=site->Point.X; + assert(m.UpdateWaitingCapital(&p,r,165));assert(r.WaitingArrived&&r.AnchorMap==p.map); + // Completed activities may roam without repeatedly recalling the bot to the POI. + p.x+=50;r.Micro.clear();unsigned n=m.queues; + assert(m.UpdateWaitingCapital(&p,r,166));assert(m.queues==n); + // Followers wait for their leader to select, instead of choosing another city. + p.group=&group;m._records[2]={};AdventureBehaviorMgr::Record pending; + unsigned before=m.queues;m.UpdateWaitingCapital(&p,pending,199);assert(m.queues==before&&pending.WaitingLocation.empty()&&pending.WaitingRetryAt==200); + // Partial groups inherit a permitted leader waiting target. + AdventureBehaviorMgr::Record follower;m._records[2].WaitingLocation="shatt";p.group=&group; + assert(!m.UpdateWaitingCapital(&p,follower,200));assert(follower.WaitingLocation=="shatt"); + // An enemy capital / underlevel neutral capital can never be adopted. + m._records[2].WaitingLocation="sw";AdventureBehaviorMgr::Record other; + m.UpdateWaitingCapital(&p,other,201);assert(other.WaitingLocation!="sw"&&other.WaitingLocation!="dalaran"); + // Current permitted capital is retained; low-level Horde cannot enter neutral capitals. + p.group=nullptr;p.level=10;p.zone=1637;p.map=1;p.x=10;AdventureBehaviorMgr::Record low; + assert(m.UpdateWaitingCapital(&p,low,202)&&low.WaitingLocation=="org"); + p.faction=1;p.zone=3519;p.map=530;p.x=0;AdventureBehaviorMgr::Record alliance; + m.UpdateWaitingCapital(&p,alliance,203);assert(alliance.WaitingLocation=="sw"); + // No eligible destination: fail closed and throttle, preserve PvP activity. + locations.sites.clear();AdventureBehaviorMgr::Record none;none.Intent="pvp"; + assert(!m.UpdateWaitingCapital(&p,none,300));assert(none.WaitingLocation.empty()&&none.WaitingRetryAt==360&&none.Intent=="pvp"); +} +''' +with tempfile.TemporaryDirectory() as d: + p=Path(d);(p/'test.cpp').write_text(code) + subprocess.run(['c++','-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('Capital waiting: faction/level/neutral access, stable random selection, leader destination, cross-map travel, arrival, roaming and retry passed.')