From 3aa780662ec51846019f7291ea780429f6aeb567 Mon Sep 17 00:00:00 2001 From: nico Date: Thu, 5 Nov 2026 10:35:34 +0100 Subject: [PATCH] Complete AdventureBot navigation, instance travel and shutdown fixes Finish capital waiting travel and adaptive flight entry up to 10,000 yards. Preserve checked vertical climbs, recover blocked ground departures and find validated dry landings. Retain group routes and Undercity arrival constraints. Restore defense from native combat references, coordinate instance rendezvous and browser reentry, finalize clientless sessions before map destruction and transfer Halaa capture-point ownership safely. Include the associated regression tests, investigation reports and reusable diagnostic scripts. Keep generated runtime evidence and server binaries local through scoped ignore rules. Validation/build/live evidence is recorded in the session checkpoint and individual reports. --- .gitignore | 11 + .../2026-11-02-instance-test-final.md | 136 +++ .../2026-11-04-combat-reference-defense.md | 55 ++ .../2026-11-04-dry-landing-recovery.md | 56 ++ .../2026-11-04-final-shutdown-fix.md | 57 ++ .../2026-11-04-ground-departure-retry.md | 27 + doc/clientless/2026-11-04-halaa-owner-fix.md | 56 ++ .../2026-11-04-halaa-shutdown-crash.md | 64 ++ .../2026-11-04-instance-rendezvous.md | 36 + .../2026-11-04-shutdown-crash-analysis.md | 107 ++ .../2026-11-04-undercity-arrival.md | 49 + doc/clientless/2026-11-04-vertical-takeoff.md | 34 + doc/clientless/2026-11-04-waiting-capital.md | 35 + .../2026-11-05-adaptive-start-radius.md | 36 + doc/clientless/SESSION-RESUME.md | 910 ++++++++++++++++++ .../pending-bots.md | 32 + .../build-recovery.md | 30 + .../2026-11-01-route-alternatives/README.md | 57 ++ .../evaluate_without_raid.py | 62 ++ .../report-preview.md | 72 ++ .../2026-11-02-1100-raid-test/observe.py | 42 + .../2026-11-02-crash-110519/README.md | 52 + .../2026-11-02-crash-110519/check_bih.py | 52 + .../2026-11-02-crash-110519/check_spawns.py | 23 + .../2026-11-02-crash-140558/README.md | 43 + .../check_spawn_identity.py | 17 + .../model-lifetime.gdb | 19 + .../start-diagnostic.sh | 13 + .../live_support.py | 24 + .../2026-11-02-lag-diagnostics/README.md | 27 + .../start-passive-diagnostic.sh | 12 + .../2026-11-03-crash-0608/README.md | 54 ++ .../2026-11-03-elevator-exit/README.md | 56 ++ .../live-roundtrip.lua | 23 + .../2026-11-03-ground-validator/README.md | 44 + .../check_los_mesh.py | 51 + .../start-passive-diagnostic.sh | 13 + .../regression.cpp | 4 + .../automatic_permission.lua | 17 + .../automatic_permission_verified.lua | 23 + .../wg_mount_cast.lua | 13 + .../2026-11-04-0200-raid-test/setup.py | 44 + .../2026-11-04-crash-0634/diagnosis.md | 23 + .../2026-11-04-crash-0634/inspect-handle.gdb | 10 + .../2026-11-04-crash-0634/inspect.gdb | 14 + .../2026-11-04-halaa-owner-fix/halaa_load.lua | 14 + .../2026-11-04-lifecycle/db_fault_proxy.py | 37 + .../repro_transfer.py | 36 + .../identity.gdb | 7 + .../targeted.gdb | 15 + .../2026-11-04-shutdown-161645/targeted.gdb | 19 + .../pet.gdb | 15 + .../transport.gdb | 20 + .../read_mesh.py | 44 + .../2026-11-04-test-hunter-gear/catalog.py | 23 + .../game/World/AdventureBehaviorMgr.cpp | 131 ++- src/server/game/World/AdventureBehaviorMgr.h | 3 + src/server/game/World/AdventureCombatMgr.cpp | 29 + .../game/World/AdventureEncounterMgr.cpp | 34 +- src/server/game/World/AdventureEncounterMgr.h | 2 + .../game/World/AdventureGroupTravelMgr.cpp | 56 +- .../game/World/AdventureGroupTravelMgr.h | 4 + .../game/World/AdventureGroupTravelPolicy.h | 1 + .../World/ClientlessActionExecutorMgr.cpp | 47 +- .../game/World/ClientlessActionExecutorMgr.h | 2 + src/server/game/World/ClientlessFlightMgr.cpp | 116 ++- src/server/game/World/ClientlessFlightMgr.h | 6 + src/server/game/World/ClientlessFlightPath.h | 10 +- src/server/game/World/ClientlessLuaEngine.cpp | 1 + src/server/game/World/ClientlessPlayerMgr.cpp | 27 + src/server/game/World/ClientlessPlayerMgr.h | 1 + .../game/World/ClientlessTravelGraphMgr.cpp | 69 +- src/server/game/World/World.cpp | 35 +- src/server/game/World/World.h | 2 + .../scripts/OutdoorPvP/OutdoorPvPNA.cpp | 21 +- src/server/scripts/OutdoorPvP/OutdoorPvPNA.h | 5 +- src/server/worldserver/Main.cpp | 2 + .../test_adventure_combat_reactions.py | 48 +- tests/clientless/test_adventure_encounter.py | 9 + .../clientless/test_adventure_group_travel.py | 41 +- tests/clientless/test_adventure_instance.py | 3 + .../clientless/test_adventure_intent_state.py | 25 +- tests/clientless/test_dry_landing_recovery.py | 89 ++ tests/clientless/test_final_shutdown.py | 105 ++ tests/clientless/test_flight_departure.py | 43 +- tests/clientless/test_flight_handoff.py | 20 +- tests/clientless/test_flight_policy.py | 6 +- tests/clientless/test_free_flight_planner.py | 36 +- .../test_halaa_capture_ownership.py | 70 ++ .../test_instance_browser_reentry.py | 60 ++ .../test_instance_group_protection.py | 2 +- tests/clientless/test_shared_group_route.py | 6 +- tests/clientless/test_undercity_arrival.py | 72 ++ .../clientless/test_vertical_flight_escape.py | 42 + tests/clientless/test_waiting_capital.py | 84 ++ 95 files changed, 4125 insertions(+), 105 deletions(-) create mode 100644 doc/clientless/2026-11-02-instance-test-final.md create mode 100644 doc/clientless/2026-11-04-combat-reference-defense.md create mode 100644 doc/clientless/2026-11-04-dry-landing-recovery.md create mode 100644 doc/clientless/2026-11-04-final-shutdown-fix.md create mode 100644 doc/clientless/2026-11-04-ground-departure-retry.md create mode 100644 doc/clientless/2026-11-04-halaa-owner-fix.md create mode 100644 doc/clientless/2026-11-04-halaa-shutdown-crash.md create mode 100644 doc/clientless/2026-11-04-instance-rendezvous.md create mode 100644 doc/clientless/2026-11-04-shutdown-crash-analysis.md create mode 100644 doc/clientless/2026-11-04-undercity-arrival.md create mode 100644 doc/clientless/2026-11-04-vertical-takeoff.md create mode 100644 doc/clientless/2026-11-04-waiting-capital.md create mode 100644 doc/clientless/2026-11-05-adaptive-start-radius.md create mode 100644 doc/clientless/checkpoints/2026-10-31-coredump-restart/pending-bots.md create mode 100644 doc/clientless/checkpoints/2026-10-31-flight-recovery/build-recovery.md create mode 100644 doc/clientless/checkpoints/2026-11-01-route-alternatives/README.md create mode 100644 doc/clientless/checkpoints/2026-11-02-0600-group-test/evaluate_without_raid.py create mode 100644 doc/clientless/checkpoints/2026-11-02-0600-group-test/report-preview.md create mode 100644 doc/clientless/checkpoints/2026-11-02-1100-raid-test/observe.py create mode 100644 doc/clientless/checkpoints/2026-11-02-crash-110519/README.md create mode 100644 doc/clientless/checkpoints/2026-11-02-crash-110519/check_bih.py create mode 100644 doc/clientless/checkpoints/2026-11-02-crash-110519/check_spawns.py create mode 100644 doc/clientless/checkpoints/2026-11-02-crash-140558/README.md create mode 100644 doc/clientless/checkpoints/2026-11-02-crash-140558/check_spawn_identity.py create mode 100644 doc/clientless/checkpoints/2026-11-02-crash-140558/model-lifetime.gdb create mode 100755 doc/clientless/checkpoints/2026-11-02-crash-140558/start-diagnostic.sh create mode 100644 doc/clientless/checkpoints/2026-11-02-elevated-flight/live_support.py create mode 100644 doc/clientless/checkpoints/2026-11-02-lag-diagnostics/README.md create mode 100644 doc/clientless/checkpoints/2026-11-02-lag-diagnostics/start-passive-diagnostic.sh create mode 100644 doc/clientless/checkpoints/2026-11-03-crash-0608/README.md create mode 100644 doc/clientless/checkpoints/2026-11-03-elevator-exit/README.md create mode 100644 doc/clientless/checkpoints/2026-11-03-elevator-exit/live-roundtrip.lua create mode 100644 doc/clientless/checkpoints/2026-11-03-ground-validator/README.md create mode 100644 doc/clientless/checkpoints/2026-11-03-ground-validator/check_los_mesh.py create mode 100644 doc/clientless/checkpoints/2026-11-03-passive-restart/start-passive-diagnostic.sh create mode 100644 doc/clientless/checkpoints/2026-11-03-takeoff-recovery/regression.cpp create mode 100644 doc/clientless/checkpoints/2026-11-03-wg-client-permission/automatic_permission.lua create mode 100644 doc/clientless/checkpoints/2026-11-03-wg-client-permission/automatic_permission_verified.lua create mode 100644 doc/clientless/checkpoints/2026-11-03-wg-mount-cast/wg_mount_cast.lua create mode 100644 doc/clientless/checkpoints/2026-11-04-0200-raid-test/setup.py create mode 100644 doc/clientless/checkpoints/2026-11-04-crash-0634/diagnosis.md create mode 100644 doc/clientless/checkpoints/2026-11-04-crash-0634/inspect-handle.gdb create mode 100644 doc/clientless/checkpoints/2026-11-04-crash-0634/inspect.gdb create mode 100644 doc/clientless/checkpoints/2026-11-04-halaa-owner-fix/halaa_load.lua create mode 100644 doc/clientless/checkpoints/2026-11-04-lifecycle/db_fault_proxy.py create mode 100644 doc/clientless/checkpoints/2026-11-04-lowpop-instance-entry/repro_transfer.py create mode 100644 doc/clientless/checkpoints/2026-11-04-outdoor-halaa-crash/identity.gdb create mode 100644 doc/clientless/checkpoints/2026-11-04-outdoor-halaa-crash/targeted.gdb create mode 100644 doc/clientless/checkpoints/2026-11-04-shutdown-161645/targeted.gdb create mode 100644 doc/clientless/checkpoints/2026-11-04-shutdown-crash-diagnosis/pet.gdb create mode 100644 doc/clientless/checkpoints/2026-11-04-shutdown-crash-diagnosis/transport.gdb create mode 100644 doc/clientless/checkpoints/2026-11-04-sylaria-navigation/read_mesh.py create mode 100644 doc/clientless/checkpoints/2026-11-04-test-hunter-gear/catalog.py create mode 100644 tests/clientless/test_dry_landing_recovery.py create mode 100644 tests/clientless/test_final_shutdown.py create mode 100644 tests/clientless/test_halaa_capture_ownership.py create mode 100644 tests/clientless/test_instance_browser_reentry.py create mode 100644 tests/clientless/test_undercity_arrival.py create mode 100644 tests/clientless/test_vertical_flight_escape.py create mode 100644 tests/clientless/test_waiting_capital.py 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.')