Improve AdventureBot planning and travel performance
This commit is contained in:
1 parent
69b5a1c4a1
commit
d1ef007cef
18 files changed
+906
-27
No files matched your search
@@ -1,3 +1,115 @@
|
||||
# Aktuell: Routenplaner-Last behoben und volle Population live geprüft
|
||||
|
||||
Nutzer meldete Serverlags und bat nach Prüfung bestehender Navigationsprobleme
|
||||
um den Fix. Vorher mit110Bots: ActionExecutor regelmäßig20–29s je30s, einzelne
|
||||
PlanAdaptiveRoute-Aufrufe bis10.9s/13693FlightEstimates. Ursache war eine implizit
|
||||
nahezu vollständige Flugverknüpfung, Flugausstiegprüfung jedes Straßenpunktes,
|
||||
virtuelle Straßeneinstiegssuche auch an normalen Graphknoten und insbesondere
|
||||
40-Yard-Gebietssampling ganzer nativer Taxipfade für neue Level/Fraktionskeys.
|
||||
|
||||
Fix in ClientlessTravelGraphMgr: maximal2 vorsortierte Flugausstiege pro Kante/
|
||||
8proPlan,16 echte FlightEstimates proPlan, Schätzcache über beide Suchtier,
|
||||
Flugconnectoren nur Ziel/mapwechselnde oder physische Transportquellen statt
|
||||
aller Taxis, direkter same-map Location-Flug vor Makrosuche, virtuelle Forward-
|
||||
Entry-Reparatur nur an echten Graph-Sackgassen. AdventureAreaMgr prüft native
|
||||
Taxipfadexistenz plus bereits fraktions-/gebietskataloggeprüfte Endknoten; das
|
||||
Überfliegen gesperrter Gebiete ist erlaubt und wird nicht mehr in40yd-Schritten
|
||||
abgetastet. Native Flug-/Gebiets-/Start-/Landesicherheit bleibt bestehen.
|
||||
|
||||
Mehrere inkrementelle Builds regelkonform: Loader/Testaufnahme0, safe logout,
|
||||
nullSitzungen oder dokumentierte5min-Ausnahme, saubererShutdown, ausschließlich
|
||||
-j10. Gezielter test_free_flight_planner bei jedem Build PASS, diff-check PASS.
|
||||
Final installiert; frischer ready-Prozess47340, Ports8085/8087. Vier Recoverybots
|
||||
54/64/89/109 in Fraktionshauptstädten geprüft; Details unter
|
||||
doc/clientless/checkpoints/2026-10-31-route-performance/. Loader/Testaufnahme
|
||||
wieder1, volle Pools50/5/50/5=110 online.
|
||||
|
||||
Live nach finalem Fix: Kaltplanung statt10–20s noch1.93s einmalig; folgende
|
||||
vergleichbare Routen51–153ms. Bei110Bots drei volle30s-Fenster Actions4.11/4.38/
|
||||
4.14s statt20–29s; Controllers6.63/7.06/6.79s, keine RECOVERY_FAILED oder
|
||||
TRAVEL_PLAN_FAILED. CPU-Prozess etwa130% über mehrere Threads; Server bleibt
|
||||
ready. Noch auffällig: Fizzira43 wiederholt alle2–3s denselben Intent-Fallback
|
||||
instance_visit_at_2217->446 (Intent none, keine Action), kein eigentlicher
|
||||
Travelplan-Fehler. Andere neue Fallbacks:154(13),95(12),50/114(8),146(7),105(6).
|
||||
Fizzira ist ein separater Tagesplan/Intent-Fallback-Bug und sollte als nächstes
|
||||
behoben werden. Kein Commit/Push für diese Arbeit angefordert.
|
||||
|
||||
---
|
||||
|
||||
# Aktuell: Brindras verwaister Gruppen-Pfadvalidator behoben und live geprüft
|
||||
|
||||
Brindra86 hing nach einer früheren Gruppe hinter der Tiefenbahn auf der
|
||||
Eisenschmiede-Seite fest. MMAP berechnete vollständige Wege, doch TravelMgr
|
||||
meldete `owner ground-safety validator rejected spline`; die Route startete
|
||||
wiederholt über `virtual_ground_entry`. Ursache: `AdventureGroupTravelMgr::Observe`
|
||||
kehrte bei `GetGroup()==nullptr` zurück, ohne den zuvor installierten
|
||||
GroundPathAdapter/PathValidator und Gruppenreise-Record zu entfernen.
|
||||
|
||||
Fix in AdventureGroupTravelMgr.cpp: Wenn keine Gruppe mehr besteht und ein
|
||||
Gruppenreise-Record vorhanden ist, wird `Clear(player)` aufgerufen. Dadurch werden
|
||||
Adapter, Validator, eigener Reisezustand und Record entfernt; Bots ohne früheren
|
||||
Record bleiben unberührt. Regression in test_adventure_group_travel.py verfolgt
|
||||
beide Registrierungen und beweist deren Entfernung samt Record/Reiseabbruch.
|
||||
|
||||
Buildregel eingehalten: Loader/Testaufnahme0, safe logout angefordert. Nach fünf
|
||||
Minuten blieben13 Bots auf regulären Flug/Taxistrecken; Saveall, vollständige TSV-
|
||||
Liste mit GUID/Name/Fraktion/Position/Taxi und native Info/Flight/Transport/Action-
|
||||
Ausgaben unter `/home/wow_admin/wow_cata_server/checkpoints/brindra-group-validator/`.
|
||||
Server sauber beendet. Gruppentravel-TestPASS. Build-j10 am Ende vollständig
|
||||
gelinkt (ELF geprüft), installiert, frischer ready-Start Prozess32369, Ports8085/
|
||||
8087. Wegen einer fehlerhaften Tool-Sitzungsrückmeldung liefen kurz mehrere eigene
|
||||
Linkprozesse auf dasselbe Artefakt; ausschließlich diese Prozesse beendet, danach
|
||||
ein einzelner sauberer Neu-Link. Installierte alte Binary war dabei nie überschrieben.
|
||||
|
||||
Alle13 Restbots eingeloggt, alte Flug-/Transportzustände waren weg, in
|
||||
Sturmwind/Orgrimmar geborgen und per Area-Ausgabe bestätigt. Loader/Testaufnahme
|
||||
wieder1; tatsächlich enabled1/schema1/busy0/known110. Brindra war erneut in einer
|
||||
Gruppe; `interaction 86 leave` führte live zu `No group travel state`, grouped0.
|
||||
Der zuvor abgelehnte Weg läuft nun mit Travel=Moving und Nav reason=`ok`, ohne
|
||||
PathRejected. Brindras Schedule ist wieder an. Kein Commit/Push angefordert.
|
||||
|
||||
---
|
||||
|
||||
# Aktuell: GM-Tagesplaneditor implementiert, gebaut, installiert und live geprüft
|
||||
|
||||
Nutzerauftrag: aktuelle/künftige Intents samtLocation perBefehl, GUID/Population,
|
||||
Fraktions- undLevelgruppenfilter. NeuerBefehl `clientless adventure editplan`
|
||||
unterbestehendemGM-Kommando. Syntax/Beispiele: doc/clientless/plan-commands.md.
|
||||
Scope ausdrücklich EINGELOGGTE AdventureBots, heutige aktuelle/künftigeStunden.
|
||||
GUID|all, current|next|next+N|Stundenrange, Intent|keep|instance-or-raid,
|
||||
Location|auto|capital|keep, alliance|horde|both, Min/Max1..85, preview|apply.
|
||||
Vorschau kopiertRecord und verändert wederDB nochRNG. Änderungen validiert mit
|
||||
normalemAllowed/Level/Area/Flight; FormationbenötigtzulässigenFolgeblock, nieStunde23.
|
||||
GültigeBots persistieren sofort und aktualisierenRAMPlan; ungültigeBots werden
|
||||
mitGrund proBot komplett übersprungen. Aktuell benutzt sicherenSetWantedHandover,
|
||||
zukünftig lässtaktuellenIntentlaufen. KeineTeleports/Gruppenauflösung durchEditor.
|
||||
KeineSchemaänderung/keineLuaänderung nötig. Current reaktiviertSchedule, Future
|
||||
behältmanuellenSchedulemode; beiBedarf schedule GUID on.
|
||||
|
||||
Betrieb: User warerneutbeimAufzeichnen, asynchrongefragt; Antwort "bin fertig gut
|
||||
aufgepasst" erlaubteInstallation/Restart. Loader/TestAdmissionpausiert, safeall um
|
||||
13:41:55CET; nachmehrals5min9Restbots53/54/67/74/75/86/89/125/145 mitSnapshot,
|
||||
saveallDBPositionen und verfügbarenFlags dokumentiert; sauberShutdown10, Prozessende
|
||||
verifiziertVORBuild. BestehenderNinjaDebugBuild-j10 EXIT0, viergezielteTestsPASS.
|
||||
InstallEXIT0, frischerreadyStartProzess25737, Ports8085/8087geprüft. NeunBots
|
||||
geprüft/indenHauptstädtenzurückgesetzt. Brindra86brauchtezweitenTeleport nachsicherem
|
||||
BehaviorStop; area5148bestätigt undSchedulewiederon. Loader/TestEnabled1undAdmission
|
||||
restauriert; tatsächlichenLoader enabled1/schema1/busy0/pools50/5/50/5bestätigt.
|
||||
Restbotpositionsquelle ausdrücklich letztesSaveall, keineunbelegtenLivekoordinaten.
|
||||
|
||||
NativeLiveprüfung: Preview26next undallianceLevel60Filter, dann26Stunde20
|
||||
social->rest(auto), DBpersistenzgeprüft; originalsocial/goldshire_tavern wiedermit
|
||||
Befehlhergestellt und alleSlots18..23gegenBackupidentischbestätigt. Dergeplante
|
||||
Gruppentest14–15/15–16bleibtbestehen. RaidJoin0/BGJoin0unverändert.
|
||||
KeinCommitfürdiesenAuftragangefordert/erstellt, vorherigerCommit69b5a1c4a1.
|
||||
Code: AdventurePlanCommand.h, AdventureBehaviorMgr.cpp/.h. Test:
|
||||
tests/clientless/test_adventure_plan_commands.py extrahiertproduktiveEditPlanMethode.
|
||||
Belege: doc/clientless/checkpoints/2026-10-31-plan-command/. VierTestsstattkompletter
|
||||
Suiteerneut, frühere120/120historisch. git diff --check erfolgreich.
|
||||
Vorheriges32MBRohlogundPythonCacheweiterlokalunversioniert, nichtlöschen.
|
||||
|
||||
---
|
||||
|
||||
# Aktueller Gruppentest geplant: 31.10., 14–15 Uhr Vorbereitung, 15–16 Uhr Ziele
|
||||
|
||||
Nutzer verlangt NÄCHSTEN Intent Gruppenbildung, danach gemischt Dungeon/Raid.
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"cause": "group travel path validator remained registered after leaving group",
|
||||
"fix": "AdventureGroupTravelMgr::Observe clears an existing group-travel record when GetGroup returns null",
|
||||
"regression_test": "PASS: test_adventure_group_travel.py",
|
||||
"build": "PASS: CMake/Ninja Debug -j10, final artifact verified as ELF",
|
||||
"installed": true,
|
||||
"startup": "PASS: fresh ready, process 32369, ports 8085 and 8087",
|
||||
"live_test": "PASS: Brindra left group, report showed no group travel state/grouped=0; same route changed from PathRejected to Moving with nav reason ok",
|
||||
"loader_restored": "enabled=1 schema=1 busy=0 known=110",
|
||||
"recovery_count": 13,
|
||||
"raw_evidence": "/home/wow_admin/wow_cata_server/checkpoints/brindra-group-validator/",
|
||||
"commit": null
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Clientless.Adventure.Loader.Enabled": 1,
|
||||
"Clientless.TestBot.Enabled": 1,
|
||||
"requested_at": "2026-10-31T13:41:55.253785+01:00",
|
||||
"monotonic": 24044.355791678
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
[
|
||||
{
|
||||
"character_guid": "26",
|
||||
"local_date": "20261031",
|
||||
"hour_slot": "18",
|
||||
"intent": "leisure",
|
||||
"location_key": "poi_guard_542"
|
||||
},
|
||||
{
|
||||
"character_guid": "26",
|
||||
"local_date": "20261031",
|
||||
"hour_slot": "19",
|
||||
"intent": "groupformation",
|
||||
"location_key": "poi_npc_314039"
|
||||
},
|
||||
{
|
||||
"character_guid": "26",
|
||||
"local_date": "20261031",
|
||||
"hour_slot": "20",
|
||||
"intent": "social",
|
||||
"location_key": "goldshire_tavern"
|
||||
},
|
||||
{
|
||||
"character_guid": "26",
|
||||
"local_date": "20261031",
|
||||
"hour_slot": "21",
|
||||
"intent": "social",
|
||||
"location_key": "goldshire_tavern"
|
||||
},
|
||||
{
|
||||
"character_guid": "26",
|
||||
"local_date": "20261031",
|
||||
"hour_slot": "22",
|
||||
"intent": "groupformation",
|
||||
"location_key": "poi_npc_314313"
|
||||
},
|
||||
{
|
||||
"character_guid": "26",
|
||||
"local_date": "20261031",
|
||||
"hour_slot": "23",
|
||||
"intent": "raid",
|
||||
"location_key": "instance_visit_at_3528"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,44 @@
|
||||
[
|
||||
{
|
||||
"character_guid": "26",
|
||||
"local_date": "20261031",
|
||||
"hour_slot": "18",
|
||||
"intent": "leisure",
|
||||
"location_key": "poi_guard_542"
|
||||
},
|
||||
{
|
||||
"character_guid": "26",
|
||||
"local_date": "20261031",
|
||||
"hour_slot": "19",
|
||||
"intent": "groupformation",
|
||||
"location_key": "poi_npc_314039"
|
||||
},
|
||||
{
|
||||
"character_guid": "26",
|
||||
"local_date": "20261031",
|
||||
"hour_slot": "20",
|
||||
"intent": "social",
|
||||
"location_key": "goldshire_tavern"
|
||||
},
|
||||
{
|
||||
"character_guid": "26",
|
||||
"local_date": "20261031",
|
||||
"hour_slot": "21",
|
||||
"intent": "social",
|
||||
"location_key": "goldshire_tavern"
|
||||
},
|
||||
{
|
||||
"character_guid": "26",
|
||||
"local_date": "20261031",
|
||||
"hour_slot": "22",
|
||||
"intent": "groupformation",
|
||||
"location_key": "poi_npc_314313"
|
||||
},
|
||||
{
|
||||
"character_guid": "26",
|
||||
"local_date": "20261031",
|
||||
"hour_slot": "23",
|
||||
"intent": "raid",
|
||||
"location_key": "instance_visit_at_3528"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"intent": "rest",
|
||||
"location_key": "goldshire_tavern"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"snapshot_at": "2026-10-31T13:47:02.811753+01:00",
|
||||
"bots": [
|
||||
{
|
||||
"guid": "53",
|
||||
"name": "Noranda",
|
||||
"race": "1",
|
||||
"map": "571",
|
||||
"position_x": "5728.95",
|
||||
"position_y": "695.284",
|
||||
"position_z": "688.058",
|
||||
"taxi_path": "",
|
||||
"faction": "alliance",
|
||||
"position_source": "saveall immediately before shutdown; last database-saved position",
|
||||
"individual_combat_teleport": "not exposed by current native status; aggregate safe logout and native flight/transport diagnostics captured separately",
|
||||
"recovery_result": "Online login confirmed; admin teleport to faction capital; native adventure area confirmed map0/area5148 Alliance or map1/area5170 Horde."
|
||||
},
|
||||
{
|
||||
"guid": "54",
|
||||
"name": "Falaeriel",
|
||||
"race": "10",
|
||||
"map": "1",
|
||||
"position_x": "1614",
|
||||
"position_y": "-4323.1",
|
||||
"position_z": "64.094",
|
||||
"taxi_path": "",
|
||||
"faction": "horde",
|
||||
"position_source": "saveall immediately before shutdown; last database-saved position",
|
||||
"individual_combat_teleport": "not exposed by current native status; aggregate safe logout and native flight/transport diagnostics captured separately",
|
||||
"recovery_result": "Online login confirmed; admin teleport to faction capital; native adventure area confirmed map0/area5148 Alliance or map1/area5170 Horde."
|
||||
},
|
||||
{
|
||||
"guid": "67",
|
||||
"name": "Volasha",
|
||||
"race": "8",
|
||||
"map": "1",
|
||||
"position_x": "-4483.48",
|
||||
"position_y": "-1854",
|
||||
"position_z": "132.231",
|
||||
"taxi_path": "25 42 ",
|
||||
"faction": "horde",
|
||||
"position_source": "saveall immediately before shutdown; last database-saved position",
|
||||
"individual_combat_teleport": "not exposed by current native status; aggregate safe logout and native flight/transport diagnostics captured separately",
|
||||
"recovery_result": "Online login confirmed; admin teleport to faction capital; native adventure area confirmed map0/area5148 Alliance or map1/area5170 Horde."
|
||||
},
|
||||
{
|
||||
"guid": "74",
|
||||
"name": "Nimbin",
|
||||
"race": "7",
|
||||
"map": "1",
|
||||
"position_x": "-3962.06",
|
||||
"position_y": "-2889.03",
|
||||
"position_z": "111.189",
|
||||
"taxi_path": "179 389 ",
|
||||
"faction": "alliance",
|
||||
"position_source": "saveall immediately before shutdown; last database-saved position",
|
||||
"individual_combat_teleport": "not exposed by current native status; aggregate safe logout and native flight/transport diagnostics captured separately",
|
||||
"recovery_result": "Online login confirmed; admin teleport to faction capital; native adventure area confirmed map0/area5148 Alliance or map1/area5170 Horde."
|
||||
},
|
||||
{
|
||||
"guid": "75",
|
||||
"name": "Faeleran",
|
||||
"race": "10",
|
||||
"map": "1",
|
||||
"position_x": "21.0141",
|
||||
"position_y": "-3100.41",
|
||||
"position_z": "119.486",
|
||||
"taxi_path": "25 23 ",
|
||||
"faction": "horde",
|
||||
"position_source": "saveall immediately before shutdown; last database-saved position",
|
||||
"individual_combat_teleport": "not exposed by current native status; aggregate safe logout and native flight/transport diagnostics captured separately",
|
||||
"recovery_result": "Online login confirmed; admin teleport to faction capital; native adventure area confirmed map0/area5148 Alliance or map1/area5170 Horde."
|
||||
},
|
||||
{
|
||||
"guid": "86",
|
||||
"name": "Brindra",
|
||||
"race": "3",
|
||||
"map": "0",
|
||||
"position_x": "-5298.76",
|
||||
"position_y": "-598.388",
|
||||
"position_z": "388.853",
|
||||
"taxi_path": "",
|
||||
"faction": "alliance",
|
||||
"position_source": "saveall immediately before shutdown; last database-saved position",
|
||||
"individual_combat_teleport": "not exposed by current native status; aggregate safe logout and native flight/transport diagnostics captured separately",
|
||||
"recovery_result": "Online login confirmed; admin teleport to faction capital; native adventure area confirmed map0/area5148 Alliance or map1/area5170 Horde. First teleport drifted back to old movement; stopped behavior safely, repeated teleport verified area5148; schedule on restored."
|
||||
},
|
||||
{
|
||||
"guid": "89",
|
||||
"name": "Tahuima",
|
||||
"race": "6",
|
||||
"map": "571",
|
||||
"position_x": "2718.31",
|
||||
"position_y": "6232.1",
|
||||
"position_z": "83.5786",
|
||||
"taxi_path": "",
|
||||
"faction": "horde",
|
||||
"position_source": "saveall immediately before shutdown; last database-saved position",
|
||||
"individual_combat_teleport": "not exposed by current native status; aggregate safe logout and native flight/transport diagnostics captured separately",
|
||||
"recovery_result": "Online login confirmed; admin teleport to faction capital; native adventure area confirmed map0/area5148 Alliance or map1/area5170 Horde."
|
||||
},
|
||||
{
|
||||
"guid": "125",
|
||||
"name": "Koruhon",
|
||||
"race": "6",
|
||||
"map": "1",
|
||||
"position_x": "2822",
|
||||
"position_y": "-481.923",
|
||||
"position_z": "112.377",
|
||||
"taxi_path": "58 25 ",
|
||||
"faction": "horde",
|
||||
"position_source": "saveall immediately before shutdown; last database-saved position",
|
||||
"individual_combat_teleport": "not exposed by current native status; aggregate safe logout and native flight/transport diagnostics captured separately",
|
||||
"recovery_result": "Online login confirmed; admin teleport to faction capital; native adventure area confirmed map0/area5148 Alliance or map1/area5170 Horde."
|
||||
},
|
||||
{
|
||||
"guid": "145",
|
||||
"name": "Gorgar",
|
||||
"race": "2",
|
||||
"map": "530",
|
||||
"position_x": "-1757.67",
|
||||
"position_y": "5231.64",
|
||||
"position_z": "-47.6846",
|
||||
"taxi_path": "",
|
||||
"faction": "horde",
|
||||
"position_source": "saveall immediately before shutdown; last database-saved position",
|
||||
"individual_combat_teleport": "not exposed by current native status; aggregate safe logout and native flight/transport diagnostics captured separately",
|
||||
"recovery_result": "Online login confirmed; admin teleport to faction capital; native adventure area confirmed map0/area5148 Alliance or map1/area5170 Horde."
|
||||
}
|
||||
],
|
||||
"completed": true
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"build": "PASS -j10",
|
||||
"installed": true,
|
||||
"startup": "fresh ready and process25737 with ports8085/8087",
|
||||
"tests": [
|
||||
"plan-command parser and production EditPlan",
|
||||
"intent state",
|
||||
"day persistence",
|
||||
"group departure"
|
||||
],
|
||||
"tests_passed": 4,
|
||||
"live_validation": "GUID26 future hour20 persisted; restored original18-23 and compared all six rows; filtered read-only previews accepted",
|
||||
"restored_loader": 1,
|
||||
"restored_test_admission": true,
|
||||
"raid_join_enabled": false,
|
||||
"bg_join_enabled": false,
|
||||
"recovery_count": 9,
|
||||
"scope": "online AdventureBots; edits persisted for restart; no offline selection",
|
||||
"git_commit": "not requested for this task"
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
# Safe-logout recovery list before route-planner performance build
|
||||
|
||||
- Safe logout requested: 2026-10-31 14:28 CET (approximately; console command)
|
||||
- Snapshot/saveall: 2026-10-31 14:34 CET
|
||||
- Loader and TestBot admission: disabled before the request
|
||||
- Native aggregate at the five-minute limit: sessions 4, combat 0, transport 0,
|
||||
flight/taxi 4, loading 0
|
||||
- Teleport-in-progress is not exposed by the current native status command.
|
||||
|
||||
| GUID | Name | Faction | Saved position | Combat | Flight/taxi | Transport | Action/status before shutdown |
|
||||
|---:|---|---|---|---:|---:|---:|---|
|
||||
| 54 | Falaeriel | Horde | map 1, 5309.99 -3820.91 1723.56 | no | failed free flight, still flying/mounted | no | `travel_to_location`, no safe bounded corridor |
|
||||
| 64 | Hildda | Alliance | map 0, -8802.94 642.241 106.207 | no | failed free flight, still flying/mounted | no | `travel_to_location`, no safe bounded corridor |
|
||||
| 89 | Tahuima | Horde | map 1, 1754.87 -4343.36 115.299 | no | failed free flight, still flying/mounted | no | `travel_to_location`, no safe bounded corridor |
|
||||
| 109 | Rixbin | Horde | map 530, 157 6047.06 57.2654; taxi 118 102 | no | native taxi active; prior free flight arrived | no | `travel_to_location`, taxi path 537 (118 to 102) |
|
||||
|
||||
The first three records demonstrate an existing safe-logout gap: failed free
|
||||
flight deliberately holds the bot mounted in the air, so the native safety gate
|
||||
can never become ready. Per the five-minute build rule they were not detached or
|
||||
force-logged-out individually; the server is shut down cleanly before compiling.
|
||||
After restart all four records must be checked and, if necessary, reset to
|
||||
Stormwind or Orgrimmar before normal loader operation resumes.
|
||||
|
||||
## Incremental performance build
|
||||
|
||||
The first live measurement showed that limiting road exits alone was
|
||||
insufficient. A second safe logout was requested at approximately 14:36 CET
|
||||
with the loader still disabled. GUIDs 64, 89 and 109 logged out safely. GUID 54
|
||||
remained in `FlightState::Failed` with movement flags still reporting flight
|
||||
(`flight segment made no confirmed progress`) beyond the five-minute limit.
|
||||
It is the sole recovery record for the incremental build; no individual forced
|
||||
logout or flight interruption was used before the clean server shutdown.
|
||||
|
||||
## Recovery result
|
||||
|
||||
After each installed build the recorded characters were logged in with the
|
||||
loader paused. All four had lost stale taxi/free-flight controller state.
|
||||
Falaeriel, Tahuima and Rixbin were verified in Orgrimmar (allowed area 5170 or
|
||||
1637); Hildda was verified in Stormwind (allowed area 5148). Their saved DB
|
||||
positions are now in the correct faction capitals and have empty taxi paths.
|
||||
Normal loader and TestBot admission were restored after the final build.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Tagespläne per GM-Befehl bearbeiten
|
||||
|
||||
In der Konsole ohne Punkt, im Spiel mit führendem Punkt. Benötigt GM-Berechtigung.
|
||||
|
||||
```
|
||||
clientless adventure editplan <guid|all> <Zeitraum> <Intent> <Location> <Fraktion> <MinLevel> <MaxLevel> <preview|apply>
|
||||
```
|
||||
|
||||
- Ziel: einzelne Charakter-GUID oder `all` für alle eingeloggten AdventureBots.
|
||||
Offlinebots, Dungeonbots und echte Spieler werden nicht verändert.
|
||||
- Zeitraum: `current`, `next`, `next+1` (übernächste Stunde), eine Stunde `15`
|
||||
oder inklusive Spanne `15-17`. Nur heutige aktuelle/künftige Stunden;
|
||||
kein stiller Überlauf zum nächsten Tag. `current-next` ist ebenfalls möglich.
|
||||
- Intent: vorhandener Intentname, `keep` zum Beibehalten oder `instance-or-raid`
|
||||
für einen gemischten Test. Letzteres versucht bei geraden GUIDs Raid, sonst Dungeon;
|
||||
bei `auto` fällt ein nicht zulässiger Raid auf Dungeon zurück.
|
||||
- Location: konkreter Location-Key, `keep`, `auto` für die normale profil- und
|
||||
zugangsabhängige Auswahl oder `capital` für einen gültigen Ort in Sturmwind bzw.
|
||||
Orgrimmar. TravelGraph-Node-Keys sind nicht automatisch Location-Keys.
|
||||
- Fraktion: `alliance`, `horde`, `both`; Levelgrenzen inklusive, 1–85.
|
||||
- `preview` zeigt die geplanten Änderungen ohne Datenbank-, Bewegungs- oder
|
||||
Zufallszustandsänderung. `apply` speichert die Änderungen im persistenten Tagesplan
|
||||
und aktualisiert die laufenden Pläne ohne Neustart oder Lua-Änderung.
|
||||
|
||||
Ungültige Ziele werden gemeldet und für den betreffenden Bot wird die gesamte
|
||||
angeforderte Spanne übersprungen. Andere gültige Bots werden bearbeitet.
|
||||
Alle normalen Level-, Fraktions-, Gebiets-, Flug- und Instanzregeln bleiben aktiv.
|
||||
Gruppenbildung benötigt eine zulässige Folgeaktivität; diese deshalb zuerst setzen.
|
||||
Gruppenbildung in Stunde23 wird abgewiesen, weil kein Folgeblock existiert.
|
||||
|
||||
Aktuelle Änderungen aktivieren die Tagesplansteuerung und gehen durch den normalen
|
||||
sicheren Intentwechsel. Gruppenführung und laufende Spielerinstanzen behalten ihre
|
||||
normalen Regeln; dieser Befehl teleportiert nicht und löst keine Gruppen auf.
|
||||
Künftige Änderungen lassen den aktuellen Intent laufen. Bei manuell deaktiviertem
|
||||
Tagesplan muss für dessen spätere Ausführung `schedule <guid> on` verwendet werden.
|
||||
Bereits begonnene vorgezogene Gruppenreisen werden durch künftige Änderungen nicht
|
||||
zurückgesetzt. Die betroffene Anzahl und jeder übersprungene Bot werden ausgegeben.
|
||||
|
||||
## Klassischer Gruppentest
|
||||
|
||||
Zuerst die Ziele der Folgestunde setzen, dann die Gruppenbildungsphase. Beispiel
|
||||
bei aktueller Stunde14: Gruppenbildung14–15, Aktivität15–16.
|
||||
|
||||
```
|
||||
clientless adventure editplan all next instance-or-raid auto both 15 85 preview
|
||||
clientless adventure editplan all next instance-or-raid auto both 15 85 apply
|
||||
clientless adventure editplan all current groupformation capital both 15 85 preview
|
||||
clientless adventure editplan all current groupformation capital both 15 85 apply
|
||||
```
|
||||
|
||||
Für einen Test erst ab der nächsten Stunde die ersten beiden Befehle mit `next+1`
|
||||
und die letzten beiden mit `next` verwenden. Bots ohne zulässiges Dungeon-/Raidziel
|
||||
werden bei der Zielvergabe übersprungen; die Ausgabe vorher prüfen.
|
||||
|
||||
Weitere Beispiele:
|
||||
|
||||
```
|
||||
clientless adventure editplan 26 current economy auto alliance 60 60 apply
|
||||
clientless adventure editplan all 18-20 rest auto horde 1 40 preview
|
||||
clientless adventure editplan all next keep poi_guard_22 alliance 15 85 preview
|
||||
clientless adventure plan 26
|
||||
clientless adventure schedule 26 on
|
||||
```
|
||||
|
||||
Browserfreigaben werden von diesen Befehlen nicht verändert. Raid- und
|
||||
Schlachtfeldbeitritt bleiben bei entsprechender Konfiguration deaktiviert.
|
||||
@@ -224,18 +224,13 @@ bool AdventureAreaMgr::AllowsEdge(Player const* player,ClientlessTravelEdge cons
|
||||
if(!source || !target || *sourceEnd || *targetEnd || source>UINT32_MAX || target>UINT32_MAX)return false;
|
||||
sObjectMgr->GetTaxiPath(uint32(source),uint32(target),path,cost);
|
||||
if(!path || path>=sTaxiPathNodesByPath.size())return false;
|
||||
auto const& points=sTaxiPathNodesByPath[path];
|
||||
for(std::size_t i=0;i<points.size() && allowed;++i)
|
||||
{
|
||||
auto p=points[i];if(!p){allowed=false;break;}
|
||||
Position next;next.Relocate(p->Loc.X,p->Loc.Y,p->Loc.Z);
|
||||
allowed=AllowsPosition(player,p->ContinentID,next);
|
||||
if(i && points[i-1] && points[i-1]->ContinentID==p->ContinentID)
|
||||
{
|
||||
auto previous=points[i-1];Position start;start.Relocate(previous->Loc.X,previous->Loc.Y,previous->Loc.Z);
|
||||
allowed=allowed && AllowsSegment(player,p->ContinentID,start,next);
|
||||
}
|
||||
}
|
||||
// Catalog hierarchy governs boarding and arrival, both checked above
|
||||
// through the faction-marked taxi nodes. A native taxi may legitimately
|
||||
// overfly an enemy or otherwise denied zone; sampling its entire spline
|
||||
// every 40 yards both rejected valid travel and blocked the world thread
|
||||
// for seconds for each new level/faction cache key. The native path
|
||||
// lookup remains mandatory, but intermediate airspace is not admission.
|
||||
allowed = true;
|
||||
}
|
||||
// Native transports have no land path across the sea. Their two dock nodes
|
||||
// and explicit node restrictions decide access; portal destinations likewise.
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "ClientlessTravelTelemetry.h"
|
||||
#include "ClientlessTravelGraphMgr.h"
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "AdventurePlanCommand.h"
|
||||
#include "AdventureCombatMgr.h"
|
||||
#include "AdventureEncounterMgr.h"
|
||||
#include "AdventureGroupPolicy.h"
|
||||
@@ -1659,6 +1660,10 @@ bool AdventureBehaviorMgr::Command(ChatHandler * handler, char const * args)
|
||||
std::string op;
|
||||
uint32 guid = 0;
|
||||
in >> op;
|
||||
if (op == "editplan")
|
||||
{
|
||||
std::string args;std::getline(in,args);return EditPlan(handler,args);
|
||||
}
|
||||
if (op == "import") return sAdventureLocationMgr->Import(handler);
|
||||
if (op == "repairimport") return sAdventureLocationMgr->Import(handler,true);
|
||||
if (op == "reload")
|
||||
@@ -2323,3 +2328,101 @@ void AdventureBehaviorMgr::ResumePersonalPlan(Player* player)
|
||||
SetWanted(player,r,r.PersonalIntent,r.PersonalLocation,r.PersonalDeadline);
|
||||
else SetWanted(player,r,"","",0);
|
||||
}
|
||||
|
||||
bool AdventureBehaviorMgr::EditPlan(ChatHandler* handler,std::string const& args)
|
||||
{
|
||||
auto now=GameTime::GetGameTime();auto local=Local(now);auto date=DateOf(local);
|
||||
AdventurePlanCommand::Request request;
|
||||
if(!AdventurePlanCommand::Parse(args,local.tm_hour,request) ||
|
||||
(request.Intent!="keep" && request.Intent!="instance-or-raid" && !Known(request.Intent)))
|
||||
{
|
||||
handler->SendSysMessage("Usage: clientless adventure editplan <guid|all> <current|next|next+N|hour|first-last> <intent|keep|instance-or-raid> <location|auto|capital|keep> <alliance|horde|both> <minLevel> <maxLevel> <preview|apply>. Online AdventureBots only; today, no past hours or midnight wrap.");
|
||||
return false;
|
||||
}
|
||||
if(!sAdventureLocationMgr->EnsureLoaded())return false;
|
||||
struct Change {uint32 Guid;AdventureBehaviorPolicy::Day Plan;};
|
||||
std::vector<Change> changes;
|
||||
std::vector<uint32> guids;for(auto const& entry:_records)guids.push_back(entry.first);
|
||||
std::sort(guids.begin(),guids.end());
|
||||
unsigned matched=0,skipped=0;
|
||||
for(auto guid:guids)
|
||||
{
|
||||
auto session=sWorld->FindClientlessSession(guid);auto player=session?session->GetPlayer():nullptr;
|
||||
if(!player || !player->IsInWorld() || !sClientlessPlayerMgr->IsManaged(guid))continue;
|
||||
if(!AdventurePlanCommand::Matches(request,guid,player->getLevel(),player->GetTeamId()==TEAM_ALLIANCE?1:2))continue;
|
||||
++matched;
|
||||
auto const& original=_records.at(guid);
|
||||
// Preview must not consume random state or modify histories, records or the DB.
|
||||
if(original.Date!=date){++skipped;handler->PSendSysMessage("Skipped GUID %u: today's plan is not initialized.",guid);continue;}
|
||||
auto copy=original;bool valid=true;
|
||||
for(unsigned h=request.First;h<=request.Last;++h)
|
||||
{
|
||||
auto intent=request.Intent=="keep"?copy.Plan[h].Intent:request.Intent;
|
||||
auto location=request.Location=="keep"?copy.Plan[h].Location:request.Location;
|
||||
if(intent=="instance-or-raid")
|
||||
{
|
||||
intent=guid%2?"instance":"raid";
|
||||
if(location=="auto")
|
||||
{
|
||||
location=PickLocation(player,copy,intent);
|
||||
if(location.empty()){intent="instance";location=PickLocation(player,copy,intent);}
|
||||
}
|
||||
}
|
||||
if(location=="auto")location=PickLocation(player,copy,intent);
|
||||
if(location=="capital")
|
||||
{
|
||||
location.clear();
|
||||
for(auto const& candidate:sAdventureLocationMgr->Locations())
|
||||
if((candidate.ZoneId==(player->GetTeamId()==TEAM_ALLIANCE?1519u:1637u)) && Allowed(player,candidate,intent) &&
|
||||
(location.empty() || candidate.Point.Id<location))location=candidate.Point.Id;
|
||||
}
|
||||
auto destination=sAdventureLocationMgr->Find(location);
|
||||
if((h==23 && intent=="groupformation") || !destination || !Allowed(player,*destination,intent))
|
||||
{valid=false;handler->PSendSysMessage("Skipped GUID %u: hour %u has no eligible destination for '%s'.",guid,h,intent.c_str());break;}
|
||||
copy.Plan[h]={intent,location};
|
||||
}
|
||||
for(unsigned h=request.First;valid && h<=request.Last;++h)
|
||||
if(copy.Plan[h].Intent=="groupformation")
|
||||
{
|
||||
auto const& next=copy.Plan[h+1];auto destination=sAdventureLocationMgr->Find(next.Location);
|
||||
if(!AdventureGroupPolicy::Eligible(next.Intent) || !destination || !Allowed(player,*destination,next.Intent))
|
||||
{valid=false;handler->PSendSysMessage("Skipped GUID %u: formation hour %u needs an eligible following activity; edit that hour first.",guid,h);}
|
||||
}
|
||||
// Each bot's requested range is atomic: no partial edit on an invalid slot.
|
||||
if(!valid){++skipped;continue;}
|
||||
changes.push_back({guid,copy.Plan});
|
||||
}
|
||||
if(!request.Preview && !changes.empty())
|
||||
{
|
||||
auto tx=WorldDatabase.BeginTransaction();
|
||||
for(auto const& change:changes)
|
||||
for(unsigned h=request.First;h<=request.Last;++h)
|
||||
{
|
||||
auto const& slot=change.Plan[h];auto key=slot.Location;WorldDatabase.EscapeString(key);
|
||||
tx->PAppend("REPLACE INTO vcore_adventure_day (character_guid,local_date,hour_slot,intent,location_key) VALUES (%u,%u,%u,'%s','%s')",change.Guid,date,h,slot.Intent.c_str(),key.c_str());
|
||||
}
|
||||
WorldDatabase.DirectCommitTransaction(tx);
|
||||
for(auto const& change:changes)
|
||||
{
|
||||
auto session=sWorld->FindClientlessSession(change.Guid);auto player=session?session->GetPlayer():nullptr;
|
||||
if(!player)continue;
|
||||
auto& r=_records.at(change.Guid);r.Plan=change.Plan;
|
||||
if(request.First==unsigned(local.tm_hour))
|
||||
{
|
||||
ClearDeparture(player,r);r.Schedule=true;r.Enabled=true;r.Hour=-1;
|
||||
r.ManualNextIntent.clear();r.ManualNextLocation.clear();
|
||||
auto const& slot=r.Plan[local.tm_hour];auto end=local;end.tm_min=0;end.tm_sec=0;
|
||||
end.tm_hour=local.tm_hour+1;
|
||||
while(end.tm_hour<24 && r.Plan[end.tm_hour].Intent==slot.Intent && r.Plan[end.tm_hour].Location==slot.Location)++end.tm_hour;
|
||||
end.tm_isdst=-1;SetWanted(player,r,slot.Intent,slot.Location,mktime(&end));
|
||||
}
|
||||
else if(r.Departure.At && !r.Departure.Departed(now) && request.First<=unsigned(local.tm_hour+1))
|
||||
ClearDeparture(player,r);
|
||||
}
|
||||
}
|
||||
for(auto const& change:changes)
|
||||
for(unsigned h=request.First;h<=request.Last;++h)
|
||||
handler->PSendSysMessage("%s GUID %u %02u:00-%02u:00 %s %s",request.Preview?"Preview":"Updated",change.Guid,h,h+1,change.Plan[h].Intent.c_str(),change.Plan[h].Location.c_str());
|
||||
handler->PSendSysMessage("Plan edit %s: date=%u onlineMatched=%u eligible=%u skipped=%u hours=%u-%u. Offline bots unchanged; movement uses normal safe handover and group/instance rules.",request.Preview?"preview":"applied",date,matched,uint32(changes.size()),skipped,request.First,request.Last);
|
||||
return true;
|
||||
}
|
||||
@@ -43,6 +43,7 @@ public:
|
||||
std::string InteractionIntent(Player* player) const;
|
||||
std::string InteractionSchedule(Player* player);
|
||||
private:
|
||||
bool EditPlan(ChatHandler* handler, std::string const& args);
|
||||
enum class Phase {Idle, Travel, OnSite, Switching, Handover, WaitingGroup};
|
||||
struct RecoveryScene
|
||||
{
|
||||
|
||||
@@ -130,7 +130,14 @@ void AdventureGroupTravelMgr::SampleLeader(Player* player,Leader& state,time_t n
|
||||
void AdventureGroupTravelMgr::Observe(Player* player,uint32 diff)
|
||||
{
|
||||
auto group=player->GetGroup();
|
||||
if(!group)return;
|
||||
// Group travel installs an owner-specific road adapter and path validator.
|
||||
// Remove both as soon as the player leaves the group; otherwise unrelated
|
||||
// autonomous travel can keep using formation safety rules indefinitely.
|
||||
if(!group)
|
||||
{
|
||||
if(_records.count(player->GetGUID().GetCounter()))Clear(player);
|
||||
return;
|
||||
}
|
||||
auto inserted=_records.try_emplace(player->GetGUID().GetCounter());
|
||||
auto& record=inserted.first->second;
|
||||
if(inserted.second)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#ifndef VCORE_ADVENTURE_PLAN_COMMAND_H
|
||||
#define VCORE_ADVENTURE_PLAN_COMMAND_H
|
||||
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace AdventurePlanCommand
|
||||
{
|
||||
struct Request
|
||||
{
|
||||
unsigned Guid=0, First=0, Last=0, MinLevel=1, MaxLevel=85, Faction=0;
|
||||
bool Preview=true;
|
||||
std::string Intent, Location;
|
||||
};
|
||||
inline bool Number(std::string const& text, unsigned& value)
|
||||
{
|
||||
if(text.empty())return false;
|
||||
unsigned result=0;
|
||||
for(char c:text)
|
||||
{
|
||||
if(c<'0' || c>'9' || result>(std::numeric_limits<unsigned>::max()-unsigned(c-'0'))/10)return false;
|
||||
result=result*10+unsigned(c-'0');
|
||||
}
|
||||
value=result;return true;
|
||||
}
|
||||
inline bool Hour(std::string const& text,unsigned current,unsigned& value)
|
||||
{
|
||||
if(text=="current")value=current;
|
||||
else if(text=="next")value=current+1;
|
||||
else if(text.compare(0,5,"next+")==0)
|
||||
{
|
||||
unsigned offset=0;
|
||||
if(!Number(text.substr(5),offset) || offset>23)return false;
|
||||
value=current+1+offset;
|
||||
}
|
||||
else if(!Number(text,value))return false;
|
||||
return value>=current && value<24;
|
||||
}
|
||||
inline bool Parse(std::string const& args,unsigned current,Request& out)
|
||||
{
|
||||
std::istringstream input(args);
|
||||
std::string target,hours,faction,min,max,mode,extra;
|
||||
Request request;
|
||||
if(!(input>>target>>hours>>request.Intent>>request.Location>>faction>>min>>max>>mode) || input>>extra)return false;
|
||||
if(target!="all" && (!Number(target,request.Guid) || !request.Guid))return false;
|
||||
auto dash=hours.find('-');
|
||||
if(dash==std::string::npos)
|
||||
{
|
||||
if(!Hour(hours,current,request.First))return false;
|
||||
request.Last=request.First;
|
||||
}
|
||||
else if(!Hour(hours.substr(0,dash),current,request.First) || !Hour(hours.substr(dash+1),current,request.Last) || request.Last<request.First)return false;
|
||||
if(faction=="alliance")request.Faction=1;
|
||||
else if(faction=="horde")request.Faction=2;
|
||||
else if(faction!="both")return false;
|
||||
if(!Number(min,request.MinLevel) || !Number(max,request.MaxLevel) || request.MinLevel<1 || request.MaxLevel>85 || request.MinLevel>request.MaxLevel)return false;
|
||||
if(mode=="apply")request.Preview=false;
|
||||
else if(mode!="preview")return false;
|
||||
out=request;return true;
|
||||
}
|
||||
inline bool Matches(Request const& request,unsigned guid,unsigned level,unsigned faction)
|
||||
{
|
||||
return (!request.Guid || request.Guid==guid) && level>=request.MinLevel && level<=request.MaxLevel && (!request.Faction || request.Faction==faction);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -574,6 +574,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
// Reuse all start-point observations only WITHIN this immutable planning
|
||||
// call, including its two search tiers. Never cache live Player paths globally.
|
||||
std::map<ClientlessGraphProbeCache::Key,ClientlessPathDiagnostics> startProbes;
|
||||
std::map<std::pair<ClientlessGraphProbeCache::Key, bool>, float> flightEstimates;
|
||||
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());
|
||||
@@ -621,12 +622,26 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
{ error="destination location denied by area catalog"; return false; }
|
||||
}
|
||||
bool const flightEnabled = sClientlessFlightMgr->Enabled(player);
|
||||
constexpr std::uint64_t MaxFlightEstimatesPerPlan = 16;
|
||||
auto flightCost = [&](ClientlessTravelNode const& from, uint32 map, float x, float y, float z, bool access = false)
|
||||
{
|
||||
if (!flightEnabled || from.MapId != map ||
|
||||
(blockedBridgeKeys && blockedBridgeKeys->count("flight_origin:"+from.Key)))
|
||||
return std::numeric_limits<float>::infinity();
|
||||
Position a,b; a.Relocate(from.X,from.Y,from.Z,from.O); b.Relocate(x,y,z);
|
||||
auto const estimateKey = std::make_pair(
|
||||
ClientlessGraphProbeCache::MakeKey(map, player->GetInstanceId(),
|
||||
from.X, from.Y, from.Z, x, y, z), access);
|
||||
auto cachedEstimate = flightEstimates.find(estimateKey);
|
||||
if (cachedEstimate != flightEstimates.end())
|
||||
return cachedEstimate->second;
|
||||
// Flight links are optional accelerators around the persistent graph.
|
||||
// A pathological graph must not monopolize the world thread while
|
||||
// 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)
|
||||
return std::numeric_limits<float>::infinity();
|
||||
auto begin=std::chrono::steady_clock::now();++trace.FlightCalls;
|
||||
float result=sClientlessFlightMgr->Estimate(player,map,a,b,access);
|
||||
if(access && std::isfinite(result))
|
||||
@@ -637,6 +652,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
40+distance*0.4f:180+distance*1.5f;
|
||||
}
|
||||
trace.FlightUs+=std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now()-begin).count();
|
||||
flightEstimates.emplace(estimateKey, result);
|
||||
return result;
|
||||
};
|
||||
if (startRadius <= 0.0f)
|
||||
@@ -649,6 +665,47 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
bridgeRadius = MaxVirtualNodeBridge;
|
||||
bridgeRadius = std::min(bridgeRadius, MaxVirtualNodeBridge);
|
||||
|
||||
// The overwhelmingly common free-flight case is a same-map Location. Do
|
||||
// not construct a macro graph first: Estimate already enforces licenses,
|
||||
// mount flags, area policy, distance and the complete catalog segment.
|
||||
// Native departure/landing validation still happens in FlightMgr::Start.
|
||||
if (locationGoal && locationGoal->MapId == player->GetMapId())
|
||||
{
|
||||
ClientlessTravelNode departureNode;
|
||||
departureNode.Key = "flight_departure";
|
||||
departureNode.MapId = player->GetMapId();
|
||||
departureNode.X = player->GetPositionX();
|
||||
departureNode.Y = player->GetPositionY();
|
||||
departureNode.Z = player->GetPositionZ();
|
||||
float const directFlight = flightCost(departureNode, locationGoal->MapId,
|
||||
locationGoal->X, locationGoal->Y, locationGoal->Z);
|
||||
if (std::isfinite(directFlight))
|
||||
{
|
||||
ClientlessTravelRouteStep departure;
|
||||
departure.NodeKey = "flight_departure";
|
||||
departure.NodeType = "ground";
|
||||
departure.EdgeType = "start";
|
||||
departure.MapId = player->GetMapId();
|
||||
departure.X = player->GetPositionX();
|
||||
departure.Y = player->GetPositionY();
|
||||
departure.Z = player->GetPositionZ();
|
||||
route.push_back(std::move(departure));
|
||||
|
||||
ClientlessTravelRouteStep flight;
|
||||
flight.NodeKey = "flight_location";
|
||||
flight.NodeType = "location";
|
||||
flight.EdgeType = "free_flight";
|
||||
flight.MapId = locationGoal->MapId;
|
||||
flight.X = locationGoal->X;
|
||||
flight.Y = locationGoal->Y;
|
||||
flight.Z = locationGoal->Z;
|
||||
route.push_back(std::move(flight));
|
||||
complete = true;
|
||||
reachedNode = "flight_location";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
struct State
|
||||
{
|
||||
std::string Node;
|
||||
@@ -1005,6 +1062,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
? std::min(300.0f, locationGoal->AnchorRadius) : 0.0f;
|
||||
std::vector<ExitCandidate> rawExitCandidates;
|
||||
std::unordered_map<std::string, std::vector<ExitCandidate>> exitsByFrom;
|
||||
std::size_t flightExitProbes = 0;
|
||||
if (locationGoal || flightEnabled)
|
||||
{
|
||||
auto const& targetNode = destinationItr->second;
|
||||
@@ -1046,6 +1104,12 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
float travelled = 0.0f;
|
||||
ExitCandidate nearest;
|
||||
nearest.DistanceSq = std::numeric_limits<float>::infinity();
|
||||
struct RankedFlightExit
|
||||
{
|
||||
float LowerBound = 0.0f;
|
||||
ExitCandidate Candidate;
|
||||
};
|
||||
std::vector<RankedFlightExit> rankedFlightExits;
|
||||
for (uint32 i = 1; i + 1 < ordered.size(); ++i)
|
||||
{
|
||||
float dx = ordered[i].X - ordered[i - 1].X;
|
||||
@@ -1078,18 +1142,59 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
blockedBridgeKeys->count("ground_exit_edge:" + identity);
|
||||
candidate.LocalCost = locationGoal && !groundExitExhausted && dsq <= locationAccessRadius*locationAccessRadius
|
||||
? std::sqrt(dsq) : std::numeric_limits<float>::infinity();
|
||||
|
||||
// A recorded edge can contain hundreds of samples. Calling
|
||||
// FlightMgr::Estimate for every sample made the synchronous
|
||||
// world-thread planner quadratic in road geometry (13k+
|
||||
// area checks for one route in production). Keep an exact
|
||||
// ground candidate, but validate only the geometrically best
|
||||
// flight exits below. Estimate still performs all catalog and
|
||||
// native flight-permission checks for those candidates.
|
||||
if (std::isfinite(candidate.LocalCost) &&
|
||||
(!std::isfinite(nearest.DistanceSq) ||
|
||||
candidate.RoadCost + candidate.LocalCost < nearest.RoadCost + nearest.LocalCost))
|
||||
nearest = candidate;
|
||||
|
||||
if (flightEnabled && !(blockedBridgeKeys && blockedBridgeKeys->count("flight_exit:" + exitKey)))
|
||||
{
|
||||
float const direct = std::sqrt(dsq);
|
||||
// Forty plus 0.4 * distance is the cheapest possible
|
||||
// free-flight cost. It is an admissible ordering key;
|
||||
// rejected area segments simply advance to the next item.
|
||||
rankedFlightExits.push_back({ candidate.RoadCost + 40.0f + direct * 0.4f,
|
||||
std::move(candidate) });
|
||||
}
|
||||
}
|
||||
std::sort(rankedFlightExits.begin(), rankedFlightExits.end(),
|
||||
[](RankedFlightExit const& left, RankedFlightExit const& right)
|
||||
{ return left.LowerBound < right.LowerBound; });
|
||||
constexpr std::size_t MaxFlightExitProbesPerEdge = 2;
|
||||
for (std::size_t i = 0; i < std::min(MaxFlightExitProbesPerEdge, rankedFlightExits.size()); ++i)
|
||||
{
|
||||
constexpr std::size_t MaxFlightExitProbesPerPlan = 8;
|
||||
if (flightExitProbes >= MaxFlightExitProbesPerPlan)
|
||||
break;
|
||||
++flightExitProbes;
|
||||
RankedFlightExit const& ranked = rankedFlightExits[i];
|
||||
ExitCandidate candidate = ranked.Candidate;
|
||||
ClientlessTravelNode exitNode;
|
||||
exitNode.MapId=point.MapId;exitNode.X=point.X;exitNode.Y=point.Y;exitNode.Z=point.Z;
|
||||
float const air = blockedBridgeKeys && blockedBridgeKeys->count("flight_exit:"+exitKey)
|
||||
? std::numeric_limits<float>::infinity() : flightCost(exitNode,goalMap,goalX,goalY,goalZ);
|
||||
if (air < candidate.LocalCost) { candidate.LocalCost=air;candidate.Flight=true; }
|
||||
if (!std::isfinite(candidate.LocalCost)) continue;
|
||||
// Never replace a node goal with an unvalidated ground exit.
|
||||
if (!locationGoal && !candidate.Flight) continue;
|
||||
exitNode.MapId = candidate.Point.MapId;
|
||||
exitNode.X = candidate.Point.X;
|
||||
exitNode.Y = candidate.Point.Y;
|
||||
exitNode.Z = candidate.Point.Z;
|
||||
float const air = flightCost(exitNode, goalMap, goalX, goalY, goalZ);
|
||||
if (!std::isfinite(air))
|
||||
continue;
|
||||
candidate.LocalCost = air;
|
||||
candidate.Flight = true;
|
||||
if (std::isfinite(nearest.DistanceSq) &&
|
||||
candidate.RoadCost+candidate.LocalCost >= nearest.RoadCost+nearest.LocalCost) continue;
|
||||
candidate.RoadCost + candidate.LocalCost >= nearest.RoadCost + nearest.LocalCost)
|
||||
continue;
|
||||
nearest = std::move(candidate);
|
||||
}
|
||||
// Never replace a node goal with an unvalidated ground exit.
|
||||
if (!locationGoal && std::isfinite(nearest.DistanceSq) && !nearest.Flight)
|
||||
nearest.DistanceSq = std::numeric_limits<float>::infinity();
|
||||
if (std::isfinite(nearest.DistanceSq))
|
||||
rawExitCandidates.push_back(std::move(nearest));
|
||||
}
|
||||
@@ -1115,6 +1220,29 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
float bestGoalCost = std::numeric_limits<float>::infinity();
|
||||
std::string bestGoalState;
|
||||
bool bestFinalFlight = false;
|
||||
// Free flight is a macro connector, not a reason to turn every graph map
|
||||
// into a complete directed graph. A flyer only needs direct links to the
|
||||
// requested destination and to nodes that enter a map-changing or physical
|
||||
// transport edge (portal, ship, zeppelin or tram). Taxi nodes are excluded:
|
||||
// a free flyer can already reach their same-map destination directly, and
|
||||
// adding every taxi source recreates an almost-complete graph. Ordinary
|
||||
// same-map ground
|
||||
// nodes remain reachable through their recorded graph, while a location
|
||||
// goal is evaluated directly from every expanded node below.
|
||||
std::unordered_set<std::string> flightConnectorNodes;
|
||||
flightConnectorNodes.insert(destinationNode);
|
||||
for (auto const& edgeGroup : _edges)
|
||||
for (ClientlessTravelEdge const& edge : edgeGroup.second)
|
||||
{
|
||||
auto fromItr = _nodes.find(edge.FromNode);
|
||||
auto toItr = _nodes.find(edge.ToNode);
|
||||
bool const changesMap = fromItr != _nodes.end() && toItr != _nodes.end() &&
|
||||
fromItr->second.MapId != toItr->second.MapId;
|
||||
bool const physicalTransport = edge.Type == "portal" || edge.Type == "ship" ||
|
||||
edge.Type == "zeppelin" || edge.Type == "tram" || edge.Type == "transport";
|
||||
if (changesMap || physicalTransport)
|
||||
flightConnectorNodes.insert(edge.FromNode);
|
||||
}
|
||||
// C8.20: a virtual entry is represented in the queue by its FAR
|
||||
// endpoint. Check exits BEFORE that endpoint independently: otherwise
|
||||
// an exit halfway along the road would pay for the entire remaining edge.
|
||||
@@ -1259,6 +1387,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
|
||||
// Persistent TravelGraph edges always reset the bridge guard.
|
||||
auto edgeItr = _edges.find(current.Value.Node);
|
||||
bool hasPersistentContinuation = false;
|
||||
if (edgeItr != _edges.end())
|
||||
{
|
||||
for (ClientlessTravelEdge const& edge : edgeItr->second)
|
||||
@@ -1268,6 +1397,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
continue;
|
||||
if (blockedEdgeIds && edge.DatabaseId && blockedEdgeIds->count(edge.DatabaseId))
|
||||
continue;
|
||||
hasPersistentContinuation = true;
|
||||
|
||||
State next{ edge.ToNode, false };
|
||||
std::string const nextKey = stateKey(next);
|
||||
@@ -1294,9 +1424,12 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
if (flightEnabled && !portalArrival)
|
||||
{
|
||||
auto const& origin=_nodes.at(current.Value.Node);
|
||||
for (auto const& pair:_nodes)
|
||||
for (std::string const& targetKey : flightConnectorNodes)
|
||||
{
|
||||
auto const& target=pair.second;
|
||||
auto targetItr = _nodes.find(targetKey);
|
||||
if (targetItr == _nodes.end())
|
||||
continue;
|
||||
auto const& target=targetItr->second;
|
||||
if (target.Key==origin.Key || target.MapId!=origin.MapId) continue;
|
||||
State next{target.Key,false};std::string key=stateKey(next);
|
||||
auto old=distance.find(key);
|
||||
@@ -1335,7 +1468,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
from.Key.rfind("portal_at_", 0) == 0 &&
|
||||
from.Key.size() >= 7 &&
|
||||
from.Key.compare(from.Key.size() - 7, 7, "_source") == 0;
|
||||
if (!futurePortalSource && persistentHops[currentKey] > 0 &&
|
||||
if (!futurePortalSource && !hasPersistentContinuation && persistentHops[currentKey] > 0 &&
|
||||
!(blockedBridgeKeys &&
|
||||
blockedBridgeKeys->count(MakeLocalBridgeKey(from.Key, "virtual_ground_entry"))))
|
||||
{
|
||||
|
||||
@@ -55,15 +55,15 @@ namespace ObjectAccessor{std::unordered_map<ObjectGuid,Player*>players;Player*Fi
|
||||
namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}}
|
||||
struct Areas{void*LookupEntry(unsigned){return nullptr;}}sAreaTriggerStore;
|
||||
enum class ClientlessActionExecutionState{Moving,SelectingTarget,Completed,Failed};
|
||||
struct Executor{struct Status{ClientlessActionExecutionState State=ClientlessActionExecutionState::Completed;}status;unsigned clears=0;auto GetStatus(unsigned){return &status;}bool RequestStopAtNode(unsigned){return true;}void Clear(unsigned){++clears;status.State=ClientlessActionExecutionState::Completed;}void SetGroundPathAdapter(unsigned,std::function<bool(Player*,std::vector<Position>&)>){}}executor;auto sClientlessActionExecutorMgr=&executor;
|
||||
struct Executor{struct Status{ClientlessActionExecutionState State=ClientlessActionExecutionState::Completed;}status;unsigned clears=0,adapters=0;auto GetStatus(unsigned){return &status;}bool RequestStopAtNode(unsigned){return true;}void Clear(unsigned){++clears;status.State=ClientlessActionExecutionState::Completed;}void SetGroundPathAdapter(unsigned,std::function<bool(Player*,std::vector<Position>&)> adapter){adapters+=adapter?1:-1;}}executor;auto sClientlessActionExecutorMgr=&executor;
|
||||
struct ClientlessAction{std::string Name;std::unordered_map<std::string,std::string>Parameters;};
|
||||
struct Core{unsigned queued=0;std::vector<std::string>nodes;unsigned GetQueuedActionCount(unsigned){return queued;}bool QueueAction(unsigned,ClientlessAction action){++queued;nodes.push_back(action.Parameters["node"]);return true;}void ClearQueuedActions(unsigned){queued=0;}}core;auto sClientlessPlayerMgr=&core;
|
||||
enum class ClientlessTravelState{Moving,Arrived,Stuck,PathRejected};
|
||||
struct ClientlessPathDiagnostics{bool Complete=true,Suspicious=false;float PathLength=20;};
|
||||
struct Travel{struct Status{ClientlessTravelState State=ClientlessTravelState::Arrived;Position Target;}status;bool valid=true;unsigned starts=0,cancels=0;std::vector<Position>targets;
|
||||
struct Travel{struct Status{ClientlessTravelState State=ClientlessTravelState::Arrived;Position Target;}status;bool valid=true;unsigned starts=0,cancels=0,validators=0;std::vector<Position>targets;
|
||||
auto GetStatus(unsigned){return &status;}bool AnalyzePath(Player*,Position const&,ClientlessPathDiagnostics&d){d.Complete=valid;return valid;}
|
||||
bool StartTravel(Player*,Position const&target,bool=true,bool=false){++starts;targets.push_back(target);status.Target=target;status.State=ClientlessTravelState::Moving;return true;}
|
||||
std::vector<Position>const*GetActivePath(unsigned){return nullptr;}void CancelTravel(unsigned){++cancels;status.State=ClientlessTravelState::Arrived;}void SetPathValidator(unsigned,std::function<bool(Player*,std::vector<Position>const&)>){}
|
||||
std::vector<Position>const*GetActivePath(unsigned){return nullptr;}void CancelTravel(unsigned){++cancels;status.State=ClientlessTravelState::Arrived;}void SetPathValidator(unsigned,std::function<bool(Player*,std::vector<Position>const&)> validator){validators+=validator?1:-1;}
|
||||
}travel;auto sClientlessTravelMgr=&travel;
|
||||
struct PathGenerator{struct Point{float x,y,z;};Player*p;std::vector<Point>points;PathGenerator(Player*who):p(who){}bool CalculatePath(float x,float y,float z){points={{p->GetPositionX(),p->GetPositionY(),p->GetPositionZ()},{x,y,z}};return travel.valid;}auto const&GetPath(){return points;}};
|
||||
struct Graph{bool FindNearestNodeToPosition(unsigned,float,float,float,std::string&n,float&distance){n="nearest";distance=5;return true;}}graph;auto sClientlessTravelGraphMgr=&graph;
|
||||
@@ -150,6 +150,12 @@ int main(){
|
||||
leader.taxi=false;executor.status.State=ClientlessActionExecutionState::Completed;
|
||||
// Recorded roads are bounded and validated, never a spline through a cliff.
|
||||
std::vector<Position>road;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);
|
||||
// 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.
|
||||
assert(mgr._records.count(26)&&executor.adapters==2&&travel.validators==2);
|
||||
auto cancelsBefore=travel.cancels;a.g=nullptr;mgr.Observe(&a,500);
|
||||
assert(!mgr._records.count(26)&&executor.adapters==1&&travel.validators==1&&travel.cancels==cancelsBefore+1);
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise the production parser and plan editor, including persistence and preview."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
source=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text()
|
||||
start=source.index('bool AdventureBehaviorMgr::EditPlan(')
|
||||
end=len(source)
|
||||
code=r'''
|
||||
#include "AdventurePlanCommand.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <ctime>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
using uint32=unsigned;constexpr unsigned TEAM_ALLIANCE=0;
|
||||
namespace AdventureGroupPolicy{bool Eligible(std::string const&s){return s=="instance"||s=="raid"||s=="adventure";}}
|
||||
namespace AdventureBehaviorPolicy{struct Slot{std::string Intent,Location;};using Day=std::array<Slot,24>;}
|
||||
struct Player{unsigned guid,level=60,team=0;bool world=true;bool IsInWorld(){return world;}unsigned getLevel(){return level;}unsigned GetTeamId(){return team;}};
|
||||
struct Session{Player* player;Player*GetPlayer(){return player;}};
|
||||
struct World{std::map<unsigned,Session> sessions;Session*FindClientlessSession(unsigned g){auto i=sessions.find(g);return i==sessions.end()?nullptr:&i->second;}} world;auto sWorld=&world;
|
||||
struct Managed{bool IsManaged(unsigned){return true;}} managed;auto sClientlessPlayerMgr=&managed;
|
||||
struct Location{unsigned ZoneId;struct{std::string Id;}Point;};
|
||||
struct Locations{std::vector<Location> locations{{1519,{"sw"}},{1637,{"org"}},{1,{"dungeon"}},{1,{"raid"}},{1,{"forbidden"}}};
|
||||
bool EnsureLoaded(){return true;}auto const& Locations(){return locations;}Location const*Find(std::string const& key){for(auto&l:locations)if(l.Point.Id==key)return &l;return nullptr;}} locations;auto sAdventureLocationMgr=&locations;
|
||||
struct Tx{std::vector<std::string> sql;void PAppend(char const*fmt,...){char buf[1024];va_list ap;va_start(ap,fmt);vsnprintf(buf,sizeof(buf),fmt,ap);va_end(ap);sql.push_back(buf);}};
|
||||
struct Database{Tx tx;unsigned commits=0;Tx*BeginTransaction(){tx.sql.clear();return &tx;}void EscapeString(std::string&){}void DirectCommitTransaction(Tx*&){++commits;}}WorldDatabase;
|
||||
struct ChatHandler{std::vector<std::string> messages;void SendSysMessage(char const*s){messages.push_back(s);}void PSendSysMessage(char const*fmt,...){char buf[1024];va_list ap;va_start(ap,fmt);vsnprintf(buf,sizeof(buf),fmt,ap);va_end(ap);messages.push_back(buf);}};
|
||||
namespace GameTime{long GetGameTime(){return 100000;}}
|
||||
std::tm Local(long){std::tm tm{};tm.tm_year=126;tm.tm_mon=9;tm.tm_mday=31;tm.tm_hour=10;return tm;}
|
||||
unsigned DateOf(std::tm const&){return 20261031;}
|
||||
bool Known(std::string const&s){return s=="social"||s=="adventure"||s=="instance"||s=="raid"||s=="groupformation";}
|
||||
struct AdventureBehaviorMgr{
|
||||
struct DepartureState{long At=0;bool departed=false;bool Departed(long)const{return departed;}};
|
||||
struct Record{unsigned Date=20261031;bool Schedule=true,Enabled=true;int Hour=10;std::string ManualNextIntent,ManualNextLocation;AdventureBehaviorPolicy::Day Plan;DepartureState Departure;unsigned picks=0;};
|
||||
std::map<unsigned,Record>_records;unsigned switches=0,clears=0;
|
||||
bool EditPlan(ChatHandler*,std::string const&);
|
||||
bool Allowed(Player*p,Location const&l,std::string const&i){return l.Point.Id!="forbidden"&&(i!="raid"||p->level>=60)&&(l.Point.Id!="sw"||p->team==0)&&(l.Point.Id!="org"||p->team==1);}
|
||||
std::string PickLocation(Player*p,Record&r,std::string const&i){++r.picks;if(i=="raid"&&p->level<60)return "";return i=="instance"?"dungeon":i=="raid"?"raid":p->team?"org":"sw";}
|
||||
void ClearDeparture(Player*,Record&r){++clears;r.Departure={};}void SetWanted(Player*,Record&,std::string const&,std::string const&,long){++switches;}
|
||||
};
|
||||
'''.replace('struct Locations{','struct LocationCatalog{').replace('}} locations;', '}} locations;')
|
||||
# Class and member names must differ in C++.
|
||||
code+=source[start:end]
|
||||
code+=r'''
|
||||
int main(){
|
||||
using namespace AdventurePlanCommand;Request q;
|
||||
assert(Parse("all current social capital both 1 85 preview",10,q));assert(q.First==10&&q.Last==10&&q.Preview);
|
||||
assert(Parse("43 next+2 raid auto alliance 60 85 apply",10,q));assert(q.First==13&&q.Guid==43&&q.Faction==1);
|
||||
assert(Parse("all 12-15 keep auto horde 1 85 apply",10,q)&&q.Last==15);
|
||||
for(auto s:{"all next social auto both 0 85 apply","all next social auto both 85 1 apply","all next social auto both 1 86 apply","all 9 social auto both 1 85 apply","all next social auto both 1 85 typo","0 next social auto both 1 85 apply","all 14-12 social auto both 1 85 apply","all next social auto both 1 85 apply extra","4294967296 next social auto both 1 85 apply"})assert(!Parse(s,10,q));
|
||||
assert(!Parse("all next social auto both 1 85 apply",23,q));assert(!Parse("all next+23 social auto both 1 85 apply",10,q));
|
||||
assert(Parse("43 next raid auto alliance 60 85 apply",10,q));assert(Matches(q,43,60,1));assert(!Matches(q,44,60,1));assert(!Matches(q,43,59,1));assert(!Matches(q,43,60,2));
|
||||
Player a{43,60,0},b{44,30,1},c{45,60,0};world.sessions={{43,{&a}},{44,{&b}},{45,{&c}}};
|
||||
AdventureBehaviorMgr mgr;for(auto g:{43,44,45}){auto&r=mgr._records[g];for(auto&s:r.Plan)s={"social","sw"};}mgr._records[45].Date=20261030;
|
||||
ChatHandler h;
|
||||
assert(mgr.EditPlan(&h,"all next raid auto both 1 85 preview"));assert(WorldDatabase.commits==0&&mgr.switches==0);assert(mgr._records[43].picks==0&&mgr._records[43].Plan[11].Intent=="social");
|
||||
assert(mgr.EditPlan(&h,"all next raid auto both 1 85 apply"));assert(WorldDatabase.commits==1&&WorldDatabase.tx.sql.size()==1);assert(mgr._records[43].Plan[11].Intent=="raid");assert(mgr._records[44].Plan[11].Intent=="social");assert(mgr.switches==0);
|
||||
assert(mgr.EditPlan(&h,"44 next instance-or-raid auto horde 15 59 apply"));assert(mgr._records[44].Plan[11].Intent=="instance");
|
||||
assert(mgr.EditPlan(&h,"43 12-14 adventure capital alliance 60 60 apply"));assert(WorldDatabase.tx.sql.size()==3);assert(mgr._records[43].Plan[14].Location=="sw");assert(mgr._records[43].Plan[15].Intent=="social");
|
||||
auto commits=WorldDatabase.commits;assert(mgr.EditPlan(&h,"43 next raid forbidden both 1 85 apply"));assert(WorldDatabase.commits==commits);
|
||||
assert(mgr.EditPlan(&h,"43 22-23 groupformation capital both 1 85 apply"));assert(WorldDatabase.commits==commits&&mgr._records[43].Plan[22].Intent=="social");
|
||||
assert(mgr.EditPlan(&h,"43 current adventure capital both 1 85 apply"));assert(mgr.switches==1&&mgr.clears==1&&mgr._records[43].Hour==-1);
|
||||
assert(WorldDatabase.tx.sql[0].find("20261031,10,'adventure','sw'")!=std::string::npos);
|
||||
// Formation validates its next block, not just the meeting location.
|
||||
mgr._records[43].Plan[11]={"social","sw"};auto rejected=WorldDatabase.commits;
|
||||
assert(mgr.EditPlan(&h,"43 current groupformation capital both 1 85 apply"));assert(WorldDatabase.commits==rejected);
|
||||
mgr._records[43].Plan[11]={"raid","raid"};assert(mgr.EditPlan(&h,"43 current groupformation capital both 1 85 apply"));assert(mgr._records[43].Plan[10].Intent=="groupformation");
|
||||
mgr._records[43].Departure.At=1;mgr._records[43].Departure.departed=true;auto cleared=mgr.clears;
|
||||
assert(mgr.EditPlan(&h,"43 next adventure auto both 1 85 apply"));assert(mgr.clears==cleared);
|
||||
mgr._records[43].Departure.departed=false;assert(mgr.EditPlan(&h,"43 next adventure auto both 1 85 apply"));assert(mgr.clears==cleared+1);
|
||||
assert(!mgr.EditPlan(&h,"all current nonsense capital both 1 85 apply"));
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
p=Path(td);(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('Adventure plan commands: PASS')
|
||||
Reference in new issue
Block a user