Add AdventureBot groups, raid preparation and shared travel
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:
1 parent
04aa87fcfc
commit
d6fea6ee68
46 files changed
+3031
-59
No files matched your search
@@ -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
|
||||
|
||||
@@ -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')
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ↦}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.')
|
||||
@@ -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 ↦}
|
||||
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.')
|
||||
@@ -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 ↦}
|
||||
} 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 ↦}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 ↦}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;
|
||||
|
||||
@@ -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]};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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.')
|
||||
Reference in new issue
Block a user