Add AdventureBot groups, raid preparation and shared travel
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s

Implement native bot recruitment and player admission, leader-owned destinations, intent-specific capacities, preparation scheduling and shared graph and flight travel. Support native group instance visits and separate raid entrances.

Include pending personality-driven faction PvP and solo fleeing, preserving the absolute group fleeing exclusion. Add regression fixtures, migration, documentation and test-server evidence.
This commit is contained in:
Nico committed 2026-10-28 15:00:41 +01:00
1 parent 04aa87fcfc
commit d6fea6ee68
46 files changed
+3031 -59

No files matched your search

+91
View File
@@ -1,3 +1,94 @@
# Aktuell: Automatische Botgruppen, Raid und Gruppenbildung
Nutzer beauftragte Umsetzung der gemeinsam bestätigten Planung. Vollständige
Doku doc/clientless/bot-groups.md. Vorherige uncommittete PvP-/Fluchtarbeit
wird zusammen mit den Gruppenänderungen auf Nutzerauftrag lokal committet; kein Push. Neue AdventureGroupMgr/Policy,
native Einladungen und Erst-Einlader-Führung, lokale Gruppen und Kontakt in
Hauptstädten via sucheNachGruppe (Spielerchat wird nicht ausgewertet).
Vorbereitungsintent groupformation vergleicht NÄCHSTEN Intent, nie Ziele;
Sammelpunkt ist Gruppenbildungs-Location des Führers. Instanz5/Raid20 inklusive
Führer, andere Intents3/social10. Offene Einladungen werden mitgezählt.
Anführer bestimmt Intent/Ziel/Frist; eigene Tagespläne bleiben erhalten.
AFK während Bildung ausgesetzt, Übergang zu geplanter Aktivität kein Austritt.
Kein Warten auf Vollständigkeit/Anwesenheit. Rollen später. Botführer darf PvP
beginnen, Mitglieder assistieren; Gruppen fliehen weiterhin nie.
Spieler-Invite an gruppierten Bot erzeugt generische GUID-JoinRequest-Queue,
Botführer kann Gegeneinladung anbieten und übergibt nach Beitritt nativ Führung.
Native provisorische Gruppenführer ignorieren eigene Einladung (Regressiontest).
Raidbildung wählt native25-normal Schwierigkeit; Katalog verlangt>=20 Kapazität
mit nativem Downscaling für alte20/40-Raids; Zehner-only-Ziele ausgeschlossen.
Neue CPC route_source-Snapshots mit nativem catch-up, gemeinsame Flugsegmentspur,
native Gruppeninstanzen statt alter Solo-Sperre. Raid/Dungeon nach nativen Maps.
Migration world2026_10_28_01 zweimal auf Test-DB angewendet, bestehende Tage erhalten.
Zusätzlicher Diagnosebefehl adventure prepare <guid> <next> <capital-location> [s].
Alle66 Tests bestanden; abschließende Interaktions- und Plan-Regressionsfälle
zusätzlich erneut bestanden. Debug-j10 erfolgreich, abschließend installiert.
Aktueller Testserver122595 läuft; frische Ready-Meldung, Prozess und Port8085
bestätigt. Vier Testbots online, Arthoran26 weiterhin offline.
Nach Anmeldung bildete Lenara31 nativ Gruppe mit Aelion29 (Log Invite accepted1,
beide grouped1/leader31); nach Stop-Intentwechsel nativ wieder aufgelöst.
Dabei gefunden/behoben: Gruppentimer darf Behavior.Update bei Botgruppen nicht
auf1Hz mit falschem diff drosseln; Regressiontest sichert normale World-Ticks.
Stop-Wanted sperrt sofort weitere Rekrutierung/Annahme, ohne veralteten Intent.
Diagnose interaction <guid> leave: sicheres natives Verlassen/Testaufräumen.
Bots29/31 abschließend beide enabled0/grouped0 in Goldhain bestätigt und gespeichert;
vor Login nativ tele name435 zur sicheren Bereitstellung abseits Flugstart.
30/32 normale soziale Intents. Keine temporären Profiländerungen/Overrides.
Persönlicher Plan wird beim Austritt wiederhergestellt; übernommene Ziele ändern
nicht die persönliche Austrittssignatur (besonders manuelle Aufträge).
Belege: checkpoints/2026-10-28-bot-groups/, /tmp/vcore-groups-tests-final.log.
Kein vollständiger Live-Gruppenlauf oder Raid abgenommen.
---
# Aktuell: Solo-Fluchtlogik ergänzt
Nutzer hat Flucht vor deutlich stärkeren Gegnern und bei HP<20% beauftragt,
mit absoluter Ausnahme für Gruppenmitglieder. Implementierung CombatMgr/Policy:
+6 Level, gleich-/höherstufige Elite/Bosse, tatsächliche Angreifer; alle Arten.
Gruppenbeitritt bricht Flucht ab. Alle2s 24 MMAP-Fluchtpunkte, Abstand vergrößern,
weitere sichtbare Feinde meiden; kein Pfad => stationäre Nahkampfverteidigung.
Ende erst außerhalb nativem Kampf UND50Y Abstand zur letzten Bedrohung.
Bestehende Tränke, Intentfristen und Todesablauf erhalten. Details combat-reactions.md.
Vorherige uncommittete PvP-Änderungen erhalten. Alle63 Tests bestanden inkl.
erweiterter produktiver Flucht-Zustandsmaschine (/tmp/vcore-flee-tests.log).
Debug-j10 erfolgreich gebaut und installiert. Testserver105886 mit frischer
Ready-Meldung und Port8085 geprüft. Vier Testbots erneut angemeldet;29/31
nach Profilinitialisierung erneut gestoppt (früher Stop während Loading wird
vom Profilstart überschrieben). Aelion begann kurz nativen Flug Goldhain->SW;
danach nativer tele name435 nach Goldhain, ebenso Lenara. Beide Status
endgültig enabled0/stopped bestätigt, saveall. Aelion am nahen taxi_168,
Lenara am Goldshire-Telepunkt. Keine Live-Fluchtabnahme.
Kein Commit/Push.
---
# Aktuell: Offensive Fraktionsbegegnungen
Nutzer beauftragte PvP-Erstangriffe gegen gegnerische Bots/Spieler/NPCs:
50 Yards, maximal +/-5 Level inklusive, Persönlichkeit und Intent maßgeblich.
Umsetzung in AdventureCombatMgr/Policy; Details combat-reactions.md.
Alle30s Entscheidung mit adventurousness*danger_tolerance*Intentgewicht;
Gruppen keine autonomen Erstangriffe, Ruhe/Instanz/deaktiviert friedlich.
Native PvP/Phase/LOS/MMAP-Regeln, HP60/Mana30 und keine Elite/Boss-NPCs.
PvE-Zielwahl schließt Fraktions-NPCs aus (kein Umgehen der PvP-Regeln).
Selbstverteidigung unverändert; eigenständige Fluchtlogik bislang NICHT vorhanden.
Neuer Produktionstest test_adventure_pvp.py. Debug-j10 erfolgreich gebaut und
installiert; alle63 Tests bestanden (/tmp/vcore-pvp-tests.log). Testserver100594
mit frischer Ready-Meldung und Port8085 verifiziert. Bots29..32 nach sauberem
Logout/Neustart wieder online;29/31 autonome Intents wieder gestoppt,29 steht
nun am nahen taxi_582 in Goldhain (kurze initiale Reise vor Stop),31 am Node.
Arthoran26 bleibt offline. Live-PvP nicht ausgeführt/abgenommen.
Vorheriger begrenzter Goldhain-Test: Lenara Emote-Antwort und Einladung
abgelehnt, Aelion Einladung angenommen im Server.log belegt. Folgen nicht
anhand dieser Logs bestätigt. Aelion29/Lenara31 am goldshire_center, autonome
Intents für den Test gestoppt. Arthoran26 weiterhin offline. Horde30/32 anderswo.
Keine neuen Charaktere, Migrationen oder Profiländerungen für PvP erforderlich.
Kein Commit/Push beauftragt für diese Änderungen.
---
# Aktuell: Automatische AdventureBot-Erzeugung abgeschlossen
Stand28.10.2026 auf feature/clientless-player-core. Nutzer beauftragte einen
+118
View File
@@ -0,0 +1,118 @@
# Autonomous AdventureBot groups
Bot groups use native invitations, party membership, leadership, raid conversion,
instance bindings and persistence. The first inviter becomes leader. Human-led
groups retain the existing interaction/follow behavior. A human invitation to an
already grouped bot acts as a join request: a bounded GUID-only CPC notification
lets the bot leader offer a reverse native invitation, subject to personality,
faction and capacity. Once the player joins, native leadership is transferred to
the human and bots switch to the existing player-led behavior. This does not
parse player chat or forcibly remove any member. Role assignment, healer
or tank AI, and replacement of absent players remain separate work.
## Recruitment and preparation
Adventure, compatible professions, social, instance and raid intents recruit.
Local encounters require phase, visibility, LOS and25-yard proximity. A lone bot
may request admission to an existing matching group; its leader sends the native
invitation. A leader may recruit individual bots. Recruitment and acceptance are
personality dependent, with20–40 seconds between searches,2–4 seconds for native
invite responses (provisional native leaders never answer their own invite) and a10-minute pair cooldown. Dead/combat/fleeing/transit bots do
not recruit. Same faction and an inclusive five-level spread are required.
Profession groups need a common primary profession.
`groupformation` prepares the next hour's intent in a capital. During this phase,
matching NEXT intents are required, not matching original destinations. The
leader's existing formation location becomes the meeting point; members inherit
it and subsequently inherit the leader's activity target and deadline. Their own
stored day plans remain intact and are restored on departure. Departure decisions
track the personal plan, so inheriting a different group destination is not
misinterpreted as an intent change. Unexpired manual
activities are restored too; expired manual activities end. New day generation inserts
preparation before instance/raid blocks, and probabilistically before other
eligible blocks; backwards processing prevents preparations preparing another
preparation. Existing stored days are preserved.
The faction-separated native custom channel `sucheNachGruppe` carries bounded
recruitment and response messages across capitals. The manager pairs structured
bot plans; it never interprets player chat. Opposite-faction channel traffic is
not used. Capital zones include racial capitals, Shattrath and Dalaran.
Announcements currently use German RP text.
Recruitment capacity includes the leader and pending invitations: instance5,
raid20, social10, adventure/profession3. Groups larger than five use native raid
membership. Full groups stop searching. These are intent-specific recruitment
limits; when the leader chooses a smaller activity, excess bot members depart
at a safe point before recruitment continues. Native map admission limits and existing/permanent lockouts remain
in force. Raid preparation requests the native25-player normal difficulty; the catalog
uses native downscaling and requires capacity for at least20 members, excluding
ten-player-only raid maps. Legacy20/40-player raids remain possible.
No role guarantees are implied.
Preparation never waits for a full group or for members to arrive. The leader's
next intent starts on its ordinary deadline. Late members catch up. Formation
suppresses the AFK/inactivity departure timer and refreshes its baseline; the
planned formation-to-activity transition is exempt from intent-change departure.
Later changes retain the existing loyalty-based departure behavior, deferred in
combat/instances/transit. Native removal handles leadership and party dissolution.
## Shared travel and instances
Followers run their AdventureBehavior controller with the leader's effective
intent/location/deadline, keeping their personal daily schedule. After arrival,
non-instance followers use native checked local following instead of independently
performing unrelated micro-actions. Leaders continue their own activities.
Generic CPC travel actions accept `route_source=<native-group-leader-guid>`.
The executor copies the leader's route edges and final local destination, keeping
execution/transport state separate. Late arrivals plan a native approach to the
shared route's previous reached node and replay the remaining edges. A bounded
15-second wait handles follower actions processed before the leader plans. If no
matching macro snapshot exists (including a local-only leader leg), the ordinary
native catch-up planner runs; this is not a synthetic transfer or an absolute
formation guarantee. Individual permissions/path failures still use native
travel recovery. `TRAVEL_SHARED_ROUTE` provides evidence of actual copying.
Free-flight members with the same target consume the leader's published flight
segments. Each member mounts normally and rechecks its own collision corridor.
A shared trail survives leader arrival/new planning through shared ownership.
Waiting for additional published points holds flight position; an aborted or
blocked trail uses existing checked breadcrumb return. No position, speed or
flight-license cheat is introduced. `FLIGHT_SHARED_TRAIL` marks attachment.
`raid` selects only native raid-map entrances; `instance` excludes them. These
are verified from native map data through the existing entrance catalog rather
than name matching. Both use the existing remainder-of-block visit and final
five-minute outside rest, individual0–50% maximum-health loss, save/restart and
native portal return. Native group binding supplies the shared instance copy;
unrelated shared solo bindings remain rejected. Members do not use separate
synthetic private copies.
Only the group leader initiates personality/intent-driven PvP. Other bots assist
through the existing damage executor. All group members retain the absolute rule against tactical fleeing,
regardless of health or level.
## Operations and verification
Apply `sql/custom/world/2026_10_28_01_adventure_groups.sql`; it is idempotent and
preserves edited weights/current day plans. No character migration is needed.
`clientless adventure reload` reloads catalog data.
Diagnostics: `clientless interaction <guid> leave` performs a safe native
departure/cancels a pending invite for test cleanup. `clientless interaction <guid>` includes next intent, formation,
capacity, bot leadership and own plan location. `clientless adventure plan <guid>`
shows stored day slots. `clientless adventure prepare <guid> <next-intent>
<capital-location> [seconds]` stages a manual preparation, with a chosen normal
next destination, lasting30–3600 seconds and followed by a one-hour activity.
It uses normal navigation, invitations and execution, and can start incomplete.
Manual preparations are runtime test state, not restart-persisted day plans.
Automated fixtures execute production recruitment, provisional-leader invite
handling, full world-tick behavior clocks in bot groups, player admission and
leadership, inheritance, instance
lifecycle, map-type filtering, route adoption and flight-trail geometry. They
cover different original destinations, singleton admission, native inviter
leadership, caps/pending reservations, raid conversion, protected transition,
unchanged personal plans, preparation deadlines, shared edges/free flight,
collision and abort behavior. Full native live group trips,20-member raids,
real AFK duration and visual gameplay remain acceptance tests for the user.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,6 @@
TrinityCore rev. 04aa87fcfc13+ 2026-10-28 11:30:47 +0100 (feature/clientless-player-core branch) (Unix, Debug, Static) (worldserver-daemon) ready...
GROUP_BOT_INVITE leader=31 target=29 intent='social' forming=0
INTERACTION_INVITE guid=29 leader=31 accepted=1
INTERACTION_GROUP_LEAVE guid=31 reason='intent-change'
Interactions guid=29 grouped=1 leader=31 partner=0 intent='social' schedule='soc
Interactions guid=31 grouped=1 leader=31 partner=0 intent='social' schedule='soc
@@ -0,0 +1,3 @@
66 clientless test scripts passed; final interaction-manager regression rechecked. Native Lenara31 -> Aelion29 invitation accepted, native grouped1/leader31 observed, later normal intent-change departure. Timer defect discovered during smoke and fixed before final deployment. Full group route/raid/visual acceptance remains open.
Latest production plan-signature regression rechecked: different inherited destinations do not trigger personal intent-change departure; restoration returns to the original manual destination.
Final corrected Debug build installed; worldserver122595 fresh ready and port8085 verified. All four bots online;29/31 confirmed enabled0/grouped0 in Goldshire,30/32 original social intents,26offline. No temporary profile changes or overrides.
@@ -0,0 +1,204 @@
TEST tests/clientless/test_adventure_area_node.py
Production area destination excludes outside and unreachable graph nodes.
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_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_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_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_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_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
Instance startup script: no schema/no visits stays idle; saved visitors resume with the Adventure controller 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_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
/tmp/tmp_vzcr4vj/test.cpp: In member function ‘void AdventureInteractionMgr::Leave(Player*, Record&, const char*)’:
/tmp/tmp_vzcr4vj/test.cpp:142:69: warning: unused parameter ‘reason’ [-Wunused-parameter]
142 | void AdventureInteractionMgr::Leave(Player* p,Record& r,char const* reason)
| ~~~~~~~~~~~~^~~~~~
production interaction manager: invitations, factions, reservations, follow, AFK and deferred departure passed
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_micro_reset.py
Micro cleanup ends physical local movement, preserves transit and does not stand up dead players.
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_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_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_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_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_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).
@@ -0,0 +1,19 @@
from pathlib import Path
import os,re,subprocess,sys
root=Path('/home/wow_admin/wow_cata_server/source/TrinityCore')
conf=Path('/home/wow_admin/wow_cata_server/server/debug/etc/worldserver.conf').read_text()
def query(kind,sql):
h,port,u,p,db=re.search(r'^'+kind+r'DatabaseInfo\s*=\s*"([^"]+)"',conf,re.M).group(1).split(';')
env=os.environ.copy();env['MYSQL_PWD']=p
r=subprocess.run(['mysql','--protocol=TCP','--host='+h,'--port='+port,'--user='+u,'--database='+db,'--batch','--connect-timeout=10'],input=sql,text=True,capture_output=True,env=env)
if r.returncode:raise SystemExit(r.stderr.replace(p,'[redacted]'))
return r.stdout
mode=sys.argv[1]
if mode=='inspect':
print(query('World',"SHOW CREATE TABLE vcore_adventure_day; SELECT zone_id,area_id,COUNT(*) n FROM vcore_adventure_location GROUP BY zone_id,area_id ORDER BY n DESC LIMIT 20;"))
elif mode=='apply':
print(query('World',(root/'sql/custom/world/2026_10_28_01_adventure_groups.sql').read_text()))
elif mode=='state':
print(query('Character',"SELECT guid,name,online,map,instance_id FROM characters WHERE guid IN(26,29,30,31,32); SELECT guid,leaderGuid,groupType FROM groups; SELECT guid,memberGuid FROM group_member ORDER BY guid,memberGuid;"))
else:raise SystemExit('unknown mode')
+52
View File
@@ -187,3 +187,55 @@ Geisterlauf und nativeLeichenaufnahme. NachLogout lebendHP4340 ohneLeiche oder
Aura15007 gespeichert. KeineUhrabkürzung, keinNormalreisebeleg durchTestpositionierung.
Belege: checkpoints/2026-10-27-combat/. Mehrfachangreifer undNeustart während
Leichenlauf produktiv mitAdaptern geprüft, nochkeine entsprechendeLiveabnahme.
## Offensive faction encounters
Autonomous solo bots and bot group leaders with an active intent can seek opposing players
(including clientless bots) and Horde/Alliance NPCs within 50 yards. Levels must
be within five levels inclusive in either direction. Native attack permission,
hostility, visibility/phase, LOS and a complete MMAP approach remain required;
this does not enable PvP flags or bypass sanctuary rules. Player-controlled pets
are not independent offensive targets. Elite/world-boss NPCs remain excluded.
Health must be at least 60%, mana users at least 30% mana.
Every 30 seconds the nearest reachable eligible opponent receives one decision:
probability = adventurousness * danger_tolerance * intent weight. Weights:
adventure 1, leisure .35, social .2, profession .15, economy .1; rest, instance,
unknown or disabled intents 0. Missing traits default to 0. Group members retain leader-directed support; only the bot leader may initiate
a PvP encounter. Player-led bots never initiate independently.
The PvE path/quarry selector excludes faction NPCs so it cannot bypass these
personality/level gates. Actual damage still takes precedence via the existing
ordered self-defence path, regardless of offensive level boundaries.
`COMBAT_PVP_INITIATE` records the initiating bot and target; ordinary combat
start/end logs continue. Solo tactical flight is described below. Automated fixtures verify the production selector,
not live PvP gameplay or class rotations beyond the existing warrior executor.
## Solo flight
Grouped bots never flee, including at critical health or against bosses.
Joining a group cancels flight on the next combat update and restores ordinary
combat support. Ungrouped bots flee from actual attackers at least six levels
higher, equal/higher-level elites/bosses, or below 20% health during combat.
A nearby strong creature alone does not trigger flight. Training alone does not
trigger flight; real damage still interrupts training through self-defence.
The combat manager owns flight, interrupts the current activity and retains the
original intent deadline. Every two seconds it tests 24 terrain-adjusted local
escape candidates, increasing minimum distance to current/last-known attackers.
Complete, non-suspicious MMAP paths up to60 yards are required. Known visible
hostiles within12 yards of the proposed corridor are avoided. This is a local
heuristic, not a guarantee against unseen enemies or the previously deferred
MMAP obstacle limitations. Existing native combat potions remain available.
If no suitable path starts, the bot stays and defends itself in melee, retrying
other directions on the next evaluation. No teleports, forced combat reset,
invulnerability or synthetic movement are used.
Flight persists until native combat ends AND at least50 yards separate the bot
from the last known threat positions. Normal self-care then runs before the
current intent resumes, with30 seconds of offensive cooldown. Death clears the
flight state and uses the existing corpse recovery. Logs:
COMBAT_FLEE_START/PATH/BLOCKED/END/CANCEL. Production state-machine fixtures cover
level/health thresholds, group override and joining mid-flight, a blocked path,
nearby hostile avoidance and safe completion. Live gameplay remains unverified.
+3 -1
View File
@@ -41,7 +41,9 @@ An actual native invitation is answered after 2–4 seconds in a safe state.
Acceptance probability clamps sociability plus an intent adjustment to [0,1]:
rest −0.50, profession/economy −0.25, adventure/instance +0.20, other +0.10.
Inviter must be alive and friendly. Bots do not accept invitations from other
clientless players yet; autonomous bot grouping is explicitly deferred.
unreserved clientless players. Reserved native bot invitations, autonomous
groups, raid/preparation intents and human admission to bot groups are described
in [bot-groups.md](bot-groups.md).
Invitations are declined inside dungeons. Normal faction/group restrictions run
before the invitation reaches the bot; none are bypassed.
@@ -0,0 +1,8 @@
-- Category weights only. Native map types distinguish dungeon from raid entrances;
-- formation locations must additionally belong to a capital-city zone.
INSERT IGNORE INTO vcore_adventure_location_intent(category,intent,weight)
SELECT category,'groupformation',1 FROM vcore_adventure_location_intent
WHERE intent='social' AND weight>0;
INSERT IGNORE INTO vcore_adventure_location_intent(category,intent,weight)
VALUES ('instance_entrance','raid',5);
-- Existing daily plans remain intact. Newly generated days add preparation hours.
@@ -21,6 +21,7 @@
#include "DatabaseEnv.h"
#include "Group.h"
#include "GroupMgr.h"
#include "ClientlessPlayerMgr.h"
#include "Log.h"
#include "ObjectAccessor.h"
#include "ObjectMgr.h"
@@ -136,6 +137,7 @@ void WorldSession::HandlePartyInviteOpcode(WorldPackets::Party::PartyInviteClien
// player already in another group or invited
if (group2 || invitedPlayer->GetGroupInvite())
{
if(group2)sClientlessPlayerMgr->GroupJoinRequest(invitedPlayer,invitingPlayer);
SendPartyResult(PARTY_OP_INVITE, packet.TargetName, ERR_ALREADY_IN_GROUP_S);
if (group2)
+106 -18
View File
@@ -1,5 +1,7 @@
#include "AdventureBehaviorMgr.h"
#include "AdventureCombatMgr.h"
#include "AdventureGroupPolicy.h"
#include "Group.h"
#include "AdventureLocationMgr.h"
#include "AdventureEconomyPolicy.h"
#include "AdventureRestSpace.h"
@@ -26,6 +28,7 @@
#include "World.h"
#include "WorldSession.h"
#include "ObjectMgr.h"
#include "ObjectAccessor.h"
#include "InstanceSaveMgr.h"
#include "Opcodes.h"
#include "WorldPacket.h"
@@ -133,7 +136,7 @@ namespace
bool Known(std::string const & intent)
{
return intent == "social" || intent == "adventure" || intent == "profession" || intent == "rest" || intent == "leisure" || intent == "economy" || intent == "instance";
return intent == "social" || intent == "adventure" || intent == "profession" || intent == "rest" || intent == "leisure" || intent == "economy" || intent == "instance" || intent == "raid" || intent == "groupformation";
}
bool Busy(uint32 guid)
{
@@ -272,19 +275,21 @@ void AdventureBehaviorMgr::Generate(Player * player, Record & record, uint32 dat
{
std::vector < double > weights
{
Affinity(profile->ActivityAffinity, "social", 1) * Affinity(profile->Personality, "sociability", 0.5f) + Affinity(profile->ActivityAffinity, "tavern", 0.5f) * Affinity(profile->Personality, "tavern_affinity", 0.5f), Affinity(profile->ActivityAffinity, "adventure", 1) * Affinity(profile->Personality, "adventurousness", 0.5f), Affinity(profile->ActivityAffinity, "profession", 0.5f), Affinity(profile->ActivityAffinity, "rest", 0.5f), Affinity(profile->ActivityAffinity, "leisure", 0.5f), Affinity(profile->ActivityAffinity, "economy", 0.5f), Affinity(profile->ActivityAffinity, "instance", 0.5f)
Affinity(profile->ActivityAffinity, "social", 1) * Affinity(profile->Personality, "sociability", 0.5f) + Affinity(profile->ActivityAffinity, "tavern", 0.5f) * Affinity(profile->Personality, "tavern_affinity", 0.5f), Affinity(profile->ActivityAffinity, "adventure", 1) * Affinity(profile->Personality, "adventurousness", 0.5f), Affinity(profile->ActivityAffinity, "profession", 0.5f), Affinity(profile->ActivityAffinity, "rest", 0.5f), Affinity(profile->ActivityAffinity, "leisure", 0.5f), Affinity(profile->ActivityAffinity, "economy", 0.5f), Affinity(profile->ActivityAffinity, "instance", 0.5f), Affinity(profile->ActivityAffinity, "raid", 0.25f)
};
auto i = Choose(weights, record.Random);
return std::array < std::string, 7 >
return std::array < std::string, 8 >
{
"social", "adventure", "profession", "rest", "leisure", "economy", "instance"
"social", "adventure", "profession", "rest", "leisure", "economy", "instance", "raid"
}
[std::min(i, std::size_t(6))];
[std::min(i, std::size_t(7))];
};
record.Plan = AdventureBehaviorPolicy::Generate(record.Random, Affinity(profile->Personality, "wanderlust", 0.3f), pick,[&](std::string const & intent)
{
return PickLocation(player, record, intent);
});
AdventureGroupPolicy::Prepare(record.Plan,record.Random,Affinity(profile->Personality,"sociability",0.5f),
[&](){return PickLocation(player,record,"groupformation");});
auto tx = WorldDatabase.BeginTransaction();
for (unsigned h = 0; h < 24; ++ h)
{
@@ -333,6 +338,8 @@ void AdventureBehaviorMgr::Queue(Player * player, Record & r, std::string const
a.Name = action;
a.Parameters[key] = value;
a.Parameters["hard_destination"] = hard ? "true" : "false";
if(auto group=player->GetGroup())if(group->GetLeaderGUID()!=player->GetGUID() && sAdventureGroupMgr->BotLed(player))
a.Parameters["route_source"]=std::to_string(group->GetLeaderGUID().GetCounter());
r.Queued = sClientlessPlayerMgr->QueueAction(player->GetGUID().GetCounter(), std::move(a));
}
@@ -396,7 +403,7 @@ void AdventureBehaviorMgr::Begin(Player * player, Record & r)
intent.Parameters["location"] = r.Location;
sClientlessPlayerMgr->SetIntent(player->GetGUID().GetCounter(), std::move(intent));
r.Publishing = false;
if (r.Intent=="instance")
if ((r.Intent=="instance" || r.Intent=="raid"))
{
if (!ResolveInstance(player,r)) {Fallback(player,r);return;}
Queue(player,r,"travel_to_location","id",r.Location,true);
@@ -1261,7 +1268,18 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff)
else r.Deadline = deadline;
}
}
else if (r.Deadline && now >= r.Deadline) SetWanted(player, r, "", "", 0);
else if (r.Deadline && now >= r.Deadline)
{
if(r.Intent=="groupformation" && !r.ManualNextIntent.empty())
{
auto intent=r.ManualNextIntent,location=r.ManualNextLocation;
r.ManualNextIntent.clear();r.ManualNextLocation.clear();
SetWanted(player,r,intent,location,now+3600);
}
else SetWanted(player,r,"","",0);
}
if(auto group=player->GetGroup())if(sAdventureGroupMgr->BotLed(player) && group->GetLeaderGUID()!=player->GetGUID())
if(auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID()))AdoptGroup(player,leader);
if (advanceLeisure()) return;
if (Unsafe(player))
{
@@ -1329,7 +1347,9 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff)
}
if (r.State == Phase::OnSite)
{
if (r.Intent=="instance") {UpdateInstance(player,r,now);return;}
if ((r.Intent=="instance" || r.Intent=="raid")) {UpdateInstance(player,r,now);return;}
if(r.Intent=="groupformation")return;
if(sAdventureGroupMgr->FollowMember(player))return;
if (r.Recovery.Active)
{
if (UpdateRecovery(player,r,1000)) return;
@@ -1455,6 +1475,17 @@ bool AdventureBehaviorMgr::Command(ChatHandler * handler, char const * args)
handler->SendSysMessage("Schedule mode updated; changes use a safe graph handover.");
return true;
}
if(op=="prepare")
{
std::string next,meeting;uint32 seconds=3600;
in>>next>>meeting;if(in>>seconds)seconds=std::clamp(seconds,30u,3600u);
auto location=sAdventureLocationMgr->Find(meeting);
if(!AdventureGroupPolicy::Eligible(next) || !location || !Allowed(player,*location,"groupformation"))return false;
auto destination=PickLocation(player,r,next);if(destination.empty())return false;
r.Schedule=false;r.Enabled=true;r.ManualNextIntent=next;r.ManualNextLocation=destination;
SetWanted(player,r,"groupformation",meeting,GameTime::GetGameTime()+seconds);
handler->SendSysMessage("Manual preparation started; next activity begins at the deadline even if the group is incomplete.");return true;
}
if (op == "intent")
{
std::string intent, location;
@@ -1464,23 +1495,23 @@ bool AdventureBehaviorMgr::Command(ChatHandler * handler, char const * args)
auto l = sAdventureLocationMgr->Find(location);
if (! Known(intent) || ! l || ! Allowed(player, * l, intent))
{
handler->SendSysMessage("Use social/adventure/profession/rest/leisure and a matching enabled location.");
handler->SendSysMessage("Use social/adventure/profession/rest/leisure/economy/instance/raid/groupformation and a matching enabled location.");
return false;
}
r.Schedule = false;
r.Enabled = true;
r.Enabled = true;r.ManualNextIntent.clear();r.ManualNextLocation.clear();
SetWanted(player, r, intent, location, GameTime::GetGameTime() + seconds);
handler->SendSysMessage("Intent requested; travel and micro actions are autonomous.");
return true;
}
if (op == "stop")
{
r.Schedule = false;
r.Schedule = false;r.ManualNextIntent.clear();r.ManualNextLocation.clear();
SetWanted(player, r, "", "", 0);
handler->SendSysMessage("Behavior stop requested at a safe point.");
return true;
}
handler->SendSysMessage("Usage: clientless adventure import|repairimport|reload|status <guid>|plan <guid>|schedule <guid> on/off|intent <guid> <intent> <location> [seconds]|micro <guid> <scene-action>|stop <guid>");
handler->SendSysMessage("Usage: clientless adventure import|repairimport|reload|status <guid>|plan <guid>|schedule <guid> on/off|intent <guid> <intent> <location> [seconds]|prepare <guid> <next-intent> <capital-location> [seconds]|micro <guid> <scene-action>|stop <guid>");
return false;
}
@@ -1519,7 +1550,8 @@ bool AdventureBehaviorMgr::RestoreInstance(Player* player, Record& r)
v.Map=f[3].GetUInt32();v.InstanceId=f[4].GetUInt32();v.BlockEnd=time_t(f[5].GetUInt64());
v.PrepareUntil=time_t(f[6].GetUInt64());v.Stage=AdventureInstancePolicy::Phase(f[7].GetUInt8());
v.LossPercent=f[8].GetUInt8();v.InjuryApplied=f[9].GetUInt8()!=0;r.Schedule=f[10].GetUInt8()!=0;
r.Intent=r.WantedIntent="instance";r.Location=r.WantedLocation=v.Location;r.Deadline=v.BlockEnd;
auto restoredMap=sMapStore.LookupEntry(v.Map);
r.Intent=r.WantedIntent=restoredMap && restoredMap->IsRaid()?"raid":"instance";r.Location=r.WantedLocation=v.Location;r.Deadline=v.BlockEnd;
r.State=v.Stage==AdventureInstancePolicy::Phase::Travel?Phase::Idle:Phase::OnSite;
r.Dirty=v.Stage==AdventureInstancePolicy::Phase::Travel;
r.Detail="resuming saved instance visit";
@@ -1561,8 +1593,8 @@ bool AdventureBehaviorMgr::ResolveInstance(Player* player, Record& r)
sClientlessTravelGraphMgr->EnsureLoaded();
if (!source || !map || !map->IsDungeon() || !outside || outside->MapType!=MAP_COMMON ||
!sClientlessTravelGraphMgr->FindNode("portal_at_"+std::to_string(entry)+"_source") ||
!sClientlessTravelGraphMgr->FindNode("portal_at_"+std::to_string(entry)+"_target") || player->GetGroup())
{r.Detail="solo instance entrance is not a valid native graph transition";return false;}
!sClientlessTravelGraphMgr->FindNode("portal_at_"+std::to_string(entry)+"_target"))
{r.Detail="instance entrance is not a valid native graph transition";return false;}
auto validExit=[&](uint32 id)
{
auto at=sAreaTriggerStore.LookupEntry(id);auto dest=sObjectMgr->GetAreaTrigger(id);
@@ -1661,7 +1693,7 @@ bool AdventureBehaviorMgr::UpdateInstance(Player* player, Record& r, time_t now)
bool(CharacterDatabase.PQuery("SELECT guid FROM character_instance WHERE instance=%u AND guid<>%u LIMIT 1",id,player->GetGUID().GetCounter())) ||
bool(CharacterDatabase.PQuery("SELECT guid FROM group_instance WHERE instance=%u LIMIT 1",id));
}
if (player->GetGroup() || shared || player->GetMapId()!=uint32(entry->ContinentID) || !player->IsInAreaTriggerRadius(entry))
if ((!player->GetGroup() && shared) || player->GetMapId()!=uint32(entry->ContinentID) || !player->IsInAreaTriggerRadius(entry))
{
r.Detail="native instance entrance not reached or private solo binding unavailable";
v.Stage=Stage::Rest;SaveInstance(player,r);return true;
@@ -1682,7 +1714,7 @@ bool AdventureBehaviorMgr::UpdateInstance(Player* player, Record& r, time_t now)
}
if (v.Stage==Stage::Inside)
{
if (!v.Abort && !player->GetGroup() && now<AdventureInstancePolicy::ReturnAt(v.BlockEnd) &&
if (!v.Abort && now<AdventureInstancePolicy::ReturnAt(v.BlockEnd) &&
player->GetMapId()==v.Map && player->GetInstanceId()==v.InstanceId)
{r.Detail="waiting just inside private instance entrance";return true;}
v.Stage=Stage::Returning;
@@ -1776,7 +1808,13 @@ std::string AdventureBehaviorMgr::InteractionSchedule(Player* player)
auto& r=it->second;
auto local=Local(GameTime::GetGameTime());
if(r.Schedule && r.Date!=DateOf(local)){Generate(player,r,DateOf(local));r.Hour=-1;}
if(!r.Schedule)return (r.Deadline && GameTime::GetGameTime()>=r.Deadline?"expired:":"")+r.Intent+":"+r.Location;
if(!r.Schedule)
{
auto deadline=r.GroupOverride?r.PersonalDeadline:r.Deadline;
auto const& intent=r.GroupOverride?r.PersonalIntent:r.WantedIntent;
auto const& location=r.GroupOverride?r.PersonalLocation:r.WantedLocation;
return (deadline && GameTime::GetGameTime()>=deadline?"expired:":"")+intent+":"+location;
}
auto const& slot=r.Plan[Local(GameTime::GetGameTime()).tm_hour];
return slot.Intent+":"+slot.Location;
}
@@ -1787,3 +1825,53 @@ void AdventureBehaviorMgr::InterruptForInteraction(Player* player)
auto it=_records.find(player->GetGUID().GetCounter());
if(it!=_records.end())it->second.Detail="social/group interaction; original deadline retained";
}
AdventureGroupPlan AdventureBehaviorMgr::GroupPlan(Player* player)
{
AdventureGroupPlan plan;
auto it=_records.find(player->GetGUID().GetCounter());if(it==_records.end())return plan;
auto& r=it->second;auto now=GameTime::GetGameTime();auto local=Local(now);
if(r.Schedule && r.Date!=DateOf(local))Generate(player,r,DateOf(local));
plan.Enabled=r.Enabled && !(r.Dirty && r.WantedIntent.empty());plan.Intent=r.Intent;plan.Location=r.Location;plan.Deadline=r.Deadline;
if(r.Schedule)
{
plan.Intent=r.Plan[local.tm_hour].Intent;
plan.Location=r.Plan[local.tm_hour].Location;
if(local.tm_hour<23){plan.NextIntent=r.Plan[local.tm_hour+1].Intent;plan.NextLocation=r.Plan[local.tm_hour+1].Location;}
}
if(!r.Schedule){plan.NextIntent=r.ManualNextIntent;plan.NextLocation=r.ManualNextLocation;}
plan.Forming=plan.Enabled && plan.Intent=="groupformation";
return plan;
}
bool AdventureBehaviorMgr::AdoptGroup(Player* player,Player* leader)
{
auto a=_records.find(player->GetGUID().GetCounter()),b=_records.find(leader->GetGUID().GetCounter());
if(a==_records.end() || b==_records.end() || !b->second.Enabled || (b->second.Dirty && b->second.WantedIntent.empty()))return false;
auto& r=a->second;auto const& source=b->second;
if(!r.GroupOverride)
{
r.GroupOverride=true;r.PersonalIntent=r.WantedIntent;r.PersonalLocation=r.WantedLocation;r.PersonalDeadline=r.Deadline;
}
if(r.Instance.Active && r.Instance.Stage!=AdventureInstancePolicy::Phase::Travel)return true;
if(r.WantedIntent!=source.Intent || r.WantedLocation!=source.Location)
SetWanted(player,r,source.Intent,source.Location,source.Deadline);
else r.Deadline=source.Deadline;
return true;
}
void AdventureBehaviorMgr::ResumePersonalPlan(Player* player)
{
auto it=_records.find(player->GetGUID().GetCounter());if(it==_records.end())return;
auto& r=it->second;if(!r.GroupOverride)return;r.GroupOverride=false;
auto now=GameTime::GetGameTime();
if(r.Schedule)
{
auto local=Local(now);if(r.Date!=DateOf(local))Generate(player,r,DateOf(local));
auto const& slot=r.Plan[local.tm_hour];
std::tm end=local;end.tm_min=0;end.tm_sec=0;++end.tm_hour;end.tm_isdst=-1;
r.Hour=local.tm_hour;SetWanted(player,r,slot.Intent,slot.Location,mktime(&end));
}
else if(!r.PersonalDeadline || now<r.PersonalDeadline)
SetWanted(player,r,r.PersonalIntent,r.PersonalLocation,r.PersonalDeadline);
else SetWanted(player,r,"","",0);
}
+8 -2
View File
@@ -1,6 +1,7 @@
#ifndef VCORE_ADVENTURE_BEHAVIOR_MGR_H
#define VCORE_ADVENTURE_BEHAVIOR_MGR_H
#include "Common.h"
#include "AdventureGroupMgr.h"
#include "Position.h"
#include "ObjectGuid.h"
#include "AdventureBehaviorPolicy.h"
@@ -31,6 +32,9 @@ public:
bool Command(ChatHandler* handler, char const* args);
void InterruptForCombat(Player* player);
void InterruptForInteraction(Player* player);
AdventureGroupPlan GroupPlan(Player* player);
void ResumePersonalPlan(Player* player);
bool AdoptGroup(Player* player,Player* leader);
std::string CombatIntent(Player* player) const;
std::string InteractionIntent(Player* player) const;
std::string InteractionSchedule(Player* player);
@@ -47,9 +51,11 @@ private:
};
struct Record
{
bool Enabled=false, Schedule=false, Publishing=false, Queued=false, Dirty=true;
bool Enabled=false, Schedule=false, Publishing=false, Queued=false, Dirty=true, GroupOverride=false;
Phase State=Phase::Idle;
std::string Intent, Location, WantedIntent, WantedLocation, Detail, Micro;
std::string Intent, Location, WantedIntent, WantedLocation, Detail, Micro, ManualNextIntent, ManualNextLocation;
std::string PersonalIntent,PersonalLocation;
time_t PersonalDeadline=0;
time_t Deadline=0;
uint32 Date=0, Timer=0, MicroTimer=0, GestureTimer=0, RetryTimer=0;
int Hour=-1;
+163 -5
View File
@@ -1,5 +1,6 @@
#include "AdventureCombatMgr.h"
#include "AdventureInteractionMgr.h"
#include "Group.h"
#include "AdventureCombatPolicy.h"
#include "AdventureBehaviorMgr.h"
#include "ClientlessLuaEngine.h"
@@ -7,6 +8,8 @@
#include "ClientlessActionExecutorMgr.h"
#include "ClientlessTravelMgr.h"
#include "Creature.h"
#include "DBCStructure.h"
#include "Random.h"
#include "Corpse.h"
#include "Player.h"
#include "PlayerAI.h"
@@ -141,6 +144,7 @@ void AdventureCombatMgr::Finish(Player* p,Record& r)
sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());
if(training && std::all_of(p->getAttackers().begin(),p->getAttackers().end(),[](Unit* u)
{auto c=u->ToCreature();return c && c->GetScriptName()=="npc_training_dummy";}))p->CombatStop(true); // Only the bounded dummy scene may end its own combat.
r.Fleeing=false;r.ThreatPositions.clear();
r.AI.reset();r.Target.Clear();r.Training=false;r.Defending=false;r.UnreachableMs=0;r.Cooldown=10000;
}
void AdventureCombatMgr::Stop(Player* p)
@@ -148,6 +152,42 @@ void AdventureCombatMgr::Stop(Player* p)
auto it=_records.find(p->GetGUID().GetCounter());if(it!=_records.end())Finish(p,it->second);
}
void AdventureCombatMgr::Offline(Player* p){_records.erase(p->GetGUID().GetCounter());}
Unit* AdventureCombatMgr::PvPTarget(Player* p) const
{
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
if(!profile)return nullptr;
auto trait=[&](char const* key)
{auto it=profile->Personality.find(key);return it==profile->Personality.end()?0.0f:it->second;};
auto chance=AdventureCombatPolicy::PvPWillingness(sAdventureBehaviorMgr->CombatIntent(p),
trait("adventurousness"),trait("danger_tolerance"));
if(chance<=0 || p->GetHealthPct()<AdventureCombatPolicy::MinimumHealth)return nullptr;
if(p->GetPowerType()==POWER_MANA && p->GetMaxPower(POWER_MANA) &&
100.0f*p->GetPower(POWER_MANA)/p->GetMaxPower(POWER_MANA)<AdventureCombatPolicy::MinimumMana)return nullptr;
Unit* best=nullptr;
auto consider=[&](Unit* target)
{
if(!Valid(p,target,false) || !p->IsHostileTo(target) ||
!AdventureCombatPolicy::PvPLevel(p->getLevel(),target->getLevel()) ||
p->GetExactDist(target)>AdventureCombatPolicy::PvPRadius ||
(best && p->GetExactDist(target)>=p->GetExactDist(best)))return;
if(auto player=target->ToPlayer())
{if(player->GetTeam()==p->GetTeam())return;}
else
{
auto creature=target->ToCreature();
auto faction=target->GetFactionTemplateEntry();
auto enemy=p->GetTeam()==ALLIANCE?FACTION_GROUP_MASK_HORDE:FACTION_GROUP_MASK_ALLIANCE;
if(!creature || creature->GetControllingPlayer() || !faction || !(faction->FactionGroup & enemy) ||
creature->isElite() || creature->isWorldBoss())return;
}
if(Reachable(p,target))best=target;
};
std::vector<Player*> players;p->GetPlayerListInGrid(players,AdventureCombatPolicy::PvPRadius);
for(auto target:players)consider(target);
std::vector<Creature*> creatures;p->GetCreatureListWithEntryInGrid(creatures,0,AdventureCombatPolicy::PvPRadius);
for(auto target:creatures)consider(target);
return best && frand(0,1)<chance?best:nullptr;
}
Creature* AdventureCombatMgr::Proactive(Player* p) const
{
auto intent=sAdventureBehaviorMgr->CombatIntent(p);
@@ -158,6 +198,8 @@ Creature* AdventureCombatMgr::Proactive(Player* p) const
Creature* best=nullptr;std::vector<Creature*> all;p->GetCreatureListWithEntryInGrid(all,0,AdventureCombatPolicy::SearchRadius);
for(auto c:all)
{
auto faction=c->GetFactionTemplateEntry();
if(faction && (faction->FactionGroup & (FACTION_GROUP_MASK_ALLIANCE|FACTION_GROUP_MASK_HORDE)))continue;
if(best && p->GetExactDist(c)>=p->GetExactDist(best))continue;
if(intent=="adventure")
{
@@ -174,11 +216,114 @@ Creature* AdventureCombatMgr::Proactive(Player* p) const
}
return best;
}
bool AdventureCombatMgr::Flee(Player* p,Record& r)
{
if(p->GetGroup())
{
if(r.Fleeing)
{
sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());p->StopMoving();
r.Fleeing=false;r.ThreatPositions.clear();
TC_LOG_INFO("server.adventurebot","COMBAT_FLEE_CANCEL guid=%u reason='group'",p->GetGUID().GetCounter());
}
return false;
}
std::vector<Unit*> threats;
auto add=[&](Unit* u)
{
if(u && u!=p && u->IsAlive() && u->IsInWorld() && p->IsInPhase(u) &&
p->CanSeeOrDetect(u) && std::find(threats.begin(),threats.end(),u)==threats.end())threats.push_back(u);
};
for(auto u:p->getAttackers())add(u);
for(auto id:r.Damagers)add(ObjectAccessor::GetUnit(*p,id));
if(r.Defending || (p->IsInCombat() && !r.Training))add(ObjectAccessor::GetUnit(*p,r.Target));
bool danger=false;
for(auto u:threats)
{
auto c=u->ToCreature();
if(AdventureCombatPolicy::ShouldFlee(false,p->GetHealthPct(),p->getLevel(),u->getLevel(),
c && c->isElite(),c && c->isWorldBoss()))danger=true;
}
if(!r.Fleeing && (!danger || r.Training))return false;
if(!r.Fleeing)
{
r.Fleeing=true;r.NextEscape=0;
sAdventureBehaviorMgr->InterruptForCombat(p);
sClientlessLuaEngine->DismountGroundMount(p,"combat-flee");
p->AttackStop();p->CastStop();p->StopMoving();p->GetMotionMaster()->Clear();
TC_LOG_INFO("server.adventurebot","COMBAT_FLEE_START guid=%u",p->GetGUID().GetCounter());
}
// Keep last known threat positions when attackers vanish from visibility.
if(!threats.empty())
{
r.ThreatPositions.clear();
for(auto u:threats)r.ThreatPositions.push_back(u->GetPosition());
}
auto clearance=[&](Position const& point)
{
float distance=1000;
for(auto const& threat:r.ThreatPositions)distance=std::min(distance,point.GetExactDist(threat));
return distance;
};
if(!p->IsInCombat() && clearance(p->GetPosition())>=50)
{
r.Fleeing=false;r.ThreatPositions.clear();r.Damagers.clear();
Finish(p,r);r.Cooldown=30000;
TC_LOG_INFO("server.adventurebot","COMBAT_FLEE_END guid=%u",p->GetGUID().GetCounter());
return true;
}
auto now=GameTime::GetGameTime();
if(now<r.NextEscape)return true;
r.NextEscape=now+2;
std::vector<Creature*> creatures;p->GetCreatureListWithEntryInGrid(creatures,0,80);
std::vector<Player*> players;p->GetPlayerListInGrid(players,80);
std::vector<Unit*> nearby;
for(auto c:creatures)nearby.push_back(c);
for(auto other:players)nearby.push_back(other);
Position best;bool found=false;float score=-1e30f;
float current=clearance(p->GetPosition());
for(float radius:{18.0f,30.0f})for(unsigned i=0;i<12;++i)
{
float angle=i*6.2831853f/12;
float x=p->GetPositionX()+radius*std::cos(angle),y=p->GetPositionY()+radius*std::sin(angle);
float z=p->GetMap()->GetHeight(p->GetPhaseShift(),x,y,p->GetPositionZ()+5,true,15);
if(z<=INVALID_HEIGHT)continue;
Position end;end.Relocate(x,y,z);
float distance=clearance(end);
if(distance<current+3)continue;
bool unsafe=false;
for(auto u:nearby)
{
if(!u->IsAlive() || !p->IsInPhase(u) || !p->CanSeeOrDetect(u) || !p->IsHostileTo(u) ||
std::find(threats.begin(),threats.end(),u)!=threats.end())continue;
if(AdventureCombatPolicy::SegmentDistance(u->GetPositionX(),u->GetPositionY(),u->GetPositionZ(),
p->GetPositionX(),p->GetPositionY(),p->GetPositionZ(),x,y,z)<12){unsafe=true;break;}
}
if(unsafe)continue;
ClientlessPathDiagnostics path;
if(!sClientlessTravelMgr->AnalyzePath(p,end,path) || !path.Complete || path.Suspicious || path.PathLength>60)continue;
float value=distance-0.15f*path.PathLength;
if(value>score){score=value;best=end;found=true;}
}
sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());
p->AttackStop();p->CastStop();
if(found && sClientlessTravelMgr->StartTravel(p,best))
TC_LOG_INFO("server.adventurebot","COMBAT_FLEE_PATH guid=%u clearance=%.2f",p->GetGUID().GetCounter(),clearance(best));
else
{
p->StopMoving();
auto target=ObjectAccessor::GetUnit(*p,r.Target);
if(r.AI && Valid(p,target,false) && p->IsWithinMeleeRange(target))r.AI->Tick(target,500);
TC_LOG_INFO("server.adventurebot","COMBAT_FLEE_BLOCKED guid=%u",p->GetGUID().GetCounter());
}
return true;
}
bool AdventureCombatMgr::Death(Player* p,Record& r)
{
if(!r.Dead)
{
if(r.AI)Finish(p,r);
r.Fleeing=false;r.ThreatPositions.clear();
sAdventureBehaviorMgr->InterruptForCombat(p);r.Damagers.clear();r.Dead=true;r.Suspended=true;
TC_LOG_INFO("server.adventurebot","DEATH_RELEASE guid=%u",p->GetGUID().GetCounter());
}
@@ -250,8 +395,8 @@ bool AdventureCombatMgr::Update(Player* p,uint32 diff)
if(!p->IsInWorld())return false;
auto& r=_records[p->GetGUID().GetCounter()];
r.Cooldown=r.Cooldown>diff?r.Cooldown-diff:0;
if(p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsCharmed())return r.AI || !p->IsAlive();
if(r.Timer>diff){r.Timer-=diff;return r.AI || !p->IsAlive() || r.Dead;}
if(p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsCharmed())return r.Fleeing || r.AI || !p->IsAlive();
if(r.Timer>diff){r.Timer-=diff;return r.Fleeing || r.AI || !p->IsAlive() || r.Dead;}
r.Timer=500;
if(!p->IsAlive())return Death(p,r);
if(r.Dead){r.Dead=false;r.Suspended=false;r.NextCorpseSearch=0;r.FailedCorpsePoints.clear();r.Cooldown=30000;r.Timer=0;return false;}
@@ -264,8 +409,21 @@ bool AdventureCombatMgr::Update(Player* p,uint32 diff)
if(r.AI)Finish(p,r);
Start(p,r,c,false,true,0);break;
}
if(!r.AI && !p->GetGroup() && !p->IsInCombat() && !r.Cooldown)
if(auto c=Proactive(p))Start(p,r,c,false,false,0);
if(Flee(p,r))return true;
if(!r.AI && (!p->GetGroup() || p->GetGroup()->GetLeaderGUID()==p->GetGUID()) && !p->IsInCombat() && !r.Cooldown)
{
auto now=GameTime::GetGameTime();
if(now>=r.NextPvP)
{
r.NextPvP=now+30;
if(auto target=PvPTarget(p))
{
TC_LOG_INFO("server.adventurebot","COMBAT_PVP_INITIATE guid=%u target='%s'",p->GetGUID().GetCounter(),target->GetGUID().ToString().c_str());
Start(p,r,target,false,false,0);
}
}
if(!r.AI && !p->GetGroup())if(auto c=Proactive(p))Start(p,r,c,false,false,0);
}
if(!r.AI)return false;
auto c=ObjectAccessor::GetUnit(*p,r.Target);
if(!Valid(p,c,r.Training) || (r.Until && GameTime::GetGameTime()>=r.Until) || p->GetExactDist(c)>80)
@@ -295,7 +453,7 @@ bool AdventureCombatMgr::Update(Player* p,uint32 diff)
bool AdventureCombatMgr::Busy(Player* player) const
{
auto it=_records.find(player->GetGUID().GetCounter());
return it!=_records.end() && (it->second.AI || it->second.Dead || !it->second.Damagers.empty());
return it!=_records.end() && (it->second.Fleeing || it->second.AI || it->second.Dead || !it->second.Damagers.empty());
}
bool AdventureCombatMgr::Assist(Player* p,Unit* target)
+5 -3
View File
@@ -32,9 +32,9 @@ private:
std::shared_ptr<Fighter> AI;
ObjectGuid Target,Helper;
std::deque<ObjectGuid> Damagers;
bool Training=false,Defending=false,Suspended=false,Dead=false,CemeteryPending=false;
time_t Until=0,NextCorpseSearch=0;
std::vector<Position> FailedCorpsePoints;
bool Fleeing=false,Training=false,Defending=false,Suspended=false,Dead=false,CemeteryPending=false;
time_t Until=0,NextCorpseSearch=0,NextPvP=0,NextEscape=0;
std::vector<Position> FailedCorpsePoints,ThreatPositions;
uint32 Timer=0,UnreachableMs=0,Cooldown=0;
};
bool Start(Player* player,Record& record,Unit* target,bool training,bool defending,time_t until);
@@ -42,7 +42,9 @@ private:
bool Safe(Player* player,Creature* target) const;
bool Reachable(Player* player,Unit* target) const;
Creature* Dummy(Player* player) const;
Unit* PvPTarget(Player* player) const;
Creature* Proactive(Player* player) const;
bool Flee(Player* player,Record& record);
bool Death(Player* player,Record& record);
void Finish(Player* player,Record& record);
std::unordered_map<uint32,Record> _records;
@@ -2,6 +2,8 @@
#define VCORE_ADVENTURE_COMBAT_POLICY_H
#include <cstdint>
#include <cmath>
#include <string>
#include <algorithm>
namespace AdventureCombatPolicy
{
// Conservative initial tuning. Self-defence deliberately bypasses this filter.
@@ -13,6 +15,21 @@ namespace AdventureCombatPolicy
return enemy<=level+MaxLevelAdvantage && health>=MinimumHealth && !elite && !boss &&
nearbyHostiles<=1 && (!manaUser || mana>=MinimumMana);
}
inline bool ShouldFlee(bool grouped,float health,unsigned level,unsigned enemy,bool elite,bool boss)
{
return !grouped && (health<20 || (enemy>level && enemy-level>=6) ||
((elite || boss) && enemy>=level));
}
constexpr float PvPRadius=50;
inline bool PvPLevel(unsigned level,unsigned enemy)
{return level>enemy?level-enemy<=5:enemy-level<=5;}
inline float PvPWillingness(std::string const& intent,float adventure,float risk)
{
float weight=intent=="adventure"?1.0f:intent=="leisure"?0.35f:
intent=="social"?0.2f:intent=="profession"?0.15f:intent=="economy"?0.1f:0.0f;
return weight*std::max(0.0f,std::min(1.0f,adventure))*
std::max(0.0f,std::min(1.0f,risk));
}
inline bool CemeteryFallback(std::int64_t now,std::int64_t released,bool noPath)
{return noPath && released>0 && now>=released && now-released>=300;}
inline float SegmentDistance(float x,float y,float z,float ax,float ay,float az,float bx,float by,float bz)
+273
View File
@@ -0,0 +1,273 @@
#include "AdventureGroupMgr.h"
#include "AdventureGroupPolicy.h"
#include "AdventureBehaviorMgr.h"
#include "AdventureCombatMgr.h"
#include "AdventureLocationMgr.h"
#include "ClientlessLuaEngine.h"
#include "ClientlessPlayerMgr.h"
#include "ClientlessTravelMgr.h"
#include "ClientlessTravelGraphMgr.h"
#include "Channel.h"
#include "ChannelMgr.h"
#include "Chat.h"
#include "GameTime.h"
#include "Group.h"
#include "ObjectAccessor.h"
#include "Opcodes.h"
#include "PartyPackets.h"
#include "Player.h"
#include "Map.h"
#include "Random.h"
#include "WorldSession.h"
#include "Log.h"
#include <algorithm>
namespace
{
float Sociability(Player* p)
{
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
if(profile){auto it=profile->Personality.find("sociability");if(it!=profile->Personality.end())return std::clamp(it->second,0.0f,1.0f);}
return .5f;
}
bool Ready(Player* p)
{return p && p->IsInWorld() && p->IsAlive() && !p->IsInCombat() && !sAdventureCombatMgr->Busy(p) &&
!p->IsBeingTeleported() && !p->GetTransport() && !p->IsInFlight() && !p->IsFlying() && !p->IsCharmed() && !p->GetMap()->IsDungeon();}
std::string Activity(AdventureGroupPlan const& plan){return plan.Forming?plan.NextIntent:plan.Intent;}
}
AdventureGroupMgr* AdventureGroupMgr::instance(){static AdventureGroupMgr mgr;return &mgr;}
bool AdventureGroupMgr::BotLed(Player* p) const
{
auto group=p->GetGroup();if(!group)return false;
auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID());
return leader && leader->GetSession()->IsClientless() && sClientlessLuaEngine->GetAdventureProfile(leader->GetGUID().GetCounter());
}
bool AdventureGroupMgr::Forming(Player* p) const
{
if(auto group=p->GetGroup())if(auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID()))
return sAdventureBehaviorMgr->GroupPlan(leader).Forming;
return sAdventureBehaviorMgr->GroupPlan(p).Forming;
}
bool AdventureGroupMgr::PlannedTransition(Player* p)
{
if(!BotLed(p))return false;
auto it=_records.find(p->GetGUID());
if(it==_records.end() || it->second.FormationIntent.empty())return false;
auto plan=sAdventureBehaviorMgr->GroupPlan(p);
if(plan.Intent!=it->second.FormationIntent)
{
if(plan.Forming)if(auto group=p->GetGroup())if(auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID()))
return sAdventureBehaviorMgr->GroupPlan(leader).Intent==it->second.FormationIntent;
return false;
}
it->second.FormationIntent.clear();return true;
}
void AdventureGroupMgr::Speak(Player* p,std::string const& message)
{
if(!AdventureGroupPolicy::Capital(p->GetZoneId()))return;
auto manager=ChannelMgr::forTeam(p->GetTeam());if(!manager)return;
auto channel=manager->GetJoinChannel(0,"sucheNachGruppe");if(!channel)return;
if(std::find(p->GetJoinedChannels().begin(),p->GetJoinedChannels().end(),channel)==p->GetJoinedChannels().end())channel->JoinChannel(p,"");
channel->Say(p->GetGUID(),message,p->GetTeam()==ALLIANCE?LANG_COMMON:LANG_ORCISH);
}
void AdventureGroupMgr::Offline(Player* p)
{
// Native player logout owns channel membership cleanup.
_records.erase(p->GetGUID());
}
bool AdventureGroupMgr::Accept(Player* invited,Player* inviter)
{
if(!Ready(invited) || !Ready(inviter) || invited->GetGroup() || !inviter->GetSession()->IsClientless() ||
invited->GetTeam()!=inviter->GetTeam())return false;
auto it=_records.find(invited->GetGUID());
if(it==_records.end() || it->second.Inviter!=inviter->GetGUID() || it->second.InviteUntil<GameTime::GetGameTime())return false;
auto a=sAdventureBehaviorMgr->GroupPlan(inviter),b=sAdventureBehaviorMgr->GroupPlan(invited);
auto intent=Activity(a);
if(!a.Enabled || !b.Enabled || a.Forming!=b.Forming ||
!AdventureGroupPolicy::Compatible(intent,Activity(b),inviter->getLevel(),invited->getLevel()))return false;
auto group=inviter->GetGroup();
return (!group || (group->GetLeaderGUID()==inviter->GetGUID() && group->GetMembersCount()<AdventureGroupPolicy::Capacity(intent))) && frand(0,1)<Sociability(invited);
}
void AdventureGroupMgr::Update(Player* p)
{
auto now=GameTime::GetGameTime();auto& r=_records[p->GetGUID()];
if(PlayerLeadership(p))return;
if(!Ready(p))return;
if(EnforceCapacity(p))return;
auto plan=sAdventureBehaviorMgr->GroupPlan(p);if(!plan.Enabled)return;
auto intent=Activity(plan);if(!AdventureGroupPolicy::Eligible(intent))return;
auto group=p->GetGroup();
if(group && BotLed(p))if(auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID()))
{auto leaderPlan=sAdventureBehaviorMgr->GroupPlan(leader);if(leaderPlan.Forming)r.FormationIntent=Activity(leaderPlan);}
if(group && group->GetLeaderGUID()!=p->GetGUID())return;
if(intent=="raid" && p->GetRaidDifficulty()!=RAID_DIFFICULTY_25MAN_NORMAL)
{
WorldPacket mode(MSG_SET_RAID_DIFFICULTY,4);mode<<uint32(RAID_DIFFICULTY_25MAN_NORMAL);
p->GetSession()->HandleSetRaidDifficultyOpcode(mode);
}
auto capacity=AdventureGroupPolicy::Capacity(intent);
if(group && capacity>5 && !group->isRaidGroup())group->ConvertToRaid();
if(group && capacity<=5 && group->isRaidGroup() && group->GetMembersCount()<=5)group->ConvertToGroup();
unsigned pending=0;
for(auto const& peer:_records)if(peer.second.Inviter==p->GetGUID() && peer.second.InviteUntil>now)
{
auto invited=ObjectAccessor::FindPlayer(peer.first);
if(invited && invited->GetGroupInvite())++pending;
}
if((group?group->GetMembersCount():1)+pending>=capacity || p->GetGroupInvite() || now<r.SearchAt)return;
r.SearchAt=now+urand(20,40);
if(plan.Forming)
{
if(!AdventureGroupPolicy::Capital(p->GetZoneId()))return;
if(now>=r.SpeakAt)
{
r.SpeakAt=now+urand(120,240);
auto place=sAdventureLocationMgr->Find(plan.Location);
std::string activity=intent=="instance"?"einen Instanzbesuch":intent=="raid"?"einen Schlachtzug":intent=="profession"?"gemeinsame Berufsarbeit":intent=="social"?"eine gesellige Runde":"ein Abenteuer";
Speak(p,"Ich suche Gefaehrten fuer "+activity+" in der naechsten Stunde. Treffpunkt: "+(place?place->Name:std::string("hier in der Stadt"))+".");
}
}
if(frand(0,1)>Sociability(p))return;
// Snapshot GUIDs: callbacks/native invitations may update manager records.
std::vector<ObjectGuid> candidates;for(auto const& entry:_records)candidates.push_back(entry.first);
std::sort(candidates.begin(),candidates.end());
for(auto guid:candidates)
{
if(guid==p->GetGUID())continue;
auto other=ObjectAccessor::FindPlayer(guid);
if(!Ready(other) || other->GetGroupInvite() || other->GetTeam()!=p->GetTeam())continue;
auto peerPlan=sAdventureBehaviorMgr->GroupPlan(other);
if(!peerPlan.Enabled || peerPlan.Forming!=plan.Forming ||
!AdventureGroupPolicy::Compatible(intent,Activity(peerPlan),p->getLevel(),other->getLevel()))continue;
if(intent=="profession")
{
auto a=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
auto b=sClientlessLuaEngine->GetAdventureProfile(other->GetGUID().GetCounter());
bool compatible=false;
if(a && b)for(auto const& profession:a->PrimaryProfessions)if(b->PrimaryProfessions.count(profession))compatible=true;
if(!compatible)continue;
}
if(auto existing=other->GetGroup())
{
if(group || existing->GetLeaderGUID()!=other->GetGUID() ||
existing->GetMembersCount()>=AdventureGroupPolicy::Capacity(intent))continue;
if(plan.Forming)
{if(!AdventureGroupPolicy::Capital(other->GetZoneId()))continue;}
else if(!p->IsInMap(other) || !p->IsInPhase(other) || !p->CanSeeOrDetect(other) || p->GetExactDist(other)>25 || !p->IsWithinLOSInMap(other))continue;
if(r.Partners[guid]>now || _records[guid].Partners[p->GetGUID()]>now || r.InviteUntil>now)continue;
// Structured join request: the existing native leader sends the
// invitation, so requesting membership never transfers leadership.
Invite(other,p,intent,plan.Forming);return;
}
auto& peer=_records[guid];
if(peer.InviteUntil>now || r.Partners[guid]>now || peer.Partners[p->GetGUID()]>now)continue;
if(plan.Forming)
{if(!AdventureGroupPolicy::Capital(other->GetZoneId()))continue;}
else if(!p->IsInMap(other) || !p->IsInPhase(other) || !p->CanSeeOrDetect(other) ||
p->GetExactDist(other)>25 || !p->IsWithinLOSInMap(other))continue;
Invite(p,other,intent,plan.Forming);
break;
}
}
void AdventureGroupMgr::Report(ChatHandler* h,Player* p) const
{
auto plan=sAdventureBehaviorMgr->GroupPlan(p);
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)
{
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;
}
void AdventureGroupMgr::Invite(Player* p,Player* other,std::string const& intent,bool forming)
{
auto now=GameTime::GetGameTime();auto& r=_records[p->GetGUID()];auto& peer=_records[other->GetGUID()];
unsigned pending=0;
for(auto const& entry:_records)if(entry.second.Inviter==p->GetGUID() && entry.second.InviteUntil>now)
if(auto target=ObjectAccessor::FindPlayer(entry.first))if(target->GetGroupInvite())++pending;
auto group=p->GetGroup();
if((group?group->GetMembersCount():1)+pending>=AdventureGroupPolicy::Capacity(intent))return;
peer.Inviter=p->GetGUID();peer.InviteUntil=now+30;
if(forming){peer.FormationIntent=intent;r.FormationIntent=intent;}
peer.Partners[p->GetGUID()]=now+600;r.Partners[other->GetGUID()]=now+600;
WorldPackets::Party::PartyInviteClient packet{WorldPacket(CMSG_PARTY_INVITE)};
packet.TargetName=other->GetName();packet.TargetGUID=other->GetGUID();
p->GetSession()->HandlePartyInviteOpcode(packet);
if(forming && other->GetGroupInvite())Speak(other,"Ich wuerde mich euch anschliessen.");
else if(other->GetGroupInvite()){p->SetFacingToObject(other);p->HandleEmoteCommand(EMOTE_ONESHOT_WAVE);}
TC_LOG_INFO("server.adventurebot","GROUP_BOT_INVITE leader=%u target=%u intent='%s' forming=%u",p->GetGUID().GetCounter(),other->GetGUID().GetCounter(),intent.c_str(),forming);
}
void AdventureGroupMgr::RequestPlayerAdmission(Player* member,Player* requester)
{
if(!requester || requester->GetSession()->IsClientless() || requester->GetGroup() || requester->GetGroupInvite() || !BotLed(member))return;
auto group=member->GetGroup();auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID());
if(!Ready(leader) || !Ready(requester) || leader->GetTeam()!=requester->GetTeam())return;
auto now=GameTime::GetGameTime();auto& r=_records[leader->GetGUID()];
if(r.Partners[requester->GetGUID()]>now)return;
auto plan=sAdventureBehaviorMgr->GroupPlan(leader);
if(group->GetMembersCount()>=AdventureGroupPolicy::Capacity(Activity(plan)) || frand(0,1)>Sociability(leader))return;
Invite(leader,requester,Activity(plan),false);
}
bool AdventureGroupMgr::PlayerLeadership(Player* leader)
{
auto group=leader->GetGroup();if(!group || group->GetLeaderGUID()!=leader->GetGUID())return false;
for(auto member=group->GetFirstMember();member;member=member->next())
{
auto player=member->GetSource();
if(!player || player->GetSession()->IsClientless())continue;
group->ChangeLeader(player->GetGUID());group->SendUpdate();
TC_LOG_INFO("server.adventurebot","GROUP_PLAYER_LEADER old=%u leader=%u",leader->GetGUID().GetCounter(),player->GetGUID().GetCounter());
return true;
}
return false;
}
bool AdventureGroupMgr::EnforceCapacity(Player* p)
{
auto group=p->GetGroup();if(!group || !BotLed(p) || group->GetLeaderGUID()==p->GetGUID())return false;
auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID());if(!leader)return false;
auto plan=sAdventureBehaviorMgr->GroupPlan(leader);auto intent=Activity(plan);
if(!AdventureGroupPolicy::Eligible(intent))return false;
auto capacity=AdventureGroupPolicy::Capacity(intent);if(group->GetMembersCount()<=capacity)return false;
unsigned rank=1;
for(auto const& member:group->GetMemberSlots())
{
if(member.guid==group->GetLeaderGUID())continue;
++rank;
if(member.guid!=p->GetGUID())continue;
if(rank<=capacity)return false;
p->HandleEmoteCommand(EMOTE_ONESHOT_WAVE);
group->RemoveMember(p->GetGUID(),GROUP_REMOVEMETHOD_LEAVE);
TC_LOG_INFO("server.adventurebot","GROUP_CAPACITY_LEAVE guid=%u capacity=%u",p->GetGUID().GetCounter(),capacity);
return true;
}
return false;
}
+45
View File
@@ -0,0 +1,45 @@
#ifndef VCORE_ADVENTURE_GROUP_MGR_H
#define VCORE_ADVENTURE_GROUP_MGR_H
#include "Common.h"
#include "ObjectGuid.h"
#include <ctime>
#include <string>
#include <unordered_map>
class Player;
class ChatHandler;
struct AdventureGroupPlan
{
bool Enabled=false,Forming=false;
std::string Intent,Location,NextIntent,NextLocation;
time_t Deadline=0;
};
class TC_GAME_API AdventureGroupMgr
{
public:
static AdventureGroupMgr* instance();
void Update(Player* player);
void RequestPlayerAdmission(Player* member,Player* requester);
bool EnforceCapacity(Player* player);
bool PlayerLeadership(Player* leader);
void Offline(Player* player);
bool Accept(Player* invited,Player* inviter);
bool FollowMember(Player* player);
bool BotLed(Player* player) const;
bool Forming(Player* player) const;
bool PlannedTransition(Player* player);
void Report(ChatHandler* handler,Player* player) const;
private:
struct Record
{
time_t SearchAt=0,SpeakAt=0;
ObjectGuid Inviter;
std::string FormationIntent;
time_t InviteUntil=0;
std::unordered_map<ObjectGuid,time_t> Partners;
};
void Invite(Player* leader,Player* member,std::string const& intent,bool forming);
void Speak(Player* player,std::string const& message);
std::unordered_map<ObjectGuid,Record> _records;
};
#define sAdventureGroupMgr AdventureGroupMgr::instance()
#endif
@@ -0,0 +1,30 @@
#ifndef VCORE_ADVENTURE_GROUP_POLICY_H
#define VCORE_ADVENTURE_GROUP_POLICY_H
#include <string>
#include <random>
namespace AdventureGroupPolicy
{
inline bool Capital(unsigned zone)
{switch(zone){case 1519:case 1537:case 1657:case 1637:case 1638:case 1497:case 3487:case 3557:case 3703:case 4395:return true;default:return false;}}
inline bool Eligible(std::string const& intent)
{return intent=="instance" || intent=="raid" || intent=="adventure" || intent=="profession" || intent=="social";}
inline unsigned Capacity(std::string const& intent)
{return intent=="raid"?20:intent=="instance"?5:intent=="social"?10:3;}
inline bool Compatible(std::string const& a,std::string const& b,unsigned level,unsigned other)
{return Eligible(a) && a==b && (level>other?level-other<=5:other-level<=5);}
template<class Day,class Meeting>
void Prepare(Day& plan,std::mt19937& random,float sociability,Meeting meeting)
{
// Work backwards: never replace the very activity a previous preparation
// hour was already created for.
for(unsigned h=23;h>0;--h)
{
auto const& next=plan[h];
if(!Eligible(next.Intent) || next.Intent==plan[h-1].Intent)continue;
if(next.Intent!="instance" && next.Intent!="raid" && std::uniform_real_distribution<float>(0,1)(random)>sociability)continue;
auto location=meeting();if(!location.empty())plan[h-1]={"groupformation",location};
}
}
}
#endif
@@ -1,5 +1,6 @@
#include "AdventureInteractionMgr.h"
#include "AdventureInteractionPolicy.h"
#include "AdventureGroupMgr.h"
#include "AdventureBehaviorMgr.h"
#include "AdventureCombatMgr.h"
#include "ClientlessLuaEngine.h"
@@ -73,6 +74,7 @@ void AdventureInteractionMgr::Leave(Player* p,Record& r,char const* reason)
TC_LOG_INFO("server.adventurebot","INTERACTION_GROUP_LEAVE guid=%u reason='%s'",p->GetGUID().GetCounter(),reason);
p->GetGroup()->RemoveMember(p->GetGUID(),GROUP_REMOVEMETHOD_LEAVE);
sAdventureBehaviorMgr->InterruptForInteraction(p);
sAdventureBehaviorMgr->ResumePersonalPlan(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)
@@ -98,15 +100,16 @@ 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){_records.erase(p->GetGUID().GetCounter());}
void AdventureInteractionMgr::Offline(Player* 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 || r.SceneUntil>now;}
if(r.Timer>diff){r.Timer-=diff;return (r.Grouped && !sAdventureGroupMgr->BotLed(p)) || r.SceneUntil>now;}
r.Timer=1000;
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;}
sAdventureGroupMgr->Update(p);
for(auto it=r.Encounters.begin();it!=r.Encounters.end();)if(now>=it->second)it=r.Encounters.erase(it);else ++it;
if(r.Helper)
{
@@ -114,7 +117,7 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
if(Visible(p,helper,25) && p->IsFriendlyTo(helper))
p->Say(German(p)?"Danke für die Hilfe!":"Thank you for the help!",p->GetTeam()==ALLIANCE?LANG_COMMON:LANG_ORCISH,helper);
}
if(auto invite=p->GetGroupInvite())
if(auto invite=p->GetGroupInvite();invite && invite->GetLeaderGUID()!=p->GetGUID())
{
auto leader=invite->GetLeaderGUID();
if(r.Invite!=leader){r.Invite=leader;r.InviteAt=now+urand(2,4);}
@@ -122,7 +125,8 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
{
auto inviter=ObjectAccessor::FindPlayer(leader);
bool accept=inviter && inviter->IsAlive() && p->IsFriendlyTo(inviter) && !p->GetMap()->IsDungeon() &&
!inviter->GetSession()->IsClientless() && frand(0,1)<AdventureInteractionPolicy::Willingness(sAdventureBehaviorMgr->InteractionIntent(p),Sociability(p));
(inviter->GetSession()->IsClientless()?sAdventureGroupMgr->Accept(p,inviter):
frand(0,1)<AdventureInteractionPolicy::Willingness(sAdventureBehaviorMgr->InteractionIntent(p),Sociability(p)));
WorldPackets::Party::PartyInviteResponse response{WorldPacket(CMSG_PARTY_INVITE_RESPONSE)};response.Accept=accept;
p->GetSession()->HandlePartyInviteResponseOpcode(response);r.Invite.Clear();
TC_LOG_INFO("server.adventurebot","INTERACTION_INVITE guid=%u leader=%u accepted=%u",p->GetGUID().GetCounter(),leader.GetCounter(),accept);
@@ -146,11 +150,13 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
{
r.Grouped=true;r.Schedule=sAdventureBehaviorMgr->InteractionSchedule(p);
r.Partner.Clear();r.SceneUntil=0;
sAdventureBehaviorMgr->InterruptForInteraction(p);
if(!sAdventureGroupMgr->BotLed(p) || group->GetLeaderGUID()!=p->GetGUID())sAdventureBehaviorMgr->InterruptForInteraction(p);
PartySay(p,"Ich begleite euch eine Weile.","I'll accompany you for a while.");
}
if(r.Leader!=group->GetLeaderGUID())
{
if(leader==p && sAdventureGroupMgr->BotLed(p) && !p->GetMap()->IsDungeon())sAdventureBehaviorMgr->ResumePersonalPlan(p);
if(r.Leader && !sAdventureGroupMgr->BotLed(p))sAdventureBehaviorMgr->InterruptForInteraction(p);
r.Leader=group->GetLeaderGUID();r.LastActivity=now;r.AfkSince=0;
if(leader){r.LeaderPosition.Relocate(leader);r.LeaderMap=leader->GetMapId();r.Selection=leader->GetTarget();}
}
@@ -163,12 +169,14 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
r.LeaderPosition.Relocate(leader);r.LeaderMap=leader->GetMapId();r.Selection=leader->GetTarget();
if(leader->isAFK()){if(!r.AfkSince)r.AfkSince=now;}else r.AfkSince=0;
}
if(AdventureInteractionPolicy::CanLeave(p->IsAlive(),p->IsInCombat(),p->GetMap()->IsDungeon(),p->GetTransport() || p->IsInFlight()))
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()))
{
if(r.Schedule!=sAdventureBehaviorMgr->InteractionSchedule(p))
{
r.Schedule=sAdventureBehaviorMgr->InteractionSchedule(p);
if(frand(0,1)>=Loyalty(p)){Leave(p,r,"intent-change");return false;}
if(!sAdventureGroupMgr->PlannedTransition(p) && frand(0,1)>=Loyalty(p)){Leave(p,r,"intent-change");return false;}
}
if(AdventureInteractionPolicy::LeaderExpired(now,r.AfkSince?r.AfkSince:r.LastActivity)){Leave(p,r,"leader-afk");return false;}
}
@@ -181,6 +189,7 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
if(!target)for(auto attacker:ally->getAttackers())if(attacker->GetVictim()==ally){target=attacker;break;}
if(target && sAdventureCombatMgr->Assist(p,target))return true;
}
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;
@@ -214,7 +223,7 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
else if(p->GetExactDist(leader)<=5)sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());
return true;
}
if(r.Grouped){r.Grouped=false;r.Leader.Clear();sAdventureBehaviorMgr->InterruptForInteraction(p);}
if(r.Grouped){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"))
{
@@ -242,6 +251,7 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
void AdventureInteractionMgr::Report(ChatHandler* handler,Player* p) const
{
sAdventureGroupMgr->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();
@@ -252,3 +262,12 @@ void AdventureInteractionMgr::Report(ChatHandler* handler,Player* p) const
static_cast<unsigned long long>(r.LastActivity && now>=r.LastActivity?now-r.LastActivity:0),
static_cast<unsigned long long>(r.AfkSince && now>=r.AfkSince?now-r.AfkSince:0),Sociability(p),Loyalty(p));
}
bool AdventureInteractionMgr::RequestLeave(Player* p)
{
if(!AdventureInteractionPolicy::CanLeave(p->IsAlive(),p->IsInCombat(),p->GetMap()->IsDungeon(),
p->GetTransport() || p->IsInFlight() || p->IsFlying()))return false;
if(p->GetGroupInvite())p->UninviteFromGroup();
if(p->GetGroup())Leave(p,_records[p->GetGUID().GetCounter()],"manual-test");
return true;
}
@@ -12,6 +12,7 @@ class TC_GAME_API AdventureInteractionMgr
{
public:
static AdventureInteractionMgr* instance();
bool RequestLeave(Player* player);
bool Update(Player* player,uint32 diff);
void Emote(Player* player,Player* source,uint32 emote);
void Offline(Player* player);
@@ -1,5 +1,6 @@
#include "AdventureLocationMgr.h"
#include "AdventureBehaviorPolicy.h"
#include "AdventureGroupPolicy.h"
#include "Chat.h"
#include "DatabaseEnv.h"
#include "DBCStores.h"
@@ -53,6 +54,12 @@ bool AdventureLocationMgr::Reload()
}
while (rows->NextRow());
}
if (WorldDatabase.Query("SHOW TABLES LIKE 'vcore_adventure_instance'"))
if(auto rows=WorldDatabase.Query("SELECT i.location_key,a.target_map FROM vcore_adventure_instance i JOIN areatrigger_teleport a ON a.ID=i.entry_trigger WHERE i.enabled=1")) do
{
auto f=rows->Fetch();
for(auto& l:_locations)if(l.Point.Id==f[0].GetString())l.InstanceMap=f[1].GetUInt16();
}while(rows->NextRow());
if (auto rows = WorldDatabase.Query("SELECT category,intent,weight FROM vcore_adventure_location_intent WHERE weight>=0")) do
{
auto f = rows->Fetch();
@@ -110,6 +117,18 @@ bool AdventureLocationMgr::IsWorkstation(uint32 entry,std::string const& role) c
float AdventureLocationMgr::Weight(AdventureLocation const & location, std::string const & intent) const
{
if(intent=="instance" || intent=="raid")
{
auto map=sMapStore.LookupEntry(location.InstanceMap);
if(!map || !map->IsDungeon() || map->IsRaid()!=(intent=="raid"))return 0;
if(intent=="raid")
{
Difficulty difficulty=RAID_DIFFICULTY_25MAN_NORMAL;
auto data=sDBCManager.GetDownscaledMapDifficultyData(location.InstanceMap,difficulty);
if(!data || data->maxPlayers<20)return 0;
}
}
if(intent=="groupformation" && !AdventureGroupPolicy::Capital(location.ZoneId?location.ZoneId:location.AreaId))return 0;
auto type = _types.find(location.Point.Category);
if (type != _types.end())
{
+1 -1
View File
@@ -11,7 +11,7 @@ struct AdventureLocation
{
ClientlessWorldPoint Point;
std::string Name, Region;
uint32 AreaId = 0, ZoneId = 0;
uint32 AreaId = 0, ZoneId = 0, InstanceMap = 0;
bool IsArea = false;
uint8 Faction = 0;
};
@@ -1,3 +1,5 @@
#include "Group.h"
#include "ObjectAccessor.h"
#include "ClientlessActionExecutorMgr.h"
#include "ClientlessTransportMgr.h"
#include "ClientlessFlightMgr.h"
@@ -256,6 +258,15 @@ bool ClientlessActionExecutorMgr::StartAction(Player* player, ClientlessAction c
if (action.Name == "move_to_point")
return StartMoveToPoint(player, action, record);
record.PendingShared.reset();record.SharedWaitMs=0;
if ((action.Name=="travel_to_node" || action.Name=="travel_to_location") && action.Parameters.count("route_source"))
{
if(AdoptRoute(player,action,record))return true;
record.PendingShared=std::make_shared<ClientlessAction>(action);record.Active=true;
record.Status.State=ClientlessActionExecutionState::SelectingTarget;
record.Status.Detail="waiting for group leader route";return true;
}
if (action.Name == "travel_to_node")
return StartTravelToNode(player, action, record);
@@ -2814,6 +2825,17 @@ void ClientlessActionExecutorMgr::Update(uint32 diff)
if (!player->IsInWorld())
continue;
if(record.Active && record.PendingShared)
{
record.SharedWaitMs+=std::min(diff,30000u);
auto action=*record.PendingShared;
if(AdoptRoute(player,action,record)){record.PendingShared.reset();continue;}
if(record.SharedWaitMs<15000)continue;
// A local leader leg has no macro route; late arrivals may also
// need their own native catch-up route before joining the convoy.
action.Parameters.erase("route_source");record.PendingShared.reset();
StartAction(player,action,record);continue;
}
if (record.Active)
{
record.GroundExitCheckMs = diff >= record.GroundExitCheckMs ? 0 : record.GroundExitCheckMs - diff;
@@ -2828,3 +2850,40 @@ void ClientlessActionExecutorMgr::Update(uint32 diff)
StartAction(player, action, record);
}
}
bool ClientlessActionExecutorMgr::AdoptRoute(Player* player,ClientlessAction const& action,Record& record)
{
auto arg=action.Parameters.find("route_source");if(arg==action.Parameters.end())return false;
uint32 sourceGuid=uint32(std::strtoul(arg->second.c_str(),nullptr,10));
auto group=player->GetGroup();
if(!group || group->GetLeaderGUID().GetCounter()!=sourceGuid || sourceGuid==player->GetGUID().GetCounter())return false;
auto source=_records.find(sourceGuid);
if(source==_records.end() || source->second.Route.empty())return false;
// Copy values: inserting/updating executor records must never leave a pointer
// into another character's unordered-map record alive across execution.
auto route=source->second.Route;
auto const snapshot=source->second;
auto goal=action.Parameters.find(action.Name=="travel_to_location"?"id":"node");
if(goal==action.Parameters.end() || goal->second!=(action.Name=="travel_to_location"?snapshot.FinalLocationId:snapshot.PlannedDestinationNode))return false;
auto start=std::min(snapshot.RouteStep?snapshot.RouteStep-1:0,route.size()-1);
auto anchor=route[start];
std::vector<ClientlessTravelRouteStep> joined;
Position point;point.Relocate(anchor.X,anchor.Y,anchor.Z);
if(player->GetMapId()==anchor.MapId && player->GetExactDist(point)<100)
{anchor.EdgeType="start";anchor.EdgeDatabaseId=0;joined.push_back(anchor);}
else
{
bool complete=false;std::string reached,error;
if(!sClientlessTravelGraphMgr->PlanAdaptiveRoute(player,anchor.NodeKey,500,300,joined,complete,reached,error) || !complete)return false;
}
joined.insert(joined.end(),route.begin()+start+1,route.end());
record.Route=std::move(joined);record.RouteStep=0;
record.FinalLocalLegPending=snapshot.FinalLocalLegPending || !snapshot.FinalLocationId.empty();
record.FinalMapId=snapshot.FinalMapId;record.FinalX=snapshot.FinalX;record.FinalY=snapshot.FinalY;
record.FinalZ=snapshot.FinalZ;record.FinalO=snapshot.FinalO;record.FinalLocationId=snapshot.FinalLocationId;
record.FinalDatabaseId=snapshot.FinalDatabaseId;record.PlannedDestinationNode=snapshot.PlannedDestinationNode;
record.RouteComplete=snapshot.RouteComplete;record.AdaptiveReachedNode=snapshot.AdaptiveReachedNode;
record.HardDestination=snapshot.HardDestination;record.Active=true;
TC_LOG_INFO("server.adventurebot.navigation","TRAVEL_SHARED_ROUTE guid=%u leader=%u steps=%u",player->GetGUID().GetCounter(),sourceGuid,uint32(record.Route.size()));
return StartRouteStep(player,player->GetGUID().GetCounter(),record);
}
@@ -8,6 +8,7 @@
#include "Common.h"
#include "ClientlessTravelGraphMgr.h"
#include <string>
#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <vector>
@@ -51,6 +52,8 @@ private:
{
ClientlessActionExecutionStatus Status;
bool Active = false;
std::shared_ptr<ClientlessAction> PendingShared;
uint32 SharedWaitMs=0;
bool StopAtNode = false;
std::vector<ClientlessTravelRouteStep> Route;
@@ -110,6 +113,7 @@ private:
std::string AdaptiveReachedNode;
};
bool AdoptRoute(Player* player,ClientlessAction const& action,Record& record);
bool StartAction(Player* player, ClientlessAction const& action, Record& record);
bool StartApproach(Player* player, ClientlessAction const& action, Record& record);
bool StartMoveToPoint(Player* player, ClientlessAction const& action, Record& record);
@@ -16,6 +16,7 @@
#include "MoveSpline.h"
#include "PhasingHandler.h"
#include "Player.h"
#include "Group.h"
#include "PathGenerator.h"
#include "Random.h"
#include "SpellInfo.h"
@@ -139,6 +140,8 @@ ClientlessFlightStatus const * ClientlessFlightMgr::GetStatus(uint32 guid) const
void ClientlessFlightMgr::Clear(uint32 guid)
{
auto it=_records.find(guid);
if(it!=_records.end() && it->second.Status.State!=ClientlessFlightState::Arrived)it->second.Published->Aborted=true;
_records.erase(guid);
}
@@ -270,6 +273,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())
{
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;
TC_LOG_INFO("server.adventurebot.navigation","FLIGHT_SHARED_TRAIL guid=%u leader=%u",player->GetGUID().GetCounter(),group->GetLeaderGUID().GetCounter());
}
}
_records[player->GetGUID().GetCounter()] = std::move(record);
if (player->CastSpell(player, spell, false) != SPELL_CAST_OK)
{
@@ -283,6 +296,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;
player->StopMoving();
record.Moving = false;
if (record.Status.State == ClientlessFlightState::Mounting) player->InterruptNonMeleeSpells(false);
@@ -338,6 +352,13 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff
back, Corridor(current, back, goal, record.Settings, sample, ray, true), true, false
};
}
else if(record.Following)
{
auto shared=FollowTrail(current,goal,record.Settings,*record.Following,record.SharedIndex,sample,ray);
if(shared.Waiting)return true;
next=shared.Step;
if(handoff && next.Valid && Distance(next.Next,PointOf(record.Segment))<1)return true;
}
else
{
auto const memory = record.Memory;
@@ -368,6 +389,7 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff
Fail(player, record, "flight spline rejected");
return false;
}
record.Published->Points.push_back(next.Next);
record.Moving = true;
record.SegmentElapsed = 0;
++ record.Status.Segments;
@@ -36,6 +36,9 @@ class TC_GAME_API ClientlessFlightMgr
bool Command(ChatHandler * handler, char const * args);
private : struct Record
{
std::shared_ptr<ClientlessFlightPath::SharedTrail> Published=std::make_shared<ClientlessFlightPath::SharedTrail>();
std::shared_ptr<ClientlessFlightPath::SharedTrail> Following;
std::size_t SharedIndex=0;
ClientlessFlightStatus Status;
ClientlessFlightPath::Settings Settings;
ClientlessFlightPath::Memory Memory;
@@ -5,6 +5,7 @@
#include <cmath>
#include <functional>
#include <limits>
#include <vector>
// Geometry-only receding-horizon navigation. Server adapters supply phased
// terrain/ceiling and static+dynamic collision queries. No Player shortcuts.
namespace ClientlessFlightPath
@@ -209,6 +210,22 @@ namespace ClientlessFlightPath
if (best.Valid) memory.Departed = true;
return best;
}
struct SharedTrail
{
std::vector<Point> Points;
bool Aborted=false;
};
struct SharedResult {Result Step;bool Waiting=false;};
inline SharedResult FollowTrail(Point const& current,Point const& goal,Settings const& settings,
SharedTrail const& trail,std::size_t& index,Sample const& sample,Ray const& ray)
{
if(trail.Aborted)return {};
while(index<trail.Points.size() && Distance(current,trail.Points[index])<0.7f)++index;
if(index==trail.Points.size())return {{},true};
auto next=trail.Points[index];
return {{next,Corridor(current,next,goal,settings,sample,ray,true),false,Distance(next,goal)<0.7f},false};
}
}
#endif
@@ -6,6 +6,7 @@
#include "AdventureBehaviorMgr.h"
#include "AdventureCombatMgr.h"
#include "AdventureInteractionMgr.h"
#include "AdventureGroupMgr.h"
#include "AdventureGeneratorMgr.h"
#include "AdventureSupplyMgr.h"
#include "AdventureSocialActions.h"
@@ -230,6 +231,8 @@ public:
}
void OnSharedTargetDamage(Player* player,ObjectGuid helper,ObjectGuid target) override
{ sAdventureCombatMgr->SharedTargetDamage(player,helper,target); }
void OnGroupJoinRequest(Player* player,Player* requester) override
{sAdventureGroupMgr->RequestPlayerAdmission(player,requester);}
void OnTextEmote(Player* player, Player* source, uint32 emote) override
{ sAdventureInteractionMgr->Emote(player,source,emote); }
void OnDamageTaken(Player* player, Unit* attacker, uint32 damage) override
+13 -1
View File
@@ -157,7 +157,8 @@ void ClientlessPlayerMgr::Update(uint32 diff)
decltype(_damageNotices) damageNotices;
decltype(_emoteNotices) emoteNotices;
decltype(_helpNotices) helpNotices;
{ std::lock_guard<std::mutex> lock(_damageMutex); damageNotices.swap(_damageNotices); emoteNotices.swap(_emoteNotices); helpNotices.swap(_helpNotices); }
decltype(_groupJoinNotices) groupJoinNotices;
{ std::lock_guard<std::mutex> lock(_damageMutex); damageNotices.swap(_damageNotices); emoteNotices.swap(_emoteNotices); helpNotices.swap(_helpNotices); groupJoinNotices.swap(_groupJoinNotices); }
// CLIENTLESS_PLAYER_CORE_C8_2_TRAVEL_ROBUSTNESS
sClientlessTravelMgr->Update(diff);
@@ -208,6 +209,9 @@ void ClientlessPlayerMgr::Update(uint32 diff)
auto helpers=helpNotices.find(characterGuid);
if(helpers!=helpNotices.end())for(auto const& notice:helpers->second)
record.Controller->OnSharedTargetDamage(player,notice.Helper,notice.Target);
auto joins=groupJoinNotices.find(characterGuid);
if(joins!=groupJoinNotices.end())for(auto const& source:joins->second)
record.Controller->OnGroupJoinRequest(player,ObjectAccessor::FindPlayer(source));
record.Controller->Update(player, diff);
}
}
@@ -268,3 +272,11 @@ void ClientlessPlayerMgr::SharedTargetDamage(Unit* victim,Unit* attacker,uint32
if(queue.size()<8)queue.push_back({helper->GetGUID(),victim->GetGUID()});
}
}
void ClientlessPlayerMgr::GroupJoinRequest(Player* target,Player* requester)
{
if(!target || !requester || target==requester || !target->GetSession()->IsClientless() || requester->GetSession()->IsClientless())return;
std::lock_guard<std::mutex> lock(_damageMutex);
auto& queue=_groupJoinNotices[target->GetGUID().GetCounter()];
if(queue.size()<8)queue.push_back(requester->GetGUID());
}
@@ -39,6 +39,7 @@ public:
virtual void OnPlayerLogout(Player*) { }
// Notification after health was reduced, never the mutable pre-damage event.
virtual void OnDamageTaken(Player*, Unit*, uint32) { }
virtual void OnGroupJoinRequest(Player*,Player*) { }
virtual void OnTextEmote(Player*, Player*, uint32) { }
virtual void OnSharedTargetDamage(Player*, ObjectGuid, ObjectGuid) { }
};
@@ -54,6 +55,7 @@ public:
bool IsManaged(uint32 characterGuid) const;
void DamageTaken(Player* player, Unit* attacker, uint32 damage);
void GroupJoinRequest(Player* target,Player* requester);
void TextEmote(Player* source, Player* target, uint32 emote);
void SharedTargetDamage(Unit* victim,Unit* attacker,uint32 damage);
void MarkActivity(Player* player);
@@ -89,6 +91,7 @@ private:
std::unordered_map<uint32,std::deque<HelpNotice>> _helpNotices;
struct EmoteNotice { ObjectGuid Source; uint32 Emote; };
std::unordered_map<uint32,std::deque<EmoteNotice>> _emoteNotices;
std::unordered_map<uint32,std::deque<ObjectGuid>> _groupJoinNotices;
std::unordered_map<ObjectGuid,time_t> _activity;
std::mutex _damageMutex;
std::unordered_map<uint32,std::deque<DamageNotice>> _damageNotices;
@@ -72,6 +72,9 @@ namespace
auto session=sWorld->FindClientlessSession(guid);auto player=session?session->GetPlayer():nullptr;
if(!player || !sClientlessPlayerMgr->IsManaged(guid))
{handler->SendSysMessage("Usage: clientless interaction <online-managed-guid>");return false;}
std::string op;input>>op;
if(op=="leave")return sAdventureInteractionMgr->RequestLeave(player);
if(!op.empty())return false;
sAdventureInteractionMgr->Report(handler,player);return true;
}
}
@@ -19,6 +19,7 @@ code=r'''
#include <unordered_map>
#include <algorithm>
#include <ctime>
#include <set>
using uint32=unsigned;using uint8=unsigned char;
#define TC_GAME_API
#define TC_LOG_INFO(...) ((void)0)
@@ -36,11 +37,13 @@ struct Map{float GetHeight(int,float,float,float,bool,float){return 0;}}map;
struct Creature;struct Player;
struct Unit:Position{
ObjectGuid guid;bool valid=true,alive=true,phase=true,visible=true,inWorld=true;
virtual unsigned getLevel(){return 60;}
virtual Creature* ToCreature(){return nullptr;}virtual Player* ToPlayer(){return nullptr;}
virtual bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}virtual ObjectGuid GetGUID(){return guid;}
Position const& GetPosition()const{return *this;}
void GetNearPoint(Player*,float&a,float&b,float&c,float,float){a=x;b=y;c=z;}float GetAngle(Player*){return 0;}
};
struct Creature:Unit{bool controlled=false,evading=false,boss=false;unsigned level=60;std::string script="mob";Creature(unsigned n){guid={n};}ObjectGuid GetGUID(){return guid;}Creature* ToCreature()override{return this;}bool GetControllingPlayer(){return controlled;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsEvadingAttacks(){return evading;}bool isWorldBoss(){return boss;}unsigned getLevel(){return level;}std::string GetScriptName(){return script;}};
struct Creature:Unit{bool controlled=false,evading=false,boss=false,elite=false;bool isElite(){return elite;}unsigned level=60;std::string script="mob";Creature(unsigned n){guid={n};}ObjectGuid GetGUID(){return guid;}Creature* ToCreature()override{return this;}bool GetControllingPlayer(){return controlled;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsEvadingAttacks(){return evading;}bool isWorldBoss(){return boss;}unsigned getLevel(){return level;}std::string GetScriptName(){return script;}};
struct Player;
struct Corpse:Position{
time_t released=100;int saves=0;ObjectGuid GetGUID(){return {700};}time_t GetGhostTime(){return released;}int GetType(){return 0;}
@@ -48,13 +51,22 @@ struct Corpse:Position{
}corpse;
struct WorldPacket{int opcode;WorldPacket(int o,int):opcode(o){}template<class T>WorldPacket&operator<<(T){return *this;}};
struct Session{Player* owner=nullptr;int releases=0,reclaims=0;void HandleRepopRequestOpcode(WorldPacket&);void HandleReclaimCorpseOpcode(WorldPacket&);};
std::vector<Creature*> escapeCreatures;
struct Group{ObjectGuid leader{26};ObjectGuid GetLeaderGUID(){return leader;}}group;
struct Player:Unit{
bool grouped=false;float health=100;std::set<Unit*> attackers;
bool alive=true,ghost=false,teleport=false,combat=false,melee=true,los=true;unsigned delay=30;int saves=0,revives=0,bones=0,graveyards=0;bool sickness=false;int durability=75;
Session session;Player(){session.owner=this;guid={26};}ObjectGuid GetGUID(){return guid;}Player* ToPlayer()override{return this;}bool GetGroup(){return false;}bool IsAlive(){return alive;}bool IsInWorld(){return true;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool GetTransport(){return false;}bool IsInFlight(){return false;}bool IsCharmed(){return false;}
Session session;Player(){session.owner=this;guid={26};}ObjectGuid GetGUID(){return guid;}Player* ToPlayer()override{return this;}Group* GetGroup(){return grouped?&group:nullptr;}bool IsAlive(){return alive;}bool IsInWorld(){return true;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool GetTransport(){return false;}bool IsInFlight(){return false;}bool IsCharmed(){return false;}
unsigned GetMapId(){return 0;}unsigned GetInstanceId(){return 0;}bool InBattleground(){return false;}bool InArena(){return false;}bool HasFlag(int,int){return ghost;}Session*GetSession(){return &session;}Corpse*GetCorpse(){return &corpse;}
uint32 GetCorpseReclaimDelay(bool){return delay;}Map*GetMap(){return &map;}int GetPhaseShift(){return 0;}
bool IsInPhase(Unit*c){return c->phase;}bool CanSeeOrDetect(Unit*c){return c->visible;}bool IsValidAttackTarget(Unit*c){return c->valid;}unsigned getLevel(){return 60;}bool IsWithinMeleeRange(Unit*){return melee;}float GetCombatReach(){return 1;}float GetAngle(Player*){return 0;}
void SaveToDB(){++saves;}void RepopAtGraveyard(){++graveyards;teleport=true;Relocate(-100,0,0);}void ResurrectPlayer(float hp,bool sick){assert(hp==0.5f);sickness=sick;alive=true;ghost=false;++revives;}void SpawnCorpseBones(){++bones;}
bool IsHostileTo(Unit*){return true;}float GetHealthPct(){return health;}
auto const& getAttackers(){return attackers;}
void GetCreatureListWithEntryInGrid(std::vector<Creature*>&v,int,float){v=escapeCreatures;}
void GetPlayerListInGrid(std::vector<Player*>&,float){}
void AttackStop(){}void CastStop(){}void SetStandState(int){}
struct Motion{void Clear(){}}motion;Motion*GetMotionMaster(){return &motion;}
void StopMoving(){}
};
bool Corpse::IsWithinDistInMap(Player*p,float r,bool){return GetExactDist(p)<=r;}
@@ -72,11 +84,14 @@ struct Travel{
TravelStatus status;bool present=false,path=true,nearOnly=false;int starts=0,analyses=0;
auto GetStatus(uint32)->TravelStatus*{return present?&status:nullptr;}
void CancelTravel(uint32){present=false;}
bool AnalyzePath(Player*,Position const&end,ClientlessPathDiagnostics& d){++analyses;d.PathLength=std::fabs(end.x+100);return path && (!nearOnly || end.x<-15);}
bool AnalyzePath(Player*p,Position const&end,ClientlessPathDiagnostics& d){++analyses;d.PathLength=p->GetExactDist(end);return path && (!nearOnly || end.x<-15);}
bool StartTravel(Player*,Position const&end){++starts;present=true;status.State=ClientlessTravelState::Moving;status.Target=end;return true;}
}travel;auto sClientlessTravelMgr=&travel;
struct Behavior{int interrupts=0;void InterruptForCombat(Player*){++interrupts;}}behavior;auto sAdventureBehaviorMgr=&behavior;
'''
code+=r'''
struct Lua{void DismountGroundMount(Player*,char const*){}}lua;auto sClientlessLuaEngine=&lua;
'''
# production Update uses direct Position members and creature near point.
code=code.replace('float x=0,y=0,z=0;','union{float x=0;float m_positionX;};union{float y=0;float m_positionY;};union{float z=0;float m_positionZ;};',1)
code=code.replace('bool GetControllingPlayer(){return controlled;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsEvadingAttacks(){return evading;}bool isWorldBoss(){return boss;}unsigned getLevel(){return level;}std::string GetScriptName(){return script;}};', 'bool GetControllingPlayer(){return controlled;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsEvadingAttacks(){return evading;}bool isWorldBoss(){return boss;}unsigned getLevel(){return level;}std::string GetScriptName(){return script;}float GetAngle(Player*){return 0;}void GetNearPoint(Player*,float&a,float&b,float&c,float,float){a=x;b=y;c=z;}};')
@@ -86,11 +101,12 @@ code+=header+r'''
class AdventureCombatMgr::Fighter{public:int ticks=0;void Tick(Unit*,uint32){++ticks;}};
bool AdventureCombatMgr::Reachable(Player*p,Unit*)const{return p->los;}
Unit* AdventureCombatMgr::PvPTarget(Player*)const{return nullptr;}
Creature* AdventureCombatMgr::Proactive(Player*)const{return nullptr;}
bool AdventureCombatMgr::Start(Player*,Record&r,Unit*c,bool train,bool defense,time_t until){r.AI=std::make_shared<Fighter>();r.Target=c->GetGUID();r.Training=train;r.Defending=defense;r.Until=until;return true;}
void AdventureCombatMgr::Finish(Player*,Record&r){r.AI.reset();r.Target.Clear();r.Defending=false;r.Training=false;r.Cooldown=10000;}
'''
for name in ('bool AdventureCombatMgr::Valid(', 'void AdventureCombatMgr::DamageTaken(', 'bool AdventureCombatMgr::Death(', 'bool AdventureCombatMgr::Update('):code+='\n'+function(name)
for name in ('bool AdventureCombatMgr::Valid(', 'void AdventureCombatMgr::DamageTaken(', 'bool AdventureCombatMgr::Flee(', 'bool AdventureCombatMgr::Death(', 'bool AdventureCombatMgr::Update('):code+='\n'+function(name)
code+=r'''
int main(){
using namespace AdventureCombatPolicy;
@@ -147,10 +163,41 @@ int main(){
AdventureCombatMgr::Record resumed;resumed.Dead=true;GameTime::now=400;mgr.Death(&p,resumed);
assert(p.graveyards==1 && p.teleport && !p.alive && resumed.CemeteryPending);
p.teleport=false;mgr.Death(&p,resumed);assert(p.alive && !p.sickness && p.revives==1 && p.bones==1 && p.durability==75);
// Flight boundaries, actual threats, group override and blocked fallback.
assert(!ShouldFlee(false,20,60,65,false,false));
assert(ShouldFlee(false,19,60,60,false,false));
assert(ShouldFlee(false,100,60,66,false,false));
assert(ShouldFlee(false,100,60,60,true,false));
assert(!ShouldFlee(false,100,60,59,true,false));
assert(!ShouldFlee(true,1,60,90,true,true));
p.alive=true;p.ghost=false;p.Relocate(0,0,0);p.combat=true;p.health=100;
r={};a.alive=true;a.valid=true;a.level=66;a.Relocate(1,0,0);p.attackers={&a};travel={};GameTime::now=1000;
mgr.Start(&p,r,&a,false,true,0);
assert(mgr.Flee(&p,r)&&r.Fleeing&&travel.starts==1);
assert(travel.status.Target.GetExactDist(a)>p.GetExactDist(a)+3);
// Reject the direct escape corridor if an additional hostile blocks it.
Creature obstacle(100);obstacle.Relocate(-15,0,0);escapeCreatures={&obstacle};
GameTime::now=1002;mgr.Flee(&p,r);
assert(SegmentDistance(-15,0,0,0,0,0,travel.status.Target.x,travel.status.Target.y,travel.status.Target.z)>=12);
escapeCreatures.clear();
// Group join aborts immediately, even while critically injured.
p.grouped=true;p.health=1;assert(!mgr.Flee(&p,r)&&!r.Fleeing&&!travel.present);
assert(!mgr.Flee(&p,r));p.grouped=false;p.health=100;
// Blocked paths preserve native self-defence; no teleport/resurrection.
travel={};travel.path=false;GameTime::now=1004;
int ticks=r.AI->ticks;assert(mgr.Flee(&p,r)&&r.Fleeing&&travel.starts==0&&r.AI->ticks>ticks);
int oldRevives=p.revives,oldGraves=p.graveyards;
// Ending native combat alone is insufficient while still close to danger.
p.combat=false;p.attackers.clear();r.Defending=false;
GameTime::now=1006;assert(mgr.Flee(&p,r)&&r.Fleeing);
p.Relocate(-60,0,0);GameTime::now=1008;assert(mgr.Flee(&p,r)&&!r.Fleeing&&!r.AI);
assert(p.revives==oldRevives&&p.graveyards==oldGraves);
// No aggression means no flight solely from low health.
r={};p.health=1;assert(!mgr.Flee(&p,r));
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['g++','-std=c++17','-I'+str(root/'src/server/game/World'),str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Production combat focus, damage ordering, risk filters, corpse radius/delay, alternate paths and malus-free persisted fallback passed.')
print('Production combat focus, damage ordering, risk filters, corpse radius/delay, flight/group override, blocked escape, danger avoidance and malus-free persisted fallback passed.')
@@ -0,0 +1,62 @@
"""Execute production leader-plan inheritance without modifying members' own days."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text()
sig='std::string AdventureBehaviorMgr::InteractionSchedule('
start=s.index(sig);begin=s.index('{',start);end=begin+1;depth=1
while depth:depth+=(s[end]=='{')-(s[end]=='}');end+=1
schedule=s[start:end]
a=s.index('AdventureGroupPlan AdventureBehaviorMgr::GroupPlan(')
code=r'''
#include "AdventureBehaviorPolicy.h"
#include "AdventureInstancePolicy.h"
#include <cassert>
#include <ctime>
#include <unordered_map>
using uint32=unsigned;
struct Guid{unsigned id;unsigned GetCounter(){return id;}};
struct Player{Guid guid;Guid GetGUID(){return guid;}};
struct AdventureGroupPlan{bool Enabled=false,Forming=false;std::string Intent,Location,NextIntent,NextLocation;time_t Deadline=0;};
namespace GameTime{time_t GetGameTime(){return 100;}}
std::tm Local(time_t){std::tm tm{};tm.tm_hour=5;return tm;}unsigned DateOf(std::tm){return 1;}
struct AdventureBehaviorMgr{
struct Record{bool Enabled=true,Schedule=true,GroupOverride=false,Dirty=false;int Hour=-1;unsigned Date=1;std::string Intent,Location,WantedIntent,WantedLocation,ManualNextIntent,ManualNextLocation;std::string PersonalIntent,PersonalLocation;time_t PersonalDeadline=0;time_t Deadline=3600;AdventureBehaviorPolicy::Day Plan;AdventureInstancePolicy::Visit Instance;};
std::unordered_map<unsigned,Record>_records;int changes=0;
void Generate(Player*,Record&r,unsigned date){r.Date=date;}
void SetWanted(Player*,Record&r,std::string const&i,std::string const&l,time_t end){++changes;r.WantedIntent=i;r.WantedLocation=l;r.Deadline=end;}
std::string InteractionSchedule(Player*);void ResumePersonalPlan(Player*);AdventureGroupPlan GroupPlan(Player*);bool AdoptGroup(Player*,Player*);
};
'''+schedule+'\n'+s[a:]+r'''
int main(){
AdventureBehaviorMgr mgr;Player leader{{29}},member{{31}};auto& a=mgr._records[29];auto& b=mgr._records[31];
a.Intent="groupformation";a.Location="leader-capital";a.Plan[5]={"groupformation","leader-capital"};a.Plan[6]={"instance","deadmines"};
b.Intent="groupformation";b.Location="other-capital";b.Plan[5]={"groupformation","other-capital"};b.Plan[6]={"instance","shadowfang"};
auto plan=mgr.GroupPlan(&member);assert(plan.Forming&&plan.NextIntent=="instance"&&plan.NextLocation=="shadowfang");
assert(mgr.AdoptGroup(&member,&leader));assert(b.WantedLocation=="leader-capital"&&b.Deadline==a.Deadline&&b.Schedule);
assert(b.Plan[6].Location=="shadowfang"); // own future plan remains available after departure
a.Intent="instance";a.Location="deadmines";a.Deadline=7200;
assert(mgr.AdoptGroup(&member,&leader));assert(b.WantedIntent=="instance"&&b.WantedLocation=="deadmines"&&b.Deadline==7200);
b.Instance.Active=true;b.Instance.Stage=AdventureInstancePolicy::Phase::Inside;a.Location="other";
int changes=mgr.changes;assert(mgr.AdoptGroup(&member,&leader)&&mgr.changes==changes);
b.Instance.Stage=AdventureInstancePolicy::Phase::Travel;mgr.AdoptGroup(&member,&leader);assert(b.WantedLocation=="other");
a.Enabled=false;assert(!mgr.AdoptGroup(&member,&leader));
b.Instance.Active=false;mgr.ResumePersonalPlan(&member);
assert(!b.GroupOverride&&b.WantedIntent=="groupformation"&&b.WantedLocation=="other-capital");
b.Schedule=false;b.Intent="groupformation";b.ManualNextIntent="raid";b.ManualNextLocation="molten-core";
plan=mgr.GroupPlan(&member);assert(plan.Forming&&plan.NextIntent=="raid"&&plan.NextLocation=="molten-core");
// Different goals are inherited without inventing a personal intent change.
b.GroupOverride=false;b.Instance.Active=false;b.Intent=b.WantedIntent="social";
b.Location=b.WantedLocation="personal-place";b.Deadline=2000;
a.Enabled=true;a.Intent="social";a.Location="leader-place";
assert(mgr.AdoptGroup(&member,&leader));b.Location="leader-place";
assert(mgr.InteractionSchedule(&member)=="social:personal-place");
mgr.ResumePersonalPlan(&member);
assert(b.WantedLocation=="personal-place"&&mgr.InteractionSchedule(&member)=="social:personal-place");
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'plan.cpp').write_text(code)
subprocess.run(['g++','-std=c++17','-I'+str(root/'src/server/game/World'),str(p/'plan.cpp'),'-o',str(p/'plan')],check=True)
subprocess.run([str(p/'plan')],check=True)
print('Production group plans: leader meeting/goal/deadline inheritance, untouched personal day, manual preparation and instance-state guards passed.')
+121
View File
@@ -0,0 +1,121 @@
"""Execute production bot recruitment with native-party/channel adapters."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/AdventureGroupMgr.cpp').read_text()
def fun(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]
header=(root/'src/server/game/World/AdventureGroupMgr.h').read_text()
header='\n'.join(l for l in header.splitlines() if not l.startswith('#include')).replace('private:','public:')
code=r'''
#include "AdventureGroupPolicy.h"
#include "AdventureBehaviorPolicy.h"
#include <algorithm>
#include <cassert>
#include <ctime>
#include <string>
#include <vector>
#include <unordered_map>
#include <unordered_set>
using uint32=unsigned;
#define TC_GAME_API
#define TC_LOG_INFO(...) ((void)0)
struct ObjectGuid{unsigned id=0;unsigned GetCounter()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 Player;struct ChatHandler;
struct Map{bool IsDungeon(){return false;}}map;
struct Member{Player*player=nullptr;Member*nextMember=nullptr;Player*GetSource(){return player;}Member*next(){return nextMember;}};
constexpr int GROUP_REMOVEMETHOD_LEAVE=1;
struct Group{struct Slot{ObjectGuid guid;};std::vector<Slot>slots;auto const&GetMemberSlots(){return slots;}void RemoveMember(ObjectGuid,int);Member*members=nullptr;Member*GetFirstMember(){return members;}void ChangeLeader(ObjectGuid g){leader=g;}void SendUpdate(){}ObjectGuid leader;unsigned count=2;bool raid=false;ObjectGuid GetLeaderGUID(){return leader;}unsigned GetMembersCount(){return count;}bool isRaidGroup(){return raid;}void ConvertToRaid(){raid=true;}void ConvertToGroup(){raid=false;}};
struct WorldPacket{unsigned value=0;WorldPacket(int,int=0){}WorldPacket&operator<<(unsigned n){value=n;return *this;}};constexpr int MSG_SET_RAID_DIFFICULTY=2,RAID_DIFFICULTY_25MAN_NORMAL=1;constexpr int CMSG_PARTY_INVITE=1,EMOTE_ONESHOT_WAVE=1;
namespace WorldPackets{namespace Party{struct PartyInviteClient{PartyInviteClient(WorldPacket&&){}std::string TargetName;ObjectGuid TargetGUID;};}}
struct Session{Player*owner;bool clientless=true;bool IsClientless(){return clientless;}void HandlePartyInviteOpcode(WorldPackets::Party::PartyInviteClient&);void HandleSetRaidDifficultyOpcode(WorldPacket&);};
struct Player{ObjectGuid guid;Group*group=nullptr,*invite=nullptr;Session session{this};bool combat=false;unsigned zone=1519,level=60,team=0;float distance=10;
unsigned difficulty=0;unsigned GetRaidDifficulty(){return difficulty;}ObjectGuid GetGUID(){return guid;}bool IsInWorld(){return true;}bool IsAlive(){return true;}bool IsInCombat(){return combat;}
bool IsBeingTeleported(){return false;}bool GetTransport(){return false;}bool IsFlying(){return false;}bool IsInFlight(){return false;}bool IsCharmed(){return false;}Map*GetMap(){return &map;}
unsigned GetZoneId(){return zone;}unsigned getLevel(){return level;}unsigned GetTeam(){return team;}
Group*GetGroup(){return group;}Group*GetGroupInvite(){return invite;}Session*GetSession(){return &session;}std::string GetName(){return std::to_string(guid.id);}
bool IsInMap(Player*o){return zone==o->zone;}bool IsInPhase(Player*){return true;}bool CanSeeOrDetect(Player*){return true;}
float GetExactDist(Player*){return distance;}bool IsWithinLOSInMap(Player*){return true;}
void SetFacingToObject(Player*){}void HandleEmoteCommand(int){}
};
namespace ObjectAccessor{std::unordered_map<ObjectGuid,Player*> players;Player*FindPlayer(ObjectGuid g){auto it=players.find(g);return it==players.end()?nullptr:it->second;}}
void Session::HandleSetRaidDifficultyOpcode(WorldPacket&p){owner->difficulty=p.value;}
void Group::RemoveMember(ObjectGuid g,int){--count;ObjectAccessor::FindPlayer(g)->group=nullptr;}
Group pending;int invites=0;
void Session::HandlePartyInviteOpcode(WorldPackets::Party::PartyInviteClient&p){++invites;pending.leader=owner->guid;ObjectAccessor::FindPlayer(p.TargetGUID)->invite=owner->group?owner->group:&pending;}
struct Profile{std::unordered_map<std::string,float> Personality{{"sociability",1}};std::unordered_set<std::string>PrimaryProfessions{"skinning"};}profile;
struct Lua{Profile*GetAdventureProfile(unsigned){return &profile;}}lua;auto sClientlessLuaEngine=&lua;
struct Combat{bool Busy(Player*){return false;}}combat;auto sAdventureCombatMgr=&combat;
namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}}
float frand(float,float){return 0;}unsigned urand(unsigned a,unsigned){return a;}
'''+header+r'''
struct Location{std::string Name="Stormwind";}location;struct Locations{Location*Find(std::string const&){return &location;}}locations;auto sAdventureLocationMgr=&locations;
struct Behavior{std::unordered_map<unsigned,AdventureGroupPlan> plans;AdventureGroupPlan GroupPlan(Player*p){return plans[p->guid.id];}}behavior;auto sAdventureBehaviorMgr=&behavior;
int messages=0;
void AdventureGroupMgr::Speak(Player*,std::string const&){++messages;}
'''
code+=fun('float Sociability(')+'\n'+fun('bool Ready(')+'\n'+fun('std::string Activity(')
for name in ['bool AdventureGroupMgr::BotLed(', 'bool AdventureGroupMgr::Forming(', 'bool AdventureGroupMgr::PlannedTransition(', 'bool AdventureGroupMgr::Accept(', 'void AdventureGroupMgr::Update(', 'void AdventureGroupMgr::Invite(', 'void AdventureGroupMgr::RequestPlayerAdmission(', 'bool AdventureGroupMgr::PlayerLeadership(', 'bool AdventureGroupMgr::EnforceCapacity(']:code+='\n'+fun(name)
code+=r'''
int main(){
using namespace AdventureGroupPolicy;
AdventureBehaviorPolicy::Day day;std::mt19937 random(42);
for(unsigned i=0;i<24;++i)day[i]={i%2?"instance":"social",std::to_string(i)};
Prepare(day,random,1,[]{return std::string("capital");});
for(unsigned h=0;h<24;++h)if(day[h].Intent=="groupformation")assert(h<23&&Eligible(day[h+1].Intent)&&day[h].Location=="capital");
assert(Capacity("instance")==5&&Capacity("raid")==20&&Capacity("social")>5);
assert(Compatible("instance","instance",60,65)&&!Compatible("instance","raid",60,60));
assert(!Compatible("raid","raid",60,66));
AdventureGroupMgr mgr;Player a,b;a.guid={29};b.guid={31};b.zone=1537;b.distance=10000;
ObjectAccessor::players={{a.guid,&a},{b.guid,&b}};
behavior.plans[29]={true,true,"groupformation","stormwind","instance","deadmines",3600};
behavior.plans[31]={true,true,"groupformation","ironforge","instance","shadowfang",3600};
mgr._records[a.guid]={};mgr._records[b.guid]={};
// Remote capital recruitment accepts different original targets/meeting points.
mgr.Update(&a);assert(invites==1&&b.invite&&b.invite->leader==a.guid&&messages>=1);
assert(mgr.Accept(&b,&a));
behavior.plans[31].NextIntent="raid";assert(!mgr.Accept(&b,&a));behavior.plans[31].NextIntent="instance";
b.team=1;assert(!mgr.Accept(&b,&a));b.team=0;
b.combat=true;assert(!mgr.Accept(&b,&a));b.combat=false;
// Native party cap and pending reservations prevent sixth members.
Group party;party.leader=a.guid;party.count=5;a.group=&party;
assert(!mgr.Accept(&b,&a));b.invite=nullptr;mgr._records[b.guid].InviteUntil=0;
mgr._records[a.guid].SearchAt=0;mgr.Update(&a);assert(invites==1);
// Existing group recruits and lone bot requests membership without replacing leader.
party.count=3;mgr._records[a.guid].Partners.clear();mgr._records[b.guid].Partners.clear();
mgr._records[b.guid].SearchAt=0;mgr.Update(&b);assert(invites==2&&b.invite==&party&&party.leader==a.guid);
assert(mgr.Accept(&b,&a));
// Intent-hour transition is exempt exactly once, no permanent exit exemption.
b.group=&party;behavior.plans[31].Intent="instance";behavior.plans[31].Forming=false;
assert(mgr.PlannedTransition(&b));assert(!mgr.PlannedTransition(&b));
// Raid native conversion and twentieth-member boundary.
b.group=nullptr;b.invite=nullptr;a.group=&party;party.count=19;behavior.plans[29].NextIntent="raid";
behavior.plans[31]={true,true,"groupformation","ironforge","raid","otherraid",3600};
mgr._records[a.guid].SearchAt=0;mgr._records[a.guid].Partners.clear();mgr._records[b.guid].Partners.clear();mgr._records[b.guid].InviteUntil=0;
mgr.Update(&a);assert(a.difficulty==RAID_DIFFICULTY_25MAN_NORMAL&&party.raid&&invites==3&&mgr.Accept(&b,&a));
party.count=20;assert(!mgr.Accept(&b,&a));
// Solo recruitment outside formation requires actual local contact.
a.group=nullptr;b.invite=nullptr;behavior.plans[29]={true,false,"social","city",{}, {},3600};behavior.plans[31]=behavior.plans[29];
mgr._records.clear();mgr._records[a.guid]={};mgr._records[b.guid]={};mgr.Update(&a);assert(invites==3);
b.zone=a.zone;b.distance=10;mgr._records[a.guid].SearchAt=0;mgr.Update(&a);assert(invites==4);
// When a large raid changes to a smaller activity, excess bots leave safely.
party.count=5;party.slots={{{29}},{{40}},{{41}},{{31}},{{42}}};party.leader=a.guid;b.group=&party;
behavior.plans[29].Intent="adventure";assert(mgr.EnforceCapacity(&b)&&!b.group&&party.count==4);
behavior.plans[29].Intent="social";
// Explicit human invite to a grouped bot offers admission, then native leadership.
Player human;human.guid={21};human.session.clientless=false;ObjectAccessor::players[human.guid]=&human;
a.group=&party;party.leader=a.guid;party.count=2;
mgr.RequestPlayerAdmission(&a,&human);assert(invites==5&&human.invite==&party);
human.group=&party;Member humanSlot;humanSlot.player=&human;party.members=&humanSlot;
mgr.Update(&a);assert(party.leader==human.guid&&!mgr.BotLed(&a));
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'groups.cpp').write_text(code)
subprocess.run(['g++','-std=c++17','-I'+str(root/'src/server/game/World'),str(p/'groups.cpp'),'-o',str(p/'groups')],check=True)
subprocess.run([str(p/'groups')],check=True)
print('Production recruitment: capitals, different goals, native inviter leadership, joining existing groups, capacity/reservations, raid conversion, planned transition and local contact passed.')
+2 -2
View File
@@ -101,9 +101,9 @@ int main(){
// Late arrival never enters, nor applies a fake injury.
r=fresh();r.State=AdventureBehaviorMgr::Phase::Travel;int calls=p.entryCalls;
assert(mgr.UpdateInstance(&p,r,3350)&&r.Instance.Stage==Stage::Rest&&p.entryCalls==calls&&p.damages==damages);
// Shared or grouped copies are not accepted for this solo performance.
// Native groups may enter their group copy; unrelated solo shared binds remain rejected.
r=fresh();r.Instance.Stage=Stage::Entering;p.group=true;
assert(mgr.UpdateInstance(&p,r,100)&&p.entryCalls==calls);p.group=false;
assert(mgr.UpdateInstance(&p,r,100)&&p.entryCalls==calls+1);++calls;p.group=false;p.map=0;p.instance=0;p.x=0;
r=fresh();r.Instance.Stage=Stage::Entering;p.bind.save=&save;CharacterDatabase.shared=true;
assert(mgr.UpdateInstance(&p,r,100)&&p.entryCalls==calls);CharacterDatabase.shared=false;p.bind.save=nullptr;
// A denied exit remains pending and cannot wound a player still inside.
@@ -10,6 +10,7 @@ def function(signature):
return s[a:n]
code=r'''
#include "AdventureInstancePolicy.h"
struct MapEntry{bool IsRaid(){return false;}}mapEntry;struct Maps{MapEntry*LookupEntry(unsigned){return &mapEntry;}}sMapStore;
#include <cassert>
#include <memory>
#include <string>
@@ -15,12 +15,13 @@ using uint32=uint32_t;using uint64=uint64_t;
#define TC_LOG_INFO(...) ((void)0)
constexpr int SPELL_VISUAL_KIT_DRINK=438,UNIT_NPC_EMOTESTATE=0;
struct Position {float x=0;void Relocate(void const*){}};
struct Guid{uint32 GetCounter(){return 26;}};
struct Guid{uint32 GetCounter(){return 26;}bool operator!=(Guid)const{return false;}};
struct Group{Guid GetLeaderGUID(){return {};}};
struct Object{} object;
struct Map{Object* GetGameObject(int){return &object;}} map;
struct Player {
bool combat=false;uint32 area=87,zone=12,mapId=0;
Guid GetGUID(){return {};}bool IsFlying(){return false;}bool IsInWorld(){return true;}bool IsInCombat(){return combat;}
Group*GetGroup(){return nullptr;}Guid GetGUID(){return {};}bool IsFlying(){return false;}bool IsInWorld(){return true;}bool IsInCombat(){return combat;}
uint32 GetMapId(){return mapId;}uint32 GetAreaId(){return area;}uint32 GetZoneId(){return zone;}
float GetPositionX(){return 0;}float GetPositionY(){return 0;}float GetPositionZ(){return 0;}
float GetExactDist(Position const&){return 0;}float GetExactDist(float,float,float){return 0;}float GetExactDist(Object*){return 0;}
@@ -28,6 +29,8 @@ struct Player {
bool IsWithinLOSInMap(Object*){return true;}void SetFacingToObject(Object*){}
Map* GetMap(){return &map;}
} player;
namespace ObjectAccessor{Player*FindPlayer(Guid){return nullptr;}}
struct BotGroups{bool BotLed(Player*){return false;}bool FollowMember(Player*){return false;}}botGroups;auto sAdventureGroupMgr=&botGroups;
bool unsafe=false;bool Unsafe(Player*){return unsafe;}
bool busy=false;bool Busy(uint32){return busy;}
time_t now=100;int hour=5;
@@ -51,7 +54,7 @@ public:
struct Record{
bool Enabled=true,Schedule=false,Queued=false,Dirty=false;
Phase State=Phase::Travel;
std::string Intent="social",Location="tavern",WantedIntent="social",WantedLocation="tavern",Detail,Micro;
std::string Intent="social",Location="tavern",WantedIntent="social",WantedLocation="tavern",Detail,Micro,ManualNextIntent,ManualNextLocation;
time_t Deadline=1000;uint32 Date=20261026,Timer=0,MicroTimer=0,GestureTimer=0,RetryTimer=0;int Hour=5;
struct{bool Active=false;}Social;
struct{bool Active=false;}Recovery;
@@ -61,6 +64,7 @@ public:
};
std::unordered_map<uint32,Record> _records{{26,{}}};
int begun=0,fallbacks=0,micros=0,resets=0,switches=0,queues=0,leisureUpdates=0;
bool AdoptGroup(Player*,Player*){return false;}
void Update(Player*,uint32);
void Generate(Player*,Record&,uint32){}
void SetWanted(Player*,Record& r,std::string const&i,std::string const&l,time_t d){++switches;r.WantedIntent=i;r.WantedLocation=l;r.Deadline=d;r.Dirty=true;r.State=Phase::Idle;}
@@ -103,6 +107,11 @@ int main(){
// Leashed companion distance is serviced at world cadence, not the 1 s planning clock.
r.Timer=1000;r.Leisure.Active=true;mgr.Update(&player,50);
assert(mgr.leisureUpdates==1&&r.Timer==950);
// Preparation ends on its deadline without waiting for a full/assembled party.
r.Leisure.Active=false;r.Timer=0;r.Intent="groupformation";r.Deadline=now;
r.ManualNextIntent="social";r.ManualNextLocation="leader-goal";
mgr.Update(&player,1000);
assert(r.WantedIntent=="social"&&r.WantedLocation=="leader-goal"&&r.Deadline==now+3600&&r.ManualNextIntent.empty());
}
'''
with tempfile.TemporaryDirectory(prefix='vcore-adventure-state-') as folder:
@@ -40,9 +40,9 @@ struct Player:Unit {
ObjectGuid guid,selection;bool alive=true,inWorld=true,combat=false,teleport=false,transport=false,flight=false,charmed=false,casting=false,afk=false,visible=true,phase=true,los=true;
unsigned mapid=0,team=ALLIANCE;Session session;Group*group=nullptr,*invite=nullptr;Map map;int gestures=0,says=0,posture=0;uint32 lastEmote=0;
explicit Player(unsigned id=26){guid.id=id;session.owner=this;}
ObjectGuid GetGUID(){return guid;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool GetTransport(){return transport;}bool IsInFlight(){return flight;}bool IsCharmed(){return charmed;}bool HasUnitState(int){return casting;}
ObjectGuid GetGUID(){return guid;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool GetTransport(){return transport;}bool IsFlying(){return false;}bool IsInFlight(){return flight;}bool IsCharmed(){return charmed;}bool HasUnitState(int){return casting;}
bool IsInMap(Player*p){return mapid==p->mapid;}bool IsInPhase(Player*p){return phase && p->phase;}bool CanSeeOrDetect(Player*p){return p->visible;}bool IsWithinLOSInMap(Player*p){return los && p->los;}bool IsFriendlyTo(Player*p){return team==p->team;}
Map*GetMap(){return &map;}unsigned GetMapId(){return mapid;}Session*GetSession(){return &session;}Group*GetGroup(){return group;}Group*GetGroupInvite(){return invite;}unsigned GetTeam(){return team;}ObjectGuid GetTarget(){return selection;}bool isAFK(){return afk;}
Map*GetMap(){return &map;}unsigned GetMapId(){return mapid;}Session*GetSession(){return &session;}Group*GetGroup(){return group;}void UninviteFromGroup(){invite=nullptr;}Group*GetGroupInvite(){return invite;}unsigned GetTeam(){return team;}ObjectGuid GetTarget(){return selection;}bool isAFK(){return afk;}
void SetStandState(int s){posture=s;}void SetFacingToObject(Player*){}void HandleEmoteCommand(uint32 e){++gestures;lastEmote=e;}void Say(char const*,int,Player*){++says;}
void GetNearPoint(Player*,float&x,float&y,float&z,float,float){x=m_positionX;y=m_positionY;z=m_positionZ;}
std::vector<Player*> people;void GetPlayerListInGrid(std::vector<Player*>&out,float){out=people;}
@@ -56,7 +56,7 @@ unsigned urand(unsigned a,unsigned){return a;}float roll=0;float frand(float,flo
namespace ObjectAccessor {std::unordered_map<ObjectGuid,Player*> players;Player*FindPlayer(ObjectGuid id){auto it=players.find(id);return it==players.end()?nullptr:it->second;}}
struct Profile {std::string Locale="deDE";std::unordered_map<std::string,float> Personality;};
struct Lua {std::unordered_map<unsigned,Profile> profiles;Profile const*GetAdventureProfile(unsigned id){auto it=profiles.find(id);return it==profiles.end()?nullptr:&it->second;}}lua;auto sClientlessLuaEngine=&lua;
struct Behavior {int interrupts=0;std::string intent="social",schedule="social:inn";std::string InteractionIntent(Player*){return intent;}std::string InteractionSchedule(Player*){return schedule;}void InterruptForInteraction(Player*){++interrupts;}}behavior;auto sAdventureBehaviorMgr=&behavior;
struct Behavior {int interrupts=0;std::string intent="social",schedule="social:inn";std::string InteractionIntent(Player*){return intent;}std::string InteractionSchedule(Player*){return schedule;}void ResumePersonalPlan(Player*){}void InterruptForInteraction(Player*){++interrupts;}}behavior;auto sAdventureBehaviorMgr=&behavior;
struct Combat {bool busy=false;int assists=0;bool Busy(Player*){return busy;}bool Assist(Player*,Unit*){++assists;return true;}}combat;auto sAdventureCombatMgr=&combat;
struct ClientlessAction{std::string Name;std::unordered_map<std::string,std::string> Parameters;};
struct Core {time_t activity=0;int queued=0;time_t LastActivity(ObjectGuid){return activity;}bool QueueAction(unsigned,ClientlessAction){++queued;return true;}}core;auto sClientlessPlayerMgr=&core;
@@ -66,6 +66,7 @@ struct Travel {Nav nav;bool active=false,valid=true;int starts=0,cancels=0;Nav*G
enum class ClientlessActionExecutionState{Moving,SelectingTarget,Completed};struct Execution {ClientlessActionExecutionState State=ClientlessActionExecutionState::Completed;};
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;
'''+header+'\n'+source+r'''
int main(){
using namespace AdventureInteractionPolicy;
@@ -102,12 +103,26 @@ int main(){
// Bot encounters reserve both participants; cooldown prevents new loops.
other.team=ALLIANCE;bot.people={&other};mgr._records.clear();tick(2200);assert(mgr._records[26].Partner==other.guid && mgr._records[27].Partner==bot.guid);
tick(2201);GameTime::now=2203;mgr.Update(&other,1000);assert(other.gestures==1);tick(2205);GameTime::now=2205;mgr.Update(&other,1000);tick(2265);assert(!mgr._records[26].Partner);
// Bot-origin invitations are declined; autonomous bot grouping is deferred.
// A native provisional leader also holds an invite; never answer his own invitation.
mgr._records.clear();group.leader=bot.guid;bot.invite=&group;tick(2270);tick(2275);
assert(bot.invite==&group);bot.invite=nullptr;
// 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);
// 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.
bot.group=&group;group.leader=other.guid;botGroups.botLed=true;mgr._records.clear();
GameTime::now=3000;int beforeInterrupts=behavior.interrupts;mgr.Update(&bot,1000);
assert(behavior.interrupts==beforeInterrupts+1);
// Bot-led groups must let the behavior clock advance every world tick.
bot.group=&group;group.leader=bot.guid;botGroups.botLed=true;
auto& rec=mgr._records[26];rec.Grouped=true;rec.SceneUntil=0;rec.Timer=1000;
assert(!mgr.Update(&bot,50)&&rec.Timer==950);botGroups.botLed=false;
// Explicit diagnostic leave uses ordinary safe-state guards.
bot.combat=true;assert(!mgr.RequestLeave(&bot)&&bot.group);bot.combat=false;
assert(mgr.RequestLeave(&bot)&&!bot.group);
mgr.Offline(&bot);assert(!mgr._records.count(26));
}
'''
@@ -10,6 +10,7 @@ def function(text,sig):
return text[a:e]
code=r'''
#include "AdventureEconomyPolicy.h"
#include "AdventureGroupPolicy.h"
#include <cassert>
#include <cstdlib>
#include <cstdint>
@@ -26,7 +27,9 @@ struct Field{
'''
code+=function(field,'uint8 Field::GetUInt8() const')+'\n'+function(field,'uint16 Field::GetUInt16() const')+'\n'+function(field,'uint32 Field::GetUInt32() const')
code+=r'''
struct AdventureLocation{struct{uint32 MapId=0;std::string Category;}Point;bool IsArea=false;};
struct AdventureLocation{struct{uint32 MapId=0;std::string Category;}Point;bool IsArea=false;uint32 InstanceMap=0,ZoneId=0,AreaId=0;};
struct MapEntry{bool raid=false;bool IsDungeon(){return true;}bool IsRaid(){return raid;}}dungeon,raid{true};struct Maps{MapEntry*LookupEntry(unsigned id){return id==36?&dungeon:(id==409||id==999)?&raid:nullptr;}}sMapStore;
using Difficulty=unsigned;constexpr unsigned RAID_DIFFICULTY_25MAN_NORMAL=1;struct MapDifficulty{unsigned maxPlayers=40;}largeRaid,smallRaid{10};struct DBC{MapDifficulty*GetDownscaledMapDifficultyData(unsigned id,Difficulty&){return id==999?&smallRaid:&largeRaid;}}sDBCManager;
struct AdventureLocationMgr{std::unordered_map<std::string,std::unordered_map<std::string,float>> _types;float Weight(AdventureLocation const&,std::string const&)const;};
'''
code+=function(source,'float AdventureLocationMgr::Weight(')
@@ -56,6 +59,14 @@ int main(){
for(auto category:{"zone","settlement","tavern","bank","service","landmark","wilderness","mine","workshop"}){
place.Point.Category=category;assert(mgr.Weight(place,"economy")==0);mgr._types[category]["economy"]=4;assert(mgr.Weight(place,"economy")==4);assert(mgr.Weight(place,"social")==0);
}
place.Point.Category="instance_entrance";mgr._types[place.Point.Category]["instance"]=5;mgr._types[place.Point.Category]["raid"]=5;
place.InstanceMap=36;assert(mgr.Weight(place,"instance")==5&&mgr.Weight(place,"raid")==0);
place.InstanceMap=409;assert(mgr.Weight(place,"instance")==0&&mgr.Weight(place,"raid")==5);
place.InstanceMap=999;assert(mgr.Weight(place,"raid")==0);
place.InstanceMap=0;assert(mgr.Weight(place,"instance")==0&&mgr.Weight(place,"raid")==0);
place.Point.Category="landmark";mgr._types["landmark"]["groupformation"]=1;
place.ZoneId=12;assert(mgr.Weight(place,"groupformation")==0);
place.ZoneId=1519;assert(mgr.Weight(place,"groupformation")==1);
// Edited weights, including an explicit zero, override economy defaults.
place.Point.Category="settlement";mgr._types["settlement"]["economy"]=0;
assert(mgr.Weight(place,"economy")==0);mgr._types["settlement"]["economy"]=2.75;
+67
View File
@@ -0,0 +1,67 @@
"""Run the production PvP selector against faction, legality and navigation fixtures."""
from pathlib import Path
import subprocess, tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/AdventureCombatMgr.cpp').read_text()
a=s.index('Unit* AdventureCombatMgr::PvPTarget');b=s.index('Creature* AdventureCombatMgr::Proactive',a)
code=r'''
#include "AdventureCombatPolicy.h"
#include <cassert>
#include <vector>
#include <unordered_map>
#include <string>
constexpr int ALLIANCE=0,POWER_MANA=0,FACTION_GROUP_MASK_HORDE=4,FACTION_GROUP_MASK_ALLIANCE=2;
struct Player;struct Creature;
struct Faction{unsigned FactionGroup=4;};
struct Guid{int GetCounter(){return 29;}};
struct Unit{unsigned level=60;float distance=10;bool valid=true,hostile=true,reachable=true;Faction faction;
virtual Player* ToPlayer(){return nullptr;}virtual Creature* ToCreature(){return nullptr;}
unsigned getLevel(){return level;}Faction*GetFactionTemplateEntry(){return &faction;}};
struct Creature:Unit{bool controlled=false,elite=false,boss=false;Creature*ToCreature()override{return this;}
bool GetControllingPlayer(){return controlled;}bool isElite(){return elite;}bool isWorldBoss(){return boss;}};
std::vector<Player*> players;std::vector<Creature*> creatures;
struct Player:Unit{int team=1,powerType=1;float health=100;unsigned mana=100;
Player*ToPlayer()override{return this;}int GetTeam(){return team;}Guid GetGUID(){return {};}
float GetHealthPct(){return health;}int GetPowerType(){return powerType;}unsigned GetMaxPower(int){return 100;}unsigned GetPower(int){return mana;}
bool IsHostileTo(Unit*t){return t->hostile;}float GetExactDist(Unit*t){return t->distance;}
void GetPlayerListInGrid(std::vector<Player*>&v,float){v=players;}void GetCreatureListWithEntryInGrid(std::vector<Creature*>&v,int,float){v=creatures;}};
struct Profile{std::unordered_map<std::string,float>Personality{{"adventurousness",1},{"danger_tolerance",1}};}profile;
struct Lua{Profile*GetAdventureProfile(int){return &profile;}}lua;
struct Behavior{std::string intent="adventure";std::string CombatIntent(Player*){return intent;}}behavior;
auto sClientlessLuaEngine=&lua;auto sAdventureBehaviorMgr=&behavior;
float roll=0;float frand(float,float){return roll;}
struct AdventureCombatMgr{bool Valid(Player*,Unit*t,bool)const{return t->valid;}bool Reachable(Player*,Unit*t)const{return t->reachable;}
Unit*PvPTarget(Player*)const;};
'''+s[a:b]+r'''
int main(){
using namespace AdventureCombatPolicy;
assert(PvPLevel(60,55)&&PvPLevel(60,65)&&!PvPLevel(60,54)&&!PvPLevel(60,66));
assert(PvPLevel(1,6)&&!PvPLevel(1,7));
assert(PvPWillingness("rest",1,1)==0&&PvPWillingness("",1,1)==0);
assert(PvPWillingness("adventure",0,1)==0);
assert(PvPWillingness("adventure",1,1)>PvPWillingness("economy",1,1));
Player bot,enemy;bot.team=ALLIANCE;players={&enemy};AdventureCombatMgr mgr;
assert(mgr.PvPTarget(&bot)==&enemy);
for(unsigned level:{54u,66u}){enemy.level=level;assert(!mgr.PvPTarget(&bot));}enemy.level=55;
assert(mgr.PvPTarget(&bot)==&enemy);enemy.level=65;assert(mgr.PvPTarget(&bot)==&enemy);
enemy.distance=50;assert(mgr.PvPTarget(&bot)==&enemy);enemy.distance=50.1;assert(!mgr.PvPTarget(&bot));enemy.distance=10;
enemy.team=ALLIANCE;assert(!mgr.PvPTarget(&bot));enemy.team=1;
enemy.valid=false;assert(!mgr.PvPTarget(&bot));enemy.valid=true;
enemy.reachable=false;assert(!mgr.PvPTarget(&bot));enemy.reachable=true;
enemy.hostile=false;assert(!mgr.PvPTarget(&bot));enemy.hostile=true;
behavior.intent="rest";assert(!mgr.PvPTarget(&bot));behavior.intent="adventure";
bot.health=59;assert(!mgr.PvPTarget(&bot));bot.health=100;
bot.powerType=POWER_MANA;bot.mana=29;assert(!mgr.PvPTarget(&bot));bot.mana=100;
roll=1;assert(!mgr.PvPTarget(&bot));roll=0;
Creature npc;npc.distance=5;creatures={&npc};assert(mgr.PvPTarget(&bot)==&npc);
npc.faction.FactionGroup=8;assert(mgr.PvPTarget(&bot)==&enemy);npc.faction.FactionGroup=4;
npc.elite=true;assert(mgr.PvPTarget(&bot)==&enemy);npc.elite=false;
npc.controlled=true;assert(mgr.PvPTarget(&bot)==&enemy);npc.controlled=false;
npc.level=54;assert(mgr.PvPTarget(&bot)==&enemy);
}
'''
with tempfile.TemporaryDirectory() as d:
src=Path(d)/'pvp.cpp';src.write_text(code);binary=Path(d)/'pvp'
subprocess.run(['g++','-std=c++17','-I'+str(root/'src/server/game/World'),str(src),'-o',str(binary)],check=True)
subprocess.run([str(binary)],check=True)
print('Production PvP selector: level boundaries, radius, factions, native legality, navigation, intent, personality and resources passed.')
@@ -24,21 +24,22 @@ struct Player:Unit {Session session;Player(){owner=this;}Player*ToPlayer()overri
struct ClientlessPlayerMgr {
struct EmoteNotice{ObjectGuid Source;uint32 Emote;};struct HelpNotice{ObjectGuid Helper,Target;};
std::mutex _damageMutex;std::unordered_map<uint32,std::deque<EmoteNotice>> _emoteNotices;std::unordered_map<uint32,std::deque<HelpNotice>> _helpNotices;
std::unordered_map<uint32,std::deque<ObjectGuid>> _groupJoinNotices;void GroupJoinRequest(Player*,Player*);
void TextEmote(Player*,Player*,uint32);void SharedTargetDamage(Unit*,Unit*,uint32);
};
'''+function('void ClientlessPlayerMgr::TextEmote(')+'\n'+function('void ClientlessPlayerMgr::SharedTargetDamage(')+r'''
'''+function('void ClientlessPlayerMgr::TextEmote(')+'\n'+function('void ClientlessPlayerMgr::SharedTargetDamage(')+'\n'+function('void ClientlessPlayerMgr::GroupJoinRequest(')+r'''
int main(){
ClientlessPlayerMgr mgr;std::vector<Player> bots(16);std::vector<std::thread> threads;
Player helper;helper.guid={77};helper.session.clientless=false;
for(unsigned i=0;i<bots.size();++i){bots[i].guid={i+1};threads.emplace_back([&,i]{
Unit target;target.guid={900+i};target.attackers={&bots[i],&helper};
for(unsigned n=1;n<=1000;++n){mgr.TextEmote(&helper,&bots[i],n);mgr.SharedTargetDamage(&target,&helper,n);}
for(unsigned n=1;n<=1000;++n){mgr.GroupJoinRequest(&bots[i],&helper);mgr.TextEmote(&helper,&bots[i],n);mgr.SharedTargetDamage(&target,&helper,n);}
});}
for(auto&t:threads)t.join();
for(unsigned i=0;i<bots.size();++i){
auto const&emotes=mgr._emoteNotices[i+1];assert(emotes.size()==8);
auto const&emotes=mgr._emoteNotices[i+1];assert(emotes.size()==8);assert(mgr._groupJoinNotices[i+1].size()==8);
auto const&helps=mgr._helpNotices[i+1];assert(helps.size()==8);
for(unsigned n=0;n<8;++n){assert(emotes[n].Source.id==77 && emotes[n].Emote==n+1);assert(helps[n].Helper.id==77 && helps[n].Target.id==900+i);}
for(unsigned n=0;n<8;++n){assert(mgr._groupJoinNotices[i+1][n].id==77);assert(emotes[n].Source.id==77 && emotes[n].Emote==n+1);assert(helps[n].Helper.id==77 && helps[n].Target.id==900+i);}
}
mgr._emoteNotices.clear();mgr.TextEmote(nullptr,&bots[0],1);mgr.TextEmote(&helper,&helper,1);mgr.TextEmote(&bots[0],&helper,1);assert(mgr._emoteNotices.empty());
mgr._helpNotices.clear();Unit target;target.guid={1000};target.attackers={&bots[0]};
+2 -1
View File
@@ -11,6 +11,7 @@ def function(signature):
harness=r'''
#include "ClientlessFlightPath.h"
#include <cassert>
#include <memory>
#include <cstdint>
#include <string>
#include <vector>
@@ -48,7 +49,7 @@ struct Flags{bool fly=true;bool HasFlag(AreaMountFlags){return fly;}};
struct Area{bool fly=true;uint32 World_pvp_ID=0;Flags GetMountFlags()const{return {fly};}} area;
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{
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;
bool Moving=true,Returning=false;std::vector<Position> Breadcrumbs{{}};std::string AbortReason;
};
+14
View File
@@ -58,6 +58,20 @@ int main(){
assert(firstDescent.Valid&&firstDescent.Next.X>660&&firstDescent.Next.Z<60);
auto highApproach=simulate({0,0,70},{1,0,0},flat,clear);
assert(highApproach.Arrived&&highApproach.Steps<20&&highApproach.Peak<=70);
// Convoy members replay exactly the published segments, retaining their own index.
SharedTrail trail;trail.Points={{20,0,30},{40,0,40}};std::size_t index=0;Settings settings;
auto shared=FollowTrail({0,0,0},{800,0,0},settings,trail,index,flat,clear);
assert(shared.Step.Valid&&shared.Step.Next.X==20&&index==0);
shared=FollowTrail({20,0,30},{800,0,0},settings,trail,index,flat,clear);
assert(shared.Step.Valid&&shared.Step.Next.X==40&&index==1);
shared=FollowTrail({40,0,40},{800,0,0},settings,trail,index,flat,clear);
assert(shared.Waiting&&index==2);trail.Points.push_back({60,0,40});
shared=FollowTrail({40,0,40},{800,0,0},settings,trail,index,flat,clear);
assert(shared.Step.Valid&&shared.Step.Next.X==60);
assert(!FollowTrail({40,0,40},{800,0,0},settings,trail,index,flat,blocked).Step.Valid);
trail.Aborted=true;shared=FollowTrail({40,0,40},{800,0,0},settings,trail,index,flat,clear);
assert(!shared.Step.Valid&&!shared.Waiting);
std::cout<<"Flight geometry passed: takeoff/landing, smoothed peak, ceiling, lateral/climbing avoidance, missing terrain and blocked corridors.\n";
}
'''
@@ -28,6 +28,7 @@ preamble = r'''
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <memory>
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)
@@ -0,0 +1,52 @@
"""Run the generic production route-copy function (no AdventureBot dependency)."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/ClientlessActionExecutorMgr.cpp').read_text()
a=s.index('bool ClientlessActionExecutorMgr::AdoptRoute(')
code=r'''
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdlib>
#include <string>
#include <vector>
#include <unordered_map>
using uint32=unsigned;
#define TC_LOG_INFO(...) ((void)0)
struct Guid{unsigned id=0;unsigned GetCounter(){return id;}};
struct Position{float x,y,z;void Relocate(float a,float b,float c){x=a;y=b;z=c;}};
struct Group{Guid leader{29};Guid GetLeaderGUID(){return leader;}}group;
struct Player{bool grouped=true;unsigned map=0;Guid GetGUID(){return {31};}Group*GetGroup(){return grouped?&group:nullptr;}unsigned GetMapId(){return map;}float GetExactDist(Position const&p){return std::hypot(p.x,p.y);}};
struct ClientlessAction{std::string Name;std::unordered_map<std::string,std::string>Parameters;};
struct ClientlessTravelRouteStep{std::string NodeKey,EdgeType;unsigned EdgeDatabaseId=0,MapId=0;float X=0,Y=0,Z=0;};
struct Graph{bool allowed=true;bool PlanAdaptiveRoute(Player*,std::string const&node,int,int,std::vector<ClientlessTravelRouteStep>&r,bool&complete,std::string&,std::string&){complete=true;r={{node,"start"}};return allowed;}}graph;auto sClientlessTravelGraphMgr=&graph;
struct ClientlessActionExecutorMgr{
struct Record{std::vector<ClientlessTravelRouteStep>Route;size_t RouteStep=0;bool FinalLocalLegPending=false,RouteComplete=true,HardDestination=false,Active=false;
unsigned FinalMapId=0,FinalDatabaseId=0;float FinalX=0,FinalY=0,FinalZ=0,FinalO=0;std::string FinalLocationId,PlannedDestinationNode,AdaptiveReachedNode;};
std::unordered_map<unsigned,Record>_records;int starts=0;bool StartRouteStep(Player*,unsigned,Record&){++starts;return true;}
bool AdoptRoute(Player*,ClientlessAction const&,Record&);
};
'''+s[a:]+r'''
int main(){
ClientlessActionExecutorMgr mgr;Player p;ClientlessAction a;a.Name="travel_to_location";a.Parameters={{"id","destination"},{"route_source","29"}};
auto& lead=mgr._records[29];lead.Route={{"start","start",0,0,0,0,0},{"taxi_end","taxi",18,0,30,0,0},{"flight_end","free_flight",0,0,80,0,0}};
lead.RouteStep=1;lead.FinalLocationId="destination";lead.FinalDatabaseId=77;lead.FinalX=90;lead.FinalLocalLegPending=true;
ClientlessActionExecutorMgr::Record peer;
assert(mgr.AdoptRoute(&p,a,peer));assert(peer.Route.size()==3&&peer.Route[1].EdgeType=="taxi"&&peer.Route[1].EdgeDatabaseId==18&&peer.Route[2].EdgeType=="free_flight");
assert(peer.FinalLocationId=="destination"&&peer.FinalLocalLegPending&&peer.FinalX==90&&peer.FinalDatabaseId==77);
assert(peer.RouteStep==0&&peer.Active&&lead.RouteStep==1);peer.Route[1].NodeKey="changed";assert(lead.Route[1].NodeKey=="taxi_end");
// Late arrival joins the active route at its previous reached node.
lead.RouteStep=2;assert(mgr.AdoptRoute(&p,a,peer));assert(peer.Route.size()==2&&peer.Route[0].NodeKey=="taxi_end"&&peer.Route[0].EdgeType=="start"&&peer.Route[1].EdgeType=="free_flight");
p.map=1;assert(mgr.AdoptRoute(&p,a,peer));graph.allowed=false;assert(!mgr.AdoptRoute(&p,a,peer));graph.allowed=true;p.map=0;
// No cross-group route copying and no stale destination reuse.
group.leader={32};assert(!mgr.AdoptRoute(&p,a,peer));group.leader={29};p.grouped=false;assert(!mgr.AdoptRoute(&p,a,peer));p.grouped=true;
a.Parameters["id"]="different";assert(!mgr.AdoptRoute(&p,a,peer));a.Parameters["id"]="destination";
lead.Route.clear();assert(!mgr.AdoptRoute(&p,a,peer));
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'route.cpp').write_text(code)
subprocess.run(['g++','-std=c++17',str(p/'route.cpp'),'-o',str(p/'route')],check=True)
subprocess.run([str(p/'route')],check=True)
print('Production generic shared routes: same edges/taxi/free flight, independent execution state, final local goal, late catch-up, permissions and stale-goal rejection passed.')