Implement safe AdventureBot group travel and formations
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s

This commit is contained in:
Nico committed 2026-10-28 23:30:28 +01:00
1 parent a758239da9
commit cac4142ec1
26 files changed
+1482 -79

No files matched your search

+33
View File
@@ -1,3 +1,36 @@
# Aktuell: Gemeinsame Gruppenreisen implementiert
Auftrag: Spieler- und Botgruppen mit sicheren Formationen, Reittieren, freiem
Flug, nativen Taxis/Portalen und aufgezeichneten Verkehrsmitteln reisen lassen.
Branch feature/clientless-player-core; Ausgangscommit a758239da9.
Dieser Stand wird auf ausdrücklichen Nutzerauftrag lokal committet; kein Push.
Doku: doc/clientless/group-travel.md.
AdventureGroupTravelMgr/Policy übernimmt lokale Gruppenverfolgung, sichere
Bodenpfade, stabile Formationsplätze, begrenzte Straßenabschnitte, Flugverfolgung
und Übergangsqueues. >500Yards startet getrennte 10-Minuten-Uhr; bekannte Reisen
und fortschreitendes Aufholen sind begrenzt geschützt. Reine Botgruppen behalten
ihre gemeinsamen Graph-/Flugrouten. Native Transportverluste führen zum erfassten
Einstieg zurück, zuerst per Pfad, bei Scheitern gezielt per Teleport. Keine
allgemeinen Teleports zum Anführer. Generic CPC verwendet optionale owner-Callbacks,
bleibt unabhängig von AdventureBots. DungeonBots unverändert.
Build6 erfolgreich: /tmp/vcore-group-travel-build6.log. Alle82 Tests erfolgreich:
/tmp/vcore-group-travel-tests2.log. Nach letzten Änderungen zusätzlich Gruppenreise,
Straßenfortsetzung und bewegter Flug erneut erfolgreich getestet. Installation
mit bestehender CMake-Konfiguration erfolgreich; vorheriger Server vollständig
beendet. Neuer Start läuft: /tmp/vcore-group-travel-start1.log, PID183811,
tmux vcore-worldserver. Startbereitschaft bestätigt: frische ready-Meldung, Prozess stabil, Port8085
LISTEN. Loader24 online,10/2/10/2, pending0, error leer.
Loader muss weiterhin aktiv mit10 persistent+2 temporär je Fraktion bleiben.
Native Spielerreisen und visuelle Formation sind noch nicht im Spiel abgenommen.
Unbekannte/scriptgesteuerte Portale oder fehlende Serviceaufzeichnungen werden
nicht erraten; Bots warten bzw. suchen vorhandene Graphwege. Keine Behauptung,
dass Adaptertests Gameplay-Abnahme ersetzen. Kein Push.
---
# Aktuell: Klassen-/Rollen-Kampf und Spieler-Instanzmodus implementiert
Nutzer autorisierte die gemeinsam besprochene Umsetzung. Ausgangsstand sauber
@@ -0,0 +1,6 @@
[1/6] cd /home/wow_admin/wow_cata_server/source/TrinityCore && /usr/bin/cmake -DBUILDDIR="/home/wow_admin/wow_cata_server/build/debug" -P /home/wow_admin/wow_cata_server/source/TrinityCore/cmake/genrev.cmake /home/wow_admin/wow_cata_server/build/debug
[2/6] Building CXX object src/server/game/CMakeFiles/game.dir/World/ClientlessLuaEngine.cpp.o
[3/6] Building CXX object src/server/game/CMakeFiles/game.dir/World/AdventureGroupTravelMgr.cpp.o
[4/6] Building CXX object src/server/game/CMakeFiles/game.dir/World/ClientlessActionExecutorMgr.cpp.o
[5/6] Linking CXX static library src/server/game/libgame.a
[6/6] Linking CXX executable src/server/worldserver/worldserver
@@ -0,0 +1,26 @@
TrinityCore rev. a758239da956+ 2026-10-28 22:37:18 +0100 (feature/clientless-player-core branch) (Unix, Debug, Static) (worldserver-daemon)
World initialized in 0 minutes 15 seconds
TrinityCore rev. a758239da956+ 2026-10-28 22:37:18 +0100 (feature/clientless-player-core branch) (Unix, Debug, Static) (worldserver-daemon) ready...
TC>Adventure loader enabled=1 schema=1 busy=0 pools=10/2/10/2 known=25 rotations=0 manager=5 error=''
pool=0 faction=alliance persistent=1 target=10 online=2 pending=0 saved=10
pool=1 faction=alliance persistent=0 target=2 online=1 pending=0 saved=3
pool=2 faction=horde persistent=1 target=10 online=1 pending=0 saved=10
pool=3 faction=horde persistent=0 target=2 online=1 pending=0 saved=2
No group travel state.
Interactions guid=29 grouped=0 leader=0 partner=0 intent='rest' schedule=''
TC>Adventure loader enabled=1 schema=1 busy=0 pools=10/2/10/2 known=25 rotations=0 manager=17 error=''
pool=0 faction=alliance persistent=1 target=10 online=7 pending=0 saved=10
pool=1 faction=alliance persistent=0 target=2 online=2 pending=0 saved=3
pool=2 faction=horde persistent=1 target=10 online=6 pending=0 saved=10
pool=3 faction=horde persistent=0 target=2 online=2 pending=0 saved=2
No group travel state.
Interactions guid=31 grouped=0 leader=0 partner=0 intent='economy' schedule=''
TC>Adventure loader enabled=1 schema=1 busy=0 pools=10/2/10/2 known=25 rotations=0 manager=24 error=''
pool=0 faction=alliance persistent=1 target=10 online=10 pending=0 saved=10
pool=1 faction=alliance persistent=0 target=2 online=2 pending=0 saved=3
pool=2 faction=horde persistent=1 target=10 online=10 pending=0 saved=10
pool=3 faction=horde persistent=0 target=2 online=2 pending=0 saved=2
Final: PID183811 remains running; TCP8085 LISTEN.
Native ungrouped interaction diagnostics work. No human-led travel acceptance performed.
Existing terrain719/config/script warnings remain; no fatal/assertion observed in this startup.
@@ -0,0 +1,263 @@
TEST tests/clientless/test_adventure_area_node.py
Production area destination excludes outside and unreachable graph nodes.
TEST tests/clientless/test_adventure_async_database.py
Async creation DB contracts: prepared connection masks, native security column names, no future waits, successful-save cache publication and world-update-only loader hook passed
TEST tests/clientless/test_adventure_behavior_policy.py
24-hour generation, shared intents with changing destinations, broad location classification and repetition penalties passed.
TEST tests/clientless/test_adventure_class_rotation.py
Production class rotations: persistent PvE solo pools, native rank aura IDs, combo/holy power/proc/execute/resource gates, Eclipse and healer coverage passed
TEST tests/clientless/test_adventure_combat_exercise.py
Production exercise: stance/charge/melee/casts, native validation delegation, bounded lifetime, invalid targets and unchanged native phases passed.
TEST tests/clientless/test_adventure_combat_preparation.py
Production talent learner: native tiers/dependencies/class gates, all 26 legal level60 points, no capstone bypass and repeat safety passed.
TEST tests/clientless/test_adventure_combat_reactions.py
Production combat focus, damage ordering, risk filters, corpse radius/delay, flight/group override, blocked escape, danger avoidance and malus-free persisted fallback passed.
TEST tests/clientless/test_adventure_day_persistence.py
Production day planner: all 24 TINYINT slots restored across logins, incomplete/old-day replacement, full transaction and curiosity/level weighting passed
TEST tests/clientless/test_adventure_economy.py
Economy: native service roles, friendly/phase/LOS/bounds gates, no bot service targets and coherent performative cues passed.
TEST tests/clientless/test_adventure_economy_catalog.py
Economy catalog: eleven actions, three faction-bound service anchors, native flag gates and idempotent preservation passed.
TEST tests/clientless/test_adventure_encounter.py
Production encounters: bot-only exclusion, mixed mode, extension hook, wait/revive, wipe fallback/reentry/gather/return, living-player priority, kick exit and offline cleanup passed
TEST tests/clientless/test_adventure_equipment_maintenance.py
Production gear maintenance: same birth/retention gate rejects wrong armor/stats, malformed effects, level, temporary, reputation, map, holiday, native-use and slot mismatches passed
TEST tests/clientless/test_adventure_exploration.py
Exploration: visible native props only, no chests/traps/furniture, phase/map/LOS/range gates and contextual preparation passed.
TEST tests/clientless/test_adventure_exploration_catalog.py
Exploration catalog: ten actions, shared scenes, default wander replacement, explicit category eligibility and preserved edits passed.
TEST tests/clientless/test_adventure_generator_equipment.py
Production equipment affinity: mana-only/direct/triggered procs, caster/physical stat effects and bounded cycles passed
TEST tests/clientless/test_adventure_generator_ownership.py
Production generator commands: batch bounds, dedicated ownership, persistent/online/removing retirement protection and retired login refusal passed
TEST tests/clientless/test_adventure_generator_policy.py
Production generator: 6144 cultural names and 61440 real Lua profiles, profession coherence, personality ranges, escaping and equipment/armor gates passed
TEST tests/clientless/test_adventure_graph_handover.py
Production graph handover waits for ship/taxi/flight/teleport and stops at safe road/node boundaries.
TEST tests/clientless/test_adventure_ground_avoidance.py
Production ground avoidance: harmful/friendly, phase169, height, reachable safe endpoint, rejected paths and reuse without cast/movement thrashing passed
TEST tests/clientless/test_adventure_group_plan.py
Production group plans: leader meeting/goal/deadline inheritance, untouched personal day, manual preparation and instance-state guards passed.
TEST tests/clientless/test_adventure_group_travel.py
Production group travel: distinct slots, phase169 cliff safety, width, mounts, flight/kick landing, taxi grace, portal queue, bounded separation and recorded-road validation passed
TEST tests/clientless/test_adventure_groups.py
Production recruitment: capitals, different goals, native inviter leadership, joining existing groups, capacity/reservations, raid conversion, planned transition and local contact passed.
TEST tests/clientless/test_adventure_instance.py
Production instance lifecycle: time budget, true entry, denied/shared entry, native rear exit, restart, expired copy, late arrival, HP and return replay passed.
TEST tests/clientless/test_adventure_instance_autoresume.py
Deferred instance script: missing schema/visits stays idle; no login work on startup; saved visitors resume with Adventure controllers and native accounts.
TEST tests/clientless/test_adventure_instance_begin.py
Production entrance travel uses the existing location planner, requires mapping and retains normal social/area travel.
TEST tests/clientless/test_adventure_instance_catalog.py
Instance migrations: outside entrances only, native graph preserved, disabled/missing transitions excluded, edits and saved visits preserved.
TEST tests/clientless/test_adventure_instance_loader.py
Production instance recovery: no startup DB/login work, asynchronous delayed/paced manual recovery, generated-bot quota ownership, absent-schema compatibility and existing preparation ticks passed
TEST tests/clientless/test_adventure_instance_persistence.py
Production instance persistence: phases, original deadline, instance ID, transactional HP marker, failed commit and intent release passed.
TEST tests/clientless/test_adventure_intent_state.py
Production intent arrival, failed/partial travel, deadlines and safe schedule handover passed.
TEST tests/clientless/test_adventure_interactions.py
production interaction manager: invitations, factions, reservations, follow, AFK and deferred departure passed
/tmp/tmp4c3prj7a/test.cpp: In member function ‘void AdventureInteractionMgr::Leave(Player*, Record&, const char*)’:
/tmp/tmp4c3prj7a/test.cpp:143:69: warning: unused parameter ‘reason’ [-Wunused-parameter]
143 | void AdventureInteractionMgr::Leave(Player* p,Record& r,char const* reason)
| ~~~~~~~~~~~~^~~~~~
TEST tests/clientless/test_adventure_leisure.py
Leisure: substantial curved MMAP routes, mirrored return, interrupted return, rejected corridors, actual arrival, companion follow, riding, flight and fishing passed.
TEST tests/clientless/test_adventure_leisure_catalog.py
Leisure migration: native city anchor, shared scene references, pet-walk mapping and preservation of edited weights passed.
TEST tests/clientless/test_adventure_location_import.py
Locations: actual Debug SMALLINT reads retain continent IDs; explicit location eligibility and edited weight overrides passed.
TEST tests/clientless/test_adventure_manager_inventory.py
Production inventory maintenance: only full backpacks, native stack trimming, level-appropriate supplies, tools, leash, fishing pole, containers and untouched equipped slots passed
TEST tests/clientless/test_adventure_micro_reset.py
Micro cleanup ends physical local movement, preserves transit and does not stand up dead players.
TEST tests/clientless/test_adventure_population.py
Production population: four independent faction/persistence targets, fair selection, pending reservations, retries, existing inventory, once-per-boot rotation and connected/persistent protection passed
TEST tests/clientless/test_adventure_profession_catalog.py
Profession catalog: native workstation roles, no workshop/resource inference, idempotence and preserved edits passed.
TEST tests/clientless/test_adventure_profession_profile.py
Production profession parser, real Lua profiles, primary restrictions and secondary permissions passed.
TEST tests/clientless/test_adventure_profession_sightline.py
Native workstation working-surface sightline and blocked geometry rejection passed.
TEST tests/clientless/test_adventure_profession_targets.py
Production profession targets: native locks, phase/spawn/loot, stations, corpses, history and trainers passed.
TEST tests/clientless/test_adventure_profile_selection.py
Production profile selection: real Lua ID dictionaries, exact 9:1 companion weighting, zero/negative/infinite/NaN/oversized weights rejected and legacy companion fallback passed
TEST tests/clientless/test_adventure_pvp.py
Production PvP selector: level boundaries, radius, factions, native legality, navigation, intent, personality and resources passed.
TEST tests/clientless/test_adventure_repair_catalog.py
Repair action catalog: proper native service requirement, economy-only intent and preserved administrator weights passed.
TEST tests/clientless/test_adventure_rest.py
Recovery: authored places, bounds, arrival, occupancy, full rest duration, failure, reload and interruption passed.
TEST tests/clientless/test_adventure_rest_fire.py
Rest fire: shared scene mapping, additive migration, preserved weights and authored creation-place policy passed.
TEST tests/clientless/test_adventure_rest_space.py
Sleep footprint: furniture, body extent, overhead collision, ledges, stairs, missing heights, rotation and scale passed.
TEST tests/clientless/test_adventure_rest_targets.py
Native sleep targets: chair exclusion, phase, despawn, floor search, indoors, water, storey, occupancy, bed/surface preference passed.
TEST tests/clientless/test_adventure_rotation.py
Production rotation: connected/native-online protection, ownership refusal, native delete failure, world cleanup failure and journal-retained retry before successful profile/plan/state removal passed
/tmp/tmpthw2w5iq/test.cpp: In instantiation of ‘std::string Trinity::StringFormat(const char*, A ...) [with A = {}; std::string = std::__cxx11::basic_string<char>]’:
/tmp/tmpthw2w5iq/test.cpp:22:126: required from ‘void Tx::PAppend(const char*, A ...) [with A = {}]’
22 | struct Tx{std::vector<std::string>sql;template<class...A>void PAppend(char const*f,A...a){sql.push_back(Trinity::StringFormat(f,a...));}};
| ~~~~~~~~~~~~~~~~~~~~~^~~~~~~~
/tmp/tmpthw2w5iq/test.cpp:51:75: required from here
51 | auto barrier=CharacterDatabase.BeginTransaction();barrier->PAppend("DO 0");
| ~~~~~~~~~~~~~~~~^~~~~~~~
/tmp/tmpthw2w5iq/test.cpp:11:113: warning: format not a string literal and no format arguments [-Wformat-security]
11 | namespace Trinity{template<class...A>std::string StringFormat(char const* f,A...args){char b[4096];std::snprintf(b,sizeof(b),f,args...);return b;}}
| ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~
TEST tests/clientless/test_adventure_social.py
Production social flow: approach before talk, coherent greeting/talk/listen/farewell, lost targets, native chair confirmation and real fire confirmation passed.
TEST tests/clientless/test_adventure_social_fire.py
Shared fire: native detection/provisioning, buildings/city/parent/zone restrictions, existing fires and outdoor/water checks passed.
TEST tests/clientless/test_adventure_supplies.py
Production supplies: exact10% boundary, bandage-before-food, native lockout fallback, recovery completion, combat potions, mana-only drinking and daily/post-combat top-ups passed.
TEST tests/clientless/test_adventure_supply_catalog.py
Supply migrations: all five roles, level60 netherweave tier, editable/disabled catalog preservation and persistent daily stamp passed.
TEST tests/clientless/test_adventure_supply_items.py
Production native-item delegation: usable bandage tiers, native cooldown/lockout gates, real inventory consumption, self targeting and level60 secondary caps passed.
TEST tests/clientless/test_adventure_talent_templates.py
Canonical Cata PvE builds: 4.3.4 class/tree/rank/prerequisite gates, all 1..41 point budgets, solo selection coverage passed
TEST tests/clientless/test_adventure_training_catalog.py
Training catalog: real dummy requirement, leisure-only intent, bounded duration and preserved administrator weights passed.
TEST tests/clientless/test_adventure_walk.py
Production travel gait reaches native spline, survives repaths, prevents mounting and preserves legacy running defaults.
TEST tests/clientless/test_clientless_character_factory.py
Production native factory: character/profile atomicity, failed preparation/commit, cache publication order, account/security/name/cap gates passed
TEST tests/clientless/test_clientless_companion.py
Native companion: provisioning, delayed summon, ownership, real item target, leash confirmation/timeout, reuse and cleanup passed.
TEST tests/clientless/test_clientless_damage_notifications.py
Production damage handoff: 16 concurrent map-worker writers, ordered GUID-only events, clientless-only/positive damage gates and post-health notification passed.
TEST tests/clientless/test_clientless_near_teleport_observer.py
Production near-teleport observer packet includes actual mover GUID and destination; clientless timestamp refreshed, regular client timestamp preserved.
TEST tests/clientless/test_clientless_skill_preparation.py
Production offline skill preparation: native tier caps, no downgrading, rank/level/race/tier gates and no modification of online players passed
TEST tests/clientless/test_clientless_social_notifications.py
Production social/helper queues: concurrent GUID-only delivery, bounded queues, human/self/zero-damage gates, pet attribution and lethal-cleanup ordering passed
TEST tests/clientless/test_default_phase.py
Native PhaseShift: implicit/default 169 visibility, genuine custom-phase separation and reference updates passed.
TEST tests/clientless/test_empty_spell_effect.py
Native empty spell effects: poisoned-memory scaling initialization, Lock and Load cleared slot, active effect values and all-empty spell values passed
TEST tests/clientless/test_flight_area_query.py
Native terrain query map/phase, area/zone ordering and cache passed.
TEST tests/clientless/test_flight_handoff.py
Flight handoff before spline end, terminal landing, return corners, collision stop and actual distance passed.
TEST tests/clientless/test_flight_policy.py
Flight native eligibility, license/profile/zone policy, RP costs and fail-safe return decisions passed.
TEST tests/clientless/test_follow_spline_velocity.py
Native follow: actual player/creature spline velocity, client gait and independent follower speeds passed.
TEST tests/clientless/test_free_flight.py
Flight geometry passed: takeoff/landing, smoothed peak, ceiling, lateral/climbing avoidance, missing terrain and blocked corridors.
TEST tests/clientless/test_free_flight_planner.py
ground planner regressions and dynamic flight/node/location/virtual-exit integration passed
TEST tests/clientless/test_ground_exit_continuation.py
optional exits: repeated probes, retained route, delayed success, forward resume, flight fallback passed
TEST tests/clientless/test_group_flight_follow.py
Moving-target flight: bounded phased corridors, obstacle hold, ceiling rejection, aerial formation hold, safe landing and return fallback passed
TEST tests/clientless/test_item_equip_cooldown.py
Native item equip cooldown: zero/sentinel/missing spells safely skipped, valid use cooldown preserved and longer/disabled cooldowns untouched passed
TEST tests/clientless/test_local_spline_segments.py
Production local spline clipping passes native path-length validation, tiny remainders and 1000 sampled paths.
TEST tests/clientless/test_lua_reload.py
Lua reload regression tests passed (production reload/bindings, real Lua 5.4).
TEST tests/clientless/test_player_pure_max_skill.py
Native pure skill cap: learned skills return stored cap; missing/deleted/zero-rank skills return0.
TEST tests/clientless/test_portal_transfer.py
Portal near/far ACK and denied same-map transfer regressions passed.
TEST tests/clientless/test_portal_use.py
Interactive portal native click, near/far/global ACK, denial, timeout and target checks passed.
Native portal destination discovery and unsupported/ambiguous spell rejection passed.
Production clientless ACK completion sequence increments only after completed near/far transfers.
TEST tests/clientless/test_shared_group_route.py
Production generic shared routes: same edges/taxi/free flight, independent execution state, final local goal, late catch-up, permissions and stale-goal rejection passed.
TEST tests/clientless/test_taxi_import_disabled.py
Taxi importer preserves disabled rows and their reasons.
TEST tests/clientless/test_transport.py
Transport dock, frame, recorder, cross-map copy and ride regressions passed
TEST tests/clientless/test_transport_grid_loading.py
Core transport grid initialization regressions passed (no CPC dependency).
TOTAL 82 FAILED []
+106
View File
@@ -0,0 +1,106 @@
# AdventureBot group travel
AdventureGroupTravelMgr is the common local-follow owner for player-led and
bot-led groups. Bot-led macro travel retains the existing shared TravelGraph
route and flight trail; DungeonBot CreatureAI remains independent.
## Formation and ground safety
Group members have stable GUID-ordered formation slots. Standing companions
form a small staggered group. Moving companions form one, two or three columns
according to checked walkable space; mounts increase spacing. Narrow/uncertain
passages use one column. This estimates usable ground, not a semantic road width.
Combat positioning and encounter avoidance retain priority.
Local following uses complete, unsuspicious MMAP paths. Phased floor samples
along paths reject holes, large height discontinuities and unsupported ground.
The same owner-supplied validator runs before every clientless local spline,
including graph approaches/repaths, while the bot is grouped. Dynamic LOS is
checked for the actual segments. A rejected path stops following and is retried;
it does not itself trigger a departure or a teleport.
Recorded graph roads have an optional CPC GroundPathAdapter. The Adventure
adapter checks bounded chunks, offsets safe lanes, falls back to single-file,
and holds a row behind the moving leader. Edge point indices remain aligned;
only an actually reached chunk endpoint advances the next road index. Native
portal/boarding endpoints remain unshifted. Optional speculative road exits are
disabled for adapted group roads so they cannot bypass the safety adapter.
Ungrouped CPC navigation and unrelated scripts have no adapter by default.
Mounted leaders cause capable profile-defined followers to use their own ground
mounts even on short follow legs. Nearby followers dismount when the leader is
on foot. Ordinary long ground catch-up can also use profile mounts.
## Flight
ClientlessFlightMgr::Follow adds a moving-target mode beside existing point
flights. Native learned mounts/capabilities, area restrictions, casting and
VMAP height/volume collision checks still decide whether flight is possible.
Followers update their own formation target and fly bounded segments. Unknown
terrain, blocked corridors or insufficient headroom cause a mounted hold.
A moving aerial target is never treated as a ground touchdown.
On landing, kick, leader change/loss or excessive separation, a tracking flight
lands on verified local solid ground or uses its checked reverse trail. It never
removes flight at an unverified height. Own group flights are serviced before
self-care/combat actions can replace their movement; death handling still runs
for dead characters. Daily-plan date refresh continues while airborne.
Bot-led shared point flights preserve their native shared corridor and use
staggered departures to form a single-file column. They finish a safe native
flight leg rather than being cancelled in the air. Ground-only followers may
follow safe projected ground points while the leader is no more than12 yards
above them; greater height or missing ground paths makes them wait.
## Known transitions and separation
The leader is observed independently of a follower's self-care or current taxi/
transport state. Taxi destinations use the final node of the native taxi queue.
Ships, zeppelins and trams use recorded, matching native services. Fixed-destination
SPELLCASTER GameObjects (including temporary portals with native DB destinations)
and native AreaTrigger portals are recognized from the actual source and arrival.
Bots approach and use the native handler/object; destination teleports are not
fabricated. Per-follower queues retain consecutive transitions for late arrivals.
Unknown/script-selected portals are not guessed; existing graph routes may still
provide a valid way to the known destination.
Player-led groups accumulate a separate10-minute timer beyond500 yards (3D,
or separated maps/instances). Coming within500 yards resets it. A missing path
alone never advances a special leave reason. Existing combat/death/instance/
transport departure guards apply, and an airborne bot lands before leaving.
Pure bot groups retain their previous late-rendezvous rules.
Known actual journeys, own native taxi/transport travel and recorded transport
recovery pause separation. Arrival has60 seconds of grace. Further catch-up stays
protected only with route execution and recent physical progress; this does not
extend unrelated later ground separation. Journey protection is bounded to30
minutes. The ordinary AFK/hour-change departures are deferred during protected
travel. No travel marker grants unlimited protection after a failed route.
## Lost transport passengers
ClientlessTransportMgr detects loss of an actually boarded recorded service.
It retains the ride and attempts a complete, unsuspicious return path to that
ride's recorded departure node. A missing/rejected/stuck path, cross-map loss,
or a return that has not completed within60 seconds allows a targeted teleport
to that same known safe entry point. It never teleports toward the leader or
forward to the destination. The next service is boarded normally. Repeated losses
are bounded; after three recoveries the ride terminates safely at departure.
Ordinary death/resurrection remains owned by combat handling.
Services require valid recorded boarding/alighting transitions and native graph
connections. Missing service/portal data causes waiting or route failure rather
than invented passenger attachment or a general catch-up teleport.
## Diagnostics and validation
`clientless interaction <guid>` reports formation travel ownership, separation
milliseconds, protection, flight, queued transitions and route detail, alongside
the existing group/interaction state.
Automated tests exercise the production formation/ground validation, widths,
mount choice, flight and kick landing, taxi grace, sequential portals, bounded
separation, recorded-road adapters and native transport-loss return paths. Flight
adapter tests exercise real corridor geometry and moving-target/landing decisions.
Compilation and these adapters do not constitute visual in-game acceptance of
player taxi, portal, flight or deck movement.
+5 -28
View File
@@ -1,4 +1,5 @@
#include "AdventureGroupMgr.h"
#include "AdventureGroupTravelMgr.h"
#include "AdventureGroupPolicy.h"
#include "AdventureBehaviorMgr.h"
#include "AdventureCombatMgr.h"
@@ -175,35 +176,11 @@ void AdventureGroupMgr::Report(ChatHandler* h,Player* p) const
h->PSendSysMessage("Bot group intent='%s' next='%s' forming=%u botLed=%u capacity=%u meeting='%s'",plan.Intent.c_str(),plan.NextIntent.c_str(),plan.Forming,BotLed(p),AdventureGroupPolicy::Capacity(Activity(plan)),plan.Location.c_str());
}
bool AdventureGroupMgr::FollowMember(Player* p)
bool AdventureGroupMgr::FollowMember(Player* player)
{
auto group=p->GetGroup();
if(!group || !BotLed(p) || group->GetLeaderGUID()==p->GetGUID())return false;
auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID());if(!leader)return true;
auto& r=_records[p->GetGUID()];auto now=GameTime::GetGameTime();
if(!p->IsInMap(leader) || p->GetExactDist(leader)>=100)
{
if(now>=r.SearchAt)
{
r.SearchAt=now+15;std::string node;float distance=0;
if(sClientlessTravelGraphMgr->FindNearestNodeToPosition(leader->GetMapId(),leader->GetPositionX(),leader->GetPositionY(),leader->GetPositionZ(),node,distance) && distance<1000)
{ClientlessAction action;action.Name="travel_to_node";action.Parameters["node"]=node;sClientlessPlayerMgr->QueueAction(p->GetGUID().GetCounter(),std::move(action));}
}
return true;
}
if(!p->IsInPhase(leader) || !p->CanSeeOrDetect(leader))return true;
if(p->GetExactDist(leader)>5)
{
auto nav=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter());
if(!nav || nav->State!=ClientlessTravelState::Moving || nav->Target.GetExactDist(leader)>5)
{
Position end;leader->GetNearPoint(p,end.m_positionX,end.m_positionY,end.m_positionZ,3,leader->GetOrientation()+3.14159265f);
ClientlessPathDiagnostics path;
if(sClientlessTravelMgr->AnalyzePath(p,end,path) && path.Complete && !path.Suspicious && path.PathLength<150)sClientlessTravelMgr->StartTravel(p,end);
}
}
else sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());
return true;
auto group=player->GetGroup();
if(!group || !BotLed(player) || group->GetLeaderGUID()==player->GetGUID())return false;
return sAdventureGroupTravelMgr->Follow(player);
}
void AdventureGroupMgr::Invite(Player* p,Player* other,std::string const& intent,bool forming)
@@ -0,0 +1,465 @@
#include "AdventureGroupTravelMgr.h"
#include "AdventureGroupTravelPolicy.h"
#include "ClientlessTravelMgr.h"
#include "ClientlessTravelGraphMgr.h"
#include "ClientlessActionExecutorMgr.h"
#include "ClientlessTransportMgr.h"
#include "ClientlessFlightMgr.h"
#include "ClientlessPlayerMgr.h"
#include "ClientlessLuaEngine.h"
#include "Chat.h"
#include "Player.h"
#include "Group.h"
#include "GameObject.h"
#include "GameTime.h"
#include "DBCStores.h"
#include "Map.h"
#include "ObjectAccessor.h"
#include "ObjectMgr.h"
#include "PathGenerator.h"
#include "SpellMgr.h"
#include "SpellInfo.h"
#include "WorldSession.h"
#include "WorldPacket.h"
#include "Opcodes.h"
#include "Log.h"
#include "VMapFactory.h"
#include <algorithm>
#include <cmath>
#include <vector>
namespace
{
bool Executing(uint32 guid)
{
auto action=sClientlessActionExecutorMgr->GetStatus(guid);
return action && (action->State==ClientlessActionExecutionState::Moving || action->State==ClientlessActionExecutionState::SelectingTarget);
}
Position OffsetPoint(Position const& origin,AdventureGroupTravelPolicy::Offset offset)
{
float angle=origin.GetOrientation();
return Position(origin.GetPositionX()-std::cos(angle)*offset.Behind-std::sin(angle)*offset.Side,
origin.GetPositionY()-std::sin(angle)*offset.Behind+std::cos(angle)*offset.Side,
origin.GetPositionZ()+offset.Height,angle);
}
bool FixedPortal(GameObject* object,uint32& map,Position& destination)
{
auto info=object->GetGOInfo();if(info->type!=GAMEOBJECT_TYPE_SPELLCASTER)return false;
auto spell=sSpellMgr->GetSpellInfo(info->spellcaster.spell);if(!spell)return false;
SpellTargetPosition const* target=nullptr;
for(uint8 i=0;i<MAX_SPELL_EFFECTS;++i)
{
auto const& effect=spell->Effects[i];if(effect.Effect!=SPELL_EFFECT_TELEPORT_UNITS)continue;
if(target || (effect.TargetA.GetTarget()!=TARGET_DEST_DB && effect.TargetB.GetTarget()!=TARGET_DEST_DB))return false;
target=sSpellMgr->GetSpellTargetPosition(spell->Id,SpellEffIndex(i));if(!target)return false;
}
if(!target)return false;
map=target->target_mapId;destination.Relocate(target->target_X,target->target_Y,target->target_Z,target->target_Orientation);return true;
}
}
AdventureGroupTravelMgr* AdventureGroupTravelMgr::instance(){static AdventureGroupTravelMgr manager;return &manager;}
void AdventureGroupTravelMgr::SampleLeader(Player* player,Leader& state,time_t now)
{
if(state.Seen && state.SampleAt==now)return;
bool taxi=player->IsInFlight(),transport=player->GetTransport()!=nullptr;
if(taxi && !player->m_taxi.GetPath().empty())
{
std::string goal="taxi_"+std::to_string(player->m_taxi.GetPath().back());
if(state.Kind!=Journey::Taxi || state.To!=goal || !state.WasTaxi)
{
state.Kind=Journey::Taxi;state.From="taxi_"+std::to_string(player->m_taxi.GetTaxiSource());state.To=goal;
state.Began=now;state.Arrived=0;++state.Generation;
if(auto node=sTaxiNodesStore.LookupEntry(player->m_taxi.GetPath().back()))
{state.GoalMap=node->ContinentID;state.Goal.Relocate(node->Pos.X,node->Pos.Y,node->Pos.Z);}
}
}
else if(transport && !state.WasTransport)
{
state.Kind=Journey::Transport;state.Began=now;state.Arrived=0;++state.Generation;
state.From.clear();state.To.clear();
sClientlessTransportMgr->ObserveJourney(player,state.Seen?state.GroundMap:player->GetMapId(),state.Seen?state.Ground:*player,state.From,state.To);
if(auto stop=sClientlessTransportMgr->GetStop(state.To)){state.GoalMap=stop->Map;state.Goal=stop->Points.front().Pos;}
}
if(!taxi && state.WasTaxi && state.Kind==Journey::Taxi)state.Arrived=now;
if(!transport && state.WasTransport && state.Kind==Journey::Transport)
{
sClientlessTransportMgr->ArrivalNode(player,state.From,state.To);
state.Arrived=now;state.GoalMap=player->GetMapId();state.Goal.Relocate(player);
}
bool jumped=state.Seen && !taxi && !transport && !state.WasTaxi && !state.WasTransport &&
(state.Map!=player->GetMapId() || state.Previous.GetExactDist(player)>80);
if(jumped)
{
bool known=false;
if(state.CandidatePortal && state.CandidateSource.GetExactDist(state.Previous)<30 && state.CandidateSourceMap==state.Map && state.CandidateGoalMap==player->GetMapId() && state.CandidateGoal.GetExactDist(player)<100)
{state.Kind=Journey::Portal;state.PortalObject=state.CandidatePortal;state.Source=state.CandidateSource;state.SourceMap=state.CandidateSourceMap;state.Goal=state.CandidateGoal;state.GoalMap=state.CandidateGoalMap;state.PortalTrigger=0;known=true;}
if(!known)for(auto area:sAreaTriggerStore)
{
if(area->ContinentID!=state.Map || state.Previous.GetExactDist(area->Pos.X,area->Pos.Y,area->Pos.Z)>35)continue;
auto target=sObjectMgr->GetAreaTrigger(area->ID);
if(!target || target->target_mapId!=player->GetMapId() || player->GetExactDist(target->target_X,target->target_Y,target->target_Z)>100)continue;
state.Kind=Journey::Portal;state.PortalTrigger=area->ID;state.PortalObject.Clear();state.SourceMap=area->ContinentID;
state.Source.Relocate(area->Pos.X,area->Pos.Y,area->Pos.Z);state.GoalMap=target->target_mapId;
state.Goal.Relocate(target->target_X,target->target_Y,target->target_Z,target->target_Orientation);known=true;break;
}
if(known){state.Began=now;state.Arrived=now;++state.Generation;state.From.clear();state.To.clear();}
else {state.Kind=Journey::None;state.PortalObject.Clear();}
}
if(!taxi && !transport && !player->IsFlying() && !player->IsFalling())
{
state.Ground.Relocate(player);state.GroundMap=player->GetMapId();
// Remember a nearby native portal before the player disappears through it.
{
state.CandidatePortal.Clear();
std::vector<GameObject*> portals;player->GetGameObjectListWithEntryInGrid(portals,0,20);
for(auto object:portals)
{
uint32 map=0;Position target;
if(!object->IsInPhase(player) || !FixedPortal(object,map,target))continue;
state.CandidatePortal=object->GetGUID();state.CandidateSourceMap=player->GetMapId();state.CandidateSource.Relocate(object);
state.CandidateGoalMap=map;state.CandidateGoal=target;break;
}
}
}
state.Seen=true;state.SampleAt=now;state.Map=player->GetMapId();state.Previous.Relocate(player);
state.WasTaxi=taxi;state.WasTransport=transport;
}
void AdventureGroupTravelMgr::Observe(Player* player,uint32 diff)
{
auto group=player->GetGroup();
if(!group)return;
auto inserted=_records.try_emplace(player->GetGUID().GetCounter());
auto& record=inserted.first->second;
if(inserted.second)
{
sClientlessActionExecutorMgr->SetGroundPathAdapter(player->GetGUID().GetCounter(),[](Player* member,std::vector<Position>& path){return sAdventureGroupTravelMgr->AdaptRoad(member,path);});
sClientlessTravelMgr->SetPathValidator(player->GetGUID().GetCounter(),[](Player* member,std::vector<Position> const& path){return sAdventureGroupTravelMgr->ValidateGroundPath(member,path);});
}
if(auto flight=sClientlessFlightMgr->GetStatus(player->GetGUID().GetCounter()))
if(flight->State==ClientlessFlightState::Mounting || flight->State==ClientlessFlightState::Flying)record.FlightOwned=true;
if(group->GetLeaderGUID()==player->GetGUID())return;
record.Timer+=std::min(diff,30000u);if(record.Timer<AdventureGroupTravelPolicy::SampleMs)return;
uint32 elapsed=record.Timer;record.Timer=0;
auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID());if(!leader)return;
auto now=GameTime::GetGameTime();
if(record.LeaderGuid!=leader->GetGUID())
{
if(record.RouteOwned)
{
sClientlessPlayerMgr->ClearQueuedActions(player->GetGUID().GetCounter());
if(player->GetTransport() || player->IsInFlight() || player->IsFlying())sClientlessActionExecutorMgr->RequestStopAtNode(player->GetGUID().GetCounter());
else {sClientlessActionExecutorMgr->Clear(player->GetGUID().GetCounter());record.RouteOwned=false;}
}
record.Pending.clear();record.LeaderGuid=leader->GetGUID();record.SeparationMs=0;record.Generation=0;record.ProgressAt=now;record.ProgressPosition.Relocate(player);record.BotMap=player->GetMapId();}
auto& state=_leaders[leader->GetGUID()];SampleLeader(leader,state,now);
if(record.Generation!=state.Generation)
{
record.Generation=state.Generation;record.ProgressAt=now;record.ProgressPosition.Relocate(player);record.BotMap=player->GetMapId();
if(state.Kind!=Journey::None && record.Pending.size()<16)record.Pending.push_back(state);
}
if(!record.Pending.empty() && record.Pending.back().Generation==state.Generation)record.Pending.back()=state;
auto ownRide=sClientlessTransportMgr->GetStatus(player->GetGUID().GetCounter());
bool riding=ownRide && ownRide->Phase!=ClientlessTransportPhase::Failed && ownRide->Phase!=ClientlessTransportPhase::Arrived;
while(!record.Pending.empty() && !riding && !player->IsInFlight() && !player->IsFlying() && !player->GetTransport() &&
player->GetMapId()==record.Pending.front().GoalMap && player->GetExactDist(record.Pending.front().Goal)<35)
{
record.Pending.pop_front();
if(record.RouteOwned){sClientlessPlayerMgr->ClearQueuedActions(player->GetGUID().GetCounter());sClientlessActionExecutorMgr->Clear(player->GetGUID().GetCounter());record.RouteOwned=false;}
}
if(record.BotMap!=player->GetMapId() || record.ProgressPosition.GetExactDist(player)>1)
{record.ProgressAt=now;record.ProgressPosition.Relocate(player);record.BotMap=player->GetMapId();}
bool near=player->IsInMap(leader) && player->GetExactDist(leader)<=AdventureGroupTravelPolicy::SeparationDistance;
bool progressing=now-record.ProgressAt<90;
bool ownTransit=player->GetTransport() || player->IsInFlight();
auto ride=sClientlessTransportMgr->GetStatus(player->GetGUID().GetCounter());
bool recordedRide=ride && ride->Phase!=ClientlessTransportPhase::Failed && ride->Phase!=ClientlessTransportPhase::Arrived;
auto const& journey=record.Pending.empty()?state:record.Pending.front();
bool bounded=journey.Kind!=Journey::None && journey.Began && now>=journey.Began && now-journey.Began<1800;
bool grace=journey.Arrived && now-journey.Arrived<60;
if(ownTransit || recordedRide){if(!record.NativeTransitAt)record.NativeTransitAt=now;}else record.NativeTransitAt=0;
bool nativeProtected=record.NativeTransitAt && now-record.NativeTransitAt<1800;
record.ProtectedTravel=nativeProtected || (bounded && (leader->IsInFlight() || leader->GetTransport() || (!record.Pending.empty() && (grace || (record.RouteOwned && Executing(player->GetGUID().GetCounter()) && progressing)))));
record.SeparationMs=AdventureGroupTravelPolicy::Separation(record.SeparationMs,elapsed,!near,record.ProtectedTravel || !player->IsAlive());
if(leader->GetSession()->IsClientless())record.SeparationMs=0; // late bot recruits retain their agreed rendezvous behavior
}
bool AdventureGroupTravelMgr::Airborne(Player* player) const
{
if(!player->IsAlive())return false;
auto found=_records.find(player->GetGUID().GetCounter());if(found==_records.end() || !found->second.FlightOwned)return false;
auto flight=sClientlessFlightMgr->GetStatus(player->GetGUID().GetCounter());
return player->IsFlying() || (flight && (flight->State==ClientlessFlightState::Mounting || flight->State==ClientlessFlightState::Flying));
}
bool AdventureGroupTravelMgr::Protected(Player* player) const
{auto found=_records.find(player->GetGUID().GetCounter());return found!=_records.end() && found->second.ProtectedTravel;}
bool AdventureGroupTravelMgr::Separated(Player* player) const
{auto found=_records.find(player->GetGUID().GetCounter());return found!=_records.end() && found->second.SeparationMs>=AdventureGroupTravelPolicy::SeparationTimeoutMs;}
bool AdventureGroupTravelMgr::GroundPoint(Player* player,Position& point) const
{
float ground=player->GetMap()->GetHeight(player->GetPhaseShift(),point.GetPositionX(),point.GetPositionY(),point.GetPositionZ()+3,true,18);
if(!std::isfinite(ground) || ground<=INVALID_HEIGHT || std::fabs(ground-point.GetPositionZ())>18)return false;
point.m_positionZ=ground;return true;
}
bool AdventureGroupTravelMgr::SafePath(Player* player,Position const& target) const
{
ClientlessPathDiagnostics diagnostic;
if(!sClientlessTravelMgr->AnalyzePath(player,target,diagnostic) || !diagnostic.Complete || diagnostic.Suspicious || diagnostic.PathLength>250)return false;
PathGenerator path(player);if(!path.CalculatePath(target.GetPositionX(),target.GetPositionY(),target.GetPositionZ()))return false;
auto const& points=path.GetPath();if(points.empty())return false;
float previous=player->GetPositionZ();
for(size_t i=1;i<points.size();++i)
{
auto const& a=points[i-1];auto const& b=points[i];float length=std::hypot(b.x-a.x,b.y-a.y);
unsigned count=std::max(1u,unsigned(std::ceil(length/2)));
for(unsigned j=1;j<=count;++j)
{
float t=float(j)/count,x=a.x+(b.x-a.x)*t,y=a.y+(b.y-a.y)*t,z=a.z+(b.z-a.z)*t;
float ground=player->GetMap()->GetHeight(player->GetPhaseShift(),x,y,z+2,true,6);
if(ground<=INVALID_HEIGHT || !AdventureGroupTravelPolicy::GroundSample(z,ground,previous,length/count))return false;
previous=ground;
}
}
return true;
}
unsigned AdventureGroupTravelMgr::Columns(Player* player,Player* leader)
{
auto& width=_widths[leader->GetGUID()];auto now=GameTime::GetGameTime();
if(width.At && now-width.At<2 && width.Center.GetExactDist(leader)<5)return width.Columns;
width.At=now;width.Center.Relocate(leader);width.Columns=1;
if(leader->GetTransport() || leader->IsInFlight() || player->GetMap()->IsDungeon())return 1;
for(unsigned columns:{3u,2u})
{
bool valid=true;
for(float behind:{4.0f,10.0f})for(float side:{-1.0f,1.0f})
{
auto point=OffsetPoint(*leader,{behind,side*(columns==3?5.0f:3.0f),0});
if(!GroundPoint(player,point) || !SafePath(player,point)){valid=false;break;}
}
if(valid){width.Columns=columns;break;}
}
return width.Columns;
}
Position AdventureGroupTravelMgr::Formation(Player* player,Player* leader,bool flight)
{
std::vector<ObjectGuid> members;
for(auto const& member:player->GetGroup()->GetMemberSlots())if(member.guid!=leader->GetGUID())members.push_back(member.guid);
std::sort(members.begin(),members.end());auto found=std::find(members.begin(),members.end(),player->GetGUID());
unsigned slot=unsigned(found-members.begin());
auto offset=AdventureGroupTravelPolicy::Formation(slot,flight?2:Columns(player,leader),leader->isMoving(),leader->IsMounted(),flight);
return OffsetPoint(*leader,offset);
}
bool AdventureGroupTravelMgr::Route(Player* player,Record& record,std::string const& node)
{
if(node.empty())return false;
uint32 guid=player->GetGUID().GetCounter();auto now=GameTime::GetGameTime();
if(Executing(guid))return true;
if(sClientlessPlayerMgr->GetQueuedActionCount(guid))return record.RouteOwned;
if(now<record.NextRoute)return false;
record.NextRoute=now+10;record.RouteGoal=node;
ClientlessAction action;action.Name="travel_to_node";action.Parameters["node"]=node;action.Parameters["hard_destination"]="true";
record.RouteOwned=sClientlessPlayerMgr->QueueAction(guid,std::move(action));return record.RouteOwned;
}
bool AdventureGroupTravelMgr::JourneyFollow(Player* player,Record& record,Leader const& leader)
{
auto now=GameTime::GetGameTime();if(leader.Kind==Journey::None || now-leader.Began>=1800)return false;
if(leader.Kind==Journey::Taxi)return Route(player,record,leader.To);
if(leader.Kind==Journey::Transport)
{
if(player->GetTransport() || player->IsInFlight())return true;
auto ride=sClientlessTransportMgr->GetStatus(player->GetGUID().GetCounter());
if(ride && ride->Phase!=ClientlessTransportPhase::Arrived && ride->Phase!=ClientlessTransportPhase::Failed)return true;
auto source=sClientlessTransportMgr->GetStop(leader.From);
if(source && !leader.To.empty())
{
if(player->GetMapId()==source->Map && player->GetExactDist(source->Points.front().Pos)<3)
{std::string error;return sClientlessTransportMgr->Start(player,leader.From,leader.To,error);}
return Route(player,record,leader.From);
}
if(leader.Arrived)
{
std::string node;float distance=0;
if(sClientlessTravelGraphMgr->FindNearestNodeToPosition(leader.GoalMap,leader.Goal.GetPositionX(),leader.Goal.GetPositionY(),leader.Goal.GetPositionZ(),node,distance) && distance<1000)return Route(player,record,node);
}
return true;
}
if(player->GetMapId()!=leader.SourceMap)
{
if(player->GetMapId()==leader.GoalMap)return false;
std::string node;float distance=0;
if(sClientlessTravelGraphMgr->FindNearestNodeToPosition(leader.SourceMap,leader.Source.GetPositionX(),leader.Source.GetPositionY(),leader.Source.GetPositionZ(),node,distance) && distance<1000)return Route(player,record,node);
return false;
}
if(Executing(player->GetGUID().GetCounter()))return true;
if(player->GetExactDist(leader.Source)>4)
{
if(player->GetExactDist(leader.Source)<100 && SafePath(player,leader.Source))return sClientlessTravelMgr->StartTravel(player,leader.Source,true,true);
std::string node;float distance=0;
if(sClientlessTravelGraphMgr->FindNearestNodeToPosition(leader.SourceMap,leader.Source.GetPositionX(),leader.Source.GetPositionY(),leader.Source.GetPositionZ(),node,distance) && distance<1000)return Route(player,record,node);
return false;
}
if(now-record.LastPortalUse<3)return true;
record.LastPortalUse=now;
if(leader.PortalTrigger)
{
auto area=sAreaTriggerStore.LookupEntry(leader.PortalTrigger);
if(area && player->IsInAreaTriggerRadius(area))
{WorldPacket packet(CMSG_AREATRIGGER,4);packet<<leader.PortalTrigger;player->GetSession()->HandleAreaTriggerOpcode(packet);return true;}
}
else if(auto portal=ObjectAccessor::GetGameObject(*player,leader.PortalObject))
{if(portal->IsInPhase(player)){portal->Use(player);return true;}}
return false;
}
bool AdventureGroupTravelMgr::Follow(Player* player)
{
uint32 guid=player->GetGUID().GetCounter();auto group=player->GetGroup();auto found=_records.find(guid);
if(!group || group->GetLeaderGUID()==player->GetGUID())
{
if(found!=_records.end() && found->second.FlightOwned)
{
sClientlessFlightMgr->LandFollowing(player);
auto flight=sClientlessFlightMgr->GetStatus(guid);
if(player->IsFlying() || (flight && (flight->State==ClientlessFlightState::Mounting || flight->State==ClientlessFlightState::Flying)))return true;
}
if(found!=_records.end())Clear(player);
return false;
}
auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID());if(!leader || !player->IsAlive() || player->IsInCombat() || player->IsBeingTeleported() || player->IsCharmed())return true;
auto& record=_records[guid];
if(record.FlightOwned)
{
auto flight=sClientlessFlightMgr->GetStatus(guid);
if(!flight || flight->State==ClientlessFlightState::Arrived || (!player->IsFlying() && flight->State==ClientlessFlightState::Failed))record.FlightOwned=false;
else if(!player->IsInMap(leader) || !leader->IsFlying() || leader->IsInFlight() || leader->GetTransport() || Separated(player))
{sClientlessFlightMgr->LandFollowing(player);record.Detail="safe landing before ground regroup/leave";return true;}
else if(!flight->FollowingPlayer)return true;
}
if(player->GetTransport() || player->IsInFlight())return true;
if(!record.Pending.empty())
{
JourneyFollow(player,record,record.Pending.front());
record.Detail="following/waiting for known travel transition";return true;
}
if(Executing(guid))return true;
if(!player->IsInMap(leader))return true;
if(!player->IsInPhase(leader) || !player->CanSeeOrDetect(leader))return true;
if(leader->IsInFlight() || leader->GetTransport())return true;
if(leader->IsFlying() && sClientlessFlightMgr->Enabled(player))
{
auto target=Formation(player,leader,true);std::string error;
if(sClientlessFlightMgr->Follow(player,target,error))
{record.FlightOwned=true;record.Detail="flying formation";return true;}
record.Detail=error;if(player->IsFlying())return true;
}
if(player->IsFlying() || player->IsFalling())return true;
auto target=Formation(player,leader,false);
if(!GroundPoint(player,target) || (leader->IsFlying() && std::fabs(leader->GetPositionZ()-target.GetPositionZ())>AdventureGroupTravelPolicy::GroundLeaderHeight))
{sClientlessTravelMgr->CancelTravel(guid);record.Detail="leader above reachable ground; waiting";return true;}
if(player->GetExactDist(leader)>100)
{
std::string node;float distance=0;
if(sClientlessTravelGraphMgr->FindNearestNodeToPosition(leader->GetMapId(),target.GetPositionX(),target.GetPositionY(),target.GetPositionZ(),node,distance) && distance<1000)Route(player,record,node);
return true;
}
if(!leader->IsMounted() && player->IsMounted() && player->GetExactDist(leader)<12)sClientlessLuaEngine->DismountGroundMount(player,"group leader on foot");
if(player->GetExactDist(target)<1.2f)
{sClientlessTravelMgr->CancelTravel(guid);record.Detail="holding assigned formation position";return true;}
auto nav=sClientlessTravelMgr->GetStatus(guid);
if(nav && nav->State==ClientlessTravelState::Moving && nav->Target.GetExactDist(target)<3)
{
if(auto active=sClientlessTravelMgr->GetActivePath(guid))if(!ValidateGroundPath(player,*active))
{sClientlessTravelMgr->CancelTravel(guid);record.Detail="active follow corridor became unsafe";}
return true;
}
if(!SafePath(player,target))
{sClientlessTravelMgr->CancelTravel(guid);record.Detail="no safe complete follow path; waiting";return true;}
if(leader->IsMounted())
{
auto profile=sClientlessLuaEngine->GetAdventureProfile(guid);
if(profile)sClientlessLuaEngine->PrepareGroundMount(player,std::max(profile->GroundMountMinDistance,player->GetExactDist(target)),"group-follow");
}
else if(player->IsMounted() && player->GetExactDist(leader)<12)sClientlessLuaEngine->DismountGroundMount(player,"group leader on foot");
bool started=sClientlessTravelMgr->StartTravel(player,target,true);
record.Detail=started?"safe ground formation":"follow spline rejected; waiting";return true;
}
bool AdventureGroupTravelMgr::AdaptRoad(Player* player,std::vector<Position>& path)
{
auto group=player->GetGroup();if(!group || path.size()<2)return true;
auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID());if(!leader || !player->IsInMap(leader))return true;
Position endpoint=path.back();
float total=0;size_t end=1;
for(;end<path.size();++end){total+=path[end-1].GetExactDist(path[end]);if(total>60 && end>1)break;}
path.resize(std::min(end,path.size()));if(path.size()<2)return false;
std::vector<ObjectGuid> members;for(auto const& member:group->GetMemberSlots())if(member.guid!=leader->GetGUID())members.push_back(member.guid);
std::sort(members.begin(),members.end());auto member=std::find(members.begin(),members.end(),player->GetGUID());
bool isLeader=player==leader;unsigned slot=unsigned(member-members.begin());
unsigned columns=isLeader?1:Columns(player,leader);
auto offset=AdventureGroupTravelPolicy::Formation(slot,columns,true,leader->IsMounted(),false);
if(!isLeader && leader->isMoving() && player->GetExactDist(leader)<65)
{
float walked=0,nearest=1e30f,progress=0;
for(size_t i=1;i<path.size();++i)
{
float distance=path[i].GetExactDist(leader);walked+=path[i-1].GetExactDist(path[i]);
if(distance<nearest){nearest=distance;progress=walked;}
}
if(nearest<15)
{
walked=0;size_t allowed=1;
for(;allowed<path.size();++allowed){walked+=path[allowed-1].GetExactDist(path[allowed]);if(walked>progress-offset.Behind)break;}
path.resize(allowed);if(path.size()<2)return true; // wait for the leading row
}
}
auto original=path;
for(unsigned attempt=0;attempt<2;++attempt)
{
path=original;float side=attempt?0:isLeader?0:offset.Side;
bool valid=true;
for(size_t i=1;i<path.size();++i)
{
float heading=std::atan2(original[i].GetPositionY()-original[i-1].GetPositionY(),original[i].GetPositionX()-original[i-1].GetPositionX());
// Merge at a nearby native graph endpoint; never shift boarding/portal nodes.
if(i+1<path.size() || path[i].GetExactDist(endpoint)>0.1f){path[i].m_positionX-=std::sin(heading)*side;path[i].m_positionY+=std::cos(heading)*side;}
if(!GroundPoint(player,path[i])){valid=false;break;}
}
if(valid)valid=ValidateGroundPath(player,path);
if(valid)return true;
}
return false;
}
bool AdventureGroupTravelMgr::ValidateGroundPath(Player* player,std::vector<Position> const& path) const
{
for(size_t i=1;i<path.size();++i)
{
auto const& a=path[i-1];auto const& b=path[i];float length=a.GetExactDist2d(b);
unsigned steps=std::max(1u,unsigned(std::ceil(length/2)));float last=a.GetPositionZ();
for(unsigned step=1;step<=steps;++step)
{
float t=float(step)/steps,x=a.GetPositionX()+(b.GetPositionX()-a.GetPositionX())*t,y=a.GetPositionY()+(b.GetPositionY()-a.GetPositionY())*t,z=a.GetPositionZ()+(b.GetPositionZ()-a.GetPositionZ())*t;
float floor=player->GetMap()->GetHeight(player->GetPhaseShift(),x,y,z+2,true,6);
if(floor<=INVALID_HEIGHT || !AdventureGroupTravelPolicy::GroundSample(z,floor,last,length/steps))return false;
last=floor;
}
if(!player->GetMap()->isInLineOfSight(player->GetPhaseShift(),a.GetPositionX(),a.GetPositionY(),a.GetPositionZ()+1,b.GetPositionX(),b.GetPositionY(),b.GetPositionZ()+1,LINEOFSIGHT_ALL_CHECKS,VMAP::ModelIgnoreFlags(0)))return false;
}
return true;
}
void AdventureGroupTravelMgr::Clear(Player* player)
{
auto found=_records.find(player->GetGUID().GetCounter());if(found==_records.end())return;
if(found->second.RouteOwned){sClientlessPlayerMgr->ClearQueuedActions(player->GetGUID().GetCounter());sClientlessActionExecutorMgr->Clear(player->GetGUID().GetCounter());}
sClientlessActionExecutorMgr->SetGroundPathAdapter(player->GetGUID().GetCounter(),{});
sClientlessTravelMgr->SetPathValidator(player->GetGUID().GetCounter(),{});
auto leader=found->second.LeaderGuid;
sClientlessTravelMgr->CancelTravel(player->GetGUID().GetCounter());_records.erase(found);
if(std::none_of(_records.begin(),_records.end(),[&](auto const& entry){return entry.second.LeaderGuid==leader;}))
{_leaders.erase(leader);_widths.erase(leader);}
}
void AdventureGroupTravelMgr::Offline(Player* player){Clear(player);}
void AdventureGroupTravelMgr::Report(ChatHandler* handler,Player* player) const
{
auto found=_records.find(player->GetGUID().GetCounter());if(found==_records.end()){handler->SendSysMessage("No group travel state.");return;}
auto const& record=found->second;
handler->PSendSysMessage("Group travel leader=%u separationMs=%u protected=%u flight=%u pending=%u route='%s' detail='%s'",record.LeaderGuid.GetCounter(),record.SeparationMs,record.ProtectedTravel,record.FlightOwned,uint32(record.Pending.size()),record.RouteGoal.c_str(),record.Detail.c_str());
}
@@ -0,0 +1,66 @@
#ifndef VCORE_ADVENTURE_GROUP_TRAVEL_MGR_H
#define VCORE_ADVENTURE_GROUP_TRAVEL_MGR_H
#include "Common.h"
#include "ObjectGuid.h"
#include "Position.h"
#include <ctime>
#include <deque>
#include <vector>
#include <string>
#include <unordered_map>
class Player;
class ChatHandler;
class TC_GAME_API AdventureGroupTravelMgr
{
public:
static AdventureGroupTravelMgr* instance();
void Observe(Player* player, uint32 diff);
bool Follow(Player* player);
bool AdaptRoad(Player* player, std::vector<Position>& path);
bool ValidateGroundPath(Player* player, std::vector<Position> const& path) const;
bool Protected(Player* player) const;
bool Airborne(Player* player) const;
bool Separated(Player* player) const;
void Clear(Player* player);
void Offline(Player* player);
void Report(ChatHandler* handler, Player* player) const;
private:
enum class Journey { None, Taxi, Transport, Portal };
struct Leader
{
bool Seen = false, WasTaxi = false, WasTransport = false;
uint32 Map = 0, GroundMap = 0, GoalMap = 0, PortalTrigger = 0;
Position Previous, Ground, Goal, Source;
ObjectGuid PortalObject, CandidatePortal;
uint32 CandidateSourceMap=0,CandidateGoalMap=0;
Position CandidateSource,CandidateGoal;
uint32 SourceMap = 0;
std::string From, To;
Journey Kind = Journey::None;
time_t SampleAt = 0, Began = 0, Arrived = 0;
uint32 Generation = 0;
};
struct Record
{
ObjectGuid LeaderGuid;
uint32 Timer = 0, SeparationMs = 0, Generation = 0, BotMap = 0;
Position ProgressPosition;
time_t ProgressAt = 0, NextRoute = 0, LastPortalUse = 0, NativeTransitAt = 0;
bool RouteOwned = false, FlightOwned = false, ProtectedTravel = false;
std::string RouteGoal, Detail;
std::deque<Leader> Pending;
};
struct Width { time_t At = 0; Position Center; unsigned Columns = 1; };
void SampleLeader(Player* player, Leader& state, time_t now);
bool GroundPoint(Player* player, Position& point) const;
bool SafePath(Player* player, Position const& point) const;
unsigned Columns(Player* player, Player* leader);
Position Formation(Player* player, Player* leader, bool flight);
bool Route(Player* player, Record& record, std::string const& node);
bool JourneyFollow(Player* player, Record& record, Leader const& leader);
std::unordered_map<uint32, Record> _records;
std::unordered_map<ObjectGuid, Leader> _leaders;
std::unordered_map<ObjectGuid, Width> _widths;
};
#define sAdventureGroupTravelMgr AdventureGroupTravelMgr::instance()
#endif
@@ -0,0 +1,36 @@
#ifndef VCORE_ADVENTURE_GROUP_TRAVEL_POLICY_H
#define VCORE_ADVENTURE_GROUP_TRAVEL_POLICY_H
#include <algorithm>
#include <cmath>
#include <cstdint>
namespace AdventureGroupTravelPolicy
{
constexpr unsigned SampleMs = 500;
constexpr float SeparationDistance = 500;
constexpr unsigned SeparationTimeoutMs = 600000;
constexpr float GroundLeaderHeight = 12;
constexpr float MaxGroundStep = 1.75f;
constexpr float MaxGroundSlope = 0.8f;
struct Offset { float Behind, Side, Height; };
inline Offset Formation(unsigned slot, unsigned columns, bool moving, bool mounted, bool flying)
{
columns = std::clamp(columns, 1u, 3u);
float spacing = flying ? 7.0f : mounted ? 4.5f : 3.0f;
if (!moving && !flying) columns = std::min(columns, 3u);
unsigned row = slot / columns;
float side = (float(slot % columns) - float(columns - 1) / 2) * spacing;
return {spacing + float(row) * spacing, side, flying ? float(slot % 2) * 2 : 0};
}
inline unsigned Separation(unsigned elapsed, unsigned diff, bool far, bool protectedTravel)
{
if (!far) return 0;
if (protectedTravel) return elapsed;
return std::min(SeparationTimeoutMs, elapsed + std::min(diff, 30000u));
}
inline bool GroundSample(float expected, float actual, float previous, float distance)
{
return std::isfinite(actual) && std::fabs(actual - expected) <= MaxGroundStep &&
std::fabs(actual - previous) <= MaxGroundStep + MaxGroundSlope * distance;
}
}
#endif
@@ -1,6 +1,7 @@
#include "AdventureInteractionMgr.h"
#include "AdventureInteractionPolicy.h"
#include "AdventureGroupMgr.h"
#include "AdventureGroupTravelMgr.h"
#include "AdventureBehaviorMgr.h"
#include "AdventureCombatMgr.h"
#include "ClientlessLuaEngine.h"
@@ -75,6 +76,7 @@ void AdventureInteractionMgr::Leave(Player* p,Record& r,char const* reason)
p->GetGroup()->RemoveMember(p->GetGUID(),GROUP_REMOVEMETHOD_LEAVE);
sAdventureBehaviorMgr->InterruptForInteraction(p);
sAdventureBehaviorMgr->ResumePersonalPlan(p);
sAdventureGroupTravelMgr->Clear(p);
r.Grouped=false;r.Leader.Clear();r.Invite.Clear();r.Partner.Clear();r.SceneUntil=0;
}
void AdventureInteractionMgr::Emote(Player* p,Player* source,uint32 emote)
@@ -100,12 +102,13 @@ void AdventureInteractionMgr::Reply(Player* p,Player* other,uint32 emote)
}
void AdventureInteractionMgr::CombatHelp(Player* p,ObjectGuid helper)
{ _records[p->GetGUID().GetCounter()].Helper=helper; }
void AdventureInteractionMgr::Offline(Player* p){sAdventureGroupMgr->Offline(p);_records.erase(p->GetGUID().GetCounter());}
void AdventureInteractionMgr::Offline(Player* p){sAdventureGroupTravelMgr->Offline(p);sAdventureGroupMgr->Offline(p);_records.erase(p->GetGUID().GetCounter());}
bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
{
auto& r=_records[p->GetGUID().GetCounter()];auto now=GameTime::GetGameTime();
if(r.Timer>diff){r.Timer-=diff;return (r.Grouped && !sAdventureGroupMgr->BotLed(p)) || r.SceneUntil>now;}
r.Timer=1000;
if(!p->GetGroup() && sAdventureGroupTravelMgr->Follow(p))return true;
bool unsafe=!p->IsAlive() || p->IsInCombat() || sAdventureCombatMgr->Busy(p) ||
p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsCharmed();
if(unsafe){r.Partner.Clear();r.SceneUntil=0;return p->GetGroup()!=nullptr;}
@@ -171,13 +174,16 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
}
bool forming=sAdventureGroupMgr->Forming(p);
if(forming){r.LastActivity=now;r.AfkSince=0;r.Schedule=sAdventureBehaviorMgr->InteractionSchedule(p);}
if(!forming && AdventureInteractionPolicy::CanLeave(p->IsAlive(),p->IsInCombat(),p->GetMap()->IsDungeon(),p->GetTransport() || p->IsInFlight() || p->IsFlying()))
bool protectedTravel=sAdventureGroupTravelMgr->Protected(p);
if(protectedTravel){r.LastActivity=now;r.AfkSince=0;}
if(!forming && !protectedTravel && AdventureInteractionPolicy::CanLeave(p->IsAlive(),p->IsInCombat(),p->GetMap()->IsDungeon(),p->GetTransport() || p->IsInFlight() || p->IsFlying()))
{
if(r.Schedule!=sAdventureBehaviorMgr->InteractionSchedule(p))
{
r.Schedule=sAdventureBehaviorMgr->InteractionSchedule(p);
if(!sAdventureGroupMgr->PlannedTransition(p) && frand(0,1)>=Loyalty(p)){Leave(p,r,"intent-change");return false;}
}
if(sAdventureGroupTravelMgr->Separated(p)){Leave(p,r,"leader-distance");return false;}
if(AdventureInteractionPolicy::LeaderExpired(now,r.AfkSince?r.AfkSince:r.LastActivity)){Leave(p,r,"leader-afk");return false;}
}
// Existing warrior executor supplies damage assistance. Roles/healing
@@ -191,39 +197,9 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
}
if(sAdventureGroupMgr->BotLed(p))return false;
if(!leader || leader==p || !leader->IsAlive())return true;
auto execution=sClientlessActionExecutorMgr->GetStatus(p->GetGUID().GetCounter());
if(execution && (execution->State==ClientlessActionExecutionState::Moving || execution->State==ClientlessActionExecutionState::SelectingTarget))return true;
if(!p->IsInMap(leader) || p->GetExactDist(leader)>=100)
{
if(now>=r.NextRoute)
{
r.NextRoute=now+15;
std::string node;float distance=0;
sClientlessTravelGraphMgr->EnsureLoaded();
if(sClientlessTravelGraphMgr->FindNearestNodeToPosition(leader->GetMapId(),leader->GetPositionX(),leader->GetPositionY(),leader->GetPositionZ(),node,distance) && distance<1000)
{
ClientlessAction action;action.Name="travel_to_node";action.Parameters["node"]=node;
sClientlessPlayerMgr->QueueAction(p->GetGUID().GetCounter(),std::move(action));
}
}
return true;
}
if(!p->IsInPhase(leader) || !p->CanSeeOrDetect(leader))return true;
if(p->GetExactDist(leader)>5 && p->GetExactDist(leader)<100)
{
auto nav=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter());
if(!nav || nav->State!=ClientlessTravelState::Moving || nav->Target.GetExactDist(leader)>5)
{
Position end;leader->GetNearPoint(p,end.m_positionX,end.m_positionY,end.m_positionZ,3,leader->GetOrientation()+3.14159265f);
ClientlessPathDiagnostics path;
if(sClientlessTravelMgr->AnalyzePath(p,end,path) && path.Complete && !path.Suspicious && path.PathLength<150)
sClientlessTravelMgr->StartTravel(p,end);
}
}
else if(p->GetExactDist(leader)<=5)sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());
return true;
return sAdventureGroupTravelMgr->Follow(p);
}
if(r.Grouped){r.Grouped=false;r.Leader.Clear();sAdventureBehaviorMgr->InterruptForInteraction(p);sAdventureBehaviorMgr->ResumePersonalPlan(p);}
if(r.Grouped){sAdventureGroupTravelMgr->Clear(p);r.Grouped=false;r.Leader.Clear();sAdventureBehaviorMgr->InterruptForInteraction(p);sAdventureBehaviorMgr->ResumePersonalPlan(p);}
auto intent=sAdventureBehaviorMgr->InteractionIntent(p);
if(now>=r.NextEncounter && now>=r.NextSocial && !p->GetMap()->IsDungeon() && (intent=="social" || intent=="leisure" || intent=="rest"))
{
@@ -252,6 +228,7 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
void AdventureInteractionMgr::Report(ChatHandler* handler,Player* p) const
{
sAdventureGroupMgr->Report(handler,p);
sAdventureGroupTravelMgr->Report(handler,p);
auto it=_records.find(p->GetGUID().GetCounter());
if(it==_records.end()){handler->SendSysMessage("No interaction state yet.");return;}
auto const& r=it->second;auto now=GameTime::GetGameTime();
@@ -167,6 +167,9 @@ ClientlessActionExecutorMgr* ClientlessActionExecutorMgr::instance()
return &instance;
}
void ClientlessActionExecutorMgr::SetGroundPathAdapter(uint32 guid,GroundPathAdapter adapter)
{if(adapter)_groundAdapters[guid]=std::move(adapter);else _groundAdapters.erase(guid);}
void ClientlessActionExecutorMgr::Clear(uint32 characterGuid)
{
_records.erase(characterGuid);
@@ -203,7 +206,7 @@ ClientlessActionExecutionStatus const* ClientlessActionExecutorMgr::GetStatus(ui
bool ClientlessActionExecutorMgr::StartAction(Player* player, ClientlessAction const& action, Record& record)
{
record.StopAtNode = false;
record.StopAtNode = false;record.FormationWaiting=false;record.GroundChunk=false;
record.TransportActive = false;
record.FlightActive = false;
record.FlightApproachActive = false;
@@ -1770,6 +1773,22 @@ bool ClientlessActionExecutorMgr::StartRecordedEdgePath(
return false;
}
float anticipatedDistance=0;
for(size_t i=1;i<path.size();++i)anticipatedDistance+=path[i-1].GetExactDist(path[i]);
record.FormationWaiting=false;record.GroundChunk=false;
auto adapter=_groundAdapters.find(characterGuid);
if(adapter!=_groundAdapters.end())
{
size_t original=path.size();
if(!adapter->second(player,path))
{
player->StopMoving();record.Active=false;record.Status.State=ClientlessActionExecutionState::Failed;
record.Status.Detail="recorded group road rejected by safety adapter";return false;
}
if(path.size()<2)
{if(player->IsSplineEnabled())player->StopMoving();record.FormationWaiting=true;record.Status.State=ClientlessActionExecutionState::Moving;record.Status.Detail="waiting for leading formation row";return true;}
record.GroundChunk=path.size()<original;record.GroundChunkEnd=path.back();record.EdgeSplinePoints.resize(path.size());
}
float recordedDistance = 0.0f;
for (std::size_t i = 1; i < path.size(); ++i)
{
@@ -1779,7 +1798,7 @@ bool ClientlessActionExecutorMgr::StartRecordedEdgePath(
recordedDistance += std::sqrt(dx * dx + dy * dy + dz * dz);
}
sClientlessLuaEngine->PrepareGroundMount(player, recordedDistance, "recorded-ground-edge");
sClientlessLuaEngine->PrepareGroundMount(player, std::max(recordedDistance,anticipatedDistance), "recorded-ground-edge");
player->GetMotionMaster()->MoveSmoothPath(
1,
@@ -2447,6 +2466,7 @@ void ClientlessActionExecutorMgr::UpdateTravelToNode(
if (!record.EdgePath.empty())
{
if(record.FormationWaiting){StartRecordedEdgePath(player,characterGuid,record);return;}
ClientlessTravelEdgePoint const& destination = record.EdgePath.back();
// VCORE_C816_PORTAL_MOUNT_GUARD: when a long recorded-road spline starts
@@ -2507,7 +2527,7 @@ void ClientlessActionExecutorMgr::UpdateTravelToNode(
if (index >= 0 && std::size_t(index) < record.EdgeSplinePoints.size())
record.EdgeNextPoint = std::max(record.EdgeNextPoint,
record.EdgeSplinePoints[std::size_t(index)]);
CheckGroundExit(player, characterGuid, record);
if(!_groundAdapters.count(characterGuid))CheckGroundExit(player, characterGuid, record);
if (record.GroundExitActive)
return;
}
@@ -2518,6 +2538,14 @@ void ClientlessActionExecutorMgr::UpdateTravelToNode(
// it until the spline is finalized. Do not evaluate arrival before then.
if (player->IsSplineEnabled())
return;
if(record.GroundChunk && !record.EdgeSplinePoints.empty())
{
if(player->GetExactDist(record.GroundChunkEnd)>2)
{record.Active=false;record.Status.State=ClientlessActionExecutionState::Failed;record.Status.Detail="group road stopped before checked chunk endpoint";return;}
record.EdgeNextPoint=std::max(record.EdgeNextPoint,record.EdgeSplinePoints.back());
StartRecordedEdgePath(player,characterGuid,record);return;
}
float dx = player->GetPositionX() - destination.X;
float dy = player->GetPositionY() - destination.Y;
@@ -8,6 +8,8 @@
#include "Common.h"
#include "ClientlessTravelGraphMgr.h"
#include <string>
#include <functional>
#include "Position.h"
#include <memory>
#include <unordered_map>
#include <unordered_set>
@@ -42,6 +44,10 @@ public:
void Update(uint32 diff);
void Clear(uint32 characterGuid);
// Optional owner-supplied recorded-ground safety/formation adapter. It may
// offset points or truncate the tail; fewer than two points means wait.
using GroundPathAdapter=std::function<bool(Player*,std::vector<Position>&)>;
void SetGroundPathAdapter(uint32 guid,GroundPathAdapter adapter);
bool RequestStopAtNode(uint32 characterGuid);
ClientlessActionExecutionStatus const* GetStatus(uint32 characterGuid) const;
@@ -55,6 +61,8 @@ private:
std::shared_ptr<ClientlessAction> PendingShared;
uint32 SharedWaitMs=0;
bool StopAtNode = false;
bool FormationWaiting = false, GroundChunk = false;
Position GroundChunkEnd;
std::vector<ClientlessTravelRouteStep> Route;
std::size_t RouteStep = 0;
@@ -138,6 +146,7 @@ private:
void UpdateTravelToLocation(Player* player, uint32 characterGuid, Record& record);
void UpdateActive(Player* player, uint32 characterGuid, Record& record);
std::unordered_map<uint32, GroundPathAdapter> _groundAdapters;
std::unordered_map<uint32, Record> _records;
};
+82 -3
View File
@@ -167,7 +167,69 @@ Surface ClientlessFlightMgr::Sample(Player * player, Record & record, float x, f
};
}
bool ClientlessFlightMgr::Start(Player * player, Position const & target, std::string & error)
bool ClientlessFlightMgr::Start(Player* player,Position const& target,std::string& error)
{return Begin(player,target,error,false);}
bool ClientlessFlightMgr::Follow(Player* player,Position const& target,std::string& error)
{
auto found=_records.find(player->GetGUID().GetCounter());
if(found!=_records.end() && (found->second.Status.State==ClientlessFlightState::Mounting || found->second.Status.State==ClientlessFlightState::Flying))
{
auto& record=found->second;
if(!record.Status.FollowingPlayer || record.Returning){error="existing flight must finish safely";return false;}
if(!Trinity::IsValidMapCoord(target.GetPositionX(),target.GetPositionY(),target.GetPositionZ()))return false;
auto surface=Sample(player,record,target.GetPositionX(),target.GetPositionY());
if(!surface.Valid || target.GetPositionZ()<surface.Ground || target.GetPositionZ()+5>surface.Ceiling)return false;
record.Status.Target=target;record.Status.Landing=target;record.TargetUpdated=record.Elapsed;return true;
}
return Begin(player,target,error,true);
}
bool ClientlessFlightMgr::LandFollowing(Player* player)
{
auto found=_records.find(player->GetGUID().GetCounter());if(found==_records.end())return false;
auto& record=found->second;if(!record.Status.FollowingPlayer)return false;
record.Status.FollowingPlayer=false;
if(record.Status.State==ClientlessFlightState::Mounting)
{Fail(player,record,"group flight cancelled before takeoff");return true;}
auto surface=Sample(player,record,player->GetPositionX(),player->GetPositionY());
float solid=player->GetMap()->GetHeight(player->GetPhaseShift(),player->GetPositionX(),player->GetPositionY(),MAX_HEIGHT,true,MAX_HEIGHT);
if(surface.Valid && std::fabs(surface.Ground-solid)<0.7f)
{
record.Status.Landing.Relocate(player->GetPositionX(),player->GetPositionY(),solid,player->GetOrientation());
record.Status.Target=record.Status.Landing;record.Memory={};record.Following.reset();record.Status.Travelled=0;
record.Status.Detail="landing after group flight";return true;
}
Fail(player,record,"no safe local landing; returning along validated flight trail");return true;
}
bool ClientlessFlightMgr::AdvanceFollowing(Player* player,Record& record)
{
Point current=PointOf(*player),goal=PointOf(record.Status.Target);
if(Distance(current,goal)<2){if(record.Moving)player->StopMoving();record.Moving=false;record.Status.Detail="holding flight formation";return true;}
auto sample=[&](float x,float y){return Sample(player,record,x,y);};
auto ray=[&](Point const& a,Point const& b){return ClearRay(player,a,b);};
auto settings=record.Settings;settings.Safety=1;
Point best;float score=std::numeric_limits<float>::infinity();
float heading=std::atan2(goal.Y-current.Y,goal.X-current.X);
float length=std::min(25.0f,Distance(current,goal));
for(float turn:{0.0f,0.4f,-0.4f,0.8f,-0.8f})for(float climb:{0.0f,4.0f})
{
auto point=Mix(current,goal,std::min(1.0f,length/std::max(0.1f,Distance(current,goal))));
if(turn!=0){float horizontal=Horizontal(current,point);point.X=current.X+std::cos(heading+turn)*horizontal;point.Y=current.Y+std::sin(heading+turn)*horizontal;}
point.Z+=climb;
if(!Corridor(current,point,goal,settings,sample,ray,true))continue;
float candidate=Distance(point,goal)+std::fabs(turn)*4+climb;
if(candidate<score){score=candidate;best=point;}
}
if(!std::isfinite(score)){player->StopMoving();record.Moving=false;record.Status.Detail="group flight corridor blocked; holding mounted";return false;}
if(record.Moving && player->IsSplineEnabled() && Distance(best,PointOf(record.Segment))<3)return true;
if(record.Breadcrumbs.size()<4096)record.Breadcrumbs.push_back(*player);
record.Segment.Relocate(best.X,best.Y,best.Z,player->GetOrientation());
Movement::MoveSplineInit move(player);move.MoveTo(best.X,best.Y,best.Z,false);move.SetFly();move.SetWalk(false);move.SetVelocity(player->GetSpeed(MOVE_FLIGHT));
player->AddUnitMovementFlag(MOVEMENTFLAG_FLYING);
if(move.Launch()<=0){record.Moving=false;return false;}
record.Moving=true;record.SegmentElapsed=0;++record.Status.Segments;record.Status.Detail="following moving group flight target";return true;
}
bool ClientlessFlightMgr::Begin(Player * player, Position const & target, std::string & error, bool following)
{
if (! Trinity::IsValidMapCoord(target.GetPositionX(), target.GetPositionY(), target.GetPositionZ()))
{
@@ -228,7 +290,14 @@ bool ClientlessFlightMgr::Start(Player * player, Position const & target, std::s
return false;
}
bool foundLanding = false;
for (float radius :
if(following)
{
auto surface=Sample(player,record,target.GetPositionX(),target.GetPositionY());
if(!surface.Valid || target.GetPositionZ()<surface.Ground || target.GetPositionZ()+5>surface.Ceiling)
{error="group flight target outside verified flight bounds";return false;}
record.Status.Landing=target;record.Status.FollowingPlayer=true;foundLanding=true;
}
if(!following) for (float radius :
{
0.0f, 6.0f, 12.0f, 20.0f
})
@@ -273,13 +342,16 @@ bool ClientlessFlightMgr::Start(Player * player, Position const & target, std::s
record.Status.State = ClientlessFlightState::Mounting;
record.Status.MountSpell = spell;
record.Status.Detail = "waiting for native flying mount";
if(auto group=player->GetGroup())if(group->GetLeaderGUID()!=player->GetGUID())
if(!following)if(auto group=player->GetGroup())if(group->GetLeaderGUID()!=player->GetGUID())
{
auto source=_records.find(group->GetLeaderGUID().GetCounter());
if(source!=_records.end() && source->second.Status.MapId==player->GetMapId() &&
source->second.Status.Target.GetExactDist(target)<1 && !source->second.Published->Aborted)
{
record.Following=source->second.Published;record.Status.Landing=source->second.Status.Landing;
std::vector<ObjectGuid> members;for(auto const& member:group->GetMemberSlots())if(member.guid!=group->GetLeaderGUID())members.push_back(member.guid);
std::sort(members.begin(),members.end());auto slot=std::find(members.begin(),members.end(),player->GetGUID())-members.begin();
record.FormationDelay=3500+uint32(slot)*600; // native shared corridor, staggered single-file departure
TC_LOG_INFO("server.adventurebot.navigation","FLIGHT_SHARED_TRAIL guid=%u leader=%u",player->GetGUID().GetCounter(),group->GetLeaderGUID().GetCounter());
}
}
@@ -297,6 +369,7 @@ bool ClientlessFlightMgr::Start(Player * player, Position const & target, std::s
void ClientlessFlightMgr::Fail(Player * player, Record & record, std::string const & reason)
{
record.Published->Aborted=true;
record.Status.FollowingPlayer=false;
player->StopMoving();
record.Moving = false;
if (record.Status.State == ClientlessFlightState::Mounting) player->InterruptNonMeleeSpells(false);
@@ -427,6 +500,7 @@ void ClientlessFlightMgr::Update(uint32 diff)
continue;
}
}
if(record.Following && record.Elapsed<record.FormationDelay)continue;
if (! player->CanFly() || record.Elapsed >(record.Returning ? 2100000u : 1800000u))
{
Fail(player, record, "flight permission lost or total timeout");
@@ -438,6 +512,11 @@ void ClientlessFlightMgr::Update(uint32 diff)
continue;
}
record.CheckTimer = 200;
if(record.Status.FollowingPlayer)
{
if(record.Elapsed-record.TargetUpdated>5000 || player->IsInCombat())LandFollowing(player);
else {AdvanceFollowing(player,record);continue;}
}
if (record.Moving && player->IsSplineEnabled())
{
if (record.SegmentElapsed > 10000)
@@ -20,6 +20,7 @@ struct ClientlessFlightStatus
uint32 MapId = 0, MountSpell = 0, Segments = 0, Avoidances = 0, Handoffs = 0;
Position Target, Landing;
float Travelled = 0;
bool FollowingPlayer = false;
};
class TC_GAME_API ClientlessFlightMgr
{
@@ -30,6 +31,8 @@ class TC_GAME_API ClientlessFlightMgr
bool Enabled(Player const * player) const;
void BeginPlanning();
bool Start(Player * player, Position const & target, std::string & error);
bool Follow(Player* player, Position const& target, std::string& error);
bool LandFollowing(Player* player);
void Update(uint32 diff);
void Clear(uint32 guid);
ClientlessFlightStatus const * GetStatus(uint32 guid) const;
@@ -45,12 +48,15 @@ class TC_GAME_API ClientlessFlightMgr
Position Start, Segment, LastPosition;
uint32 Elapsed = 0, SegmentElapsed = 0, CheckTimer = 0;
bool Moving = false, Returning = false;
uint32 TargetUpdated = 0, FormationDelay = 0;
std::string AbortReason;
std::vector < Position > Breadcrumbs;
std::unordered_map < std::string, bool > Tiles;
};
ClientlessFlightPath::Surface Sample(Player * player, Record & record, float x, float y) const;
bool Advance(Player * player, Record & record, bool handoff = false);
bool Begin(Player* player, Position const& target, std::string& error, bool following);
bool AdvanceFollowing(Player* player, Record& record);
void Fail(Player * player, Record & record, std::string const & reason);
std::pair < uint32, uint32 > AreaZone(Player const * player, uint32 map, Position const & point) const;
mutable std::unordered_map < std::string, std::pair < uint32, uint32 >> _planningAreas;
@@ -8,6 +8,7 @@
#include "AdventureCombatMgr.h"
#include "AdventureInteractionMgr.h"
#include "AdventureGroupMgr.h"
#include "AdventureGroupTravelMgr.h"
#include "AdventureGeneratorMgr.h"
#include "AdventurePopulationMgr.h"
#include "AdventureSupplyMgr.h"
@@ -229,7 +230,9 @@ public:
{ sAdventureBehaviorMgr->IntentChanged(player, intent); }
void Update(Player* player, uint32 diff) override
{
sAdventureGroupTravelMgr->Observe(player,diff);
sAdventurePopulationMgr->Maintain(player,diff);
if(sAdventureGroupTravelMgr->Airborne(player)){sAdventureGroupTravelMgr->Follow(player);return;}
if(sAdventureSupplyMgr->Update(player,diff,sAdventureCombatMgr->Busy(player)))return;
if(!sAdventureCombatMgr->Update(player,diff) && !sAdventureInteractionMgr->Update(player,diff))
sAdventureBehaviorMgr->Update(player,diff);
@@ -314,6 +314,76 @@ bool ClientlessTransportMgr::Save(Player* player, std::string const& stand, std:
return true;
}
ClientlessTransportStop const* ClientlessTransportMgr::GetStop(std::string const& node)
{
if(!EnsureLoaded())return nullptr;
auto found=_stops.find(node);return found==_stops.end()?nullptr:&found->second;
}
bool ClientlessTransportMgr::ObserveJourney(Player* player,uint32 lastMap,Position const& ground,std::string& from,std::string& to)
{
from.clear();to.clear();if(!player->GetTransport() || !EnsureLoaded())return false;
ClientlessTransportStop const* source=nullptr;float best=100;
for(auto const& pair:_stops)
{
auto const& stop=pair.second;if(stop.Map!=lastMap)continue;
auto object=Resolve(player,stop.Spawn);
if(!object || object->ToTransportBase()!=player->GetTransport())continue;
float distance=Distance(ground,stop.Points.front().Pos);
if(distance<best){best=distance;source=&stop;}
}
if(!source)return false;
from=source->Node;
unsigned count=0;
for(auto const& pair:_stops)if(pair.first!=from && SameService(*source,pair.second))
{to=pair.first;++count;}
if(count!=1)to.clear(); // multi-stop services resolve the actual arrival instead of guessing
return true;
}
bool ClientlessTransportMgr::ArrivalNode(Player* player,std::string const& from,std::string& to)
{
auto source=GetStop(from);if(!source)return false;
float best=100;to.clear();
for(auto const& pair:_stops)
{
auto const& stop=pair.second;
if(stop.Map!=player->GetMapId() || pair.first==from || !SameService(*source,stop))continue;
float distance=Distance(*player,stop.Points.front().Pos);
if(distance<best){best=distance;to=pair.first;}
}
return !to.empty();
}
void ClientlessTransportMgr::RecoverPassenger(Player* player,Ride& ride,uint32 diff)
{
ride.RecoveryMs+=std::min(diff,30000u);
auto const& destination=ride.From.Points.front().Pos;
if(player->IsBeingTeleported()){player->GetSession()->CompleteClientlessTeleport();return;}
if(!player->IsAlive())return; // the ordinary death controller retains resurrection ownership
if(player->GetTransport())return;
if(player->GetMapId()==ride.From.Map && Distance(*player,destination)<2.5f && !player->IsFalling())
{
sClientlessTravelMgr->CancelTravel(player->GetGUID().GetCounter());
ride.Elapsed=0;ride.RecoveryMs=0;ride.RecoveryMoving=false;
if(ride.Abort || ride.Recoveries>=3)
{SetPhase(player,ride,ClientlessTransportPhase::Failed,"safely returned to departure; ride cancelled/recovery limit");return;}
ride.Point=0;SetPhase(player,ride,ClientlessTransportPhase::Waiting,"recovered at safe departure; waiting for next service");return;
}
auto nav=sClientlessTravelMgr->GetStatus(player->GetGUID().GetCounter());
bool failed=nav && (nav->State==ClientlessTravelState::Stuck || nav->State==ClientlessTravelState::PathRejected);
if(ride.RecoveryMoving && nav && nav->State==ClientlessTravelState::Moving && ride.RecoveryMs<60000)return;
if(!ride.RecoveryMoving && !failed && !player->IsFalling() && player->GetMapId()==ride.From.Map)
{
ClientlessPathDiagnostics path;
if(sClientlessTravelMgr->AnalyzePath(player,destination,path) && path.Complete && !path.Suspicious && path.PathLength<2000 &&
sClientlessTravelMgr->StartTravel(player,destination,true))
{ride.RecoveryMoving=true;return;}
}
sClientlessTravelMgr->CancelTravel(player->GetGUID().GetCounter());
// Only a recorded, actually boarded journey can reach this fallback.
// Never teleport toward the leader or to an inferred unrecorded dock.
player->TeleportTo(ride.From.Map,destination.GetPositionX(),destination.GetPositionY(),destination.GetPositionZ(),destination.GetOrientation());
TC_LOG_WARN("server.adventurebot.navigation","TRANSPORT_RECOVERY_TELEPORT guid=%u node='%s'",player->GetGUID().GetCounter(),ride.From.Node.c_str());
}
bool ClientlessTransportMgr::Start(Player* player, std::string const& from, std::string const& to, std::string& error)
{
if (!player || !player->IsInWorld() || player->GetTransport() || !HasConnection(from, to))
@@ -326,6 +396,9 @@ bool ClientlessTransportMgr::Start(Player* player, std::string const& from, std:
ride.RequiredPause = uint32(PathLength(source) / std::max(1.0f, player->GetSpeed(MOVE_RUN)) * 1000.0f) + uint32(source.Points.size()) * 100 + 1500;
ride.Status.Phase = ClientlessTransportPhase::Waiting;
ride.Status.Detail = "waiting for recorded transport at departure dock";
player->RemoveAurasByType(SPELL_AURA_MOUNTED);
if(player->IsMounted())player->Dismount();
player->UpdateSpeed(MOVE_RUN);
sClientlessTravelMgr->CancelTravel(player->GetGUID().GetCounter());
player->StopMoving();
_rides[player->GetGUID().GetCounter()] = std::move(ride);
@@ -363,6 +436,7 @@ void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff)
player->GetSession()->CompleteClientlessTeleport();
return;
}
if(ride.Status.Recovering){RecoverPassenger(player,ride,diff);return;}
GameObject* object = FindObject(player, ride.From);
uint32 limit = uint32(std::min<uint64>(1800000, std::max<uint64>(180000, uint64(ride.From.Period) * 3 + 30000)));
auto fail = [&](char const* detail)
@@ -390,7 +464,15 @@ void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff)
if (ride.Status.Phase == Phase::Riding)
{
if (!object || player->GetTransport() != object->ToTransportBase())
{ fail("transport passenger identity lost"); return; }
{
if(!player->GetTransport())
{
player->StopMoving();ride.Status.Recovering=true;ride.Status.Detail="lost passenger; returning to recorded safe departure";
ride.RecoveryMs=0;ride.RecoveryMoving=false;++ride.Recoveries;
RecoverPassenger(player,ride,diff);return;
}
fail("transport passenger identity changed; attachment retained");return;
}
ClientlessTransportStop const* arrival = nullptr;
if (AtDock(player, object, ride.To, uint32(PathLength(ride.To) / std::max(1.0f, player->GetSpeed(MOVE_RUN)) * 1000) + uint32(ride.To.Points.size()) * 100 + 1500)) arrival = &ride.To;
else if (ride.Abort && AtDock(player, object, ride.From, ride.RequiredPause)) arrival = &ride.From;
@@ -41,6 +41,7 @@ struct ClientlessTransportStatus
ClientlessTransportPhase Phase = ClientlessTransportPhase::Failed;
std::string Detail;
bool Cancelled = false;
bool Recovering = false;
};
class TC_GAME_API ClientlessTransportMgr
@@ -49,6 +50,9 @@ public:
static ClientlessTransportMgr* instance();
bool Reload();
bool HasConnection(std::string const& from, std::string const& to);
bool ObserveJourney(Player* passenger, uint32 lastMap, Position const& lastGround, std::string& from, std::string& to);
bool ArrivalNode(Player* player, std::string const& from, std::string& to);
ClientlessTransportStop const* GetStop(std::string const& node);
bool Start(Player* player, std::string const& from, std::string const& to, std::string& error);
ClientlessTransportStatus const* GetStatus(uint32 guid) const;
void Cancel(uint32 guid);
@@ -77,11 +81,14 @@ private:
uint32 PointTimeout = 0;
bool Moving = false;
bool Abort = false;
uint32 RecoveryMs = 0, Recoveries = 0;
bool RecoveryMoving = false;
};
bool EnsureLoaded();
bool Sample(Player* player, Recording& recording, bool force);
bool Save(Player* player, std::string const& stand, std::string& error);
void UpdateRide(Player* player, Ride& ride, uint32 diff);
void RecoverPassenger(Player* player, Ride& ride, uint32 diff);
void SetPhase(Player* player, Ride& ride, ClientlessTransportPhase phase, char const* detail);
GameObject* FindObject(Player* player, ClientlessTransportStop const& stop) const;
bool AtDock(Player* player, GameObject* object, ClientlessTransportStop const& stop, uint32 requiredPause) const;
@@ -77,6 +77,9 @@ char const* ClientlessTravelMgr::StateName(ClientlessTravelState state)
return "Unknown";
}
void ClientlessTravelMgr::SetPathValidator(uint32 guid,PathValidator validator)
{if(validator)_validators[guid]=std::move(validator);else _validators.erase(guid);}
bool ClientlessTravelMgr::AnalyzePath(
Player* player,
Position const& target,
@@ -410,6 +413,12 @@ bool ClientlessTravelMgr::LaunchPath(Player* player, TravelRecord& record)
return false;
}
auto validator=_validators.find(player->GetGUID().GetCounter());
if(validator!=_validators.end() && !validator->second(player,spline))
{
player->StopMoving();record.Status.State=ClientlessTravelState::PathRejected;
record.Status.Path.Reason="owner ground-safety validator rejected spline";return false;
}
record.ActiveSpline = spline;
++record.Status.SegmentIndex;
@@ -8,6 +8,7 @@
#include "Common.h"
#include "Position.h"
#include <string>
#include <functional>
#include <unordered_map>
#include <vector>
@@ -57,6 +58,8 @@ class TC_GAME_API ClientlessTravelMgr
public:
static ClientlessTravelMgr* instance();
using PathValidator=std::function<bool(Player*,std::vector<Position> const&)>;
void SetPathValidator(uint32 guid,PathValidator validator);
bool StartTravel(Player* player, Position const& target, bool allowGroundMount = false, bool portalApproach = false, bool walk = false);
bool AnalyzePath(Player* player, Position const& target, ClientlessPathDiagnostics& diagnostics) const;
void CancelTravel(uint32 characterGuid);
@@ -85,6 +88,7 @@ private:
bool LaunchPath(Player* player, TravelRecord& record);
float DistanceFromActiveSpline(Player const* player, TravelRecord const& record) const;
std::unordered_map<uint32, PathValidator> _validators;
std::unordered_map<uint32, TravelRecord> _records;
uint64 _nextNavSequence = 1;
};
@@ -0,0 +1,125 @@
"""Exercise production group formation, safety, transition queue and separation."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/AdventureGroupTravelMgr.cpp').read_text()
def method(sig):
a=s.index(sig);b=s.index('{',a);e=b+1;n=1
while n:n+=(s[e]=='{')-(s[e]=='}');e+=1
return s[a:e]
h=(root/'src/server/game/World/AdventureGroupTravelMgr.h').read_text()
h='\n'.join(v for v in h.splitlines() if not v.startswith('#include')).replace('private:','public:')
code=r'''
#include "AdventureGroupTravelPolicy.h"
#include <algorithm>
#include <cassert>
#include <cmath>
#include <ctime>
#include <deque>
#include <functional>
#include <string>
#include <unordered_map>
#include <vector>
#include <set>
using uint32=unsigned;
#define TC_GAME_API
constexpr float INVALID_HEIGHT=-100000;
constexpr int LINEOFSIGHT_ALL_CHECKS=0,CMSG_AREATRIGGER=0;
namespace VMAP{enum class ModelIgnoreFlags{None};}
struct ObjectGuid{unsigned id=0;unsigned GetCounter()const{return id;}void Clear(){id=0;}explicit operator bool()const{return id;}bool operator==(ObjectGuid b)const{return id==b.id;}bool operator!=(ObjectGuid b)const{return id!=b.id;}bool operator<(ObjectGuid b)const{return id<b.id;}};
namespace std{template<>struct hash<ObjectGuid>{size_t operator()(ObjectGuid g)const{return g.id;}};}
struct Position{
float m_positionX=0,m_positionY=0,m_positionZ=0,o=0;Position()=default;Position(float x,float y,float z=0,float a=0):m_positionX(x),m_positionY(y),m_positionZ(z),o(a){}
void Relocate(Position const*p){*this=*p;}void Relocate(float x,float y,float z,float a=0){*this={x,y,z,a};}
float GetPositionX()const{return m_positionX;}float GetPositionY()const{return m_positionY;}float GetPositionZ()const{return m_positionZ;}float GetOrientation()const{return o;}
float GetExactDist(Position const&p)const{return std::hypot(std::hypot(m_positionX-p.m_positionX,m_positionY-p.m_positionY),m_positionZ-p.m_positionZ);}
float GetExactDist(Position const*p)const{return GetExactDist(*p);}float GetExactDist(float x,float y,float z)const{return GetExactDist(Position{x,y,z});}
float GetExactDist2d(Position const&p)const{return std::hypot(m_positionX-p.m_positionX,m_positionY-p.m_positionY);}
};
struct Map{bool cliff=false,los=true,narrow=false;bool IsDungeon(){return false;}
float GetHeight(unsigned phase,float x,float y,float,bool,float){assert(phase==169);if(cliff && x>4 && x<15)return -1000;if(narrow && std::fabs(y)>2)return -1000;return 0;}
bool isInLineOfSight(unsigned,float,float,float,float,float,float,int,VMAP::ModelIgnoreFlags){return los;}
}map;
struct Session{bool clientless=false;bool IsClientless(){return clientless;}void HandleAreaTriggerOpcode(struct WorldPacket&){}};
struct Group{ObjectGuid leader{21};struct Slot{ObjectGuid guid;};std::vector<Slot>slots{{{21}},{{26}},{{27}}};ObjectGuid GetLeaderGUID(){return leader;}auto const&GetMemberSlots(){return slots;}}group;
struct Player:Position{
ObjectGuid guid;bool alive=true,combat=false,teleport=false,charmed=false,flying=false,falling=false,mounted=false,moving=false,taxi=false,transport=false,phase=true,visible=true;unsigned mapid=0;Group*g=&group;Session session;
Player(unsigned id):guid{id}{}ObjectGuid GetGUID(){return guid;}Group*GetGroup(){return g;}Map*GetMap(){return &map;}unsigned GetMapId(){return mapid;}unsigned GetPhaseShift(){return 169;}
Session*GetSession(){return &session;}bool IsAlive(){return alive;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool IsCharmed(){return charmed;}
bool IsFlying(){return flying;}bool IsFalling(){return falling;}bool IsMounted(){return mounted;}bool isMoving(){return moving;}bool IsInFlight(){return taxi;}bool GetTransport(){return transport;}
bool IsInMap(Player*p){return mapid==p->mapid;}bool IsInPhase(Player*p){return phase && p->phase;}bool CanSeeOrDetect(Player*p){return visible && p->visible;}bool IsInAreaTriggerRadius(void*){return false;}
};
struct WorldPacket{WorldPacket(int,int){}template<class T>WorldPacket&operator<<(T){return *this;}};
struct GameObject{bool IsInPhase(Player*){return true;}void Use(Player*){}};
namespace ObjectAccessor{std::unordered_map<ObjectGuid,Player*>players;Player*FindPlayer(ObjectGuid g){auto it=players.find(g);return it==players.end()?nullptr:it->second;}GameObject*GetGameObject(Player&,ObjectGuid){return nullptr;}}
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 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;
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&)>){}
}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;
enum class ClientlessTransportPhase{Waiting,Riding,Arrived,Failed};
struct TransportStatus{ClientlessTransportPhase Phase=ClientlessTransportPhase::Failed;};struct Stop{unsigned Map=0;struct Point{Position Pos;};std::vector<Point>Points{{{0,0,0}}};};
struct Transport{TransportStatus status;auto GetStatus(unsigned){return &status;}Stop*GetStop(std::string const&){return nullptr;}bool Start(Player*,std::string const&,std::string const&,std::string&){return false;}}transport;auto sClientlessTransportMgr=&transport;
enum class ClientlessFlightState{Mounting,Flying,Arrived,Failed};
struct Flight{struct Status{ClientlessFlightState State=ClientlessFlightState::Failed;bool FollowingPlayer=false;}status;bool enabled=false;unsigned follows=0,lands=0;auto GetStatus(unsigned){return &status;}bool Enabled(Player*){return enabled;}
bool Follow(Player*,Position const&,std::string&){++follows;status.State=ClientlessFlightState::Flying;status.FollowingPlayer=true;return true;}bool LandFollowing(Player*){++lands;status.FollowingPlayer=false;return true;}}flight;auto sClientlessFlightMgr=&flight;
struct Profile{float GroundMountMinDistance=60;};struct Lua{Profile profile;auto GetAdventureProfile(unsigned){return &profile;}void PrepareGroundMount(Player*p,float,char const*){p->mounted=true;}void DismountGroundMount(Player*p,char const*){p->mounted=false;}}lua;auto sClientlessLuaEngine=&lua;
struct ChatHandler{void SendSysMessage(char const*){}template<class...T>void PSendSysMessage(char const*,T...){}};
'''+h+r'''
AdventureGroupTravelMgr* AdventureGroupTravelMgr::instance(){static AdventureGroupTravelMgr m;return &m;}
AdventureGroupTravelMgr::Leader observed;
void AdventureGroupTravelMgr::SampleLeader(Player*,Leader&state,time_t){state=observed;}
'''+method(' bool Executing(')+method(' Position OffsetPoint(')
for sig in ['void AdventureGroupTravelMgr::Observe(', 'bool AdventureGroupTravelMgr::Airborne(', 'bool AdventureGroupTravelMgr::Protected(', 'bool AdventureGroupTravelMgr::Separated(', 'bool AdventureGroupTravelMgr::GroundPoint(', 'bool AdventureGroupTravelMgr::SafePath(', 'unsigned AdventureGroupTravelMgr::Columns(', 'Position AdventureGroupTravelMgr::Formation(', 'bool AdventureGroupTravelMgr::Route(', 'bool AdventureGroupTravelMgr::JourneyFollow(', 'bool AdventureGroupTravelMgr::Follow(', 'bool AdventureGroupTravelMgr::AdaptRoad(', 'bool AdventureGroupTravelMgr::ValidateGroundPath(', 'void AdventureGroupTravelMgr::Clear(']:code+=method(sig)+'\n'
code+=r'''
int main(){
using namespace AdventureGroupTravelPolicy;
for(unsigned columns:{1,2,3})for(bool moving:{false,true})for(bool mounted:{false,true})for(bool flying:{false,true}){
std::set<std::tuple<float,float,float>>points;for(unsigned slot=0;slot<20;++slot){auto p=Formation(slot,columns,moving,mounted,flying);assert(points.emplace(p.Behind,p.Side,p.Height).second);}
}
unsigned elapsed=0;for(unsigned i=0;i<1199;++i)elapsed=Separation(elapsed,500,true,false);assert(elapsed==599500);
assert(Separation(elapsed,500,true,true)==elapsed);assert(Separation(elapsed,500,true,false)==600000);assert(Separation(600000,500,false,false)==0);
Player leader(21),a(26),b(27);a.session.clientless=b.session.clientless=true;leader.m_positionX=20;
ObjectAccessor::players={{{21},&leader},{{26},&a},{{27},&b}};
AdventureGroupTravelMgr mgr;mgr.Observe(&a,500);mgr.Observe(&b,500);assert(mgr.Follow(&a));auto first=travel.targets.back();travel.status.State=ClientlessTravelState::Arrived;assert(mgr.Follow(&b));auto second=travel.targets.back();assert(first.GetExactDist(second)>2);
// A valid-looking MMAP endpoint beyond a hole must never authorize a shortcut.
travel.status.State=ClientlessTravelState::Arrived;map.cliff=true;unsigned starts=travel.starts;assert(mgr.Follow(&a));assert(travel.starts==starts);map.cliff=false;
// Formation compresses on narrow ground; this is checked against phased terrain.
mgr._widths.clear();map.narrow=true;assert(mgr.Columns(&a,&leader)==1);map.narrow=false;mgr._widths.clear();assert(mgr.Columns(&a,&leader)==3);
travel.status.State=ClientlessTravelState::Arrived;leader.mounted=true;mgr.Follow(&a);assert(a.mounted);leader.mounted=false;a=Player(26); // retain a clean origin
// A ground-only bot waits safely when the leader gains excessive height.
leader.flying=true;leader.m_positionZ=80;starts=travel.starts;mgr.Follow(&a);assert(travel.starts==starts);leader.flying=false;leader.m_positionZ=0;
// A native-capable follower uses flight, and safely lands after a kick.
flight.enabled=true;leader.flying=true;mgr.Follow(&a);assert(flight.follows==1 && mgr.Airborne(&a));a.g=nullptr;mgr.Follow(&a);assert(flight.lands==1);a.g=&group;flight.status.State=ClientlessFlightState::Arrived;leader.flying=false;flight.enabled=false;mgr.Follow(&a);
// Taxi journey and its arrival grace pause distance counting. Completion must
// not protect unrelated later ground separation.
auto& state=observed;state.Kind=AdventureGroupTravelMgr::Journey::Taxi;state.Generation=1;state.Began=GameTime::now;state.To="taxi_42";state.GoalMap=1;state.Goal={1000,0,0};leader.taxi=true;leader.m_positionX=900;
mgr.Observe(&a,500);assert(mgr.Protected(&a));mgr.Follow(&a);assert(core.nodes.back()=="taxi_42");unsigned queued=core.queued;mgr.Follow(&a);assert(core.queued==queued);
leader.taxi=false;leader.mapid=1;leader.m_positionX=1000;state.Arrived=++GameTime::now;mgr.Observe(&a,500);assert(mgr.Protected(&a));
a.mapid=1;a.m_positionX=1000;mgr.Observe(&a,500);assert(mgr._records.at(26).Pending.empty() && !mgr.Protected(&a));
// Sequential portals retain the first transition until the follower arrives.
state.Kind=AdventureGroupTravelMgr::Journey::Portal;state.Generation=2;state.SourceMap=1;state.GoalMap=2;state.Goal={100,0,0};state.Began=++GameTime::now;mgr.Observe(&a,500);
state.Generation=3;state.SourceMap=2;state.GoalMap=3;state.Goal={200,0,0};mgr.Observe(&a,500);assert(mgr._records.at(26).Pending.size()==2 && mgr._records.at(26).Pending.front().GoalMap==2);
a.mapid=2;a.m_positionX=100;mgr.Observe(&a,500);assert(mgr._records.at(26).Pending.size()==1 && mgr._records.at(26).Pending.front().GoalMap==3);
// A blocked/unrecognized trip cannot grant unlimited separation protection.
mgr._records.at(26).Pending.clear();state.Kind=AdventureGroupTravelMgr::Journey::None;state.Generation=4;leader.mapid=0;leader.m_positionX=1000;a.mapid=0;a.m_positionX=0;executor.status.State=ClientlessActionExecutionState::Completed;
for(unsigned i=0;i<1200;++i){if(i%2==0)++GameTime::now;mgr.Observe(&a,500);}assert(mgr.Separated(&a));leader.m_positionX=10;mgr.Observe(&a,500);assert(!mgr.Separated(&a));
// 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);
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['g++','-std=c++17','-I',str(root/'src/server/game/World'),str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Production group travel: distinct slots, phase169 cliff safety, width, mounts, flight/kick landing, taxi grace, portal queue, bounded separation and recorded-road validation passed')
@@ -67,6 +67,7 @@ enum class ClientlessActionExecutionState{Moving,SelectingTarget,Completed};stru
struct Executor {Execution status;Execution*GetStatus(unsigned){return &status;}}executor;auto sClientlessActionExecutorMgr=&executor;
struct Graph {void EnsureLoaded(){}bool FindNearestNodeToPosition(unsigned,float,float,float,std::string&node,float&distance){node="entrance";distance=10;return true;}}graph;auto sClientlessTravelGraphMgr=&graph;
struct BotGroups{void Offline(Player*){}void Update(Player*){}bool Accept(Player*,Player*){return false;}bool botLed=false;bool BotLed(Player*){return botLed;}bool Forming(Player*){return false;}bool PlannedTransition(Player*){return false;}void Report(ChatHandler*,Player*){}}botGroups;auto sAdventureGroupMgr=&botGroups;
struct GroupTravel{bool protectedTravel=false,separated=false;int follows=0;bool Follow(Player*p){++follows;return p->GetGroup()!=nullptr;}bool Protected(Player*){return protectedTravel;}bool Separated(Player*){return separated;}void Clear(Player*){}void Offline(Player*){}void Report(ChatHandler*,Player*){}}groupTravel;auto sAdventureGroupTravelMgr=&groupTravel;
'''+header+'\n'+source+r'''
int main(){
using namespace AdventureInteractionPolicy;
@@ -82,7 +83,7 @@ int main(){
bot.invite=&group;tick(100);assert(!bot.group);tick(102);assert(bot.group && accepts==1);
assert(mgr._records[26].Grouped && behavior.interrupts==1);
// Follow uses verified local MMAP paths; invalid paths never move the player.
leader.m_positionX=20;tick(103);assert(travel.starts==1);travel.active=false;travel.valid=false;leader.m_positionX=30;tick(104);assert(travel.starts==1);travel.valid=true;
leader.m_positionX=20;auto follows=groupTravel.follows;tick(103);assert(groupTravel.follows==follows+1);leader.m_positionX=30;tick(104);assert(groupTravel.follows==follows+2);
// Changing leader resets both AFK and inactivity timers.
mgr._records[26].AfkSince=1;group.leader=other.guid;tick(105);assert(mgr._records[26].AfkSince==0 && mgr._records[26].LastActivity==105);
group.leader=leader.guid;tick(106);
@@ -109,7 +110,7 @@ int main(){
// Unreserved bot-origin invitations are declined.
mgr._records.clear();group.leader=other.guid;bot.invite=&group;tick(2300);tick(2302);assert(!bot.group && declines==1);
// Missing leader map uses the existing graph executor, without teleporting.
group.leader=leader.guid;bot.group=&group;leader.mapid=1;tick(2400);assert(core.queued==1);executor.status.State=ClientlessActionExecutionState::Moving;tick(2416);assert(core.queued==1);
group.leader=leader.guid;bot.group=&group;leader.mapid=1;follows=groupTravel.follows;tick(2400);assert(groupTravel.follows>follows);executor.status.State=ClientlessActionExecutionState::Moving;tick(2416);assert(bot.group);
// Pending help is acknowledged only outside combat, after caller's self-care.
mgr.CombatHelp(&bot,leader.guid);leader.mapid=0;executor.status.State=ClientlessActionExecutionState::Completed;bot.combat=true;auto says=bot.says;tick(2417);assert(bot.says==says);bot.combat=false;tick(2418);assert(bot.says==says+1);
// A newly joined bot follower cancels its previous scene without stopping the leader.
+5 -3
View File
@@ -11,6 +11,7 @@ update = source[a:b]
harness = r'''
#include "ClientlessFlightPath.h"
#include <cassert>
#include <memory>
#include <cstdint>
#include <string>
#include <unordered_map>
@@ -26,7 +27,7 @@ struct Spline {int remaining=500; int timeRemaining()const{return remaining;}};
struct Player:Position {
Spline spline; Spline* movespline=&spline;
bool alive=true,fly=true,running=true;int stops=0;
bool IsInWorld(){return true;} uint32 GetMapId(){return 0;}
bool IsInCombat(){return false;}bool IsInWorld(){return true;} uint32 GetMapId(){return 0;}
bool IsAlive(){return alive;} bool IsBeingTeleported(){return false;}
bool IsMounted(){return true;} bool CanFly(){return fly;}
bool IsSplineEnabled(){return running;}
@@ -41,9 +42,9 @@ struct World {
auto sWorld=&world;
enum class ClientlessFlightState {Mounting,Flying,Arrived,Failed};
struct Record {
struct {ClientlessFlightState State=ClientlessFlightState::Flying;uint32 MapId=0;Position Landing{800,0,0};float Travelled=0;} Status;
struct {bool FollowingPlayer=false;ClientlessFlightState State=ClientlessFlightState::Flying;uint32 MapId=0;Position Landing{800,0,0};float Travelled=0;} Status;
Position LastPosition,Start,Segment{25,0,60};Settings Settings;
uint32 Elapsed=0,SegmentElapsed=0,CheckTimer=0;
std::shared_ptr<int> Following;uint32 FormationDelay=0,TargetUpdated=0,Elapsed=0,SegmentElapsed=0,CheckTimer=0;
bool Moving=true,Returning=false;
};
bool clear=true;
@@ -55,6 +56,7 @@ public:
Surface Sample(Player*,Record&,float,float){return {0,300,true};}
void Fail(Player*,Record& r,std::string const&){++failures;r.Status.State=ClientlessFlightState::Failed;}
bool Advance(Player*,Record&,bool handoff=false){++advances;handoffs+=handoff;return true;}
bool LandFollowing(Player*){return true;}bool AdvanceFollowing(Player*,Record&){return true;}
void Update(uint32 diff);
};
'''.replace('Settings Settings;', 'ClientlessFlightPath::Settings Settings;')
+1 -1
View File
@@ -50,7 +50,7 @@ struct Area{bool fly=true;uint32 World_pvp_ID=0;Flags GetMountFlags()const{retur
struct Areas{bool known=true;auto LookupEntry(uint32){return known?&area:nullptr;}} areas;auto& sAreaTableStore=areas;
enum class ClientlessFlightState{Mounting,Flying,Arrived,Failed};
struct Record{std::shared_ptr<ClientlessFlightPath::SharedTrail>Published=std::make_shared<ClientlessFlightPath::SharedTrail>();
struct{ClientlessFlightState State=ClientlessFlightState::Flying;uint32 MapId=0;std::string Detail;}Status;
struct{bool FollowingPlayer=false;ClientlessFlightState State=ClientlessFlightState::Flying;uint32 MapId=0;std::string Detail;}Status;
bool Moving=true,Returning=false;std::vector<Position> Breadcrumbs{{}};std::string AbortReason;
};
class ClientlessFlightMgr{public:
@@ -29,6 +29,7 @@ preamble = r'''
#include <unordered_set>
#include <vector>
#include <memory>
#include <functional>
using uint8=uint8_t; using uint32=uint32_t; using uint64=uint64_t; using int32=int32_t;
#define TC_GAME_API
#define TC_LOG_INFO(...) ((void)0)
@@ -40,6 +41,7 @@ struct Position {
float x=0,y=0,z=0,o=0;
void Relocate(float a,float b,float c,float d=0){x=a;y=b;z=c;o=d;}
void Relocate(Player const*);
float GetExactDist(Position const&p)const{return std::hypot(x-p.x,y-p.y);}
float GetPositionX()const{return x;} float GetPositionY()const{return y;}
float GetPositionZ()const{return z;} float GetOrientation()const{return o;}
};
@@ -49,7 +51,7 @@ struct Motion {
};
struct Player {
float x=0; bool spline=true, flying=false;Motion motion;
bool IsFlying()const{return flying;}bool IsSplineEnabled()const{return spline;}
void StopMoving(){spline=false;}bool IsFlying()const{return flying;}bool IsSplineEnabled()const{return spline;}
bool IsInWorld()const{return true;}uint32 GetMapId()const{return 0;}
float GetPositionX()const{return x;}float GetPositionY()const{return 0;}float GetPositionZ()const{return 0;}
float GetOrientation()const{return 0;}
@@ -184,6 +186,15 @@ int main(){
air.GroundResumeActive=true;air.GroundExitActive=false;p.x=140;travel.status.State=ClientlessTravelState::Arrived;
before=p.motion.launches;mgr.UpdateGroundExit(&p,26,air);
assert(!air.GroundResumeActive && p.motion.launches==before && air.Active);
// Owner adapters can reject, wait, or shorten a recorded road without
// losing the original index mapping or launching an unsafe spline.
Record adapted;adapted.Active=true;adapted.EdgePath.resize(5);for(unsigned i=0;i<5;++i)adapted.EdgePath[i].X=150+i*10;p.x=140;
mgr._groundAdapters[26]=[](Player*,std::vector<Position>&){return false;};before=p.motion.launches;
assert(!mgr.StartRecordedEdgePath(&p,26,adapted) && p.motion.launches==before);
mgr._groundAdapters[26]=[](Player*,std::vector<Position>&path){path.resize(1);return true;};
assert(mgr.StartRecordedEdgePath(&p,26,adapted) && adapted.FormationWaiting && p.motion.launches==before);
mgr._groundAdapters[26]=[](Player*,std::vector<Position>&path){path.resize(3);return true;};
assert(mgr.StartRecordedEdgePath(&p,26,adapted) && adapted.GroundChunk && !adapted.FormationWaiting && adapted.EdgeSplinePoints.size()==3);
std::cout<<"optional exits: repeated probes, retained route, delayed success, forward resume, flight fallback passed\n";
}
'''
@@ -0,0 +1,64 @@
"""Execute moving-target flight and safe landing adapters over real geometry."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/ClientlessFlightMgr.cpp').read_text()
def method(sig):
a=s.index(sig);b=s.index('{',a);e=b+1;n=1
while n:n+=(s[e]=='{')-(s[e]=='}');e+=1
return s[a:e]
code=r'''
#include "ClientlessFlightPath.h"
#include <cassert>
#include <memory>
#include <string>
#include <unordered_map>
using uint32=unsigned;using namespace ClientlessFlightPath;
constexpr int MAX_HEIGHT=100000,MOVEMENTFLAG_FLYING=1,MOVE_FLIGHT=2;
struct Position{float x=0,y=0,z=0,o=0;void Relocate(float a,float b,float c,float d=0){x=a;y=b;z=c;o=d;}float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}float GetOrientation()const{return o;}};
Point PointOf(Position const&p){return {p.x,p.y,p.z};}
namespace Trinity{bool IsValidMapCoord(float x,float y,float z){return std::isfinite(x)&&std::isfinite(y)&&std::isfinite(z);}}
struct Map{float floor=0;float GetHeight(int,float,float,float,bool,float){return floor;}}map;
struct Guid{unsigned GetCounter(){return 26;}};
struct Player:Position{bool spline=false;unsigned launches=0,stops=0;Guid GetGUID(){return {};}bool IsSplineEnabled(){return spline;}void StopMoving(){++stops;spline=false;}void AddUnitMovementFlag(int){}float GetSpeed(int){return 20;}Map*GetMap(){return &map;}int GetPhaseShift(){return 169;}};
namespace Movement{struct MoveSplineInit{Player*p;Point target;MoveSplineInit(Player*who):p(who){}void MoveTo(float x,float y,float z,bool){target={x,y,z};}void SetFly(){}void SetWalk(bool){}void SetVelocity(float){}int Launch(){++p->launches;p->spline=true;return 1;}};}
enum class ClientlessFlightState{Mounting,Flying,Arrived,Failed};
struct Record{
struct{ClientlessFlightState State=ClientlessFlightState::Flying;bool FollowingPlayer=true;Position Target,Landing;unsigned Segments=0;float Travelled=5000;std::string Detail;}Status;
bool Returning=false,Moving=false;unsigned Elapsed=1000,TargetUpdated=0,SegmentElapsed=0;Position Segment;
ClientlessFlightPath::Settings Settings;ClientlessFlightPath::Memory Memory;
std::shared_ptr<int>Following;std::vector<Position>Breadcrumbs;
};
bool clear=true,valid=true;float ceiling=300;
bool ClearRay(Player*,Point const&,Point const&){return clear;}
struct ClientlessFlightMgr{
std::unordered_map<unsigned,Record>_records;unsigned beginnings=0,failures=0;
Surface Sample(Player*,Record&,float,float){return {map.floor,ceiling,valid};}
bool Begin(Player*,Position const&,std::string&,bool following){assert(following);++beginnings;return true;}
void Fail(Player*,Record&r,std::string const&){++failures;r.Status.State=ClientlessFlightState::Failed;}
bool Follow(Player*,Position const&,std::string&);bool LandFollowing(Player*);bool AdvanceFollowing(Player*,Record&);
};
'''
for sig in ['bool ClientlessFlightMgr::Follow(', 'bool ClientlessFlightMgr::LandFollowing(', 'bool ClientlessFlightMgr::AdvanceFollowing(']:code+=method(sig)+'\n'
code+=r'''
int main(){
ClientlessFlightMgr mgr;Player p;p.z=20;Position target;target.Relocate(100,0,20);std::string error;
assert(mgr.Follow(&p,target,error) && mgr.beginnings==1);auto&r=mgr._records[26];
assert(mgr.Follow(&p,target,error));assert(r.TargetUpdated==r.Elapsed && r.Status.Target.x==100);
assert(mgr.AdvanceFollowing(&p,r));assert(p.launches==1 && r.Segment.x>0 && r.Segment.x<=25 && r.Segment.z==20);
// Moving the leader updates the destination without inventing a ground landing.
target.Relocate(110,20,30);r.Elapsed=2000;assert(mgr.Follow(&p,target,error));assert(r.Status.Target.z==30 && r.TargetUpdated==2000);
clear=false;assert(!mgr.AdvanceFollowing(&p,r));assert(p.stops==1 && r.Status.State==ClientlessFlightState::Flying);
clear=true;ceiling=25;assert(!mgr.Follow(&p,target,error));ceiling=300;
p.Relocate(110,20,30);r.Moving=true;assert(mgr.AdvanceFollowing(&p,r));assert(!r.Moving && p.stops==2); // holds in air, never dismounts at moving goal
assert(mgr.LandFollowing(&p));assert(!r.Status.FollowingPlayer && r.Status.Landing.z==0 && r.Status.Travelled==0);
assert(!mgr.Follow(&p,target,error)); // landing cannot be replaced by a new pursuit
r.Status.FollowingPlayer=true;r.Status.State=ClientlessFlightState::Mounting;assert(mgr.LandFollowing(&p) && mgr.failures==1);
r.Status.State=ClientlessFlightState::Flying;r.Status.FollowingPlayer=true;valid=false;assert(mgr.LandFollowing(&p) && mgr.failures==2);
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['g++','-std=c++17','-I',str(root/'src/server/game/World'),str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Moving-target flight: bounded phased corridors, obstacle hold, ceiling rejection, aerial formation hold, safe landing and return fallback passed')
+21 -3
View File
@@ -58,7 +58,7 @@ struct Position {
float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}float GetOrientation()const{return o;}
float GetExactDist(Position const*p)const{return std::sqrt((x-p->x)*(x-p->x)+(y-p->y)*(y-p->y)+(z-p->z)*(z-p->z));}
};
constexpr int MOVE_RUN=0;
constexpr int MOVE_RUN=0,SPELL_AURA_MOUNTED=1;
struct TransportDockState {uint32 Progress=0,Period=0,RemainingPause=0;bool Stopped=false;};
class Player; class GameObject;
class TransportBase {
@@ -86,6 +86,9 @@ class Player:public Position {
public:
TransportBase* transport=nullptr;uint32 map=0;GameObject* object=nullptr;Motion motion{this};bool pending=false;
struct {struct {Position pos;ObjectGuid guid;uint32 time=0;void Reset(){pos={};guid={};time=0;}}transport;}m_movementInfo;
int teleports=0;bool falling=false;
bool IsFalling()const{return falling;}bool IsMounted(){return false;}void Dismount(){}void UpdateSpeed(int){}void RemoveAurasByType(int){}
bool TeleportTo(uint32 m,float a,float b,float c,float d){++teleports;map=m;Relocate(a,b,c,d);falling=false;return true;}
bool IsAlive()const{return true;}bool IsFlying()const{return false;}bool IsInFlight()const{return false;}
bool IsInWorld()const{return true;}bool IsInPhase(GameObject*)const{return true;}
bool IsBeingTeleported()const{return pending;}TransportBase* GetTransport()const{return transport;}
@@ -98,7 +101,11 @@ void TransportBase::CalculatePassengerOffset(float&x,float&y,float&z,float*o)con
void TransportBase::AddPassenger(Player*p){p->transport=this;p->m_movementInfo.transport.guid=go->GetGUID();}
void TransportBase::RemovePassenger(Player*p){p->transport=nullptr;p->m_movementInfo.transport.Reset();}
void Motion::MoveSmoothPath(int,Position* path,int,bool){p->Relocate(path[1]);if(p->transport){Position pos=*p;p->transport->CalculatePassengerOffset(pos.x,pos.y,pos.z,&pos.o);p->m_movementInfo.transport.pos=pos;}}
struct Travel {void CancelTravel(uint32){}}travel;
enum class ClientlessTravelState{Moving,Arrived,Stuck,PathRejected};
struct ClientlessPathDiagnostics{bool Complete=true,Suspicious=false;float PathLength=50;};
struct Travel{bool path=false,present=false;int starts=0;struct Status{ClientlessTravelState State=ClientlessTravelState::Moving;}status;
auto GetStatus(uint32)->Status*{return present?&status:nullptr;}bool AnalyzePath(Player*,Position const&,ClientlessPathDiagnostics&){return path;}
bool StartTravel(Player*p,Position const&point,bool){++starts;present=true;p->Relocate(point);return true;}void CancelTravel(uint32){present=false;}}travel;
auto sClientlessTravelMgr=&travel;
// Model the real Field API's strict debug type checks for SMALLINT versus INT.
struct Field {
@@ -120,7 +127,7 @@ methods = '\n'.join(function(source, marker) for marker in [
'bool ClientlessTransportMgr::HasConnection(', 'bool ClientlessTransportMgr::AtDock(',
'bool ClientlessTransportMgr::Sample(', 'bool ClientlessTransportMgr::Start(',
'void ClientlessTransportMgr::SetPhase(', 'void ClientlessTransportMgr::Cancel(',
'void ClientlessTransportMgr::UpdateRide('
'void ClientlessTransportMgr::RecoverPassenger(', 'void ClientlessTransportMgr::UpdateRide('
])
stubs = r'''
void PublishPassengerState(Player*){}
@@ -203,6 +210,17 @@ int main(){
run(ships,sailor,crossing);assert(crossing.Status.Phase==Phase::Riding&&sailor.transport==&destination.base);
destination.x=100;sailor.x=101;
run(ships,sailor,crossing);assert(crossing.Status.Phase==Phase::Arrived&&!sailor.transport&&sailor.x==104&&sailor.map==0);
// A lost, previously boarded passenger returns to the recorded departure,
// never to the moving leader or the destination harbor.
Player lost;lost.object=&go;lost.x=50;auto recovery=mgr._rides.at(26);
recovery.Status.Phase=Phase::Riding;recovery.Status.Recovering=false;recovery.Abort=false;
travel.path=false;mgr.UpdateRide(&lost,recovery,100);
assert(recovery.Status.Recovering && lost.teleports==1 && lost.x==0);
mgr.UpdateRide(&lost,recovery,100);assert(!recovery.Status.Recovering && recovery.Status.Phase==Phase::Waiting);
// A valid walking return uses native point travel, without teleporting.
lost.x=50;lost.teleports=0;travel.path=true;recovery.Status.Phase=Phase::Riding;
mgr.UpdateRide(&lost,recovery,100);assert(lost.teleports==0 && travel.starts==1);
mgr.UpdateRide(&lost,recovery,100);assert(!recovery.Status.Recovering);
std::cout<<"Transport dock, frame, recorder, cross-map copy and ride regressions passed\n";
}
'''