Improve AdventureBot instance and flight navigation
This commit is contained in:
1 parent
be049baacc
commit
3c361e435a
14 files changed
+525
-63
No files matched your search
@@ -1,3 +1,43 @@
|
||||
# Aktuell: Quest-unabhängige Portalnutzung für AdventureBots ab Stufe 80
|
||||
|
||||
AdventureBots können keinen Questfortschritt erwerben. Graphgebundene öffentliche
|
||||
`portal_use`-Verbindungen gelten deshalb für AdventureBots ab Stufe80 als
|
||||
freigeschaltet. Normale Spieler, Testbots und niedrigere AdventureBots behalten
|
||||
die nativen Spell-/Questbedingungen. Fraktion, Gebietskatalog, Node/Edge-Zugang,
|
||||
sichtbares/erreichbares Portalobjekt und bestätigte Zielankunft bleiben zwingend.
|
||||
Sind native Bedingungen erfüllt, läuft weiterhin der echte CMSG_GAMEOBJ_USE-Pfad.
|
||||
Andernfalls teleportiert ausschließlich der qualifizierte AdventureBot zum beim
|
||||
Import validierten festen Portalziel; der Executor verlangt danach neue Teleport-
|
||||
Sequenz, richtige Karte und maximal5Yards Zielfehler. Portal-, Prerequisite-,
|
||||
Flug- und Ground-Exit-Tests PASS, Build-j10 PASS, installiert. Frischer PID174775
|
||||
ready, Ports8085/8087. Elf Restbots nach Restart ohne alte Flug/Taxi/Transport-
|
||||
Zustände geprüft. Temporäres Elanora-Override entfernt; Loader/Testbetrieb
|
||||
wiederhergestellt (`enabled=1`, `schema=1`, `busy=0`, Pools `50/5/50/5`). Details unter Checkpoint
|
||||
`doc/clientless/checkpoints/2026-11-01-adventure-portal-unlock/`. Kein Commit.
|
||||
|
||||
---
|
||||
|
||||
# Aktuell: generischer Flug-Wiedereinstieg gebaut, installiert und live geprüft
|
||||
|
||||
PlanAdaptiveRoute kann nun jeden erlaubten Graphknoten als begrenzten freien
|
||||
Flug-Wiedereinstieg verwenden, ohne daraus einen vollständigen Luftgraphen zu
|
||||
materialisieren. Makroanschlüsse werden priorisiert, maximal16 Flugschätzungen
|
||||
pro Plan und6 pro Ausgangsknoten; der konkrete Zielknoten wird in jedem lokalen
|
||||
Fenster zuerst geprüft. Portalquellen werden über ihre tatsächlichen ausgehenden
|
||||
`portal`/`portal_use`-Kanten erkannt statt über Namenskonventionen. Am erreichten
|
||||
Portal sind nur diese Kanten zulässig. Regression deckt Flug->beliebig benannter
|
||||
Portalquellknoten->portal_use->Flug ab. Drei gezielte Tests, diff-check und zwei
|
||||
abschließende Builds ausschließlich-j10 bestanden. Installiert; frischer PID
|
||||
172446 ready, Ports8085/8087. Restbots77/153 nach Restart ohne alte Flug- oder
|
||||
Transportzustände. Elanora erreichte live eine gültige freie Flugroute zu einem
|
||||
regulären Zielknoten. Das SW-Twilight-Portal wurde wegen nativer Spell-/Quest-
|
||||
Voraussetzungen für Elanora nicht gewählt, nicht wegen des Plannerbudgets.
|
||||
Temporäres Override entfernt, Schedule wieder an, Loader/Testbetrieb restauriert;
|
||||
Status enabled1/schema1/busy0, Pools50/5/50/5. Details im Checkpoint
|
||||
`doc/clientless/checkpoints/2026-11-01-generic-flight-entry/`. Kein Commit/Push.
|
||||
|
||||
---
|
||||
|
||||
# Aktuell: finaler Gruppenbildungsmanager implementiert und installiert
|
||||
|
||||
Nutzerauftrag: Ab zehn Minuten vor Ende von `groupformation` sollen Botgruppen
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Adventure portal unlock build recovery
|
||||
|
||||
Safe logout was requested while the `-j10` build started in parallel. The build
|
||||
completed successfully. At the five-real-minute installation boundary eleven
|
||||
managed sessions remained (`combat=4`, `transport=0`, `flight/taxi=7`). `saveall`
|
||||
completed before shutdown.
|
||||
|
||||
| GUID | Name | Faction | Saved position | Observed transition/action |
|
||||
|---:|---|---|---|---|
|
||||
| 26 | Arthoran | Alliance | map0 `-11080.00,-483.45,54.49` | failed bounded free flight; mounted hold |
|
||||
| 60 | Ashani | Alliance | map530 `-3013.51,-13204.10,59.63` | native taxi 94 -> 27 |
|
||||
| 77 | Drakogzug | Horde | map1 `1852.91,-4570.06,24.98` | no flight/transport; failed departure recovery |
|
||||
| 80 | Noralin | Alliance | map530 `-3013.51,-13204.10,59.63` | native taxi 94 -> 27 |
|
||||
| 90 | Dorndin | Alliance | map1 `7990.23,1296.75,59.63` | native taxi 27 -> 94 |
|
||||
| 93 | Gargar | Horde | map571 `2277.05,5202.07,12.49` | flight arrived; zeppelin state arrived |
|
||||
| 96 | Darden | Alliance | map0 `-9381.78,-59.96,93.02` | failed bounded free flight; mounted hold |
|
||||
| 97 | Talzul | Horde | map1 `2035.92,-3467.29,171.71` | failed bounded free flight; mounted hold |
|
||||
| 132 | Braneron | Horde | map0 `2602.82,-112.11,34.02` | idle; no flight/transport |
|
||||
| 135 | Ragnhild | Alliance | map1 `7990.23,1296.75,59.63` | native taxi 27 -> 94 |
|
||||
| 153 | Ragnli | Alliance | map0 `-11787.06,-3180.12,-29.71` | idle; no flight/transport |
|
||||
|
||||
Loader configuration and test admission remained disabled/paused through the
|
||||
build, installation and restart window. Post-start recovery outcomes are added
|
||||
below after verification.
|
||||
|
||||
## Post-start verification
|
||||
|
||||
- Fresh worldserver PID 174775 reached ready state; TCP 8085 and 8087 listen.
|
||||
- All eleven recorded characters were loaded. Every prior free-flight, native
|
||||
taxi and transport state was absent after login; ordinary schedules then
|
||||
produced new behavior independently of the saved transition state.
|
||||
- Targeted production-method tests exercise both paths: native GameObject use
|
||||
when prerequisites pass, and the importer-validated fixed teleport when a
|
||||
level-80+ AdventureBot lacks quest prerequisites. Arrival still requires a
|
||||
changed clientless teleport sequence, correct map and <=5-yard target error.
|
||||
- A level-85 Elanora live probe planned and traversed graph-bound capital portal
|
||||
travel. The explicit Twilight Highlands acceptance and its actual native
|
||||
condition state are documented below.
|
||||
|
||||
## Explicit Twilight Highlands acceptance
|
||||
|
||||
Elanora (level 85) was placed within native interaction range of Stormwind spawn
|
||||
200627 and given only target `portal_go_200627_target`. Production navigation
|
||||
log at 2026-11-01 16:57:17 records edge 1801 `portal_use`, spawn 200627, spell
|
||||
85545, followed in the same second by `EDGE_COMPLETE` on map 0 with
|
||||
`arrivalError=0.00`. Runtime area query at the destination resolved area 5140,
|
||||
allowed, level band 84-85. The World DB has no spell-condition rows for 85545,
|
||||
so this particular portal correctly used the unchanged native click path rather
|
||||
than `PORTAL_ADVENTURE_UNLOCK`. The questless unlock path remains covered by the
|
||||
production-method regression. Temporary Elanora profile and override removed.
|
||||
@@ -0,0 +1,103 @@
|
||||
# Generic flight-entry build recovery
|
||||
|
||||
- Safe logout requested at approximately 2026-11-01 12:02 CET after pausing
|
||||
`Clientless.Adventure.Loader` and TestBot admission.
|
||||
- The `-j10` build ran in parallel with the safe drain, per the corrected build
|
||||
policy in `AGENTS.md`, and completed successfully.
|
||||
- At 12:08 CET, more than five real minutes later, 13 managed sessions remained:
|
||||
3 combat flags, 0 active transports and 9 native flight flags. The database
|
||||
positions below are from the immediately preceding online snapshot; native
|
||||
action/flight/transport diagnostics were also captured in the tmux console.
|
||||
|
||||
| GUID | Name | faction | saved map / position | observed state |
|
||||
|---:|---|---|---|---|
|
||||
| 40 | Drakagmok | Horde | 1 / -4777.22 -1264.89 15.78 | native taxi 22->42; no free-flight/transport record |
|
||||
| 48 | Elaniel | Horde | 1 / -4647.84 -1609.87 23.93 | native taxi 22->42; no free-flight/transport record |
|
||||
| 54 | Falaeriel | Horde | 0 / 1875.88 -5018.08 166.81 | native taxi chain; prior free flight arrived |
|
||||
| 77 | Drakogzug | Horde | 1 / 1857.05 -4568.03 24.98 | travel completed; no active flight/transport |
|
||||
| 80 | Noralin | Alliance | 1 / 7990.23 1296.75 59.63 | native taxi; old ship record already Arrived |
|
||||
| 83 | Mahuwa | Horde | 571 / 4234.13 3346.45 391.19 | failed free flight/permission timeout |
|
||||
| 87 | Moggra | Horde | 1 / -3230.20 -1945.24 156.47 | native taxi to node55 |
|
||||
| 91 | Norara | Horde | 571 / 3588.17 6692.83 208.30 | failed bounded flight corridor |
|
||||
| 104 | Lillini | Alliance | 1 / 2669.46 1667.13 393.02 | native taxi; old flight/ship records Arrived |
|
||||
| 118 | Lillina | Alliance | 0 / -8768.04 583.35 133.27 | failed bounded flight corridor |
|
||||
| 132 | Braneron | Horde | 0 / 2539.96 -53.78 28.11 | idle; no flight/transport state |
|
||||
| 146 | Loraria | Horde | 1 / -4215.50 -1913.36 131.73 | native taxi 22->42 |
|
||||
| 153 | Ragnli | Alliance | 0 / -11787.10 -3180.12 -29.71 | idle; no flight/transport state |
|
||||
|
||||
No bot was individually force-logged-out, teleported, detached, or removed to
|
||||
enable the build. Installation/restart uses the documented five-minute clean
|
||||
shutdown exception. Post-start recovery results must be appended below.
|
||||
|
||||
## Restart outcome
|
||||
|
||||
Pending.
|
||||
|
||||
## Second build drain
|
||||
|
||||
- A second safe drain was requested at approximately 12:15 CET for the
|
||||
flight-budget correction. The incremental `-j10` build again ran in parallel
|
||||
and succeeded.
|
||||
- After the five-minute gate, only two sessions remained online; neither had an
|
||||
active free-flight or transport record:
|
||||
|
||||
| GUID | Name | faction | saved map / position | observed state |
|
||||
|---:|---|---|---|---|
|
||||
| 77 | Drakogzug | Horde | 1 / 1852.91 -4570.06 24.98 | completed location travel; outdoors=0 |
|
||||
| 153 | Ragnli | Alliance | 0 / -11787.10 -3180.12 -29.71 | idle; no flight/transport state |
|
||||
|
||||
The other three members of the earlier five-bot snapshot logged out safely
|
||||
before this final snapshot. No individual force logout was used.
|
||||
|
||||
## Third build drain
|
||||
|
||||
- Final macro-connector prioritization build started in parallel with a safe
|
||||
drain at approximately 15:47 CET and completed successfully with `-j10`.
|
||||
- At the 15:52 CET installation gate, GUID 77 Drakogzug and GUID 153 Ragnli
|
||||
remained in combat at the same positions recorded above. Both had zero active
|
||||
free-flight and transport records. They were not interrupted or individually
|
||||
logged out; the documented clean-shutdown exception was used.
|
||||
## Fourth build drain (portal-continuation budget fix)
|
||||
|
||||
- Safe logout requested at approximately 2026-11-01 15:54:30 CET. The `-j10`
|
||||
build started immediately in parallel and completed successfully.
|
||||
- At the five-minute installation boundary, two clientless sessions remained;
|
||||
neither was in free flight, taxi travel or on a transport:
|
||||
- GUID 77, Drakogzug, Horde, map 1 at `(1852.91, -4570.06, 24.98)`, area
|
||||
1637. Action had failed at `flight_departure`; no free-flight or transport
|
||||
state was present.
|
||||
- GUID 153, Ragnli, Alliance, map 0 at `(-11787.06, -3180.12, -29.71)`, area
|
||||
72. Action executor was idle; no free-flight or transport state was present.
|
||||
- GUID 168 Elanora completed her active movement state and logged out safely
|
||||
before the boundary. The temporary route-test Lua override was removed from
|
||||
the source tree before this build.
|
||||
- Testbot admission remained paused. AdventureBot loader remained disabled in
|
||||
runtime configuration throughout the build and restart window.
|
||||
|
||||
## Fifth build drain (destination-first probe fix)
|
||||
|
||||
- Safe logout requested at approximately 2026-11-01 16:04:15 CET. Build
|
||||
`-j10` again ran immediately in parallel and succeeded.
|
||||
- GUID 168 Elanora completed her active free-flight transition and logged out
|
||||
safely. At the five-minute installation boundary GUID 77 Drakogzug and GUID
|
||||
153 Ragnli remained online in the same saved positions recorded above.
|
||||
- `saveall` completed before shutdown. Both remaining bots again reported no
|
||||
free-flight or transport state. Drakogzug had a failed travel action at
|
||||
`flight_departure`; Ragnli was idle. Test admission and the AdventureBot
|
||||
loader remained paused/disabled.
|
||||
|
||||
## Final runtime result
|
||||
|
||||
- Fresh worldserver PID 172446 reached `(worldserver-daemon) ready`; TCP 8085
|
||||
and 8087 are listening.
|
||||
- GUIDs 77 and 153 were loaded after restart. Both had no stale free-flight or
|
||||
transport state and resumed ordinary behavior.
|
||||
- The controlled Elanora plan produced a valid direct free-flight route from a
|
||||
regular graph node to `portal_at_6354_source`; no MMAP attachment was needed.
|
||||
The Stormwind Twilight Highlands portal did not compete because edge 1801
|
||||
correctly failed `PortalPrerequisites` for this character's native spell/
|
||||
quest state. The relevant portal is otherwise the first geometric connector,
|
||||
so this was not a probe-budget failure.
|
||||
- Temporary Elanora override removed. Schedule restored. Loader configuration
|
||||
restored to enabled and test admission resumed. Loader status confirmed
|
||||
`enabled=1 schema=1 busy=0`, pools `50/5/50/5`.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Instance/navigation first-fix build recovery
|
||||
|
||||
- Safe logout requested approximately 2026-11-01 11:26 CET after pausing
|
||||
`Clientless.Adventure.Loader` and TestBot admission.
|
||||
- Build was allowed to run in parallel by the user's explicit clarification;
|
||||
installation/restart remained gated by zero bots or the five-minute limit.
|
||||
- At 11:31 CET, nine managed bots remained after more than five minutes.
|
||||
The server was therefore eligible for a clean shutdown under the agreed rule.
|
||||
- The separately logged-in human `Allitester` (GUID 21) is not a managed bot.
|
||||
|
||||
| GUID | Name | faction | saved map/position | observed state before shutdown |
|
||||
|---:|---|---|---|---|
|
||||
| 40 | Drakagmok | Horde | 1 / -4009.801 -2003.480 121.296 | groupformation; native taxi chain; no free-flight/transport record |
|
||||
| 43 | Fizzira | Alliance | 0 / -8830.011 635.672 119.259 | social; failed held free flight, 94 segments |
|
||||
| 60 | Ashani | Alliance | 1 / 7990.230 1296.750 59.628 | leisure; no flight capability/state |
|
||||
| 77 | Drakogzug | Horde | 1 / 1852.910 -4570.060 24.981 | economy; flight and local travel arrived |
|
||||
| 83 | Mahuwa | Horde | 571 / 2906.229 6057.960 80.671 | profession; zeppelin arrived; free flight failed touchdown confirmation |
|
||||
| 96 | Darden | Alliance | 0 / -9337.900 12.744 105.163 | profession; failed held free flight, 162 segments |
|
||||
| 121 | Braneven | Horde | 0 / 1964.780 -1567.223 123.819 | rest; failed held free flight, 462 segments |
|
||||
| 132 | Braneron | Horde | 0 / 2539.960 -53.784 28.107 | no intent/travel/flight/transport state |
|
||||
| 153 | Ragnli | Alliance | 0 / -11787.057 -3180.120 -29.714 | no intent/travel/flight/transport state |
|
||||
|
||||
Aggregate immediately before diagnostics: nine sessions, three combat and six
|
||||
flight flags. No bot was individually force-logged-out, teleported, detached or
|
||||
removed to enable the build.
|
||||
|
||||
## Restart outcome
|
||||
|
||||
- Build `-j10`, installation and fresh startup succeeded.
|
||||
- Fresh worldserver PID 153100 reached `(worldserver-daemon) ready...`; TCP
|
||||
listeners 8085 and 8087 were verified.
|
||||
- All nine bots were loaded explicitly before restoring the population loader.
|
||||
- None retained an old free-flight or transport controller record. Normal new
|
||||
intent travel/combat resumed, so no recovery teleport was necessary.
|
||||
- Loader and TestBot admission were restored. Population status reported
|
||||
`enabled=1 schema=1 busy=0`, targets `50/5/50/5`; staggered loading resumed.
|
||||
@@ -510,7 +510,11 @@ void AdventureBehaviorMgr::Begin(Player * player, Record & r)
|
||||
if ((r.Intent=="instance" || r.Intent=="raid"))
|
||||
{
|
||||
if (!ResolveInstance(player,r)) {Fallback(player,r);return;}
|
||||
Queue(player,r,"travel_to_location","id",l->BaseKey.empty()?r.Location:l->BaseKey,true);
|
||||
// The entrance is a first-class TravelGraph transition. Route to its
|
||||
// source node instead of an approximate catalog point; the native
|
||||
// AreaTrigger radius remains the final arrival contract.
|
||||
Queue(player,r,"travel_to_node","node",
|
||||
"portal_at_"+std::to_string(r.Instance.Entry)+"_source",true);
|
||||
}
|
||||
else if (l->IsArea)
|
||||
{
|
||||
@@ -1619,11 +1623,36 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff)
|
||||
if (Busy(guid)) return;
|
||||
auto status = sClientlessActionExecutorMgr->GetStatus(guid);
|
||||
auto location = sAdventureLocationMgr->Find(r.Location);
|
||||
bool arrived = status && status->State == ClientlessActionExecutionState::Completed && location && player->GetMapId() == location->Point.MapId;
|
||||
if (arrived && location->IsArea) arrived = location->AreaId &&(player->GetAreaId() == location->AreaId || player->GetZoneId() == location->AreaId) && status->Detail != "best-effort travel completed at nearest useful reachable point";
|
||||
else if (arrived) arrived = player->GetExactDist(location->Point.X, location->Point.Y, location->Point.Z) <= 6;
|
||||
bool const completed=status && status->State==ClientlessActionExecutionState::Completed;
|
||||
bool arrived=completed && location && player->GetMapId()==location->Point.MapId;
|
||||
char const* arrivalContract="catalog-radius";
|
||||
if (arrived && (r.Intent=="instance" || r.Intent=="raid") && r.Instance.Active)
|
||||
{
|
||||
arrivalContract="native-area-trigger";
|
||||
auto entrance=sAreaTriggerStore.LookupEntry(r.Instance.Entry);
|
||||
arrived=entrance && player->GetMapId()==uint32(entrance->ContinentID) &&
|
||||
player->IsInAreaTriggerRadius(entrance);
|
||||
}
|
||||
else if (arrived && location->IsArea)
|
||||
{
|
||||
arrivalContract="area-or-zone";
|
||||
arrived=location->AreaId && (player->GetAreaId()==location->AreaId ||
|
||||
player->GetZoneId()==location->AreaId) &&
|
||||
status->Detail!="best-effort travel completed at nearest useful reachable point";
|
||||
}
|
||||
else if (arrived)
|
||||
arrived=player->GetExactDist(location->Point.X,location->Point.Y,location->Point.Z)<=6;
|
||||
if (! arrived)
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"INTENT_ARRIVAL_REJECT guid=%u intent='%s' location='%s' contract='%s' completed=%u status='%s' map=%u expectedMap=%u distance=%.2f entry=%u inTrigger=%u",
|
||||
guid,r.Intent.c_str(),r.Location.c_str(),arrivalContract,completed?1u:0u,
|
||||
status?status->Detail.c_str():"missing",player->GetMapId(),
|
||||
location?location->Point.MapId:0u,
|
||||
location?player->GetExactDist(location->Point.X,location->Point.Y,location->Point.Z):-1.0f,
|
||||
r.Instance.Active?r.Instance.Entry:0u,
|
||||
(r.Instance.Active && sAreaTriggerStore.LookupEntry(r.Instance.Entry) &&
|
||||
player->IsInAreaTriggerRadius(sAreaTriggerStore.LookupEntry(r.Instance.Entry)))?1u:0u);
|
||||
Fallback(player, r);
|
||||
return;
|
||||
}
|
||||
@@ -1632,7 +1661,7 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff)
|
||||
r.State = Phase::OnSite;
|
||||
r.Detail = "performing intent at destination";
|
||||
sClientlessLuaEngine->UpdateWorldPresence(player,r.Intent,true);
|
||||
TC_LOG_INFO("server.adventurebot", "INTENT_ARRIVED guid=%u intent='%s' location='%s'", guid, r.Intent.c_str(), r.Location.c_str());
|
||||
TC_LOG_INFO("server.adventurebot", "INTENT_ARRIVED guid=%u intent='%s' location='%s' contract='%s'", guid, r.Intent.c_str(), r.Location.c_str(),arrivalContract);
|
||||
}
|
||||
if (r.State == Phase::OnSite)
|
||||
{
|
||||
|
||||
@@ -990,6 +990,12 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
|
||||
}
|
||||
if(!access)
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot.navigation",
|
||||
"ROUTE_ACCESS_DENIED guid=%u name='%s' step=%u/%u node='%s' edgeType='%s' "
|
||||
"edgeDbId=%u nodeAllowed=%u",
|
||||
characterGuid, player->GetName().c_str(), uint32(record.RouteStep + 1),
|
||||
uint32(record.Route.size()), step.NodeKey.c_str(), step.EdgeType.c_str(),
|
||||
step.EdgeDatabaseId, accessNode && sAdventureAreaMgr->AllowsNode(player,*accessNode) ? 1u : 0u);
|
||||
record.Status.State=ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail="route denied by area/node catalog";record.Active=false;
|
||||
return false;
|
||||
@@ -1583,7 +1589,11 @@ bool ClientlessActionExecutorMgr::StartPortalUse(Player* player,
|
||||
record.Status.Detail = reason;
|
||||
return false;
|
||||
};
|
||||
if(portal && !sClientlessTravelGraphMgr->PortalPrerequisites(player,step.EdgeDatabaseId))
|
||||
bool const nativePrerequisites = portal &&
|
||||
sClientlessTravelGraphMgr->PortalNativePrerequisites(player,step.EdgeDatabaseId);
|
||||
bool const adventureUnlock = portal && !nativePrerequisites &&
|
||||
sClientlessTravelGraphMgr->AdventurePortalUnlockAllowed(player,step.EdgeDatabaseId);
|
||||
if(portal && !nativePrerequisites && !adventureUnlock)
|
||||
return fail("native portal prerequisites not satisfied");
|
||||
if (!portal || !record.RouteStep || step.EdgeReverse ||
|
||||
player->GetMapId() != portal->Source.MapId || !player->IsAlive() ||
|
||||
@@ -1626,11 +1636,27 @@ bool ClientlessActionExecutorMgr::StartPortalUse(Player* player,
|
||||
TC_LOG_INFO("server.adventurebot.navigation", "PORTAL_USE_START guid=%u edge=%u spawn=%u entry=%u spell=%u sourceMap=%u targetMap=%u",
|
||||
characterGuid, step.EdgeDatabaseId, portal->SpawnId, portal->Entry, portal->SpellId,
|
||||
portal->Source.MapId, step.MapId);
|
||||
// Same packet handler as a player's click. Native Use/AI, party checks,
|
||||
// spell cast restrictions and scripted conditions remain authoritative.
|
||||
WorldPacket usePacket(CMSG_GAMEOBJ_USE, 8);
|
||||
usePacket << object->GetGUID();
|
||||
player->GetSession()->HandleGameObjectUseOpcode(usePacket);
|
||||
if (adventureUnlock)
|
||||
{
|
||||
// These performative bots never complete quests. Reproduce the fixed,
|
||||
// importer-validated destination of the bound public portal while the
|
||||
// normal executor still verifies source object, range, faction/area
|
||||
// policy and the resulting teleport sequence/arrival coordinates.
|
||||
TC_LOG_INFO("server.adventurebot.navigation",
|
||||
"PORTAL_ADVENTURE_UNLOCK guid=%u level=%u edge=%u spawn=%u",
|
||||
characterGuid, player->getLevel(), step.EdgeDatabaseId, portal->SpawnId);
|
||||
if (!player->TeleportTo(portal->Target.MapId, portal->Target.X,
|
||||
portal->Target.Y, portal->Target.Z, portal->Target.O))
|
||||
return fail("adventure portal unlock teleport failed");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Same packet handler as a player's click. Native Use/AI, party checks,
|
||||
// spell cast restrictions and scripted conditions remain authoritative.
|
||||
WorldPacket usePacket(CMSG_GAMEOBJ_USE, 8);
|
||||
usePacket << object->GetGUID();
|
||||
player->GetSession()->HandleGameObjectUseOpcode(usePacket);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2664,7 +2690,10 @@ void ClientlessActionExecutorMgr::UpdateTravelToNode(
|
||||
float dz = player->GetPositionZ() - destination.Z;
|
||||
float remaining = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
|
||||
if (remaining > 2.0f)
|
||||
bool const portalApproach = record.RouteStep + 1 < record.Route.size() &&
|
||||
record.Route[record.RouteStep + 1].EdgeType == "portal_use";
|
||||
float const arrivalTolerance = portalApproach ? 5.0f : 2.0f;
|
||||
if (remaining > arrivalTolerance)
|
||||
{
|
||||
uint32 const failedEdgeId =
|
||||
record.RouteStep < record.Route.size()
|
||||
|
||||
@@ -66,6 +66,18 @@ namespace
|
||||
if(!ClearRay(player,{x+offset.X,y+offset.Y,surface.Ground+0.8f},{x+offset.X,y+offset.Y,top}))return false;
|
||||
return true;
|
||||
}
|
||||
bool OpenColumnFrom(Player* player,float x,float y,float z,float height)
|
||||
{
|
||||
// A height query from MAX_HEIGHT can legitimately return a bridge,
|
||||
// tree canopy or other collision surface far above the player. Such a
|
||||
// surface is useful as an obstacle, but it is not the player's support
|
||||
// floor. Test the actual take-off column from the player's feet.
|
||||
if (!std::isfinite(z) || height < 5.0f) return false;
|
||||
for (auto offset : std::array<Point,5>{{{0,0,0},{2,0,0},{-2,0,0},{0,2,0},{0,-2,0}}})
|
||||
if (!ClearRay(player,{x+offset.X,y+offset.Y,z+0.8f},
|
||||
{x+offset.X,y+offset.Y,z+height})) return false;
|
||||
return true;
|
||||
}
|
||||
char const * StateName(ClientlessFlightState state)
|
||||
{
|
||||
switch (state)
|
||||
@@ -398,13 +410,30 @@ bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::s
|
||||
bool const airborne=existingMount && origin.Valid && player->GetPositionZ()>=origin.Ground+0.7f &&
|
||||
player->GetPositionZ()+5<=origin.Ceiling && ClearRay(player,PointOf(*player),{player->GetPositionX(),player->GetPositionY(),player->GetPositionZ()+5});
|
||||
float const feet=player->GetMap()->GetHeight(player->GetPhaseShift(),player->GetPositionX(),player->GetPositionY(),player->GetPositionZ()+2,true,6);
|
||||
if (!origin.Valid || (!airborne && (!std::isfinite(feet) || feet<=INVALID_HEIGHT+1 || std::fabs(origin.Ground-player->GetPositionZ())>3 || std::fabs(feet-player->GetPositionZ())>3)))
|
||||
bool const localSupport=std::isfinite(feet) && feet>INVALID_HEIGHT+1 &&
|
||||
std::fabs(feet-player->GetPositionZ())<=3;
|
||||
bool const overheadSurface=origin.Valid && origin.Ground>player->GetPositionZ()+3;
|
||||
bool const openFromFeet=overheadSurface && OpenColumnFrom(player,player->GetPositionX(),
|
||||
player->GetPositionY(),player->GetPositionZ(),
|
||||
std::min(record.Settings.Clearance,origin.Ground-player->GetPositionZ()-1.0f));
|
||||
// Not falling/in water was verified above. If the global height sample
|
||||
// selected a remote surface above us, a clear local vertical column is a
|
||||
// valid departure even though that remote surface cannot be our floor.
|
||||
bool const supportedDeparture=localSupport || openFromFeet;
|
||||
if (!origin.Valid || (!airborne && !supportedDeparture))
|
||||
{
|
||||
if(std::isfinite(feet) && feet>INVALID_HEIGHT+1 && std::fabs(feet-player->GetPositionZ())<=3 && ApproachDeparture(player,record,following))return true;
|
||||
if(localSupport && ApproachDeparture(player,record,following))return true;
|
||||
error = "departure-unavailable: no supported open takeoff point";
|
||||
TC_LOG_WARN("server.adventurebot.navigation","FLIGHT_DEPARTURE_REJECT guid=%u z=%.3f surface=%.3f feet=%.3f valid=%u mounted=%u canFly=%u",player->GetGUID().GetCounter(),player->GetPositionZ(),origin.Ground,feet,origin.Valid?1:0,player->IsMounted()?1:0,player->CanFly()?1:0);
|
||||
TC_LOG_WARN("server.adventurebot.navigation","FLIGHT_DEPARTURE_REJECT guid=%u z=%.3f surface=%.3f feet=%.3f valid=%u localSupport=%u overhead=%u openFromFeet=%u mounted=%u canFly=%u",
|
||||
player->GetGUID().GetCounter(),player->GetPositionZ(),origin.Ground,feet,
|
||||
origin.Valid?1:0,localSupport?1:0,overheadSurface?1:0,openFromFeet?1:0,
|
||||
player->IsMounted()?1:0,player->CanFly()?1:0);
|
||||
return false;
|
||||
}
|
||||
if(openFromFeet)
|
||||
ClientlessTravelTelemetry::Note(player,"flight-departure-overhead-surface",
|
||||
"z="+std::to_string(player->GetPositionZ())+" surface="+std::to_string(origin.Ground)+
|
||||
" feet="+std::to_string(feet));
|
||||
bool foundLanding = false;
|
||||
std::array<uint32,5> landingRejected{}; // surface, water, column, MMAP, area
|
||||
auto landingSummary=[&]()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "ClientlessTravelGraphMgr.h"
|
||||
#include "ClientlessTransportMgr.h"
|
||||
#include "ClientlessFlightMgr.h"
|
||||
#include "ClientlessLuaEngine.h"
|
||||
|
||||
#include "DatabaseEnv.h"
|
||||
#include "Log.h"
|
||||
@@ -125,7 +126,7 @@ ClientlessPortalUse const* ClientlessTravelGraphMgr::GetPortalUse(uint32 edgeDat
|
||||
return itr != _portalUses.end() ? &itr->second : nullptr;
|
||||
}
|
||||
|
||||
bool ClientlessTravelGraphMgr::PortalPrerequisites(Player const* player,uint32 edgeDatabaseId) const
|
||||
bool ClientlessTravelGraphMgr::PortalNativePrerequisites(Player const* player,uint32 edgeDatabaseId) const
|
||||
{
|
||||
auto portal=GetPortalUse(edgeDatabaseId);
|
||||
auto info=portal?sObjectMgr->GetGameObjectTemplate(portal->Entry):nullptr;
|
||||
@@ -136,6 +137,23 @@ bool ClientlessTravelGraphMgr::PortalPrerequisites(Player const* player,uint32 e
|
||||
return !portal->Requirements.empty();
|
||||
}
|
||||
|
||||
bool ClientlessTravelGraphMgr::AdventurePortalUnlockAllowed(Player const* player,
|
||||
uint32 edgeDatabaseId) const
|
||||
{
|
||||
// AdventureBots cannot acquire quest progress. Once they have reached the
|
||||
// Cataclysm level range, graph-bound public travel portals are therefore
|
||||
// considered unlocked. This does not bypass faction/area/node policy or
|
||||
// make arbitrary, unbound GameObjects usable.
|
||||
return player && player->getLevel() >= 80 && GetPortalUse(edgeDatabaseId) &&
|
||||
sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter());
|
||||
}
|
||||
|
||||
bool ClientlessTravelGraphMgr::PortalPrerequisites(Player const* player,uint32 edgeDatabaseId) const
|
||||
{
|
||||
return PortalNativePrerequisites(player,edgeDatabaseId) ||
|
||||
AdventurePortalUnlockAllowed(player,edgeDatabaseId);
|
||||
}
|
||||
|
||||
bool ClientlessTravelGraphMgr::Reload()
|
||||
{
|
||||
_positionProbes.Clear();
|
||||
@@ -623,6 +641,10 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
}
|
||||
bool const flightEnabled = sClientlessFlightMgr->Enabled(player);
|
||||
constexpr std::uint64_t MaxFlightEstimatesPerPlan = 16;
|
||||
// Preserve part of the global budget for nodes reached later in the
|
||||
// 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)
|
||||
{
|
||||
if (!flightEnabled || from.MapId != map ||
|
||||
@@ -815,9 +837,15 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
ClientlessTravelNode departure;
|
||||
departure.Key="flight_departure";
|
||||
departure.MapId=player->GetMapId();departure.X=player->GetPositionX();departure.Y=player->GetPositionY();departure.Z=player->GetPositionZ();
|
||||
float air=blockedBridgeKeys && blockedBridgeKeys->count("flight:flight_departure->"+node.Key)?
|
||||
std::numeric_limits<float>::infinity():flightCost(departure,node.MapId,node.X,node.Y,node.Z,!reachable);
|
||||
if(std::isfinite(air) && (!reachable || air<entryNav.PathLength))
|
||||
// A valid local MMAP entry is the normal, organic graph access and
|
||||
// must not spend the bounded flight-estimate budget. Free flight is
|
||||
// the recovery/re-entry mode for nodes that local navigation cannot
|
||||
// reach, not a shortcut racing every nearby pavement node.
|
||||
float air = std::numeric_limits<float>::infinity();
|
||||
if (!reachable && !(blockedBridgeKeys &&
|
||||
blockedBridgeKeys->count("flight:flight_departure->"+node.Key)))
|
||||
air=flightCost(departure,node.MapId,node.X,node.Y,node.Z,true);
|
||||
if(std::isfinite(air) && !reachable)
|
||||
{flightRoots.insert(node.Key);startCost=air;}
|
||||
else
|
||||
{
|
||||
@@ -1220,29 +1248,37 @@ 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)
|
||||
// Every permitted node is a potential way back into the persistent graph.
|
||||
// Do not materialize an all-to-all air graph, though: candidates are ranked
|
||||
// on demand below and the shared per-plan Estimate cap remains authoritative.
|
||||
// This is important for isolated anchors such as the capital Cataclysm
|
||||
// portals, but is deliberately not portal-specific.
|
||||
auto flightRemainderLowerBound = [&](ClientlessTravelNode const& candidate)
|
||||
{
|
||||
ClientlessTravelNode const& destination = destinationItr->second;
|
||||
auto lowerBoundToGoal = [&](ClientlessTravelNode const& node)
|
||||
{
|
||||
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);
|
||||
}
|
||||
if (node.MapId != destination.MapId)
|
||||
return std::numeric_limits<float>::infinity();
|
||||
float dx = destination.X-node.X, dy = destination.Y-node.Y, dz = destination.Z-node.Z;
|
||||
return 40.0f + std::sqrt(dx*dx+dy*dy+dz*dz)*0.4f;
|
||||
};
|
||||
float best = lowerBoundToGoal(candidate);
|
||||
auto edges = _edges.find(candidate.Key);
|
||||
if (edges != _edges.end())
|
||||
for (ClientlessTravelEdge const& edge : edges->second)
|
||||
{
|
||||
if ((!RescueEdgeAllowed(edge) || !sAdventureAreaMgr->AllowsEdge(player,edge)))
|
||||
continue;
|
||||
if (blockedEdgeIds && edge.DatabaseId && blockedEdgeIds->count(edge.DatabaseId))
|
||||
continue;
|
||||
auto next = _nodes.find(edge.ToNode);
|
||||
if (next == _nodes.end())
|
||||
continue;
|
||||
best = std::min(best, edge.Cost + lowerBoundToGoal(next->second));
|
||||
}
|
||||
return best;
|
||||
};
|
||||
// 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.
|
||||
@@ -1379,10 +1415,11 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
// ground transit. Once reached during a planned route, its only legal
|
||||
// continuation is the portal edge itself. Physical AreaTriggers may
|
||||
// fire even before the route executor advances to that edge.
|
||||
bool const isPortalSource =
|
||||
current.Value.Node.rfind("portal_at_", 0) == 0 &&
|
||||
current.Value.Node.size() >= 7 &&
|
||||
current.Value.Node.compare(current.Value.Node.size() - 7, 7, "_source") == 0;
|
||||
auto const sourceEdges = _edges.find(current.Value.Node);
|
||||
bool const isPortalSource = sourceEdges != _edges.end() &&
|
||||
std::any_of(sourceEdges->second.begin(), sourceEdges->second.end(),
|
||||
[](ClientlessTravelEdge const& edge)
|
||||
{ return edge.Type == "portal" || edge.Type == "portal_use"; });
|
||||
bool const portalArrival = isPortalSource && previous.find(currentKey) != previous.end();
|
||||
|
||||
// Persistent TravelGraph edges always reset the bridge guard.
|
||||
@@ -1393,7 +1430,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
for (ClientlessTravelEdge const& edge : edgeItr->second)
|
||||
{
|
||||
if ((!RescueEdgeAllowed(edge) || !sAdventureAreaMgr->AllowsEdge(player,edge))) continue;
|
||||
if (portalArrival && edge.Type != "portal")
|
||||
if (portalArrival && edge.Type != "portal" && edge.Type != "portal_use")
|
||||
continue;
|
||||
if (blockedEdgeIds && edge.DatabaseId && blockedEdgeIds->count(edge.DatabaseId))
|
||||
continue;
|
||||
@@ -1421,15 +1458,15 @@ 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)
|
||||
if (flightEnabled && !portalArrival && trace.FlightCalls < MaxFlightEstimatesPerPlan)
|
||||
{
|
||||
auto const& origin=_nodes.at(current.Value.Node);
|
||||
for (std::string const& targetKey : flightConnectorNodes)
|
||||
std::vector<std::pair<float, std::string>> macroFlightCandidates;
|
||||
std::vector<std::pair<float, std::string>> flightCandidates;
|
||||
flightCandidates.reserve(_nodes.size());
|
||||
for (auto const& candidate : _nodes)
|
||||
{
|
||||
auto targetItr = _nodes.find(targetKey);
|
||||
if (targetItr == _nodes.end())
|
||||
continue;
|
||||
auto const& target=targetItr->second;
|
||||
auto const& target=candidate.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);
|
||||
@@ -1441,6 +1478,58 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
if(old!=distance.end() && old->second<=current.Cost+lowerBound)continue;
|
||||
if ((!RescueNodeAllowed(target) || !sAdventureAreaMgr->AllowsNode(player,target))) continue;
|
||||
if (blockedBridgeKeys && blockedBridgeKeys->count("flight:"+origin.Key+"->"+target.Key)) continue;
|
||||
float const remainder = flightRemainderLowerBound(target);
|
||||
if (!std::isfinite(remainder))
|
||||
continue;
|
||||
bool macroConnector = target.Key == destinationNode;
|
||||
auto outgoing = _edges.find(target.Key);
|
||||
if (outgoing != _edges.end())
|
||||
for (ClientlessTravelEdge const& edge : outgoing->second)
|
||||
{
|
||||
auto next = _nodes.find(edge.ToNode);
|
||||
bool const changesMap = next != _nodes.end() &&
|
||||
next->second.MapId != target.MapId;
|
||||
bool const physical = edge.Type == "portal" ||
|
||||
edge.Type == "portal_use" || edge.Type == "ship" ||
|
||||
edge.Type == "zeppelin" || edge.Type == "tram" ||
|
||||
edge.Type == "transport";
|
||||
if (changesMap || physical)
|
||||
{
|
||||
macroConnector = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto& candidates = macroConnector ? macroFlightCandidates : flightCandidates;
|
||||
candidates.emplace_back(lowerBound + remainder, target.Key);
|
||||
}
|
||||
std::sort(macroFlightCandidates.begin(), macroFlightCandidates.end(),
|
||||
[&](auto const& left, auto const& right)
|
||||
{
|
||||
// Every newly reached portal/transport endpoint must get
|
||||
// one chance to finish the requested journey before the
|
||||
// bounded probe window is spent on further connectors.
|
||||
bool const leftIsDestination = left.second == destinationNode;
|
||||
bool const rightIsDestination = right.second == destinationNode;
|
||||
if (leftIsDestination != rightIsDestination)
|
||||
return leftIsDestination;
|
||||
return left < right;
|
||||
});
|
||||
std::sort(flightCandidates.begin(), flightCandidates.end());
|
||||
macroFlightCandidates.insert(macroFlightCandidates.end(),
|
||||
flightCandidates.begin(), flightCandidates.end());
|
||||
std::uint64_t const callsAtOrigin = trace.FlightCalls;
|
||||
for (auto const& ranked : macroFlightCandidates)
|
||||
{
|
||||
if (trace.FlightCalls >= MaxFlightEstimatesPerPlan ||
|
||||
trace.FlightCalls - callsAtOrigin >= MaxFlightEstimatesPerOrigin)
|
||||
break;
|
||||
auto const& target = _nodes.at(ranked.second);
|
||||
State next{target.Key,false};std::string key=stateKey(next);
|
||||
auto old=distance.find(key);
|
||||
auto const blockedFlight = blockedBridgeKeys &&
|
||||
blockedBridgeKeys->count("flight:"+origin.Key+"->"+target.Key);
|
||||
if (blockedFlight)
|
||||
continue;
|
||||
float cost=flightCost(origin,target.MapId,target.X,target.Y,target.Z);
|
||||
if (!std::isfinite(cost)) continue;
|
||||
float score=current.Cost+cost;
|
||||
@@ -1464,10 +1553,11 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
// road at an MMAP-reachable waypoint. Only remaining portions of
|
||||
// stored, directed ground edges are admitted; no off-graph shortcut.
|
||||
// A portal source may not be treated as an ordinary intermediate node.
|
||||
bool const futurePortalSource =
|
||||
from.Key.rfind("portal_at_", 0) == 0 &&
|
||||
from.Key.size() >= 7 &&
|
||||
from.Key.compare(from.Key.size() - 7, 7, "_source") == 0;
|
||||
auto const futureEdges = _edges.find(from.Key);
|
||||
bool const futurePortalSource = futureEdges != _edges.end() &&
|
||||
std::any_of(futureEdges->second.begin(), futureEdges->second.end(),
|
||||
[](ClientlessTravelEdge const& edge)
|
||||
{ return edge.Type == "portal" || edge.Type == "portal_use"; });
|
||||
if (!futurePortalSource && !hasPersistentContinuation && persistentHops[currentKey] > 0 &&
|
||||
!(blockedBridgeKeys &&
|
||||
blockedBridgeKeys->count(MakeLocalBridgeKey(from.Key, "virtual_ground_entry"))))
|
||||
|
||||
@@ -177,6 +177,8 @@ public:
|
||||
|
||||
static bool ResolvePortalUse(uint32 spawnId, ClientlessPortalUse& portal, std::string& error);
|
||||
ClientlessPortalUse const* GetPortalUse(uint32 edgeDatabaseId) const;
|
||||
bool PortalNativePrerequisites(Player const* player,uint32 edgeDatabaseId) const;
|
||||
bool AdventurePortalUnlockAllowed(Player const* player,uint32 edgeDatabaseId) const;
|
||||
bool PortalPrerequisites(Player const* player,uint32 edgeDatabaseId) const;
|
||||
|
||||
private:
|
||||
|
||||
@@ -3980,6 +3980,7 @@ Appender.Console=1,3,0
|
||||
Appender.Server=2,2,0,Server.log,w
|
||||
Appender.GM=2,2,15,gm/gm_%s.log
|
||||
Appender.DBErrors=2,2,0,DBErrors.log
|
||||
Appender.AdventureBotNavigation=2,2,7,adventurebot-navigation.log,a,67108864
|
||||
|
||||
# Logger config values: Given a logger "name"
|
||||
# Logger.name
|
||||
@@ -4005,6 +4006,7 @@ Logger.commands.gm=3,Console GM
|
||||
Logger.scripts.hotswap=3,Console Server
|
||||
Logger.sql.sql=5,Console DBErrors
|
||||
Logger.sql.updates=3,Console Server
|
||||
Logger.server.adventurebot.navigation=2,AdventureBotNavigation
|
||||
#Logger.mmaps=3,Server
|
||||
|
||||
#Logger.achievement=3,Console Server
|
||||
|
||||
@@ -22,6 +22,19 @@ assert "r.GroupOverride" in follower_hold
|
||||
assert "r.Dirty=false" in follower_hold
|
||||
assert "GROUP_MEMBER_FALLBACK_HOLD" in follower_hold
|
||||
|
||||
begin_start = source.index("void AdventureBehaviorMgr::Begin(")
|
||||
begin_end = source.index("void AdventureBehaviorMgr::Fallback(", begin_start)
|
||||
begin = source[begin_start:begin_end]
|
||||
assert '"portal_at_"+std::to_string(r.Instance.Entry)+"_source"' in begin
|
||||
assert 'Queue(player,r,"travel_to_node","node"' in begin
|
||||
|
||||
update_start = source.index("void AdventureBehaviorMgr::Update(Player * player")
|
||||
update_end = source.index("void AdventureBehaviorMgr::FinishInstance(", update_start)
|
||||
update = source[update_start:update_end]
|
||||
assert 'arrivalContract="native-area-trigger"' in update
|
||||
assert "player->IsInAreaTriggerRadius(entrance)" in update
|
||||
assert "INTENT_ARRIVAL_REJECT" in update
|
||||
|
||||
protect_start = source.index("bool AdventureBehaviorMgr::ProtectGroupLeave(")
|
||||
protect_end = source.index("void AdventureBehaviorMgr::InterruptForInteraction(", protect_start)
|
||||
protection = source[protect_start:protect_end]
|
||||
@@ -35,4 +48,9 @@ for invariant in (
|
||||
):
|
||||
assert invariant in protection
|
||||
|
||||
print("Instance visit retention, inherited group-target hold and formation leave protection passed.")
|
||||
flight = (root / "src/server/game/World/ClientlessFlightMgr.cpp").read_text()
|
||||
assert "OpenColumnFrom" in flight
|
||||
assert "overheadSurface" in flight
|
||||
assert "flight-departure-overhead-surface" in flight
|
||||
|
||||
print("Instance graph entrance/arrival contract, visit retention, flight overhead handling, group-target hold and formation leave protection passed.")
|
||||
@@ -34,12 +34,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 {
|
||||
bool enabled=false;float minimumX=-10000;
|
||||
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){
|
||||
float d=dist(a,b);return enabled&&map==0&&a.x>=minimumX&&(recovery||d>=60)?(recovery?d:180+d*1.5f):std::numeric_limits<float>::infinity();
|
||||
bool forbiddenPair=std::fabs(a.x-forbiddenPairFromX)<0.1f&&std::fabs(b.x-forbiddenPairTargetX)<0.1f;
|
||||
float d=dist(a,b);return enabled&&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<float>::infinity();
|
||||
}
|
||||
} flight;
|
||||
auto sClientlessFlightMgr=&flight;
|
||||
@@ -162,6 +164,29 @@ travel.complete=true;route.clear();
|
||||
assert(accessFlight.PlanAdaptiveRoute(&start,"B",400,200,route,done,endKey,why,nullptr,&deniedFlightRoots));
|
||||
assert(route.front().NodeKey!="flight_departure");
|
||||
travel.complete=true;
|
||||
// Any allowed graph node can be a bounded, on-demand flight re-entry. The
|
||||
// planner must not require a portal/transport node type or an MMAP bridge.
|
||||
flight.forbiddenTargetX=2000;
|
||||
ClientlessTravelGraphMgr genericEntry;node(genericEntry,"A",0);node(genericEntry,"R",500);node(genericEntry,"B",2000);
|
||||
edge(genericEntry,"R","B",88,10,false,"ground");
|
||||
travel.complete=false;route.clear();
|
||||
assert(genericEntry.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why)&&done);
|
||||
assert(route.size()==3&&route[0].NodeKey=="A"&&route[1].NodeKey=="R");
|
||||
assert(route[1].EdgeType=="free_flight"&&route[2].EdgeDatabaseId==88);
|
||||
flight.forbiddenTargetX=1e9;travel.complete=true;
|
||||
// Keep probe budget for a later graph state: fly to a generic portal source,
|
||||
// cross it, and then fly to the destination. A rejected direct flight must
|
||||
// not consume all estimates at the initial node.
|
||||
flight.forbiddenPairFromX=0;flight.forbiddenPairTargetX=2000;
|
||||
ClientlessTravelGraphMgr portalEntry;node(portalEntry,"A",0);node(portalEntry,"P",500);
|
||||
node(portalEntry,"Q",1500);node(portalEntry,"B",2000);
|
||||
edge(portalEntry,"P","Q",89,1,false,"portal_use");
|
||||
travel.complete=false;route.clear();
|
||||
assert(portalEntry.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why)&&done);
|
||||
assert(route.size()==4&&route[1].NodeKey=="P"&&route[1].EdgeType=="free_flight");
|
||||
assert(route[2].NodeKey=="Q"&&route[2].EdgeType=="portal_use");
|
||||
assert(route[3].NodeKey=="B"&&route[3].EdgeType=="free_flight");
|
||||
flight.forbiddenPairFromX=flight.forbiddenPairTargetX=1e9;travel.complete=true;
|
||||
std::cout<<"ground planner regressions and dynamic flight/node/location/virtual-exit integration passed\n";
|
||||
}
|
||||
'''
|
||||
|
||||
@@ -77,7 +77,7 @@ struct Flight {
|
||||
bool enabled=false,starts=false,recovery=false;int launches=0;FlightStatus status;
|
||||
void BeginPlanning(){}
|
||||
float Estimate(Player*,uint32,Position const&,Position const&,bool fallback=false){recovery=fallback;return enabled?100:std::numeric_limits<float>::infinity();}
|
||||
bool Start(Player*,Position const&,std::string&){++launches;return starts;}
|
||||
bool Start(Player*,Position const&,std::string&,bool=false){++launches;return starts;}
|
||||
FlightStatus const* GetStatus(uint32){return &status;}
|
||||
} flight;
|
||||
auto sClientlessFlightMgr=&flight;
|
||||
|
||||
@@ -68,6 +68,8 @@ struct Player {
|
||||
bool IsInPhase(GameObject*){return phase;}bool CanSeeOrDetect(GameObject*){return visible;}
|
||||
GameObject* GetGameObjectIfCanInteractWith(int guid){return inRange&&guid==go.guid?&go:nullptr;}
|
||||
float GetPositionX(){return x;}float GetPositionY(){return y;}float GetPositionZ(){return z;}
|
||||
unsigned getLevel(){return 85;}
|
||||
bool TeleportTo(uint32 map,float tx,float ty,float tz,float){pending=true;world=false;mapId=map;x=tx;y=ty;z=tz;return true;}
|
||||
std::string GetName(){return "Arthoran";}void StopMoving(){}
|
||||
};
|
||||
void Session::HandleGameObjectUseOpcode(WorldPacket& packet) {
|
||||
@@ -80,8 +82,11 @@ void Session::CompleteClientlessTeleport(){
|
||||
player->mapId=wrongTarget?1:0;player->x=-8998;player->y=861;player->z=29;
|
||||
}
|
||||
struct Node {std::string Key;uint32 MapId=530;};
|
||||
struct Portal {uint32 SpawnId=83,Entry=183325,SpellId=17334;Node Source;};
|
||||
struct Graph {bool prerequisites=true;bool PortalPrerequisites(Player*,uint32){return prerequisites;}bool available=true;Portal portal;auto GetPortalUse(uint32){return available?&portal:nullptr;}} graph;
|
||||
struct Portal {uint32 SpawnId=83,Entry=183325,SpellId=17334;Node Source;struct{uint32 MapId=0;float X=-8998,Y=861,Z=29,O=0;}Target;};
|
||||
struct Graph {bool nativePrerequisites=true,adventureUnlock=false;
|
||||
bool PortalNativePrerequisites(Player*,uint32){return nativePrerequisites;}
|
||||
bool AdventurePortalUnlockAllowed(Player*,uint32){return adventureUnlock;}
|
||||
bool available=true;Portal portal;auto GetPortalUse(uint32){return available?&portal:nullptr;}} graph;
|
||||
struct Travel {void CancelTravel(uint32){}} travel;
|
||||
struct Lua {void DismountGroundMount(Player*,const char*){}} lua;
|
||||
auto sClientlessTravelGraphMgr=&graph;auto sClientlessTravelMgr=&travel;auto sClientlessLuaEngine=&lua;
|
||||
@@ -105,7 +110,11 @@ harness += methods
|
||||
harness += r'''
|
||||
int main(){
|
||||
ClientlessActionExecutorMgr mgr;
|
||||
{Player p;Record r;graph.prerequisites=false;assert(!mgr.StartPortalUse(&p,26,r)&&p.session.clicks==0);graph.prerequisites=true;}
|
||||
{Player p;Record r;graph.nativePrerequisites=false;assert(!mgr.StartPortalUse(&p,26,r)&&p.session.clicks==0);graph.nativePrerequisites=true;}
|
||||
{Player p;Record r;graph.nativePrerequisites=false;graph.adventureUnlock=true;
|
||||
assert(mgr.StartPortalUse(&p,26,r)&&p.session.clicks==0&&p.pending);
|
||||
mgr.UpdatePortalUse(&p,26,r);assert(r.RouteStep==2&&p.session.sequence==1);
|
||||
graph.nativePrerequisites=true;graph.adventureUnlock=false;}
|
||||
{Player p;Record r;assert(mgr.StartPortalUse(&p,26,r));assert(p.session.clicks==1&&!p.world);
|
||||
mgr.UpdatePortalUse(&p,26,r);assert(!r.PortalUseActive&&r.RouteStep==2&&p.mapId==0&&p.session.sequence==1);}
|
||||
// The global lifecycle may have ACKed a delayed cast before our next update.
|
||||
|
||||
Reference in new issue
Block a user