Implement safe AdventureBot group travel and formations
This commit is contained in:
1 parent
a758239da9
commit
cac4142ec1
26 files changed
+1482
-79
No files matched your search
@@ -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 []
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 ↦}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.
|
||||
|
||||
@@ -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;')
|
||||
|
||||
@@ -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 ↦}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')
|
||||
@@ -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";
|
||||
}
|
||||
'''
|
||||
|
||||
Reference in new issue
Block a user