From a758239da956d524f3d0e56ee5007de23f2da41f Mon Sep 17 00:00:00 2001 From: nico Date: Wed, 28 Oct 2026 22:37:18 +0100 Subject: [PATCH] Implement AdventureBot class rotations, group roles and instance encounters --- doc/clientless/SESSION-RESUME.md | 69 + .../2026-10-28-class-combat/loader-24.txt | 5 + .../native-lock-and-load-backtrace.txt | 13 + .../runtime-summary.txt | 11 + .../2026-10-28-class-combat/tests.log | 257 ++ doc/clientless/class-combat.md | 146 + src/server/game/Spells/Spell.cpp | 3 +- src/server/game/Spells/SpellInfo.h | 2 +- .../game/World/AdventureBehaviorMgr.cpp | 12 + src/server/game/World/AdventureBehaviorMgr.h | 1 + src/server/game/World/AdventureCombatMgr.cpp | 410 +- src/server/game/World/AdventureCombatMgr.h | 7 +- .../game/World/AdventureEncounterMgr.cpp | 236 ++ src/server/game/World/AdventureEncounterMgr.h | 37 + .../game/World/AdventureGeneratorMgr.cpp | 170 +- src/server/game/World/AdventureGeneratorMgr.h | 3 + .../game/World/AdventureGeneratorPolicy.h | 10 +- .../game/World/AdventurePopulationMgr.cpp | 3 +- .../game/World/AdventureRotationPolicy.h | 85 + src/server/game/World/AdventureSupplyMgr.cpp | 2 +- src/server/game/World/AdventureTalentPolicy.h | 58 + src/server/game/World/ClientlessLuaEngine.cpp | 3 + src/server/game/World/ClientlessLuaEngine.h | 1 + .../clientless/fixtures/cata_pve_talents.json | 3284 +++++++++++++++++ .../test_adventure_class_rotation.py | 58 + .../test_adventure_combat_reactions.py | 24 +- tests/clientless/test_adventure_encounter.py | 83 + .../test_adventure_equipment_maintenance.py | 16 +- .../test_adventure_ground_avoidance.py | 47 + .../clientless/test_adventure_intent_state.py | 9 + .../test_adventure_manager_inventory.py | 12 +- .../test_adventure_talent_templates.py | 46 + tests/clientless/test_empty_spell_effect.py | 45 + 33 files changed, 5102 insertions(+), 66 deletions(-) create mode 100644 doc/clientless/checkpoints/2026-10-28-class-combat/loader-24.txt create mode 100644 doc/clientless/checkpoints/2026-10-28-class-combat/native-lock-and-load-backtrace.txt create mode 100644 doc/clientless/checkpoints/2026-10-28-class-combat/runtime-summary.txt create mode 100644 doc/clientless/checkpoints/2026-10-28-class-combat/tests.log create mode 100644 doc/clientless/class-combat.md create mode 100644 src/server/game/World/AdventureEncounterMgr.cpp create mode 100644 src/server/game/World/AdventureEncounterMgr.h create mode 100644 src/server/game/World/AdventureRotationPolicy.h create mode 100644 src/server/game/World/AdventureTalentPolicy.h create mode 100644 tests/clientless/fixtures/cata_pve_talents.json create mode 100644 tests/clientless/test_adventure_class_rotation.py create mode 100644 tests/clientless/test_adventure_encounter.py create mode 100644 tests/clientless/test_adventure_ground_avoidance.py create mode 100644 tests/clientless/test_adventure_talent_templates.py create mode 100644 tests/clientless/test_empty_spell_effect.py diff --git a/doc/clientless/SESSION-RESUME.md b/doc/clientless/SESSION-RESUME.md index a04d81dd75..e4e72332ff 100644 --- a/doc/clientless/SESSION-RESUME.md +++ b/doc/clientless/SESSION-RESUME.md @@ -1,3 +1,72 @@ +# Aktuell: Klassen-/Rollen-Kampf und Spieler-Instanzmodus implementiert + +Nutzer autorisierte die gemeinsam besprochene Umsetzung. Ausgangsstand sauber +bei d3273c677e auf feature/clientless-player-core. Dieser Implementierungsstand wird auf ausdrücklichen Nutzerauftrag lokal +committet; kein Push. Doku: doc/clientless/class-combat.md. + +Neue AdventureRotationPolicy/TalentPolicy und AdventureEncounterMgr.28 feste +Cataclysm-PvE-Talentvorlagen (27 Spezialisierungen plus separates Bär/Katze) gegen +lokale4.3.4-DBC abgeglichen; alle Punktbudgets1..41 testbar. Solo-Spec im neuen +Lua-Profil, alte Profile deterministisch aus gespeichertem Seed. Alle Klassen +mit gemeinsamen Prioritäten und nativen Casts; native Ressourcen/Procs/Combo/ +HolyPower, Cata-Spezialisierungen, Heiler/Tanks/Damage und DungeonBot-Unterstützung. +Gruppenwechsel schaltet Skillung/Ausrüstung sicher um, nie im Kampf/Flug/Fallen. +Alternativausrüstung geschützt/wiederverwendet. Level85 episch ilvl>=359, +Furor vor31 Talentpunkten Einhand; Frost-DK zwei Einhandwaffen/Unholy Presence. +Native Gifte/Schamanenimbues, Begleiter und Rollenunterstützung. + +Spielergruppen erhalten echten Instanzmodus mit ausgesetzter Planausführung, +nativen Portalwegen, allgemeinem DynamicObject-Flächenausweichen und C++-Hooks +für spätere Instanz-/Boss-Skripte. Alte performative Visits werden ausdrücklich +beim echten Eintritt ohne künstlichen Ausgang/Verwundung beendet. Reine Botgruppen +bleiben performativ. Tote warten auf Wiederbelebung, nach Wipe Eingangsrückweg +mit Teleport bei fehlendem/gescheitertem Weg, malusfreie Wiederbelebung, Sammeln, +Rückkehr/Todespunkt bzw. lebenden Spieler priorisieren. Kick verlässt Instanz +sicher und nimmt Solo-Spec/Tagesplan wieder auf. Keine Boss-Skripte eingeführt. + +Native Integration deckte CORE-Bug auf: Hunter Lock and Load56342 leert Effect2, +SpellValue berechnete trotzdem diesen parentlosen Default-Effekt. Scaling war +zudem uninitialisiert. Crash untergdb reproduziert; ROOTFIX jetzt SpellInfo.h +Scaling{} und Spell.cpp IsEffect-Gate. Regression test_empty_spell_effect.py. +Erste zwei Starts scheiterten daran, dritter korrigierter Start läuft stabil. +Alter vorheriger Server PID139425 zeigte außerdem vorhandenen Shutdown-Segfault. + +80/80 automatisierte Tests erfolgreich: /tmp/vcore-role-tests-recovery.log. +Danach letzte gezielte Klassen-/Talent-/Ausrüstungs-/Empty-Effect-Tests erfolgreich. +Vollständiger1046-Schritt-Build /tmp/vcore-role-build-recovery.log und finaler +9-Schritt-Build /tmp/vcore-role-build-final-fixed.log erfolgreich, installiert +/tmp/vcore-role-install-fixed.log. Testserver LIVE PID167319, tmux +vcore-worldserver, Console /tmp/vcore-class-combat-20261028-start3.log. +Frisches ready, fortlaufender Prozess und Port8085 geprüft. Loader unverändert +AKTIV10 persistent+2 temporär je Fraktion, alle24 online/pending0/error leer. +NICHT auf historische deaktivierte Defaults zurücksetzen. + +Native Hunter44 gab einmal initial unvollständige Talentvorbereitung aus, +automatisch korrigiert: gespeicherter Survival-Baum809/28 Punkte bei Level63 +passt zur nativen DBC. Vorhandene Missing-map719 und Into-the-Wilderness84729 +misc9-Warnungen bleiben, kein behobenes vollständiges Terrain/System versprechen. +Inventarhashes von menschlichem Allitester21 und Arthoran26 unverändert. +Belege: doc/clientless/checkpoints/2026-10-28-class-combat/. + +Noch abnehmen: tatsächliche Tank-/Heiler-/DD-Kämpfe mit Spielern, DungeonBot- +Zusammenspiel und Wipe/Portalwege in der Spielwelt. Automatisierte Adaptertests +sind KEINE Ingame-Abnahme. Bossmechaniken jenseits generischer Bodenflächen +werden später pro Instanz ergänzt. Kein laufender Build. + +Nächster besprochener, NOCH NICHT implementierter Schritt: gemeinsame Gruppenreise +für Spieler-/Botführer, getrennte Formationsplätze, sichere Boden-/Flugverfolgung, +Bodenreittiere/Taxi/Schiffe/Tiefenbahn/Portale. Im Stand kleine Formation, unterwegs +Kolonne, mehrreihig nur bei geprüftem Platz; Engstellen einreihig. Kein gültiger +Pfad -> stehenbleiben/wiederprüfen, keine Klippensprünge. >500Yards über10Minuten +führt unter bisherigen Austrittsschutzregeln zum Verlassen; bekannte Übergänge, +Nachreisen und Transport-Recovery pausieren den Abstandszähler. Nicht flugfähige +Bots folgen nur zu sicheren erreichbaren Bodenpunkten bei begrenzter Höhe. +Transportverlust -> letzter sicherer Einstiegsknoten, zuerst sicherer Weg, bei +Scheitern gezielter Rücksetzteleport, nächste Fahrt abwarten. Nutzer bat jetzt +zuerst um Commit der vorherigen Klassen-/Rollenänderungen. + +--- + # Aktuell: Loader auf Nutzerauftrag für24 Bots aktiviert Nutzer hat ausdrücklich aktive Testpopulation festgelegt: Allianz10 persistent diff --git a/doc/clientless/checkpoints/2026-10-28-class-combat/loader-24.txt b/doc/clientless/checkpoints/2026-10-28-class-combat/loader-24.txt new file mode 100644 index 0000000000..357e8be5eb --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-28-class-combat/loader-24.txt @@ -0,0 +1,5 @@ +TC> Adventure loader enabled=1 schema=1 busy=0 pools=10/2/10/2 known=25 rotations=0 manager=24 error='' +pool=0 faction=alliance persistent=1 target=10 online=10 pending=0 saved=10 +pool=1 faction=alliance persistent=0 target=2 online=2 pending=0 saved=3 +pool=2 faction=horde persistent=1 target=10 online=10 pending=0 saved=10 +pool=3 faction=horde persistent=0 target=2 online=2 pending=0 saved=2 diff --git a/doc/clientless/checkpoints/2026-10-28-class-combat/native-lock-and-load-backtrace.txt b/doc/clientless/checkpoints/2026-10-28-class-combat/native-lock-and-load-backtrace.txt new file mode 100644 index 0000000000..80a4f891f5 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-28-class-combat/native-lock-and-load-backtrace.txt @@ -0,0 +1,13 @@ +Thread 1 (Thread 0x7ffff7e22380 (LWP 160076) "worldserver"): +#0 SpellEffectInfo::CalcBaseValue (this=0x7fffdff27230, caster=0x7fffaa8bd000, target=0x0) at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/Spells/SpellInfo.cpp:550 +#1 0x0000555557ebd906 in SpellValue::SpellValue (this=0x7fff12f6a520, proto=0x7fffdff26f00, caster=0x7fffaa8bd000) at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/Spells/Spell.cpp:539 +#2 0x0000555557ebdaa1 in Spell::Spell (this=0x7fff4c696000, caster=0x7fffaa8bd000, info=0x7fffdff26f00, triggerFlags=TRIGGERED_FULL_MASK, originalCasterGUID=...) at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/Spells/Spell.cpp:566 +#3 0x00005555575d8d49 in WorldObject::CastSpell (this=0x7fffaa8bd000, targets=..., spellId=56342, args=...) at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/Entities/Object/Object.cpp:2959 +#4 0x000055555761a312 in Player::AddSpell (this=0x7fffaa8bd000, spellId=56342, active=true, learning=true, dependent=false, disabled=false, loading=false, fromSkill=0) at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/Entities/Player/Player.cpp:2918 +#5 0x000055555761b9d5 in Player::LearnSpell (this=0x7fffaa8bd000, spell_id=56342, dependent=false, fromSkill=0) at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/Entities/Player/Player.cpp:3109 +#6 0x00005555576a6ccd in Player::LearnTalent (this=0x7fffaa8bd000, talentId=9452, talentRank=0) at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/Entities/Player/Player.cpp:25810 +#7 0x00005555580b2532 in operator() (__closure=0x7fffffffba80, id=9452, rank=0) at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/World/AdventureGeneratorMgr.cpp:117 +#8 0x00005555580bca94 in AdventureTalentPolicy::Spend<(anonymous namespace)::Talents(Player*, const AdventureGeneratorPolicy::Archetype&)::, (anonymous namespace)::Talents(Player*, const AdventureGeneratorPolicy::Archetype&):: >(const std::vector > &, struct {...}, struct {...}) (build=std::vector of length 19, capacity 19 = {...}, learn=..., remaining=...) at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/World/AdventureTalentPolicy.h:18 +#9 0x00005555580b28fc in (anonymous namespace)::Talents (p=0x7fffaa8bd000, a=...) at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/World/AdventureGeneratorMgr.cpp:117 +#10 0x00005555580b7385 in AdventureGeneratorMgr::Prepare (this=0x55555a18ff00 , p=0x7fffaa8bd000, a=..., error="") at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/World/AdventureGeneratorMgr.cpp:419 +#11 0x00005555580bad71 in AdventureGeneratorMgr::Reconcile (this=0x55555a18ff00 , player=0x7fffaa8bd000, error="") at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/World/AdventureGeneratorMgr.cpp:601 diff --git a/doc/clientless/checkpoints/2026-10-28-class-combat/runtime-summary.txt b/doc/clientless/checkpoints/2026-10-28-class-combat/runtime-summary.txt new file mode 100644 index 0000000000..ce1a37f708 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-28-class-combat/runtime-summary.txt @@ -0,0 +1,11 @@ +Build: Debug, existing CMake configuration, -j10. Full1046-step rebuild +for SpellInfo header fix plus final9-step class refinements succeeded. +Installed with cmake --install after verifying no worldserver/authserver active. +Third fresh startup: /tmp/vcore-class-combat-20261028-start3.log. +Fresh ready marker, persistent worldserver PID167319 and port8085 LISTEN verified. +Loader enabled, pools online10/2/10/2, pending0, manager24, error empty. +Initial native Hunter44 talent preparation warning recovered: saved level63 +Survival tree809 with28 talent points, matching NumTalentsAtLevel.dbc. +Original inventories21/26 unchanged against pre-task hashes. +No in-game tank/healer/player-party acceptance or raid completion claimed. +Existing missing-map719 and Into the Wilderness84729 misc9 warnings remain. diff --git a/doc/clientless/checkpoints/2026-10-28-class-combat/tests.log b/doc/clientless/checkpoints/2026-10-28-class-combat/tests.log new file mode 100644 index 0000000000..81fb13605f --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-28-class-combat/tests.log @@ -0,0 +1,257 @@ +TEST tests/clientless/test_adventure_area_node.py +Production area destination excludes outside and unreachable graph nodes. + +TEST tests/clientless/test_adventure_async_database.py +Async creation DB contracts: prepared connection masks, native security column names, no future waits, successful-save cache publication and world-update-only loader hook passed + +TEST tests/clientless/test_adventure_behavior_policy.py +24-hour generation, shared intents with changing destinations, broad location classification and repetition penalties passed. + +TEST tests/clientless/test_adventure_class_rotation.py +Production class rotations: persistent PvE solo pools, native rank aura IDs, combo/holy power/proc/execute/resource gates, Eclipse and healer coverage passed + +TEST tests/clientless/test_adventure_combat_exercise.py +Production exercise: stance/charge/melee/casts, native validation delegation, bounded lifetime, invalid targets and unchanged native phases passed. + +TEST tests/clientless/test_adventure_combat_preparation.py +Production talent learner: native tiers/dependencies/class gates, all 26 legal level60 points, no capstone bypass and repeat safety passed. + +TEST tests/clientless/test_adventure_combat_reactions.py +Production combat focus, damage ordering, risk filters, corpse radius/delay, flight/group override, blocked escape, danger avoidance and malus-free persisted fallback passed. + +TEST tests/clientless/test_adventure_day_persistence.py +Production day planner: all 24 TINYINT slots restored across logins, incomplete/old-day replacement, full transaction and curiosity/level weighting passed + +TEST tests/clientless/test_adventure_economy.py +Economy: native service roles, friendly/phase/LOS/bounds gates, no bot service targets and coherent performative cues passed. + +TEST tests/clientless/test_adventure_economy_catalog.py +Economy catalog: eleven actions, three faction-bound service anchors, native flag gates and idempotent preservation passed. + +TEST tests/clientless/test_adventure_encounter.py +Production encounters: bot-only exclusion, mixed mode, extension hook, wait/revive, wipe fallback/reentry/gather/return, living-player priority, kick exit and offline cleanup passed + +TEST tests/clientless/test_adventure_equipment_maintenance.py +Production gear maintenance: same birth/retention gate rejects wrong armor/stats, malformed effects, level, temporary, reputation, map, holiday, native-use and slot mismatches passed + +TEST tests/clientless/test_adventure_exploration.py +Exploration: visible native props only, no chests/traps/furniture, phase/map/LOS/range gates and contextual preparation passed. + +TEST tests/clientless/test_adventure_exploration_catalog.py +Exploration catalog: ten actions, shared scenes, default wander replacement, explicit category eligibility and preserved edits passed. + +TEST tests/clientless/test_adventure_generator_equipment.py +Production equipment affinity: mana-only/direct/triggered procs, caster/physical stat effects and bounded cycles passed + +TEST tests/clientless/test_adventure_generator_ownership.py +Production generator commands: batch bounds, dedicated ownership, persistent/online/removing retirement protection and retired login refusal passed + +TEST tests/clientless/test_adventure_generator_policy.py +Production generator: 6144 cultural names and 61440 real Lua profiles, profession coherence, personality ranges, escaping and equipment/armor gates passed + +TEST tests/clientless/test_adventure_graph_handover.py +Production graph handover waits for ship/taxi/flight/teleport and stops at safe road/node boundaries. + +TEST tests/clientless/test_adventure_ground_avoidance.py +Production ground avoidance: harmful/friendly, phase169, height, reachable safe endpoint, rejected paths and reuse without cast/movement thrashing passed + +TEST tests/clientless/test_adventure_group_plan.py +Production group plans: leader meeting/goal/deadline inheritance, untouched personal day, manual preparation and instance-state guards passed. + +TEST tests/clientless/test_adventure_groups.py +Production recruitment: capitals, different goals, native inviter leadership, joining existing groups, capacity/reservations, raid conversion, planned transition and local contact passed. + +TEST tests/clientless/test_adventure_instance.py +Production instance lifecycle: time budget, true entry, denied/shared entry, native rear exit, restart, expired copy, late arrival, HP and return replay passed. + +TEST tests/clientless/test_adventure_instance_autoresume.py +Deferred instance script: missing schema/visits stays idle; no login work on startup; saved visitors resume with Adventure controllers and native accounts. + +TEST tests/clientless/test_adventure_instance_begin.py +Production entrance travel uses the existing location planner, requires mapping and retains normal social/area travel. + +TEST tests/clientless/test_adventure_instance_catalog.py +Instance migrations: outside entrances only, native graph preserved, disabled/missing transitions excluded, edits and saved visits preserved. + +TEST tests/clientless/test_adventure_instance_loader.py +Production instance recovery: no startup DB/login work, asynchronous delayed/paced manual recovery, generated-bot quota ownership, absent-schema compatibility and existing preparation ticks passed + +TEST tests/clientless/test_adventure_instance_persistence.py +Production instance persistence: phases, original deadline, instance ID, transactional HP marker, failed commit and intent release passed. + +TEST tests/clientless/test_adventure_intent_state.py +Production intent arrival, failed/partial travel, deadlines and safe schedule handover passed. + +TEST tests/clientless/test_adventure_interactions.py +/tmp/tmpefb2v_zu/test.cpp: In member function ‘void AdventureInteractionMgr::Leave(Player*, Record&, const char*)’: +/tmp/tmpefb2v_zu/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_manager_inventory.py +Production inventory maintenance: only full backpacks, native stack trimming, level-appropriate supplies, tools, leash, fishing pole, containers and untouched equipped slots passed + +TEST tests/clientless/test_adventure_micro_reset.py +Micro cleanup ends physical local movement, preserves transit and does not stand up dead players. + +TEST tests/clientless/test_adventure_population.py +Production population: four independent faction/persistence targets, fair selection, pending reservations, retries, existing inventory, once-per-boot rotation and connected/persistent protection passed + +TEST tests/clientless/test_adventure_profession_catalog.py +Profession catalog: native workstation roles, no workshop/resource inference, idempotence and preserved edits passed. + +TEST tests/clientless/test_adventure_profession_profile.py +Production profession parser, real Lua profiles, primary restrictions and secondary permissions passed. + +TEST tests/clientless/test_adventure_profession_sightline.py +Native workstation working-surface sightline and blocked geometry rejection passed. + +TEST tests/clientless/test_adventure_profession_targets.py +Production profession targets: native locks, phase/spawn/loot, stations, corpses, history and trainers passed. + +TEST tests/clientless/test_adventure_profile_selection.py +Production profile selection: real Lua ID dictionaries, exact 9:1 companion weighting, zero/negative/infinite/NaN/oversized weights rejected and legacy companion fallback passed + +TEST tests/clientless/test_adventure_pvp.py +Production PvP selector: level boundaries, radius, factions, native legality, navigation, intent, personality and resources passed. + +TEST tests/clientless/test_adventure_repair_catalog.py +Repair action catalog: proper native service requirement, economy-only intent and preserved administrator weights passed. + +TEST tests/clientless/test_adventure_rest.py +Recovery: authored places, bounds, arrival, occupancy, full rest duration, failure, reload and interruption passed. + +TEST tests/clientless/test_adventure_rest_fire.py +Rest fire: shared scene mapping, additive migration, preserved weights and authored creation-place policy passed. + +TEST tests/clientless/test_adventure_rest_space.py +Sleep footprint: furniture, body extent, overhead collision, ledges, stairs, missing heights, rotation and scale passed. + +TEST tests/clientless/test_adventure_rest_targets.py +Native sleep targets: chair exclusion, phase, despawn, floor search, indoors, water, storey, occupancy, bed/surface preference passed. + +TEST tests/clientless/test_adventure_rotation.py +/tmp/tmpk5e83d51/test.cpp: In instantiation of ‘std::string Trinity::StringFormat(const char*, A ...) [with A = {}; std::string = std::__cxx11::basic_string]’: +/tmp/tmpk5e83d51/test.cpp:22:126: required from ‘void Tx::PAppend(const char*, A ...) [with A = {}]’ + 22 | struct Tx{std::vectorsql;templatevoid PAppend(char const*f,A...a){sql.push_back(Trinity::StringFormat(f,a...));}}; + | ~~~~~~~~~~~~~~~~~~~~~^~~~~~~~ +/tmp/tmpk5e83d51/test.cpp:51:75: required from here + 51 | auto barrier=CharacterDatabase.BeginTransaction();barrier->PAppend("DO 0"); + | ~~~~~~~~~~~~~~~~^~~~~~~~ +/tmp/tmpk5e83d51/test.cpp:11:113: warning: format not a string literal and no format arguments [-Wformat-security] + 11 | namespace Trinity{templatestd::string StringFormat(char const* f,A...args){char b[4096];std::snprintf(b,sizeof(b),f,args...);return b;}} + | ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~ +Production rotation: connected/native-online protection, ownership refusal, native delete failure, world cleanup failure and journal-retained retry before successful profile/plan/state removal passed + +TEST tests/clientless/test_adventure_social.py +Production social flow: approach before talk, coherent greeting/talk/listen/farewell, lost targets, native chair confirmation and real fire confirmation passed. + +TEST tests/clientless/test_adventure_social_fire.py +Shared fire: native detection/provisioning, buildings/city/parent/zone restrictions, existing fires and outdoor/water checks passed. + +TEST tests/clientless/test_adventure_supplies.py +Production supplies: exact10% boundary, bandage-before-food, native lockout fallback, recovery completion, combat potions, mana-only drinking and daily/post-combat top-ups passed. + +TEST tests/clientless/test_adventure_supply_catalog.py +Supply migrations: all five roles, level60 netherweave tier, editable/disabled catalog preservation and persistent daily stamp passed. + +TEST tests/clientless/test_adventure_supply_items.py +Production native-item delegation: usable bandage tiers, native cooldown/lockout gates, real inventory consumption, self targeting and level60 secondary caps passed. + +TEST tests/clientless/test_adventure_talent_templates.py +Canonical Cata PvE builds: 4.3.4 class/tree/rank/prerequisite gates, all 1..41 point budgets, solo selection coverage passed + +TEST tests/clientless/test_adventure_training_catalog.py +Training catalog: real dummy requirement, leisure-only intent, bounded duration and preserved administrator weights passed. + +TEST tests/clientless/test_adventure_walk.py +Production travel gait reaches native spline, survives repaths, prevents mounting and preserves legacy running defaults. + +TEST tests/clientless/test_clientless_character_factory.py +Production native factory: character/profile atomicity, failed preparation/commit, cache publication order, account/security/name/cap gates passed + +TEST tests/clientless/test_clientless_companion.py +Native companion: provisioning, delayed summon, ownership, real item target, leash confirmation/timeout, reuse and cleanup passed. + +TEST tests/clientless/test_clientless_damage_notifications.py +Production damage handoff: 16 concurrent map-worker writers, ordered GUID-only events, clientless-only/positive damage gates and post-health notification passed. + +TEST tests/clientless/test_clientless_near_teleport_observer.py +Production near-teleport observer packet includes actual mover GUID and destination; clientless timestamp refreshed, regular client timestamp preserved. + +TEST tests/clientless/test_clientless_skill_preparation.py +Production offline skill preparation: native tier caps, no downgrading, rank/level/race/tier gates and no modification of online players passed + +TEST tests/clientless/test_clientless_social_notifications.py +Production social/helper queues: concurrent GUID-only delivery, bounded queues, human/self/zero-damage gates, pet attribution and lethal-cleanup ordering passed + +TEST tests/clientless/test_default_phase.py +Native PhaseShift: implicit/default 169 visibility, genuine custom-phase separation and reference updates passed. + +TEST tests/clientless/test_empty_spell_effect.py +Native empty spell effects: poisoned-memory scaling initialization, Lock and Load cleared slot, active effect values and all-empty spell values passed + +TEST tests/clientless/test_flight_area_query.py +Native terrain query map/phase, area/zone ordering and cache passed. + +TEST tests/clientless/test_flight_handoff.py +Flight handoff before spline end, terminal landing, return corners, collision stop and actual distance passed. + +TEST tests/clientless/test_flight_policy.py +Flight native eligibility, license/profile/zone policy, RP costs and fail-safe return decisions passed. + +TEST tests/clientless/test_follow_spline_velocity.py +Native follow: actual player/creature spline velocity, client gait and independent follower speeds passed. + +TEST tests/clientless/test_free_flight.py +Flight geometry passed: takeoff/landing, smoothed peak, ceiling, lateral/climbing avoidance, missing terrain and blocked corridors. + +TEST tests/clientless/test_free_flight_planner.py +ground planner regressions and dynamic flight/node/location/virtual-exit integration passed + +TEST tests/clientless/test_ground_exit_continuation.py +optional exits: repeated probes, retained route, delayed success, forward resume, flight fallback passed + +TEST tests/clientless/test_item_equip_cooldown.py +Native item equip cooldown: zero/sentinel/missing spells safely skipped, valid use cooldown preserved and longer/disabled cooldowns untouched passed + +TEST tests/clientless/test_local_spline_segments.py +Production local spline clipping passes native path-length validation, tiny remainders and 1000 sampled paths. + +TEST tests/clientless/test_lua_reload.py +Lua reload regression tests passed (production reload/bindings, real Lua 5.4). + +TEST tests/clientless/test_player_pure_max_skill.py +Native pure skill cap: learned skills return stored cap; missing/deleted/zero-rank skills return0. + +TEST tests/clientless/test_portal_transfer.py +Portal near/far ACK and denied same-map transfer regressions passed. + +TEST tests/clientless/test_portal_use.py +Interactive portal native click, near/far/global ACK, denial, timeout and target checks passed. +Native portal destination discovery and unsupported/ambiguous spell rejection passed. +Production clientless ACK completion sequence increments only after completed near/far transfers. + +TEST tests/clientless/test_shared_group_route.py +Production generic shared routes: same edges/taxi/free flight, independent execution state, final local goal, late catch-up, permissions and stale-goal rejection passed. + +TEST tests/clientless/test_taxi_import_disabled.py +Taxi importer preserves disabled rows and their reasons. + +TEST tests/clientless/test_transport.py +Transport dock, frame, recorder, cross-map copy and ride regressions passed + +TEST tests/clientless/test_transport_grid_loading.py +Core transport grid initialization regressions passed (no CPC dependency). + +TOTAL 80 FAILED [] diff --git a/doc/clientless/class-combat.md b/doc/clientless/class-combat.md new file mode 100644 index 0000000000..02c45af1ab --- /dev/null +++ b/doc/clientless/class-combat.md @@ -0,0 +1,146 @@ +# AdventureBot class combat and player instance groups + +Implementation target: Cataclysm 4.3.4. DungeonBot CreatureAI remains independent. + +## Shared class behavior + +AdventureRotationPolicy supplies deterministic PvE priorities for the supported +specializations. AdventureTalentPolicy supplies one canonical 41-point template +per specialization (separate bear/cat templates). At lower levels native talent +point, tier and prerequisite checks select the attainable prefix. The same +class/spec/level uses the same rules; personality does not randomly select spells. +Native player casts enforce ranks, cooldowns, resources, equipment, line of sight, +range, procs, combo points and holy power. Successful casts use COMBAT_CAST logs. + +Each generated profile stores identity.solo_specialization alongside the existing +group specialization and role. Older v2 profiles derive the same stable choice +from their persisted seed. Solo choices are Arms/Fury, Retribution, +Marksmanship/Survival, Assassination/Combat, Shadow, Frost/Unholy, +Elemental/Enhancement, Arcane/Fire, all three Warlock specs, Balance/Cat. +Group membership selects the profile group specialization. Leaving/kicking selects +solo again at a safe out-of-combat opportunity. Reconciliation retries with a +backoff. Named profiles share talent templates without replacing their inventory, +professions or fixed level. Temporary poison/weapon imbues use native enchant/proc +handling; no synthetic damage or generated combat resources. + +Generated loadouts reuse stored compatible equipment through native swaps. Full +inventory cleanup protects strongest candidates for both loadouts. Level 85 uses +native epic items of item level >=359 (no legendaries); class, stats, armor, native +use and existing restriction checks still apply. Tank avoidance/mastery/stamina +and healer spirit receive extra weight; resilience receives a PvE penalty. + +## Group roles + +Healers prioritize living allies and summoned DungeonBots by missing health and +attack pressure, with a large emergency-health priority. They use direct healing, +HoTs/shields, suitable group healing, dispels and native mana abilities. They +approach an injured ally when range/LOS prevents healing. Capable classes resurrect +players outside group combat. Food/bandages yield to group combat; personal +recovery rules otherwise remain unchanged. + +Tanks pick up attackers threatening non-tanks, use native taunts and AoE threat +abilities, and do not taunt from recognized player or DungeonBot tanks. Damage +bots prefer tank targets. Group protection supersedes solo first-damager focus; +solo focus and the no-group-flight rule are unchanged. A leading tank can pull +nearby hostiles while the player advances, or a valid player-selected target, +after checking nearby companions, own health and healer mana. Missing roles do +not block play and are not filled by changing somebody's profile. Secondary +tanks stay near the leading tank in actual instance groups. + +Ranged classes attack from range; approaching melee assistants prefer the target's +rear when another member tanks. Native spell validation and conservative AoE +checks prevent casting into nearby unengaged hostile creatures. Hunter pets are +loaded or created through native tame/pet functions; Warlock summons and Unholy +ghouls use native spells. Pet AI assists against the owner's current combat target. + +## Actual and performative visits + +Only player-containing groups enter actual encounter mode. Bot-only visits retain +their existing abstract entrance stay, block deadline, exit and injury behavior. +Following a player into an instance routes to a native entrance via TravelGraph +and local MMAP, then submits the normal area-trigger handler: native access, +difficulty and instance binding still apply. + +AdventureEncounterMgr pauses personal intent execution inside actual instances. +An old abstract visit record is finished without abstract exit/injury before real +play proceeds. The current civil-hour plan resumes outside, without replaying +missed slots. A dismissed bot leaves to the native exit destination when safe. + +Dead bots accept native resurrection requests. Otherwise they wait while players +or DungeonBot companions survive. After a full wipe they release, path to the +outside entrance and re-enter with a 50% resurrection without sickness or additional spirit-healer durability +penalties. Failed/rejected/stuck entrance paths allow an exterior entrance teleport. +Bots gather inside before returning to the recorded death point; a living player +in the instance takes precedence. A ghost login may reconstruct context from its +native corpse. Surviving players outside cause companions to leave safely too. + +## Encounter extensions and limits + +Register a map-specific C++ callback with AdventureEncounterMgr::Register(map, +callback) during script registration. Return true when it owns that update; +false runs the generic ground-avoidance behavior. An empty callback unregisters +an override. Callbacks can inspect encounter states, spells, auras and threat, +and delegate normal class casting to the common combat loop by returning false. +Do not mutate the callback registry from concurrent map updates. + +The generic fallback detects hostile damaging DynamicObjects in the current +phase and height band, then selects a complete, unsuspicious MMAP path to a safe +endpoint. It retains an existing safe escape path and interrupts casting only +when starting a necessary escape. It does not claim to understand every scripted +NPC hazard, frontal attack, soak, stack mechanic, vehicle or special-action button. +Those belong in later map/boss-specific callbacks. No specific boss scripts are +introduced in this change. This is functional shared combat, not a promise of +simulator-optimal DPS or verified completion of every raid. + +## Cataclysm sources + +Retrieved 2026-10-28. Only the Cataclysm paths/isCata data were used; no TBC, +Wrath, MoP or Retail builds were substituted. Talent positions and names were +matched against local 4.3.4 Talent/TalentTab/Spell DBC data, including rejecting +a duplicate obsolete Arcane talent whose spell no longer exists. Numeric DBC +metadata needed for reproducible rank/tier/prerequisite tests lives in +`tests/clientless/fixtures/cata_pve_talents.json`. + +- https://wowtbc.gg/cata/class-guides/arms-warrior/ +- https://wowtbc.gg/cata/class-guides/fury-warrior/ +- https://wowtbc.gg/cata/class-guides/protection-warrior/ +- https://wowtbc.gg/cata/class-guides/holy-paladin/ +- https://wowtbc.gg/cata/class-guides/protection-paladin/ +- https://wowtbc.gg/cata/class-guides/retribution-paladin/ +- https://wowtbc.gg/cata/class-guides/marksmanship-hunter/ +- https://wowtbc.gg/cata/class-guides/survival-hunter/ +- https://wowtbc.gg/cata/class-guides/assassination-rogue/ +- https://wowtbc.gg/cata/class-guides/combat-rogue/ +- https://wowtbc.gg/cata/class-guides/discipline-priest/ +- https://wowtbc.gg/cata/class-guides/holy-priest/ +- https://wowtbc.gg/cata/class-guides/shadow-priest/ +- https://wowtbc.gg/cata/class-guides/blood-tank-death-knight/ +- https://wowtbc.gg/cata/class-guides/frost-death-knight/ +- https://wowtbc.gg/cata/class-guides/unholy-death-knight/ +- https://wowtbc.gg/cata/class-guides/elemental-shaman/ +- https://wowtbc.gg/cata/class-guides/enhancement-shaman/ +- https://wowtbc.gg/cata/class-guides/restoration-shaman/ +- https://wowtbc.gg/cata/class-guides/arcane-mage/ +- https://wowtbc.gg/cata/class-guides/fire-mage/ +- https://wowtbc.gg/cata/class-guides/affliction-warlock/ +- https://wowtbc.gg/cata/class-guides/demonology-warlock/ +- https://wowtbc.gg/cata/class-guides/destruction-warlock/ +- https://wowtbc.gg/cata/class-guides/balance-druid/ +- https://wowtbc.gg/cata/class-guides/feral-dps-druid/ +- https://wowtbc.gg/cata/class-guides/restoration-druid/ +- https://wowtbc.gg/cata/class-guides/feral-tank-druid/ + +Additional healing/tanking context: + +- https://www.icy-veins.com/cataclysm-classic/holy-priest-pve-rotation-cooldowns-abilities +- https://www.wowhead.com/cata/guide/classes/warrior/protection/tank-rotation-cooldowns-abilities-pve + +## Native integration correction + +Canonical Survival talents exposed a native Lock and Load (56342) crash: +SpellMgr clears one effect slot, while SpellValue previously evaluated every +slot unconditionally. Empty SpellEffectInfo also left its Scaling fields +uninitialized. Empty scaling now initializes to zero and SpellValue evaluates +only active effects. The native reproduction and regression test are separate +from gameplay acceptance. Fury equipment additionally respects the canonical +31-point Titan's Grip unlock before choosing two-handed weapons. diff --git a/src/server/game/Spells/Spell.cpp b/src/server/game/Spells/Spell.cpp index 12ea4e7a11..4adb6da380 100644 --- a/src/server/game/Spells/Spell.cpp +++ b/src/server/game/Spells/Spell.cpp @@ -536,7 +536,8 @@ SpellValue::SpellValue(SpellInfo const* proto, WorldObject const* caster) { memset(EffectBasePoints, 0, sizeof(EffectBasePoints)); for (uint32 i = 0; i < MAX_SPELL_EFFECTS; ++i) - EffectBasePoints[i] = proto->Effects[i].CalcBaseValue(caster, nullptr); + if (proto->Effects[i].IsEffect()) + EffectBasePoints[i] = proto->Effects[i].CalcBaseValue(caster, nullptr); CustomBasePointsMask = 0; MaxAffectedTargets = proto->MaxAffectedTargets; diff --git a/src/server/game/Spells/SpellInfo.h b/src/server/game/Spells/SpellInfo.h index 65a660678c..551f6a9044 100644 --- a/src/server/game/Spells/SpellInfo.h +++ b/src/server/game/Spells/SpellInfo.h @@ -252,7 +252,7 @@ public: SpellEffectInfo() : _spellInfo(nullptr), _effIndex(0), Effect(0), ApplyAuraName(0), AuraPeriod(0), DieSides(0), RealPointsPerLevel(0.f), BasePoints(0), PointsPerComboPoint(0), Amplitude(0.f), DamageMultiplier(0.f), BonusMultiplier(0.f), MiscValue(0), MiscValueB(0), Mechanic(MECHANIC_NONE), TargetARadiusEntry(nullptr), TargetBRadiusEntry(nullptr), - ChainTarget(0), ItemType(0), TriggerSpell(0), ImplicitTargetConditions(nullptr) { } + ChainTarget(0), ItemType(0), TriggerSpell(0), ImplicitTargetConditions(nullptr), Scaling{} { } SpellEffectInfo(SpellEntry const* spellEntry, SpellInfo const* spellInfo, uint8 effIndex, SpellEffectEntry const* effect); diff --git a/src/server/game/World/AdventureBehaviorMgr.cpp b/src/server/game/World/AdventureBehaviorMgr.cpp index 5254b863b6..e0f81121e8 100644 --- a/src/server/game/World/AdventureBehaviorMgr.cpp +++ b/src/server/game/World/AdventureBehaviorMgr.cpp @@ -1,5 +1,6 @@ #include "AdventureBehaviorMgr.h" #include "AdventureCombatMgr.h" +#include "AdventureEncounterMgr.h" #include "AdventureGroupPolicy.h" #include "Group.h" #include "AdventureLocationMgr.h" @@ -198,6 +199,12 @@ void AdventureBehaviorMgr::Online(Player * player) if (! record.Schedule && Known(profile->InitialIntent)) SetWanted(player, record, profile->InitialIntent, profile->InitialLocation, GameTime::GetGameTime() + profile->InitialDurationSeconds); } +void AdventureBehaviorMgr::EnterPlayerInstance(Player* player) +{ + auto it=_records.find(player->GetGUID().GetCounter()); + if(it!=_records.end() && it->second.Instance.Active)FinishInstance(player,it->second); +} + void AdventureBehaviorMgr::RefreshDay(Player* player) { auto it=_records.find(player->GetGUID().GetCounter());if(it==_records.end() || !it->second.Schedule)return; @@ -1249,6 +1256,11 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff) auto entry = _records.find(guid); if (entry == _records.end() || ! entry->second.Enabled || ! player->IsInWorld()) return; auto & r = entry->second; + if(sAdventureEncounterMgr->Active(player)) + { + if(r.Instance.Active)FinishInstance(player,r); + return; + } r.MicroTimer = r.MicroTimer > diff ? r.MicroTimer - diff : 0; r.GestureTimer = r.GestureTimer > diff ? r.GestureTimer - diff : 0; r.RetryTimer = r.RetryTimer > diff ? r.RetryTimer - diff : 0; diff --git a/src/server/game/World/AdventureBehaviorMgr.h b/src/server/game/World/AdventureBehaviorMgr.h index e472b5e97c..d052221938 100644 --- a/src/server/game/World/AdventureBehaviorMgr.h +++ b/src/server/game/World/AdventureBehaviorMgr.h @@ -30,6 +30,7 @@ public: void IntentChanged(Player* player, ClientlessIntent const* intent); void Update(Player* player, uint32 diff); void RefreshDay(Player* player); + void EnterPlayerInstance(Player* player); bool Command(ChatHandler* handler, char const* args); void InterruptForCombat(Player* player); void InterruptForInteraction(Player* player); diff --git a/src/server/game/World/AdventureCombatMgr.cpp b/src/server/game/World/AdventureCombatMgr.cpp index 646afe8c26..3ede5bb717 100644 --- a/src/server/game/World/AdventureCombatMgr.cpp +++ b/src/server/game/World/AdventureCombatMgr.cpp @@ -1,7 +1,14 @@ #include "AdventureCombatMgr.h" #include "AdventureInteractionMgr.h" #include "Group.h" +#include "LFG.h" #include "AdventureCombatPolicy.h" +#include "AdventureRotationPolicy.h" +#include "AdventureGeneratorMgr.h" +#include "AdventureEncounterMgr.h" +#include "Pet.h" +#include "CreatureAI.h" +#include "ObjectMgr.h" #include "AdventureBehaviorMgr.h" #include "ClientlessLuaEngine.h" #include "ClientlessPlayerMgr.h" @@ -18,6 +25,7 @@ #include "MoveSpline.h" #include "Map.h" #include "Spell.h" +#include "SpellMgr.h" #include "SpellAuras.h" #include "WorldSession.h" #include "WorldPacket.h" @@ -27,6 +35,33 @@ #include #include +namespace +{ + bool AdventureTank(Unit* unit) + { + if(auto creature=unit->ToCreature()) + return creature->IsVianoAdventurer() && creature->GetScriptName().find("_tank")!=std::string::npos; + auto player=unit->ToPlayer();if(!player)return false; + if(auto group=player->GetGroup())if(group->GetLfgRoles(player->GetGUID())&lfg::PLAYER_ROLE_TANK)return true; + auto profile=sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter()); + if(profile)return profile->Role=="tank"; + auto spec=PlayerAI::GetPlayerSpec(player); + return (player->getClass()==CLASS_WARRIOR && spec==2) || (player->getClass()==CLASS_PALADIN && spec==1) || + (player->getClass()==CLASS_DEATH_KNIGHT && spec==0) || (player->getClass()==CLASS_DRUID && player->HasAura(5487)); + } + std::list AdventureAllies(Player* player) + { + std::list result;player->GetPartyMembers(result); + if(auto group=player->GetGroup())for(auto ref=group->GetFirstMember();ref;ref=ref->next()) + if(auto member=ref->GetSource())if(player->IsInMap(member)) + { + result.push_back(member); + std::list subgroup;member->GetPartyMembers(subgroup); + result.splice(result.end(),subgroup); + } + result.sort([](Unit* a,Unit* b){return a->GetGUID()GetGUID();});result.unique();return result; + } +} // Native player spell checks, cooldowns, rage and autoattacks. No charmer/fake AI // installation and no generated resources or synthetic damage. class AdventureCombatMgr::Fighter : public PlayerAI @@ -34,33 +69,215 @@ class AdventureCombatMgr::Fighter : public PlayerAI public: explicit Fighter(Player* player):PlayerAI(player) { } void UpdateAI(uint32) override { } - void Tick(Unit* target,uint32 diff) + bool Ranged() const {return AdventureRotationPolicy::Ranged(me->getClass(),GetPlayerSpec(me));} + bool Resurrect(Unit* target) + { + uint32 spell=me->getClass()==CLASS_PRIEST?2006:me->getClass()==CLASS_PALADIN?7328:me->getClass()==CLASS_SHAMAN?2008:me->getClass()==CLASS_DRUID?50769:0; + return spell && Cast(spell,target); + } + bool Heal(Unit* target,unsigned injured=0) + { + if(me->HasUnitState(UNIT_STATE_CASTING))return true; + auto klass=me->getClass(); + float mana=me->GetMaxPower(POWER_MANA)?100.0f*me->GetPower(POWER_MANA)/me->GetMaxPower(POWER_MANA):100; + if(mana<30 && target->GetHealthPct()>50) + { + uint32 recovery=klass==CLASS_DRUID?29166:klass==CLASS_SHAMAN?16190:klass==CLASS_PALADIN?54428:0; + if(recovery && Cast(recovery,me))return true; + } + if(target->GetHealthPct()>45 && injured>=3) + { + uint32 groupSpell=klass==CLASS_DRUID?48438:klass==CLASS_SHAMAN?1064:klass==CLASS_PRIEST?34861:klass==CLASS_PALADIN?85222:0; + if(groupSpell && Allowed({groupSpell},target) && Cast(groupSpell,target))return true; + if(klass==CLASS_PRIEST && Cast(596,target))return true; + } + if(target->GetHealthPct()>50) + { + bool harmful=false; + for(auto const& pair:target->GetAppliedAuras()) + if(!pair.second->IsPositive() && pair.second->GetBase()->GetSpellInfo()->Dispel!=DISPEL_NONE){harmful=true;break;} + uint32 dispel=klass==CLASS_PRIEST?527:klass==CLASS_PALADIN?4987:klass==CLASS_DRUID?88423:klass==CLASS_SHAMAN?51886:0; + if(harmful && dispel && Cast(dispel,target))return true; + } + for(auto const& action:AdventureRotationPolicy::Healing(me->getClass(),GetPlayerSpec(me),target->GetHealthPct())) + if(Allowed(action,target) && Cast(action.Spell,target))return true; + return false; + } + void Tick(Unit* target,uint32) { - _rend=_rend>diff?_rend-diff:0;_filler=_filler>diff?_filler-diff:0; me->SetFacingToObject(target); if(me->HasUnitState(UNIT_STATE_CASTING))return; - if(me->getClass()==CLASS_WARRIOR && !me->HasAura(2457) && Cast(2457,me))return; - if(!me->IsWithinMeleeRange(target))return; // MMAP approach is owned by manager. - if(me->GetVictim()!=target)me->Attack(target,true); - DoAutoAttackIfReady(); - if(me->getClass()!=CLASS_WARRIOR)return; - if(!me->HasAura(6673) && Cast(6673,me))return; - if(Cast(12294,target))return; - if(!_rend && !target->GetAuraApplicationOfRankedSpell(772,me->GetGUID()) && Cast(772,target)) - {_rend=12000;return;} - if(!_filler)for(uint32 id:{7384u,1464u,78u})if(Cast(id,target)){_filler=3000;break;} + if(auto pet=me->GetPet()) + { + if(pet->IsAlive() && pet->GetVictim()!=target)pet->AI()->AttackStart(target); + if(!pet->IsAlive()) + { + if(me->getClass()==CLASS_HUNTER && Cast(982,pet))return; + if(me->getClass()==CLASS_WARLOCK && Cast(688,me))return; + } + } + else if(me->getClass()==CLASS_WARLOCK) + { + if(GetPlayerSpec(me)==1 && Cast(30146,me))return; + if(Cast(688,me))return; + } + else if(me->getClass()==CLASS_DEATH_KNIGHT && GetPlayerSpec(me)==2) + { + if(Cast(46584,me))return; + } + else if(me->getClass()==CLASS_HUNTER && me->HasSpell(883)) + { + if(auto creature=sObjectMgr->GetCreatureTemplate(299)) + if(creature->type==CREATURE_TYPE_BEAST) + { + auto pet=me->SummonPet(0,me->GetPositionX(),me->GetPositionY(),me->GetPositionZ(),me->GetOrientation(),HUNTER_PET,0); + if(!pet)if(auto created=me->CreateTamedPetFrom(299)) + { + if(me->GetMap()->AddToMap(created->ToCreature())) + { + me->SetMinion(created,true);created->InitTalentForLevel(); + created->SavePetToDB(PET_SAVE_NEW_PET);me->PetSpellInitialize(); + } + else delete created; + } + } + } + if(me->getClass()==CLASS_WARRIOR && !me->HasAura(6673) && Cast(6673,me))return; + if(me->getClass()==CLASS_HUNTER && !me->HasAura(13165) && Cast(13165,me))return; + if(me->getClass()==CLASS_PALADIN && !me->HasAura(31801) && !me->HasAura(20154)) + if(Cast(31801,me) || Cast(20154,me))return; + bool ranged=Ranged(); + if(me->GetVictim()!=target)me->Attack(target,!ranged); + if(me->getClass()==CLASS_HUNTER && !me->GetCurrentSpell(CURRENT_AUTOREPEAT_SPELL) && Cast(75,target))return; + if(!ranged && me->IsWithinMeleeRange(target))DoAutoAttackIfReady(); + if(target->HasUnitState(UNIT_STATE_CASTING)) + { + uint32 interrupt=0; + switch(me->getClass()) + { + case CLASS_WARRIOR:interrupt=6552;break;case CLASS_PALADIN:interrupt=96231;break; + case CLASS_ROGUE:interrupt=1766;break;case CLASS_DEATH_KNIGHT:interrupt=47528;break; + case CLASS_SHAMAN:interrupt=57994;break;case CLASS_MAGE:interrupt=2139;break; + case CLASS_DRUID:interrupt=80965;break;default:break; + } + if(interrupt && Cast(interrupt,target))return; + } + auto manaMax=me->GetMaxPower(POWER_MANA); + float mana=manaMax?100.0f*me->GetPower(POWER_MANA)/manaMax:100; + if(me->getClass()==CLASS_MAGE && mana<20 && me->getAttackers().empty() && Cast(12051,me))return; + if(me->getClass()==CLASS_WARLOCK && mana<25 && me->GetHealthPct()>70 && Cast(1454,me))return; + if(me->getClass()==CLASS_PRIEST && mana<40 && Cast(34433,target))return; + bool boss=false;if(auto creature=target->ToCreature())boss=creature->IsDungeonBoss() || creature->isWorldBoss(); + if(boss || target->GetHealth()>me->GetMaxHealth()) + { + uint32 cooldown=0;auto spec=GetPlayerSpec(me); + switch(me->getClass()) + { + case CLASS_WARRIOR:cooldown=spec==2?0:spec==1?12292:1719;break; + case CLASS_PALADIN:cooldown=31884;break; + case CLASS_HUNTER:cooldown=3045;break; + case CLASS_ROGUE:cooldown=spec==1?13750:0;break; + case CLASS_DEATH_KNIGHT:cooldown=spec==1?51271:0;break; + case CLASS_SHAMAN:cooldown=spec==0?16166:0;break; + case CLASS_WARLOCK:cooldown=spec==1?47241:77801;break; + case CLASS_DRUID:cooldown=spec==1?50334:0;break; + default:break; + } + if(cooldown && Cast(cooldown,me))return; + } + if(boss) + { + if(me->getClass()==CLASS_SHAMAN && Cast(me->GetTeam()==ALLIANCE?32182:2825,me))return; + if(me->getClass()==CLASS_MAGE && Cast(80353,me))return; + } + auto profile=sClientlessLuaEngine->GetAdventureProfile(me->GetGUID().GetCounter()); + bool tank=me->GetGroup() && profile && profile->Role=="tank"; + if(tank && target->GetVictim() && target->GetVictim()!=me) + { + // Never steal from another tank, including summoned DungeonBots. + bool otherTank=AdventureTank(target->GetVictim()); + uint32 taunt=me->getClass()==CLASS_WARRIOR?355:me->getClass()==CLASS_PALADIN?62124:me->getClass()==CLASS_DEATH_KNIGHT?56222:me->getClass()==CLASS_DRUID?6795:0; + if(!otherTank && taunt && Cast(taunt,target))return; + } + std::vector nearby;me->GetCreatureListWithEntryInGrid(nearby,0,12); + unsigned engaged=0; + for(auto enemy:nearby)if(enemy->IsAlive() && enemy->IsInCombat() && me->IsHostileTo(enemy))++engaged; + if(tank && engaged>=2) + { + auto klass=me->getClass(); + if(klass==CLASS_WARRIOR){if(Cast(6343,me) || Cast(46968,target))return;} + if(klass==CLASS_PALADIN){if(Cast(53595,target) || Cast(26573,me))return;} + if(klass==CLASS_DEATH_KNIGHT){if(Cast(48721,me) || Cast(50842,target))return;} + if(klass==CLASS_DRUID && me->HasAura(5487)){if(Cast(77758,me) || Cast(779,me))return;} + } + if(tank && me->getClass()==CLASS_DRUID) + { + if(!me->HasAura(5487) && Cast(5487,me))return; + if(me->GetHealthPct()<40 && Cast(61336,me))return; + for(uint32 id:{33878u,33745u,6807u})if(Cast(id,target))return; + return; + } + for(auto const& action:AdventureRotationPolicy::Damage(me->getClass(),GetPlayerSpec(me))) + { + Unit* recipient=action.Self?me:target; + if(Allowed(action,recipient) && Cast(action.Spell,recipient))return; + } } private: + bool Allowed(AdventureRotationPolicy::Action const& action,Unit* target) const + { + if(action.Combo && me->GetComboPoints()GetPower(POWER_HOLY_POWER)<3 && !me->HasAura(90174))return false; + if(action.Spell==8042 && me->getClass()==CLASS_SHAMAN && GetPlayerSpec(me)==0) + { + auto aura=me->GetAura(324);if(!aura || aura->GetCharges()<7)return false; + } + if(action.Spell==403 && me->getClass()==CLASS_SHAMAN && GetPlayerSpec(me)==1) + {auto aura=me->GetAura(53817);if(!aura || aura->GetStackAmount()<5)return false;} + if(action.Spell==2912 && me->HasAura(67483))return false; + if(action.Spell==5176 && me->HasAura(67484))return false; + if(action.Proc && !me->HasAura(action.Proc))return false; + using C=AdventureRotationPolicy::Condition; + switch(action.When) + { + case C::MissingAura:return !target->GetAuraApplicationOfRankedSpell(action.Aura?action.Aura:action.Spell,me->GetGUID()); + case C::Execute:return target->GetHealthPct()<=20; + case C::Injured:return me->GetHealthPct()<65; + case C::Emergency:return me->GetHealthPct()<35; + case C::Resource: + case C::LowResource: + { + auto type=me->GetPowerType();auto maximum=me->GetMaxPower(type); + if(!maximum)return false; + auto percent=me->GetPower(type)*100/maximum; + return action.When==C::Resource?percent>=70:percent<40; + } + case C::Interrupt:return target->HasUnitState(UNIT_STATE_CASTING); + default:return true; + } + } bool Cast(uint32 id,Unit* target) { auto cast=VerifySpellCast(id,target);if(!cast)return false; - uint32 actual=cast.first->GetSpellInfo()->Id; + auto info=cast.first->GetSpellInfo(); + uint32 actual=info->Id; + if(!info->IsPositive() && info->IsAffectingArea()) + { + float radius=8; + for(auto const& effect:info->Effects)radius=std::max(radius,effect.CalcRadius(me)); + std::vector nearby;me->GetCreatureListWithEntryInGrid(nearby,0,std::min(80.0f,radius+me->GetExactDist(target))); + for(auto enemy:nearby) + if(enemy!=target && enemy->IsAlive() && me->IsInPhase(enemy) && me->IsHostileTo(enemy) && + !enemy->IsInCombat() && (enemy->GetExactDist(target)GetExactDist(me)GetExplicitTargetMask()&TARGET_FLAG_DEST_LOCATION)targets.SetDst(cast.second->GetPosition()); auto result=cast.first->prepare(targets); if(result==SPELL_CAST_OK)TC_LOG_INFO("server.adventurebot","COMBAT_CAST guid=%u target='%s' spell=%u",me->GetGUID().GetCounter(),target->GetGUID().ToString().c_str(),actual); return result==SPELL_CAST_OK; } - uint32 _rend=0,_filler=0; }; AdventureCombatMgr* AdventureCombatMgr::instance(){static AdventureCombatMgr mgr;return &mgr;} bool AdventureCombatMgr::Valid(Player* p,Unit* c,bool training) const @@ -151,7 +368,7 @@ 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());} +void AdventureCombatMgr::Offline(Player* p){sAdventureEncounterMgr->Offline(p);_records.erase(p->GetGUID().GetCounter());} Unit* AdventureCombatMgr::PvPTarget(Player* p) const { auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter()); @@ -341,6 +558,13 @@ bool AdventureCombatMgr::Death(Player* p,Record& r) TC_LOG_INFO("server.adventurebot","DEATH_RELEASE guid=%u",p->GetGUID().GetCounter()); } if(p->InBattleground() || p->InArena() || p->IsBeingTeleported())return true; + if(p->IsResurrectRequested()){p->ResurrectUsingRequestData();return true;} + if(sAdventureEncounterMgr->Death(p))return true; + if(p->GetMap()->IsDungeon() && p->GetGroup()) + { + std::list allies;p->GetPartyMembers(allies); + if(std::any_of(allies.begin(),allies.end(),[&](Unit* ally){return ally!=p && ally->IsAlive() && p->IsInMap(ally);}))return true; + } if(!p->HasFlag(PLAYER_FLAGS,PLAYER_FLAGS_GHOST)) { WorldPacket release(CMSG_REPOP_REQUEST,1);release<GetSession()->HandleRepopRequestOpcode(release); @@ -403,6 +627,129 @@ bool AdventureCombatMgr::Death(Player* p,Record& r) } return true; } +bool AdventureCombatMgr::GroupTarget(Player* p,Record& r) +{ + if(!p->GetGroup() || r.Training)return false; + auto allies=AdventureAllies(p);Unit* tank=nullptr;Player* human=nullptr; + for(auto ally:allies) + { + if(!ally->IsAlive() || !p->IsInMap(ally))continue; + if(AdventureTank(ally) && (!tank || ally->GetGUID()GetGUID()))tank=ally; + if(auto player=ally->ToPlayer())if(!player->GetSession()->IsClientless()) + if(!human || player->GetGUID()==p->GetGroup()->GetLeaderGUID())human=player; + } + Unit* chosen=nullptr;bool isTank=AdventureTank(p); + if(isTank) + { + // Protect companions first, but do not steal another tank's targets. + for(auto ally:allies)if(ally->IsAlive() && !AdventureTank(ally)) + for(auto attacker:ally->getAttackers()) + if(Valid(p,attacker,false) && attacker->GetVictim()==ally && p->GetExactDist(attacker)<35 && + (!chosen || p->GetExactDist(attacker)GetExactDist(chosen)))chosen=attacker; + } + if(!chosen && tank && tank->IsInCombat() && Valid(p,tank->GetVictim(),false))chosen=tank->GetVictim(); + if(!chosen && human && human->IsInCombat() && Valid(p,human->GetVictim(),false))chosen=human->GetVictim(); + if(!chosen)for(auto ally:allies) + { + if(ally->IsInCombat() && Valid(p,ally->GetVictim(),false)){chosen=ally->GetVictim();break;} + for(auto attacker:ally->getAttackers())if(Valid(p,attacker,false)){chosen=attacker;break;} + if(chosen)break; + } + if(!chosen && isTank && tank==p && human && sAdventureEncounterMgr->Active(p) && !r.AI && !r.Cooldown) + { + bool ready=p->GetHealthPct()>=70 && human->IsAlive() && p->GetExactDist(human)<25; + for(auto ally:allies)if(ally->IsAlive()) + { + if(p->GetExactDist(ally)>35)ready=false; + if(auto member=ally->ToPlayer()) + { + auto profile=sClientlessLuaEngine->GetAdventureProfile(member->GetGUID().GetCounter()); + if((profile && profile->Role=="healer") || PlayerAI::IsPlayerHealer(member)) + if(member->GetMaxPower(POWER_MANA) && member->GetPower(POWER_MANA)*100/member->GetMaxPower(POWER_MANA)<35)ready=false; + } + if(auto creature=ally->ToCreature()) + if(creature->IsVianoAdventurer() && creature->GetScriptName().find("_healer")!=std::string::npos && + creature->GetMaxPower(POWER_MANA) && creature->GetPower(POWER_MANA)*100/creature->GetMaxPower(POWER_MANA)<35)ready=false; + } + if(ready) + { + auto selected=ObjectAccessor::GetUnit(*p,human->GetTarget()); + if(selected && Valid(p,selected,false) && p->IsHostileTo(selected) && p->GetExactDist(selected)<25)chosen=selected; + if(!chosen && human->isMoving()) + { + std::vector enemies;p->GetCreatureListWithEntryInGrid(enemies,0,20); + for(auto enemy:enemies) + if(Valid(p,enemy,false) && p->IsHostileTo(enemy) && !enemy->GetControllingPlayer() && + p->HasInArc(3.14159265f,enemy) && (!chosen || p->GetExactDist(enemy)GetExactDist(chosen)) && Reachable(p,enemy))chosen=enemy; + } + } + } + if(!chosen && isTank && tank && tank!=p && sAdventureEncounterMgr->Active(p) && !r.AI && !p->IsInCombat()) + { + if(p->GetExactDist(tank)>4) + { + auto travel=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter()); + if(!travel || travel->State!=ClientlessTravelState::Moving) + { + Position point;tank->GetNearPoint(p,point.m_positionX,point.m_positionY,point.m_positionZ,2,tank->GetAngle(p)); + sClientlessTravelMgr->StartTravel(p,point); + } + } + return true; + } + if(chosen && p->GetExactDist(chosen)<=60 && (!r.AI || r.Target!=chosen->GetGUID())) + { + // Group role changes intentionally supersede solo first-damager focus. + if(r.AI)Finish(p,r); + Start(p,r,chosen,false,false,0); + } + return false; +} +bool AdventureCombatMgr::GroupHeal(Player* p,Record& r) +{ + auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter()); + if(!p->GetGroup() || !profile)return false; + auto allies=AdventureAllies(p); + bool fighting=std::any_of(allies.begin(),allies.end(),[](Unit* ally){return ally->IsAlive() && ally->IsInCombat();}); + if(!fighting && !p->HasUnitState(UNIT_STATE_CASTING)) + for(auto ally:allies)if(auto player=ally->ToPlayer()) + if(!player->IsAlive() && !player->IsResurrectRequested() && p->IsInMap(player) && p->IsWithinLOSInMap(player) && p->GetExactDist(player)<30) + { + if(!r.Support)r.Support=std::make_shared(p); + if(r.Support->Resurrect(player))return true; + } + if(profile->Role!="healer")return false; + Unit* best=nullptr;float priority=-1; + for(auto ally:allies) + { + if(!ally->IsAlive() || !p->IsInMap(ally) || !p->IsInPhase(ally) || !p->IsFriendlyTo(ally) || + p->GetExactDist(ally)>60 || ally->GetHealthPct()>90)continue; + float score=100-ally->GetHealthPct()+std::min(ally->getAttackers().size(),5)*8; + if(ally->GetHealthPct()<35)score+=150; + if(!best || score>priority){best=ally;priority=score;} + } + if(!best)return false; + if(p->GetExactDist(best)>35 || !p->IsWithinLOSInMap(best)) + { + auto current=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter()); + if(current && current->State==ClientlessTravelState::Moving)return true; + Position point;best->GetNearPoint(p,point.m_positionX,point.m_positionY,point.m_positionZ,15,best->GetAngle(p)); + ClientlessPathDiagnostics path; + if(sClientlessTravelMgr->AnalyzePath(p,point,path) && path.Complete && !path.Suspicious) + return sClientlessTravelMgr->StartTravel(p,point); + return best->GetHealthPct()<70; + } + if(!r.Support)r.Support=std::make_shared(p); + if(p->HasUnitState(UNIT_STATE_CASTING))return true; + sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());p->StopMoving(); + unsigned injured=0; + for(auto ally:allies)if(ally->IsAlive() && ally->GetHealthPct()<80 && ally->GetExactDist(best)<15)++injured; + p->SetFacingToObject(best); + if(r.Support->Heal(best,injured))return true; + // A healer awaiting resources/cooldowns keeps its assignment rather than + // starting a damage rotation while somebody still needs substantial help. + return best->GetHealthPct()<70; +} bool AdventureCombatMgr::Update(Player* p,uint32 diff) { if(!p->IsInWorld())return false; @@ -411,11 +758,32 @@ bool AdventureCombatMgr::Update(Player* p,uint32 diff) 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() && sAdventureEncounterMgr->Update(p,500))return true; if(!p->IsAlive())return Death(p,r); + if(p->IsResurrectRequested())p->ClearResurrectRequestData(); + if(!p->IsInCombat() && !p->HasUnitState(UNIT_STATE_CASTING) && !p->IsFlying() && !p->IsFalling() && GameTime::GetGameTime()>=r.NextRole) + { + auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter()); + if(profile) + { + unsigned desired=profile->Class?(p->GetGroup()?profile->Specialization:profile->SoloSpecialization): + AdventureRotationPolicy::SoloSpec(p->getClass(),p->GetGUID().GetCounter()); + if(p->getLevel()>=10 && (PlayerAI::GetPlayerSpec(p)!=desired || r.PreparedMode!=int(p->GetGroup()!=nullptr))) + { + r.NextRole=GameTime::GetGameTime()+10; + std::string error; + if(!sAdventureGeneratorMgr->PrepareCombat(p,error)) + TC_LOG_WARN("server.adventurebot","COMBAT_ROLE_PREPARE guid=%u error='%s'",p->GetGUID().GetCounter(),error.c_str()); + else r.PreparedMode=int(p->GetGroup()!=nullptr); + } + } + } + if(GroupHeal(p,r))return true; + if(GroupTarget(p,r))return true; if(r.Dead){r.Dead=false;r.Suspended=false;r.NextCorpseSearch=0;r.FailedCorpsePoints.clear();r.Cooldown=30000;r.Timer=0;return false;} // First actual damager wins, including interrupting a proactive/training // target. Keep all subsequent damagers ordered for after that focus dies. - if(!r.Defending)while(!r.Damagers.empty()) + if(!r.Defending && !p->GetGroup())while(!r.Damagers.empty()) { auto id=r.Damagers.front();r.Damagers.pop_front();auto c=ObjectAccessor::GetUnit(*p,id); if(!Valid(p,c,false))continue; @@ -441,19 +809,22 @@ bool AdventureCombatMgr::Update(Player* p,uint32 diff) auto c=ObjectAccessor::GetUnit(*p,r.Target); if(!Valid(p,c,r.Training) || (r.Until && GameTime::GetGameTime()>=r.Until) || p->GetExactDist(c)>80) {Finish(p,r);return true;} - if(!Reachable(p,c)) + if(!Reachable(p,c) && !(r.AI->Ranged() && p->GetExactDist(c)<=25 && p->IsWithinLOSInMap(c))) { r.UnreachableMs+=500; if(r.UnreachableMs>=20000){TC_LOG_WARN("server.adventurebot","COMBAT_TARGET_UNREACHABLE guid=%u",p->GetGUID().GetCounter());Finish(p,r);} return true; } r.UnreachableMs=0; - if(!p->IsWithinMeleeRange(c)) + bool inRange=r.AI->Ranged()?(p->GetExactDist(c)<=25 && p->IsWithinLOSInMap(c)):p->IsWithinMeleeRange(c); + if(!inRange) { auto nav=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter()); if(!nav || nav->State!=ClientlessTravelState::Moving || nav->Target.GetExactDist(c)>5) { - Position end;c->GetNearPoint(p,end.m_positionX,end.m_positionY,end.m_positionZ,0.5f,c->GetAngle(p)); + float angle=c->GetAngle(p); + if(p->GetGroup() && !AdventureTank(p) && c->GetVictim()!=p && !r.AI->Ranged())angle=c->GetOrientation()+3.14159265f; + Position end;c->GetNearPoint(p,end.m_positionX,end.m_positionY,end.m_positionZ,0.5f,angle); sClientlessTravelMgr->StartTravel(p,end); } return true; @@ -465,6 +836,7 @@ bool AdventureCombatMgr::Update(Player* p,uint32 diff) bool AdventureCombatMgr::Busy(Player* player) const { + if(player->GetGroup())for(auto ally:AdventureAllies(player))if(ally->IsAlive() && ally->IsInCombat())return true; auto it=_records.find(player->GetGUID().GetCounter()); return it!=_records.end() && (it->second.Fleeing || it->second.AI || it->second.Dead || !it->second.Damagers.empty()); } diff --git a/src/server/game/World/AdventureCombatMgr.h b/src/server/game/World/AdventureCombatMgr.h index ed38ace5db..96ca1af70f 100644 --- a/src/server/game/World/AdventureCombatMgr.h +++ b/src/server/game/World/AdventureCombatMgr.h @@ -29,13 +29,14 @@ private: class Fighter; struct Record { - std::shared_ptr AI; + std::shared_ptr AI,Support; ObjectGuid Target,Helper; std::deque Damagers; bool Fleeing=false,Training=false,Defending=false,Suspended=false,Dead=false,CemeteryPending=false; - time_t Until=0,NextCorpseSearch=0,NextPvP=0,NextEscape=0; + time_t Until=0,NextCorpseSearch=0,NextPvP=0,NextEscape=0,NextRole=0; std::vector FailedCorpsePoints,ThreatPositions; uint32 Timer=0,UnreachableMs=0,Cooldown=0; + int PreparedMode=-1; }; bool Start(Player* player,Record& record,Unit* target,bool training,bool defending,time_t until); bool Valid(Player* player,Unit* target,bool training) const; @@ -46,6 +47,8 @@ private: Creature* Proactive(Player* player) const; bool Flee(Player* player,Record& record); bool Death(Player* player,Record& record); + bool GroupHeal(Player* player,Record& record); + bool GroupTarget(Player* player,Record& record); void Finish(Player* player,Record& record); std::unordered_map _records; }; diff --git a/src/server/game/World/AdventureEncounterMgr.cpp b/src/server/game/World/AdventureEncounterMgr.cpp new file mode 100644 index 0000000000..f405b43f80 --- /dev/null +++ b/src/server/game/World/AdventureEncounterMgr.cpp @@ -0,0 +1,236 @@ +#include "AdventureEncounterMgr.h" +#include "AdventureBehaviorMgr.h" +#include "ClientlessTravelMgr.h" +#include "ClientlessTravelGraphMgr.h" +#include "ClientlessPlayerMgr.h" +#include "ClientlessActionExecutorMgr.h" +#include "DBCStores.h" +#include "Corpse.h" +#include "Player.h" +#include "Group.h" +#include "WorldSession.h" +#include "WorldPacket.h" +#include "Opcodes.h" +#include "ObjectMgr.h" +#include "ObjectAccessor.h" +#include "Map.h" +#include "DynamicObject.h" +#include "SpellInfo.h" +#include "CellImpl.h" +#include "GridNotifiersImpl.h" +#include "MotionMaster.h" +#include +#include +#include + +AdventureEncounterMgr* AdventureEncounterMgr::instance(){static AdventureEncounterMgr mgr;return &mgr;} +bool AdventureEncounterMgr::MixedGroup(Player* p) const +{ + if(auto group=p->GetGroup())for(auto ref=group->GetFirstMember();ref;ref=ref->next()) + if(auto member=ref->GetSource())if(member->GetSession() && !member->GetSession()->IsClientless())return true; + return false; +} +bool AdventureEncounterMgr::Active(Player* p) const +{ + auto it=_records.find(p->GetGUID().GetCounter()); + return p->GetMap()->IsDungeon() && p->GetGroup() && + (MixedGroup(p) || (it!=_records.end() && it->second.Map==p->GetMapId())); +} +void AdventureEncounterMgr::Register(uint32 map,Behavior behavior){if(behavior)_behaviors[map]=std::move(behavior);else _behaviors.erase(map);} +void AdventureEncounterMgr::Offline(Player* p){_records.erase(p->GetGUID().GetCounter());} +bool AdventureEncounterMgr::Update(Player* p,uint32 diff) +{ + auto guid=p->GetGUID().GetCounter();auto it=_records.find(guid); + if(!Active(p)) + { + // A dismissed companion leaves via the native exit destination before + // resuming its personal plan. Never move a bot out during combat. + if(it!=_records.end() && p->GetMapId()==it->second.Map && !p->GetGroup() && + p->IsAlive() && !p->IsInCombat() && !p->IsBeingTeleported()) + { + auto const& r=it->second; + return p->TeleportTo(r.OutsideMap,r.Outside.GetPositionX(),r.Outside.GetPositionY(),r.Outside.GetPositionZ(),r.Outside.GetOrientation()); + } + if(p->IsAlive() && !p->GetMap()->IsDungeon())_records.erase(guid); + return p->IsAlive() && !p->GetMap()->IsDungeon() && FollowIntoInstance(p); + } + sAdventureBehaviorMgr->EnterPlayerInstance(p); + if(it==_records.end() || it->second.Map!=p->GetMapId() || it->second.Instance!=p->GetInstanceId()) + { + auto entrance=sObjectMgr->GetMapEntranceTrigger(p->GetMapId()); + auto exit=sObjectMgr->GetGoBackTrigger(p->GetMapId()); + if(!entrance || !exit)return false; + Record r;r.Map=p->GetMapId();r.Instance=p->GetInstanceId();r.OutsideMap=exit->target_mapId; + r.Entrance.Relocate(entrance->target_X,entrance->target_Y,entrance->target_Z,entrance->target_Orientation); + r.Outside.Relocate(exit->target_X,exit->target_Y,exit->target_Z,exit->target_Orientation); + it=_records.insert_or_assign(guid,r).first; + } + if(!p->IsAlive())return false; // Death() owns wipe/release/re-entry. + bool humanPresent=false,offlineMember=false,humanOutside=false; + for(auto ref=p->GetGroup()->GetFirstMember();ref;ref=ref->next()) + { + auto member=ref->GetSource();if(!member){offlineMember=true;continue;} + if(!member->GetSession()->IsClientless()) + {humanPresent=true;if(member->IsAlive() && !member->GetMap()->IsDungeon())humanOutside=true;} + } + if(!p->IsInCombat() && (humanOutside || (!humanPresent && !offlineMember))) + { + auto const& r=it->second; + return p->TeleportTo(r.OutsideMap,r.Outside.GetPositionX(),r.Outside.GetPositionY(),r.Outside.GetPositionZ(),r.Outside.GetOrientation()); + } + if(it->second.Returning) + { + bool gathering=false;Player* livingHuman=nullptr; + for(auto ref=p->GetGroup()->GetFirstMember();ref;ref=ref->next())if(auto member=ref->GetSource()) + { + if(!member->GetSession()->IsClientless() && member->IsAlive() && p->IsInMap(member))livingHuman=member; + if(member->GetSession()->IsClientless() && (!member->IsAlive() || !p->IsInMap(member)))gathering=true; + } + if(livingHuman){it->second.Returning=false;return false;} + if(gathering)return true; + auto const point=it->second.DeathPoint; + if(p->GetExactDist(&point)>5) + { + auto status=sClientlessTravelMgr->GetStatus(guid); + if(!status || status->State!=ClientlessTravelState::Moving)sClientlessTravelMgr->StartTravel(p,point); + return true; + } + it->second.Returning=false; + } + if(auto behavior=_behaviors.find(p->GetMapId());behavior!=_behaviors.end()) + if(behavior->second(p,diff))return true; + return AvoidGroundDamage(p); +} +bool AdventureEncounterMgr::Death(Player* p) +{ + auto it=_records.find(p->GetGUID().GetCounter()); + if(it==_records.end() && Active(p)) + { + Update(p,0);it=_records.find(p->GetGUID().GetCounter()); + } + if(it==_records.end() && MixedGroup(p))if(auto corpse=p->GetCorpse()) + { + auto map=sMapStore.LookupEntry(corpse->GetMapId()); + auto entrance=sObjectMgr->GetMapEntranceTrigger(corpse->GetMapId()); + auto exit=sObjectMgr->GetGoBackTrigger(corpse->GetMapId()); + if(map && map->IsDungeon() && entrance && exit) + { + Record restored;restored.Map=corpse->GetMapId();restored.Instance=corpse->GetInstanceId();restored.OutsideMap=exit->target_mapId; + restored.Entrance.Relocate(entrance->target_X,entrance->target_Y,entrance->target_Z,entrance->target_Orientation); + restored.Outside.Relocate(exit->target_X,exit->target_Y,exit->target_Z,exit->target_Orientation); + restored.DeathPoint.Relocate(corpse);restored.Returning=p->HasFlag(PLAYER_FLAGS,PLAYER_FLAGS_GHOST); + it=_records.emplace(p->GetGUID().GetCounter(),restored).first; + } + } + if(it==_records.end())return false; + auto& r=it->second; + if(p->IsResurrectRequested()){p->ResurrectUsingRequestData();return true;} + if(!r.Returning) + { + if(auto group=p->GetGroup()) + { + // Native enumeration includes living summoned DungeonBots. + std::list units;p->GetPartyMembers(units); + for(auto unit:units)if(unit!=p && unit->IsAlive() && unit->GetMapId()==r.Map)return true; + for(auto ref=group->GetFirstMember();ref;ref=ref->next()) + if(auto member=ref->GetSource())if(member!=p && member->IsAlive() && member->GetMapId()==r.Map)return true; + } + r.DeathPoint.Relocate(p);r.Returning=true; + } + if(!p->HasFlag(PLAYER_FLAGS,PLAYER_FLAGS_GHOST)) + { + WorldPacket release(CMSG_REPOP_REQUEST,1);release<GetSession()->HandleRepopRequestOpcode(release);return true; + } + if(p->IsBeingTeleported())return true; + if(p->GetMapId()!=r.OutsideMap || p->GetExactDist(&r.Outside)>5) + { + auto travel=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter()); + if(travel && travel->State==ClientlessTravelState::Moving)return true; + ClientlessPathDiagnostics path; + bool failed=travel && (travel->State==ClientlessTravelState::Stuck || travel->State==ClientlessTravelState::PathRejected); + if(!failed && p->GetMapId()==r.OutsideMap && sClientlessTravelMgr->AnalyzePath(p,r.Outside,path) && path.Complete && !path.Suspicious && + sClientlessTravelMgr->StartTravel(p,r.Outside))return true; + p->TeleportTo(r.OutsideMap,r.Outside.GetPositionX(),r.Outside.GetPositionY(),r.Outside.GetPositionZ(),r.Outside.GetOrientation()); + return true; + } + sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter()); + if(p->TeleportTo(r.Map,r.Entrance.GetPositionX(),r.Entrance.GetPositionY(),r.Entrance.GetPositionZ(),r.Entrance.GetOrientation())) + {p->ResurrectPlayer(0.5f,false);p->SpawnCorpseBones();r.AtEntrance=true;} + return true; +} +bool AdventureEncounterMgr::FollowIntoInstance(Player* p) +{ + if(!MixedGroup(p) || p->IsInCombat() || p->IsBeingTeleported())return false; + Player* human=nullptr; + for(auto ref=p->GetGroup()->GetFirstMember();ref;ref=ref->next())if(auto member=ref->GetSource()) + if(member->IsAlive() && !member->GetSession()->IsClientless() && member->GetMap()->IsDungeon()) + {human=member;break;} + if(!human)return false; + AreaTriggerEntry const* entrance=nullptr;float distance=1e30f; + for(auto area:sAreaTriggerStore) + { + auto target=sObjectMgr->GetAreaTrigger(area->ID); + if(!target || target->target_mapId!=human->GetMapId())continue; + // Prefer an entrance on our continent, then its nearest native portal. + float score=area->ContinentID==p->GetMapId()?p->GetExactDist(area->Pos.X,area->Pos.Y,area->Pos.Z):1e20f+area->ID; + if(!entrance || scoreIsInAreaTriggerRadius(entrance)) + { + WorldPacket packet(CMSG_AREATRIGGER,4);packet<ID;p->GetSession()->HandleAreaTriggerOpcode(packet); + return true; // Native access, difficulty and group-bind checks apply. + } + auto execution=sClientlessActionExecutorMgr->GetStatus(p->GetGUID().GetCounter()); + if(execution && (execution->State==ClientlessActionExecutionState::Moving || execution->State==ClientlessActionExecutionState::SelectingTarget))return true; + auto travel=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter()); + if(travel && travel->State==ClientlessTravelState::Moving)return true; + if(entrance->ContinentID==p->GetMapId() && distance<80) + { + Position point(entrance->Pos.X,entrance->Pos.Y,entrance->Pos.Z,p->GetOrientation()); + return sClientlessTravelMgr->StartTravel(p,point,false,true); + } + std::string node;float nearest=0;sClientlessTravelGraphMgr->EnsureLoaded(); + if(sClientlessTravelGraphMgr->FindNearestNodeToPosition(entrance->ContinentID,entrance->Pos.X,entrance->Pos.Y,entrance->Pos.Z,node,nearest) && nearest<1000 && + !sClientlessPlayerMgr->GetQueuedActionCount(p->GetGUID().GetCounter())) + { + ClientlessAction action;action.Name="travel_to_node";action.Parameters["node"]=node; + sClientlessPlayerMgr->QueueAction(p->GetGUID().GetCounter(),std::move(action));return true; + } + return false; +} +bool AdventureEncounterMgr::AvoidGroundDamage(Player* p) +{ + std::vector objects; + auto check=[](WorldObject* object){return object->ToDynObject()!=nullptr;}; + Trinity::WorldObjectListSearcher searcher(p,objects,check,GRID_MAP_TYPE_MASK_DYNAMICOBJECT); + Cell::VisitAllObjects(p,searcher,60); + struct Danger{Position Center;float Radius;};std::vector dangers; + for(auto object:objects) + { + auto area=object->ToDynObject();auto caster=area->GetCaster();auto spell=area->GetSpellInfo(); + if(!caster || !spell || !p->IsInPhase(area) || !p->IsHostileTo(caster) || std::fabs(area->GetPositionZ()-p->GetPositionZ())>4)continue; + bool damage=false; + for(auto const& effect:spell->Effects) + if(effect.Effect==SPELL_EFFECT_SCHOOL_DAMAGE || effect.ApplyAuraName==SPELL_AURA_PERIODIC_DAMAGE || effect.ApplyAuraName==SPELL_AURA_PERIODIC_DAMAGE_PERCENT)damage=true; + if(damage)dangers.push_back({*area,area->GetRadius()+2}); + } + auto safe=[&](Position const& point){for(auto const& d:dangers)if(point.GetExactDist2d(&d.Center)GetStatus(p->GetGUID().GetCounter()); + if(current && current->State==ClientlessTravelState::Moving && safe(current->Target))return true; + float shortest=1e30f;Position best; + for(unsigned ring=1;ring<=4;++ring)for(unsigned i=0;i<16;++i) + { + float angle=i*6.2831853f/16;float radius=ring*5.0f; + Position point(p->GetPositionX()+std::cos(angle)*radius,p->GetPositionY()+std::sin(angle)*radius,p->GetPositionZ(),p->GetOrientation()); + float height=p->GetMap()->GetHeight(p->GetPhaseShift(),point.GetPositionX(),point.GetPositionY(),p->GetPositionZ()+3,true,8); + if(height<=INVALID_HEIGHT || std::fabs(height-p->GetPositionZ())>4)continue; + point.m_positionZ=height;if(!safe(point))continue; + ClientlessPathDiagnostics path; + if(sClientlessTravelMgr->AnalyzePath(p,point,path) && path.Complete && !path.Suspicious && path.PathLengthCastStop();return sClientlessTravelMgr->StartTravel(p,best); +} diff --git a/src/server/game/World/AdventureEncounterMgr.h b/src/server/game/World/AdventureEncounterMgr.h new file mode 100644 index 0000000000..f17f9cd8da --- /dev/null +++ b/src/server/game/World/AdventureEncounterMgr.h @@ -0,0 +1,37 @@ +#ifndef VCORE_ADVENTURE_ENCOUNTER_MGR_H +#define VCORE_ADVENTURE_ENCOUNTER_MGR_H +#include "Common.h" +#include "Position.h" +#include "ObjectGuid.h" +#include +#include +class Player; +// Adventure policy only. DungeonBot CreatureAI and generic CPC stay independent. +class TC_GAME_API AdventureEncounterMgr +{ +public: + using Behavior=std::function; + static AdventureEncounterMgr* instance(); + bool MixedGroup(Player* player) const; + bool Active(Player* player) const; + bool Update(Player* player,uint32 diff); + bool Death(Player* player); + void Offline(Player* player); + // A map-specific behavior may consume an update; false uses generic behavior. + // Register once when scripts are loaded, never from concurrent world updates. + void Register(uint32 map,Behavior behavior); +private: + struct Record + { + uint32 Map=0,Instance=0; + Position Entrance,Outside,DeathPoint; + uint32 OutsideMap=0; + bool Returning=false,AtEntrance=false; + }; + bool AvoidGroundDamage(Player* player); + bool FollowIntoInstance(Player* player); + std::unordered_map _records; + std::unordered_map _behaviors; +}; +#define sAdventureEncounterMgr AdventureEncounterMgr::instance() +#endif diff --git a/src/server/game/World/AdventureGeneratorMgr.cpp b/src/server/game/World/AdventureGeneratorMgr.cpp index 9c1737d760..21619a17b0 100644 --- a/src/server/game/World/AdventureGeneratorMgr.cpp +++ b/src/server/game/World/AdventureGeneratorMgr.cpp @@ -1,5 +1,6 @@ #include "AdventureGeneratorMgr.h" #include "AdventureCombatPreparationPolicy.h" +#include "AdventureTalentPolicy.h" #include "AdventureBehaviorMgr.h" #include "AdventureGroupPolicy.h" #include "AdventureLocationMgr.h" @@ -7,6 +8,7 @@ #include "AdventureSupplyMgr.h" #include "ClientlessCompanionMgr.h" #include "Bag.h" +#include "Group.h" #include "CharacterCache.h" #include "Chat.h" #include "ClientlessCharacterFactory.h" @@ -103,27 +105,46 @@ namespace bool Talents(Player* p,Archetype const& a) { if(p->getLevel()<10)return true; - p->InitTalentForLevel(); - if(!p->GetPrimaryTalentTree(p->GetActiveSpec()) && !p->LearnPrimaryTalentSpecialization(a.Spec))return false; + static std::unordered_map prepared; + uint32 key=(p->getLevel()*10+a.Spec)*2+(std::string(AdventureGeneratorPolicy::Role(a))=="tank"); auto tabs=sDBCManager.GetTalentTabPages(p->getClass());if(!tabs)return false; - if(a.Class==CLASS_WARRIOR && a.Spec==0) - AdventureCombatPreparationPolicy::SpendArms60([&](uint32 id,unsigned rank){return p->LearnTalent(id,rank);},[&](){return p->GetFreeTalentPoints();}); - // Native LearnTalent enforces tier/dependency/rank and 31-point gates. - // The named solo spec is preferred; secondary trees only use leftovers. - for(unsigned tree=0;treeGetFreeTalentPoints();++tree) + if(auto it=prepared.find(p->GetGUID().GetCounter());it!=prepared.end() && it->second==key && + !p->GetFreeTalentPoints() && p->GetPrimaryTalentTree(p->GetActiveSpec())==tabs[a.Spec])return true; + auto build=AdventureTalentPolicy::Build(a.Class,a.Spec,std::string(AdventureGeneratorPolicy::Role(a))=="tank"); + if(build.empty())return false; + p->ResetTalents(true);p->InitTalentForLevel(); + if(!p->GetPrimaryTalentTree(p->GetActiveSpec()) && !p->LearnPrimaryTalentSpecialization(a.Spec))return false; + bool complete=AdventureTalentPolicy::Spend(build,[&](uint32 id,unsigned rank){return p->LearnTalent(id,rank);},[&](){return p->GetFreeTalentPoints();}); + if(complete)prepared[p->GetGUID().GetCounter()]=key; + return complete; + } + void WeaponEnhancements(Player* p) + { + for(auto slot:{EQUIPMENT_SLOT_MAINHAND,EQUIPMENT_SLOT_OFFHAND}) { - uint32 tab=tabs[(a.Spec+tree)%MAX_TALENT_TABS];std::vector talents; - for(auto entry:sTalentStore)if(entry->TabID==tab)talents.push_back(entry); - std::sort(talents.begin(),talents.end(),[](TalentEntry const* x,TalentEntry const* y){return std::tie(x->TierID,x->ColumnIndex,x->ID)TierID,y->ColumnIndex,y->ID);}); - for(unsigned pass=0;pass<50 && p->GetFreeTalentPoints();++pass) + auto item=p->GetItemByPos(INVENTORY_SLOT_BAG_0,slot);if(!item)continue; + if(item->GetTemplate()->GetClass()!=ITEM_CLASS_WEAPON)continue; + uint32 spell=0; + if(p->getClass()==CLASS_ROGUE)spell=slot==EQUIPMENT_SLOT_MAINHAND?8681:2823; + if(p->getClass()==CLASS_SHAMAN)spell=slot==EQUIPMENT_SLOT_MAINHAND && p->GetPrimaryTalentTree(p->GetActiveSpec())==sDBCManager.GetTalentTabPages(CLASS_SHAMAN)[1]?8232:8024; + if(!spell)continue; + uint32 selected=0; + for(uint32 next=spell;next;next=sSpellMgr->GetNextSpellInChain(next)) { - bool progress=false; - for(auto talent:talents)for(unsigned rank=0;rankGetFreeTalentPoints();++rank) - if(talent->SpellRank[rank] && p->LearnTalent(talent->ID,rank))progress=true; - if(!progress)break; + auto info=sSpellMgr->GetSpellInfo(next); + if(info && info->SpellLevel<=p->getLevel() && (p->getClass()==CLASS_ROGUE || p->HasSpell(next)))selected=next; + } + auto info=sSpellMgr->GetSpellInfo(selected);if(!info)continue; + for(auto const& effect:info->Effects)if(effect.Effect==SPELL_EFFECT_ENCHANT_ITEM_TEMPORARY && effect.MiscValue>0 && + sSpellItemEnchantmentStore.LookupEntry(effect.MiscValue) && item->GetEnchantmentId(TEMP_ENCHANTMENT_SLOT)!=uint32(effect.MiscValue)) + { + // Same native enchant/proc machinery as applying the supplied + // poison or weapon imbue. No direct damage or fake resources. + p->ApplyEnchantment(item,TEMP_ENCHANTMENT_SLOT,false); + item->SetEnchantment(TEMP_ENCHANTMENT_SLOT,effect.MiscValue,30*60*1000,0,p->GetGUID()); + p->ApplyEnchantment(item,TEMP_ENCHANTMENT_SLOT,true); } } - return !p->GetFreeTalentPoints(); } bool Profession(Player* p,unsigned wanted) { @@ -190,13 +211,18 @@ namespace case EQUIPMENT_SLOT_TRINKET1:case EQUIPMENT_SLOT_TRINKET2:return type==INVTYPE_TRINKET; case EQUIPMENT_SLOT_BACK:return type==INVTYPE_CLOAK; case EQUIPMENT_SLOT_MAINHAND: - if(a.Class==CLASS_ROGUE)return type==INVTYPE_WEAPON || type==INVTYPE_WEAPONMAINHAND; - if(std::string(AdventureGeneratorPolicy::Role(a))=="tank" && a.Class!=CLASS_DEATH_KNIGHT)return type==INVTYPE_WEAPON || type==INVTYPE_WEAPONMAINHAND; + if(a.Class==CLASS_ROGUE)return (type==INVTYPE_WEAPON || type==INVTYPE_WEAPONMAINHAND) && (a.Spec!=0 || item->GetSubClass()==ITEM_SUBCLASS_WEAPON_DAGGER); + if(a.Class==CLASS_PALADIN && a.Spec==0)return type==INVTYPE_WEAPON || type==INVTYPE_WEAPONMAINHAND; + if((a.Class==CLASS_SHAMAN || a.Class==CLASS_DEATH_KNIGHT) && a.Spec==1)return type==INVTYPE_WEAPON || type==INVTYPE_WEAPONMAINHAND; + if(a.Class==CLASS_WARRIOR && a.Spec==1)return type==INVTYPE_2HWEAPON || type==INVTYPE_WEAPON || type==INVTYPE_WEAPONMAINHAND; + if(std::string(AdventureGeneratorPolicy::Role(a))=="tank" && (a.Class==CLASS_WARRIOR || a.Class==CLASS_PALADIN))return type==INVTYPE_WEAPON || type==INVTYPE_WEAPONMAINHAND; if(a.Attribute==ITEM_MOD_INTELLECT)return type==INVTYPE_2HWEAPON && item->GetSubClass()==ITEM_SUBCLASS_WEAPON_STAFF; return type==INVTYPE_2HWEAPON; case EQUIPMENT_SLOT_OFFHAND: - if(std::string(AdventureGeneratorPolicy::Role(a))=="tank" && a.Class!=CLASS_DEATH_KNIGHT)return type==INVTYPE_SHIELD; - return a.Class==CLASS_ROGUE && (type==INVTYPE_WEAPON || type==INVTYPE_WEAPONOFFHAND); + if(std::string(AdventureGeneratorPolicy::Role(a))=="tank" && (a.Class==CLASS_WARRIOR || a.Class==CLASS_PALADIN))return type==INVTYPE_SHIELD; + if(a.Class==CLASS_PALADIN && a.Spec==0)return type==INVTYPE_SHIELD; + if(a.Class==CLASS_WARRIOR && a.Spec==1)return type==INVTYPE_2HWEAPON || type==INVTYPE_WEAPON || type==INVTYPE_WEAPONOFFHAND; + return ((a.Class==CLASS_ROGUE && (a.Spec!=0 || item->GetSubClass()==ITEM_SUBCLASS_WEAPON_DAGGER)) || ((a.Class==CLASS_SHAMAN || a.Class==CLASS_DEATH_KNIGHT) && a.Spec==1)) && (type==INVTYPE_WEAPON || type==INVTYPE_WEAPONOFFHAND); case EQUIPMENT_SLOT_RANGED: return a.Class==CLASS_HUNTER?item->IsRangedWeapon():a.Armor==ITEM_SUBCLASS_ARMOR_CLOTH && item->GetClass()==ITEM_CLASS_WEAPON && item->GetSubClass()==ITEM_SUBCLASS_WEAPON_WAND; default:return false; @@ -243,6 +269,9 @@ namespace float EquipmentScore(Player* p,ItemTemplate const* item,unsigned slot,Archetype const& a) { float invalid=-std::numeric_limits::infinity(); + // The shared Fury template unlocks Titan's Grip at 31 talent points. + // Before that, both weapons must be one-handed, including stored gear. + if(a.Class==CLASS_WARRIOR && a.Spec==1 && item->GetInventoryType()==INVTYPE_2HWEAPON && p->CalculateTalentsPoints()<31)return invalid; if(!Slot(item,slot,a) || !AdventureGeneratorPolicy::EligibleItem(p->getLevel(),std::max(0,item->GetRequiredLevel()),item->GetQuality(),item->GetBaseItemLevel(), item->GetDuration() || (item->GetFlags()&ITEM_FLAG_CONJURED),item->GetRequiredReputationFaction() || item->GetArea() || (item->GetMap()!=0 && item->GetMap()!=uint32(-1)) || item->GetHolidayID()!=HOLIDAY_NONE || item->GetRandomProperty() || item->GetRandomSuffix()) || @@ -263,18 +292,51 @@ namespace (a.Attribute!=ITEM_MOD_INTELLECT && (type==ITEM_MOD_SPIRIT || type==ITEM_MOD_MANA || type==ITEM_MOD_MANA_REGENERATION || type==ITEM_MOD_SPELL_POWER || type==ITEM_MOD_SPELL_PENETRATION)))wrong+=value; } if(wrong>0 && stat==0)return invalid; - return item->GetBaseItemLevel()+stat*2-wrong; + float roleStats=0;bool tank=std::string(AdventureGeneratorPolicy::Role(a))=="tank"; + bool healer=std::string(AdventureGeneratorPolicy::Role(a))=="healer"; + for(unsigned index=0;indexGetItemStatType(index);float value=item->GetStatValue(index,p); + if(tank && (type==ITEM_MOD_DODGE_RATING || type==ITEM_MOD_PARRY_RATING || type==ITEM_MOD_MASTERY_RATING))roleStats+=value; + if(tank && type==ITEM_MOD_STAMINA)roleStats+=value*0.5f; + if(healer && type==ITEM_MOD_SPIRIT)roleStats+=value; + if(type==ITEM_MOD_RESILIENCE_RATING)roleStats-=value; + } + return item->GetBaseItemLevel()+stat*2+roleStats-wrong; } - bool Equip(Player* p,Archetype const& a,std::mt19937& random,std::string& error,bool missingOnly=false) + bool Equip(Player* p,Archetype const& requested,std::mt19937& random,std::string& error,bool missingOnly=false) { + auto a=requested; + if(a.Class==CLASS_PALADIN)a.Attribute=a.Spec==0?ITEM_MOD_INTELLECT:ITEM_MOD_STRENGTH; + if(a.Class==CLASS_SHAMAN || a.Class==CLASS_DRUID)a.Attribute=a.Spec==1?ITEM_MOD_AGILITY:ITEM_MOD_INTELLECT; for(unsigned slot=EQUIPMENT_SLOT_START;slotGetItemByPos(INVENTORY_SLOT_BAG_0,slot)) if(std::isfinite(EquipmentScore(p,current->GetTemplate(),slot,a)))continue; if(slot==EQUIPMENT_SLOT_BODY || slot==EQUIPMENT_SLOT_TABARD || - (slot==EQUIPMENT_SLOT_OFFHAND && a.Class!=CLASS_ROGUE && (std::string(AdventureGeneratorPolicy::Role(a))!="tank" || a.Class==CLASS_DEATH_KNIGHT)) || + (slot==EQUIPMENT_SLOT_OFFHAND && a.Class!=CLASS_ROGUE && !(a.Class==CLASS_WARRIOR && a.Spec==1) && !((a.Class==CLASS_SHAMAN || a.Class==CLASS_DEATH_KNIGHT) && a.Spec==1) && !(a.Class==CLASS_PALADIN && a.Spec==0) && (std::string(AdventureGeneratorPolicy::Role(a))!="tank" || a.Class==CLASS_DEATH_KNIGHT || a.Class==CLASS_DRUID)) || (slot==EQUIPMENT_SLOT_RANGED && a.Class!=CLASS_HUNTER && a.Armor!=ITEM_SUBCLASS_ARMOR_CLOTH))continue; + Item* stored=nullptr;float storedScore=-std::numeric_limits::infinity(); + auto consider=[&](Item* item) + { + if(!item)return; + auto score=EquipmentScore(p,item->GetTemplate(),slot,a); + if(score>storedScore){stored=item;storedScore=score;} + }; + for(uint8 bagSlot=INVENTORY_SLOT_ITEM_START;bagSlotGetItemByPos(INVENTORY_SLOT_BAG_0,bagSlot)); + for(uint8 bag=INVENTORY_SLOT_BAG_START;bagGetBagByPos(bag))for(uint8 bagSlot=0;bagSlotGetBagSize();++bagSlot)consider(container->GetItemByPos(bagSlot)); + if(stored) + { + uint16 destination=0; + if(p->CanEquipItem(slot,destination,stored,true)==EQUIP_ERR_OK) + { + p->SwapItem(stored->GetPos(),destination); + if(p->GetItemByPos(INVENTORY_SLOT_BAG_0,slot)==stored)continue; + } + } struct Candidate {uint32 Id;float Score;};std::vector candidates; uint32 salt=random(); for(auto const& pair:*sObjectMgr->GetItemTemplateStore()) @@ -358,6 +420,7 @@ bool AdventureGeneratorMgr::Prepare(Player* p,Archetype const& a,std::string& er { ClassTraining(p); if(!Talents(p,a)){error="Native talent plan could not be completed.";return false;} + ClassTraining(p); // specialization-dependent trainer abilities after native talent changes for(auto skill:a.Skills)if(!Profession(p,skill) && p->getLevel()>=5){error="No native profession rank could be learned for skill "+std::to_string(skill);return false;} if(p->getLevel()>=5)p->SetFreePrimaryProfessions(0); // the generated profile owns exactly two primaries if(!sAdventureSupplyMgr->PrepareCharacter(p)){error="Could not prepare secondary professions and bags.";return false;} @@ -458,6 +521,58 @@ void AdventureGeneratorMgr::GenerateAsync(uint32 account,uint32 seed,uint8 level { if(!Create(nullptr,account,seed,level,faction,"any",persistent,done))done(0,"No valid name, archetype or home in loaded catalogs."); } +std::unordered_set AdventureGeneratorMgr::ProtectedEquipment(Player* player) const +{ + std::unordered_set result; + auto profile=sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter()); + if(!profile || profile->SourcePath.find("generated:")!=0)return result; + std::vector items; + for(uint8 slot=INVENTORY_SLOT_ITEM_START;slotGetItemByPos(INVENTORY_SLOT_BAG_0,slot))items.push_back(item); + for(uint8 bag=INVENTORY_SLOT_BAG_START;bagGetBagByPos(bag))for(uint8 slot=0;slotGetBagSize();++slot) + if(auto item=container->GetItemByPos(slot))items.push_back(item); + auto found=std::find_if(AdventureGeneratorPolicy::Archetypes().begin(),AdventureGeneratorPolicy::Archetypes().end(), + [&](Archetype const& a){return a.Class==player->getClass();}); + if(found==AdventureGeneratorPolicy::Archetypes().end())return result; + for(auto spec:{profile->Specialization,profile->SoloSpecialization}) + { + auto a=*found;a.Spec=spec; + if(a.Class==CLASS_DRUID && spec==profile->Specialization && profile->Role=="tank")a.RoleOverride="tank"; + if(a.Class==CLASS_PALADIN)a.Attribute=spec==0?ITEM_MOD_INTELLECT:ITEM_MOD_STRENGTH; + if(a.Class==CLASS_SHAMAN || a.Class==CLASS_DRUID)a.Attribute=spec==1?ITEM_MOD_AGILITY:ITEM_MOD_INTELLECT; + for(unsigned slot=EQUIPMENT_SLOT_START;slot::infinity();Item* selected=nullptr; + for(auto item:items) + { + if(result.count(item->GetGUID().GetCounter()))continue; + auto score=EquipmentScore(player,item->GetTemplate(),slot,a); + if(score>best){best=score;selected=item;} + } + if(selected)result.insert(selected->GetGUID().GetCounter()); + } + } + return result; +} +bool AdventureGeneratorMgr::PrepareCombat(Player* player,std::string& error) +{ + auto profile=sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter()); + if(!profile)return false; + if(profile->Class && profile->SourcePath.find("generated:")==0)return Reconcile(player,error); + // Named Adventure profiles share the canonical combat templates, while + // retaining their manually assigned inventory, professions and level. + auto found=std::find_if(AdventureGeneratorPolicy::Archetypes().begin(),AdventureGeneratorPolicy::Archetypes().end(), + [&](Archetype const& a){return a.Class==player->getClass();}); + if(found==AdventureGeneratorPolicy::Archetypes().end())return false; + auto a=*found; + a.Spec=player->GetGroup() && profile->Class?profile->Specialization: + profile->Class?profile->SoloSpecialization:AdventureRotationPolicy::SoloSpec(player->getClass(),player->GetGUID().GetCounter()); + if(a.Class==CLASS_DRUID && a.Spec==1 && player->GetGroup() && profile->Role=="tank")a.RoleOverride="tank"; + if(!Talents(player,a)){error="Cannot apply canonical class talent template.";return false;} + WeaponEnhancements(player); + return true; +} bool AdventureGeneratorMgr::Reconcile(Player* player,std::string& error) { auto profile=sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter()); @@ -467,7 +582,12 @@ bool AdventureGeneratorMgr::Reconcile(Player* player,std::string& error) [&](Archetype const& a){return a.Class==profile->Class;}); if(found==AdventureGeneratorPolicy::Archetypes().end())return false; auto a=*found;a.Spec=profile->Specialization; + if(a.Class==CLASS_DRUID && a.Spec==1 && profile->Role=="tank")a.RoleOverride="tank"; if(profile->Role!=AdventureGeneratorPolicy::Role(a)){error="Profile role and specialization disagree.";return false;} + if(!player->GetGroup()){a.Spec=profile->SoloSpecialization;a.RoleOverride=nullptr;player->RemoveAurasDueToSpell(25780);} + if(a.Class==CLASS_PALADIN && a.Spec==0)a.Attribute=ITEM_MOD_INTELLECT; + if(a.Class==CLASS_SHAMAN)a.Attribute=a.Spec==1?ITEM_MOD_AGILITY:ITEM_MOD_INTELLECT; + if(a.Class==CLASS_DRUID)a.Attribute=a.Spec==1?ITEM_MOD_AGILITY:ITEM_MOD_INTELLECT; bool changedLevel=player->getLevel()!=profile->Level; if(changedLevel) { @@ -479,10 +599,12 @@ bool AdventureGeneratorMgr::Reconcile(Player* player,std::string& error) for(auto id:remove)player->RemoveSpell(id,false,false); } auto tabs=sDBCManager.GetTalentTabPages(player->getClass()); - if(changedLevel || (tabs && player->getLevel()>=10 && player->GetPrimaryTalentTree(player->GetActiveSpec())!=tabs[a.Spec]))player->ResetTalents(true); + if(changedLevel || (tabs && player->getLevel()>=10 && player->GetPrimaryTalentTree(player->GetActiveSpec())!=tabs[a.Spec])) + {player->RemoveAurasByType(SPELL_AURA_MOD_SHAPESHIFT);player->ResetTalents(true);} if(!Prepare(player,a,error))return false; std::mt19937 random(player->GetGUID().GetCounter()); if(!Equip(player,a,random,error,true))return false; + WeaponEnhancements(player); for(auto const& pet:profile->Companions)ClientlessCompanionMgr::Prepare(player,pet.DisplayId,profile->CompanionLeashItem); if(player->getLevel()>=20 && profile->CanGroundMount) { diff --git a/src/server/game/World/AdventureGeneratorMgr.h b/src/server/game/World/AdventureGeneratorMgr.h index 5846116dc8..cd34584bb6 100644 --- a/src/server/game/World/AdventureGeneratorMgr.h +++ b/src/server/game/World/AdventureGeneratorMgr.h @@ -4,6 +4,7 @@ #include "AdventureGeneratorPolicy.h" #include #include +#include class Player; class ChatHandler; class TC_GAME_API AdventureGeneratorMgr @@ -17,6 +18,8 @@ public: using Completion=std::function; void GenerateAsync(uint32 account,uint32 seed,uint8 level,unsigned faction,bool persistent,Completion done); bool Reconcile(Player* player,std::string& error); + bool PrepareCombat(Player* player,std::string& error); + std::unordered_set ProtectedEquipment(Player* player) const; private: bool Create(ChatHandler* handler,uint32 account,uint32 seed,uint8 level,unsigned faction, std::string const& archetype,bool persistent,Completion done={}); diff --git a/src/server/game/World/AdventureGeneratorPolicy.h b/src/server/game/World/AdventureGeneratorPolicy.h index 78080e87d0..ed0e22ee6f 100644 --- a/src/server/game/World/AdventureGeneratorPolicy.h +++ b/src/server/game/World/AdventureGeneratorPolicy.h @@ -7,6 +7,7 @@ #include #include #include "AdventureProfessionPolicy.h" +#include "AdventureRotationPolicy.h" namespace AdventureGeneratorPolicy { struct Archetype @@ -14,6 +15,7 @@ namespace AdventureGeneratorPolicy char const* Key;unsigned Class,Spec,Attribute,Armor; std::array Skills; char const* Vocation; + char const* RoleOverride = nullptr; }; inline std::array const& Archetypes() { @@ -81,15 +83,16 @@ namespace AdventureGeneratorPolicy {return level<40 && a.Armor>=3?a.Armor-1:a.Armor;} inline bool EligibleItem(unsigned level,unsigned required,unsigned quality,unsigned itemLevel,bool temporary,bool restricted) { - return required<=level && quality>=1 && quality<=3 && !temporary && !restricted && + return required<=level && (level==85?(quality==4 && itemLevel>=359):quality>=1 && quality<=3) && !temporary && !restricted && (required+15>=level || level<20) && itemLevel<=level*5+20; } inline unsigned Capital(unsigned race) {return race==3 || race==7?1537:race==4?1657:race==11?3557:race==5?1497:race==6?1638:race==10?3487:Faction(race)==1?1519:1637;} inline char const* Role(Archetype const& a) { + if(a.RoleOverride)return a.RoleOverride; if ((a.Class==1 && a.Spec==2) || (a.Class==2 && a.Spec==1) || (a.Class==6 && a.Spec==0)) return "tank"; - if ((a.Class==5 && a.Spec!=2) || (a.Class==7 && a.Spec==2) || (a.Class==11 && a.Spec==2)) return "healer"; + if ((a.Class==2 && a.Spec==0) || (a.Class==5 && a.Spec!=2) || (a.Class==7 && a.Spec==2) || (a.Class==11 && a.Spec==2)) return "healer"; return "damage"; } inline Archetype Personalize(Archetype a, std::mt19937& random) @@ -102,6 +105,8 @@ namespace AdventureGeneratorPolicy if(a.Class==5)a.Spec=random()%2; if(a.Class==7 || a.Class==11)a.Spec=2; } + if(a.Class==11 && random()%4==0){a.Spec=1;a.Attribute=3;a.RoleOverride="tank";} + if(a.Class==2 && random()%4==0){a.Spec=0;a.Attribute=5;} return a; } inline std::string Profile(std::string const& name,Archetype const& a,unsigned race, @@ -114,6 +119,7 @@ namespace AdventureGeneratorPolicy auto personality=[&](){return 0.25f+float(random()%651)/1000;}; lua<<"return { identity = { name = "<=0 && value<=0xFFFFFFFFll)profile.Seed=uint32(value);} lua_pop(_state,1); + profile.SoloSpecialization=uint32(std::clamp(ReadFloatField(_state,-1,"solo_specialization", + float(AdventureRotationPolicy::SoloSpec(profile.Class,profile.Seed))),0.0f,2.0f)); profile.Role = ReadStringField(_state,-1,"role","damage"); if(profile.Role!="tank" && profile.Role!="healer" && profile.Role!="damage")profile.Role="damage"; profile.HomeZone = uint32(std::max(0.0f,ReadFloatField(_state,-1,"home_zone",0))); diff --git a/src/server/game/World/ClientlessLuaEngine.h b/src/server/game/World/ClientlessLuaEngine.h index 1b6e6d6d70..7305c02211 100644 --- a/src/server/game/World/ClientlessLuaEngine.h +++ b/src/server/game/World/ClientlessLuaEngine.h @@ -29,6 +29,7 @@ struct TC_GAME_API ClientlessAdventureProfile std::string HomeRegion; std::string Background, Vocation; uint32 ProfileVersion = 0, Level = 0, Class = 0, Specialization = 0, Seed = 0; + uint32 SoloSpecialization = 0; std::string Role = "damage", FavoriteRegion; uint32 HomeZone = 0, HomeCapital = 0, FavoriteZone = 0; std::unordered_map ZoneAffinity; diff --git a/tests/clientless/fixtures/cata_pve_talents.json b/tests/clientless/fixtures/cata_pve_talents.json new file mode 100644 index 0000000000..54f0fab663 --- /dev/null +++ b/tests/clientless/fixtures/cata_pve_talents.json @@ -0,0 +1,3284 @@ +{ + "source": "Cataclysm 4.3.4 local DBC snapshot; selected PvE talents and native tier/prerequisite metadata", + "talents": { + "6395": [ + 181, + 0, + 2, + 0, + 0 + ], + "201": [ + 181, + 0, + 3, + 0, + 0 + ], + "181": [ + 181, + 0, + 3, + 0, + 0 + ], + "1827": [ + 181, + 1, + 2, + 0, + 0 + ], + "1122": [ + 181, + 1, + 3, + 0, + 0 + ], + "186": [ + 181, + 2, + 3, + 0, + 0 + ], + "11171": [ + 181, + 2, + 1, + 0, + 0 + ], + "203": [ + 181, + 2, + 2, + 0, + 0 + ], + "1825": [ + 181, + 3, + 3, + 0, + 0 + ], + "205": [ + 181, + 4, + 1, + 0, + 0 + ], + "2074": [ + 181, + 4, + 2, + 0, + 0 + ], + "11174": [ + 181, + 5, + 3, + 0, + 0 + ], + "6513": [ + 181, + 5, + 2, + 0, + 0 + ], + "2076": [ + 181, + 6, + 1, + 205, + 0 + ], + "276": [ + 182, + 0, + 3, + 0, + 0 + ], + "269": [ + 182, + 0, + 3, + 0, + 0 + ], + "273": [ + 182, + 1, + 3, + 0, + 0 + ], + "1721": [ + 182, + 1, + 2, + 0, + 0 + ], + "277": [ + 182, + 1, + 3, + 0, + 0 + ], + "280": [ + 182, + 2, + 1, + 0, + 0 + ], + "682": [ + 182, + 2, + 3, + 0, + 0 + ], + "11209": [ + 182, + 3, + 3, + 0, + 0 + ], + "283": [ + 182, + 3, + 2, + 280, + 0 + ], + "6516": [ + 182, + 4, + 2, + 0, + 0 + ], + "281": [ + 182, + 4, + 1, + 0, + 0 + ], + "1715": [ + 182, + 4, + 1, + 682, + 2 + ], + "2070": [ + 182, + 5, + 3, + 0, + 0 + ], + "6517": [ + 182, + 5, + 2, + 0, + 0 + ], + "2071": [ + 182, + 6, + 1, + 0, + 0 + ], + "244": [ + 183, + 0, + 2, + 0, + 0 + ], + "2244": [ + 183, + 0, + 3, + 0, + 0 + ], + "261": [ + 183, + 1, + 3, + 0, + 0 + ], + "11218": [ + 261, + 0, + 3, + 0, + 0 + ], + "563": [ + 261, + 0, + 3, + 0, + 0 + ], + "561": [ + 261, + 1, + 2, + 0, + 0 + ], + "575": [ + 261, + 1, + 2, + 0, + 0 + ], + "1685": [ + 261, + 1, + 3, + 0, + 0 + ], + "11767": [ + 261, + 2, + 2, + 0, + 0 + ], + "574": [ + 261, + 2, + 1, + 0, + 0 + ], + "1641": [ + 261, + 2, + 2, + 0, + 0 + ], + "2049": [ + 261, + 3, + 2, + 574, + 0 + ], + "2051": [ + 261, + 3, + 3, + 0, + 0 + ], + "11769": [ + 261, + 4, + 1, + 11767, + 1 + ], + "573": [ + 261, + 4, + 1, + 0, + 0 + ], + "5565": [ + 261, + 4, + 1, + 0, + 0 + ], + "11368": [ + 261, + 5, + 3, + 573, + 0 + ], + "5566": [ + 261, + 5, + 2, + 0, + 0 + ], + "5568": [ + 262, + 0, + 2, + 0, + 0 + ], + "593": [ + 262, + 0, + 3, + 0, + 0 + ], + "11177": [ + 262, + 0, + 3, + 0, + 0 + ], + "583": [ + 262, + 1, + 2, + 0, + 0 + ], + "595": [ + 262, + 1, + 2, + 0, + 0 + ], + "5567": [ + 262, + 1, + 3, + 0, + 0 + ], + "581": [ + 262, + 2, + 2, + 0, + 0 + ], + "591": [ + 262, + 2, + 1, + 0, + 0 + ], + "1696": [ + 262, + 2, + 3, + 0, + 0 + ], + "588": [ + 262, + 3, + 2, + 0, + 0 + ], + "2084": [ + 262, + 3, + 1, + 0, + 0 + ], + "590": [ + 262, + 4, + 1, + 0, + 0 + ], + "7705": [ + 262, + 4, + 2, + 0, + 0 + ], + "15487": [ + 262, + 4, + 1, + 0, + 0 + ], + "2063": [ + 262, + 5, + 3, + 0, + 0 + ], + "2064": [ + 262, + 6, + 1, + 0, + 0 + ], + "611": [ + 263, + 0, + 2, + 0, + 0 + ], + "5560": [ + 263, + 0, + 3, + 0, + 0 + ], + "607": [ + 263, + 0, + 3, + 0, + 0 + ], + "11216": [ + 263, + 1, + 3, + 0, + 0 + ], + "602": [ + 263, + 1, + 3, + 0, + 0 + ], + "605": [ + 263, + 1, + 2, + 0, + 0 + ], + "901": [ + 263, + 2, + 1, + 0, + 0 + ], + "2055": [ + 263, + 2, + 3, + 0, + 0 + ], + "2083": [ + 263, + 3, + 3, + 0, + 0 + ], + "1693": [ + 263, + 4, + 1, + 0, + 0 + ], + "1689": [ + 263, + 4, + 2, + 0, + 0 + ], + "2057": [ + 263, + 5, + 3, + 0, + 0 + ], + "5563": [ + 263, + 5, + 2, + 2083, + 2 + ], + "2058": [ + 263, + 6, + 1, + 0, + 0 + ], + "1939": [ + 398, + 0, + 2, + 0, + 0 + ], + "2017": [ + 398, + 0, + 3, + 0, + 0 + ], + "1938": [ + 398, + 0, + 3, + 0, + 0 + ], + "12223": [ + 398, + 1, + 2, + 0, + 0 + ], + "1948": [ + 398, + 1, + 3, + 0, + 0 + ], + "7462": [ + 398, + 1, + 2, + 0, + 0 + ], + "7459": [ + 398, + 2, + 1, + 0, + 0 + ], + "7458": [ + 398, + 2, + 3, + 0, + 0 + ], + "2105": [ + 398, + 2, + 2, + 0, + 0 + ], + "7461": [ + 398, + 3, + 2, + 0, + 0 + ], + "1960": [ + 398, + 3, + 2, + 0, + 0 + ], + "1936": [ + 398, + 3, + 2, + 0, + 0 + ], + "1959": [ + 398, + 4, + 3, + 0, + 0 + ], + "1941": [ + 398, + 4, + 1, + 0, + 0 + ], + "2019": [ + 398, + 4, + 1, + 0, + 0 + ], + "2259": [ + 398, + 5, + 3, + 0, + 0 + ], + "7463": [ + 398, + 5, + 2, + 0, + 0 + ], + "1961": [ + 398, + 6, + 1, + 0, + 0 + ], + "2031": [ + 399, + 0, + 3, + 0, + 0 + ], + "2022": [ + 399, + 0, + 3, + 0, + 0 + ], + "2048": [ + 399, + 1, + 3, + 0, + 0 + ], + "1971": [ + 399, + 1, + 2, + 0, + 0 + ], + "1993": [ + 399, + 2, + 2, + 0, + 0 + ], + "1981": [ + 399, + 2, + 2, + 0, + 0 + ], + "2044": [ + 399, + 2, + 3, + 0, + 0 + ], + "1992": [ + 399, + 3, + 3, + 0, + 0 + ], + "1979": [ + 399, + 3, + 1, + 0, + 0 + ], + "2223": [ + 399, + 3, + 1, + 0, + 0 + ], + "1980": [ + 399, + 3, + 2, + 0, + 0 + ], + "1999": [ + 399, + 4, + 1, + 0, + 0 + ], + "2029": [ + 399, + 4, + 2, + 0, + 0 + ], + "2284": [ + 399, + 5, + 3, + 0, + 0 + ], + "1989": [ + 399, + 6, + 1, + 1999, + 0 + ], + "2025": [ + 400, + 0, + 2, + 0, + 0 + ], + "1932": [ + 400, + 0, + 3, + 0, + 0 + ], + "1963": [ + 400, + 0, + 3, + 0, + 0 + ], + "11178": [ + 400, + 1, + 3, + 0, + 0 + ], + "2047": [ + 400, + 2, + 2, + 0, + 0 + ], + "7574": [ + 400, + 2, + 1, + 0, + 0 + ], + "12119": [ + 400, + 2, + 2, + 1963, + 2 + ], + "11179": [ + 400, + 2, + 3, + 0, + 0 + ], + "2009": [ + 400, + 3, + 3, + 0, + 0 + ], + "2082": [ + 400, + 3, + 3, + 0, + 0 + ], + "1996": [ + 400, + 4, + 1, + 0, + 0 + ], + "2013": [ + 400, + 4, + 2, + 0, + 0 + ], + "2085": [ + 400, + 4, + 1, + 11179, + 2 + ], + "2043": [ + 400, + 5, + 2, + 0, + 0 + ], + "7575": [ + 400, + 5, + 3, + 0, + 0 + ], + "2000": [ + 400, + 6, + 1, + 0, + 0 + ], + "10134": [ + 746, + 0, + 3, + 0, + 0 + ], + "11163": [ + 746, + 0, + 2, + 0, + 0 + ], + "9664": [ + 746, + 0, + 2, + 0, + 0 + ], + "11416": [ + 746, + 1, + 2, + 0, + 0 + ], + "8176": [ + 746, + 1, + 3, + 0, + 0 + ], + "8184": [ + 746, + 1, + 2, + 0, + 0 + ], + "10138": [ + 746, + 2, + 3, + 0, + 0 + ], + "8192": [ + 746, + 2, + 1, + 0, + 0 + ], + "10741": [ + 746, + 2, + 2, + 8176, + 2 + ], + "11418": [ + 746, + 3, + 2, + 0, + 0 + ], + "11223": [ + 746, + 3, + 1, + 0, + 0 + ], + "9662": [ + 746, + 3, + 2, + 0, + 0 + ], + "10520": [ + 746, + 4, + 3, + 0, + 0 + ], + "8208": [ + 746, + 4, + 1, + 0, + 0 + ], + "8214": [ + 746, + 4, + 2, + 0, + 0 + ], + "8194": [ + 746, + 5, + 2, + 0, + 0 + ], + "8222": [ + 746, + 6, + 1, + 8208, + 0 + ], + "8227": [ + 748, + 0, + 2, + 0, + 0 + ], + "8237": [ + 748, + 0, + 2, + 0, + 0 + ], + "11699": [ + 748, + 0, + 2, + 0, + 0 + ], + "11715": [ + 748, + 0, + 3, + 0, + 0 + ], + "11279": [ + 748, + 1, + 3, + 0, + 0 + ], + "8277": [ + 748, + 1, + 1, + 8237, + 1 + ], + "8245": [ + 748, + 1, + 3, + 0, + 0 + ], + "8269": [ + 748, + 2, + 2, + 0, + 0 + ], + "8249": [ + 748, + 2, + 1, + 0, + 0 + ], + "8255": [ + 748, + 3, + 3, + 0, + 0 + ], + "8762": [ + 748, + 3, + 2, + 0, + 0 + ], + "12146": [ + 748, + 3, + 2, + 0, + 0 + ], + "8263": [ + 748, + 4, + 3, + 0, + 0 + ], + "8279": [ + 748, + 4, + 1, + 0, + 0 + ], + "8763": [ + 748, + 4, + 1, + 0, + 0 + ], + "11280": [ + 748, + 5, + 3, + 0, + 0 + ], + "8265": [ + 748, + 5, + 1, + 0, + 0 + ], + "8271": [ + 748, + 6, + 1, + 8279, + 0 + ], + "8295": [ + 750, + 0, + 2, + 0, + 0 + ], + "11716": [ + 750, + 0, + 3, + 0, + 0 + ], + "9026": [ + 750, + 0, + 2, + 0, + 0 + ], + "8305": [ + 750, + 1, + 3, + 0, + 0 + ], + "8761": [ + 750, + 1, + 2, + 0, + 0 + ], + "11285": [ + 750, + 1, + 2, + 0, + 0 + ], + "8323": [ + 750, + 2, + 3, + 0, + 0 + ], + "8299": [ + 750, + 2, + 1, + 0, + 0 + ], + "8301": [ + 750, + 2, + 2, + 0, + 0 + ], + "8293": [ + 750, + 2, + 3, + 0, + 0 + ], + "8325": [ + 750, + 3, + 1, + 0, + 0 + ], + "8307": [ + 750, + 3, + 2, + 0, + 0 + ], + "8303": [ + 750, + 3, + 2, + 0, + 0 + ], + "8335": [ + 750, + 4, + 2, + 8323, + 2 + ], + "8313": [ + 750, + 4, + 1, + 0, + 0 + ], + "8759": [ + 750, + 4, + 2, + 0, + 0 + ], + "8758": [ + 750, + 4, + 2, + 0, + 0 + ], + "8341": [ + 750, + 5, + 2, + 0, + 0 + ], + "8343": [ + 750, + 5, + 3, + 0, + 0 + ], + "8319": [ + 750, + 5, + 1, + 0, + 0 + ], + "8347": [ + 750, + 6, + 1, + 0, + 0 + ], + "8359": [ + 752, + 0, + 3, + 0, + 0 + ], + "8349": [ + 752, + 0, + 3, + 0, + 0 + ], + "11281": [ + 752, + 0, + 2, + 0, + 0 + ], + "11284": [ + 752, + 1, + 3, + 0, + 0 + ], + "8353": [ + 752, + 1, + 3, + 0, + 0 + ], + "8383": [ + 752, + 1, + 2, + 0, + 0 + ], + "8389": [ + 752, + 2, + 2, + 0, + 0 + ], + "11278": [ + 752, + 2, + 1, + 0, + 0 + ], + "11282": [ + 752, + 2, + 1, + 0, + 0 + ], + "8381": [ + 752, + 2, + 2, + 0, + 0 + ], + "8391": [ + 752, + 3, + 3, + 11278, + 0 + ], + "8361": [ + 752, + 3, + 1, + 0, + 0 + ], + "12149": [ + 752, + 4, + 2, + 8389, + 1 + ], + "8399": [ + 752, + 4, + 1, + 0, + 0 + ], + "12150": [ + 752, + 4, + 1, + 0, + 0 + ], + "11277": [ + 752, + 4, + 1, + 0, + 0 + ], + "8405": [ + 752, + 6, + 1, + 0, + 0 + ], + "10736": [ + 760, + 0, + 2, + 0, + 0 + ], + "8577": [ + 760, + 0, + 3, + 0, + 0 + ], + "8595": [ + 760, + 0, + 3, + 0, + 0 + ], + "8593": [ + 760, + 1, + 2, + 0, + 0 + ], + "11608": [ + 760, + 1, + 1, + 0, + 0 + ], + "8581": [ + 760, + 1, + 3, + 0, + 0 + ], + "8607": [ + 760, + 1, + 2, + 0, + 0 + ], + "11224": [ + 760, + 2, + 2, + 0, + 0 + ], + "8611": [ + 760, + 2, + 1, + 0, + 0 + ], + "11812": [ + 760, + 2, + 2, + 0, + 0 + ], + "8591": [ + 760, + 2, + 1, + 0, + 0 + ], + "8617": [ + 760, + 3, + 3, + 0, + 0 + ], + "11523": [ + 760, + 3, + 2, + 0, + 0 + ], + "8609": [ + 760, + 4, + 3, + 0, + 0 + ], + "8623": [ + 760, + 4, + 1, + 0, + 0 + ], + "8625": [ + 760, + 5, + 2, + 0, + 0 + ], + "8603": [ + 760, + 6, + 1, + 8609, + 2 + ], + "9032": [ + 795, + 0, + 3, + 0, + 0 + ], + "9036": [ + 795, + 0, + 2, + 0, + 0 + ], + "9046": [ + 795, + 0, + 2, + 0, + 0 + ], + "9042": [ + 795, + 1, + 3, + 0, + 0 + ], + "9062": [ + 795, + 1, + 2, + 0, + 0 + ], + "11673": [ + 795, + 1, + 2, + 0, + 0 + ], + "9064": [ + 795, + 2, + 1, + 0, + 0 + ], + "11606": [ + 795, + 2, + 2, + 0, + 0 + ], + "9054": [ + 795, + 3, + 1, + 0, + 0 + ], + "11778": [ + 795, + 3, + 2, + 0, + 0 + ], + "9060": [ + 795, + 3, + 2, + 0, + 0 + ], + "9076": [ + 795, + 4, + 2, + 0, + 0 + ], + "9074": [ + 795, + 4, + 1, + 9054, + 0 + ], + "11605": [ + 795, + 5, + 2, + 9074, + 0 + ], + "9070": [ + 795, + 5, + 3, + 0, + 0 + ], + "9080": [ + 795, + 6, + 1, + 0, + 0 + ], + "9154": [ + 799, + 0, + 3, + 0, + 0 + ], + "9200": [ + 799, + 0, + 3, + 0, + 0 + ], + "9170": [ + 799, + 1, + 3, + 0, + 0 + ], + "10737": [ + 799, + 1, + 2, + 0, + 0 + ], + "9172": [ + 799, + 1, + 2, + 0, + 0 + ], + "9192": [ + 799, + 2, + 2, + 0, + 0 + ], + "9174": [ + 799, + 2, + 1, + 0, + 0 + ], + "9198": [ + 799, + 2, + 2, + 10737, + 1 + ], + "9178": [ + 799, + 2, + 3, + 0, + 0 + ], + "10733": [ + 799, + 3, + 1, + 9174, + 0 + ], + "9188": [ + 799, + 3, + 2, + 0, + 0 + ], + "11825": [ + 799, + 3, + 2, + 0, + 0 + ], + "9180": [ + 799, + 4, + 2, + 0, + 0 + ], + "9196": [ + 799, + 4, + 1, + 0, + 0 + ], + "10578": [ + 799, + 5, + 1, + 0, + 0 + ], + "9194": [ + 799, + 5, + 2, + 0, + 0 + ], + "9186": [ + 799, + 6, + 1, + 9196, + 0 + ], + "9390": [ + 807, + 0, + 2, + 0, + 0 + ], + "9380": [ + 807, + 0, + 3, + 0, + 0 + ], + "9396": [ + 807, + 1, + 2, + 9390, + 1 + ], + "9402": [ + 807, + 1, + 3, + 0, + 0 + ], + "9398": [ + 807, + 1, + 2, + 0, + 0 + ], + "11225": [ + 807, + 2, + 3, + 0, + 0 + ], + "9408": [ + 807, + 3, + 2, + 0, + 0 + ], + "9412": [ + 807, + 3, + 1, + 0, + 0 + ], + "9416": [ + 807, + 3, + 2, + 0, + 0 + ], + "9422": [ + 807, + 4, + 2, + 0, + 0 + ], + "9418": [ + 807, + 4, + 3, + 0, + 0 + ], + "9404": [ + 807, + 4, + 1, + 0, + 0 + ], + "9426": [ + 807, + 5, + 2, + 0, + 0 + ], + "9428": [ + 807, + 5, + 2, + 0, + 0 + ], + "9430": [ + 807, + 6, + 1, + 9418, + 2 + ], + "9432": [ + 809, + 0, + 3, + 0, + 0 + ], + "9450": [ + 809, + 0, + 2, + 0, + 0 + ], + "9444": [ + 809, + 1, + 2, + 0, + 0 + ], + "10753": [ + 809, + 1, + 3, + 0, + 0 + ], + "9484": [ + 809, + 2, + 3, + 0, + 0 + ], + "9452": [ + 809, + 2, + 2, + 0, + 0 + ], + "9460": [ + 809, + 3, + 3, + 0, + 0 + ], + "9462": [ + 809, + 3, + 2, + 9452, + 1 + ], + "9464": [ + 809, + 4, + 2, + 0, + 0 + ], + "9468": [ + 809, + 4, + 1, + 0, + 0 + ], + "9474": [ + 809, + 4, + 2, + 0, + 0 + ], + "9476": [ + 809, + 4, + 1, + 0, + 0 + ], + "9478": [ + 809, + 5, + 3, + 0, + 0 + ], + "11698": [ + 809, + 5, + 2, + 0, + 0 + ], + "9480": [ + 809, + 6, + 1, + 9468, + 0 + ], + "9490": [ + 811, + 0, + 3, + 0, + 0 + ], + "9492": [ + 811, + 0, + 3, + 0, + 0 + ], + "9512": [ + 811, + 1, + 3, + 0, + 0 + ], + "10746": [ + 813, + 0, + 2, + 0, + 0 + ], + "9553": [ + 813, + 0, + 3, + 0, + 0 + ], + "9549": [ + 813, + 0, + 3, + 0, + 0 + ], + "11669": [ + 813, + 1, + 1, + 0, + 0 + ], + "11765": [ + 813, + 1, + 2, + 0, + 0 + ], + "9561": [ + 813, + 1, + 2, + 0, + 0 + ], + "9593": [ + 813, + 2, + 2, + 10746, + 1 + ], + "9577": [ + 813, + 2, + 2, + 0, + 0 + ], + "11666": [ + 813, + 2, + 1, + 0, + 0 + ], + "12184": [ + 813, + 2, + 2, + 0, + 0 + ], + "14738": [ + 813, + 3, + 1, + 9593, + 1 + ], + "9573": [ + 813, + 3, + 2, + 0, + 0 + ], + "11667": [ + 813, + 4, + 1, + 9577, + 1 + ], + "11755": [ + 813, + 4, + 1, + 0, + 0 + ], + "9597": [ + 813, + 5, + 3, + 0, + 0 + ], + "11668": [ + 813, + 5, + 2, + 11667, + 0 + ], + "9595": [ + 813, + 5, + 1, + 0, + 0 + ], + "9601": [ + 813, + 6, + 1, + 0, + 0 + ], + "9606": [ + 815, + 0, + 3, + 0, + 0 + ], + "9608": [ + 815, + 0, + 2, + 0, + 0 + ], + "9644": [ + 815, + 1, + 2, + 0, + 0 + ], + "9624": [ + 815, + 1, + 2, + 0, + 0 + ], + "11415": [ + 815, + 1, + 2, + 0, + 0 + ], + "9636": [ + 815, + 2, + 3, + 0, + 0 + ], + "9630": [ + 815, + 2, + 1, + 0, + 0 + ], + "9612": [ + 815, + 2, + 3, + 0, + 0 + ], + "11208": [ + 815, + 3, + 1, + 9630, + 0 + ], + "9650": [ + 815, + 3, + 1, + 0, + 0 + ], + "9648": [ + 815, + 3, + 1, + 0, + 0 + ], + "9642": [ + 815, + 4, + 2, + 0, + 0 + ], + "10743": [ + 815, + 4, + 2, + 0, + 0 + ], + "9654": [ + 815, + 5, + 3, + 11208, + 0 + ], + "10744": [ + 815, + 5, + 2, + 0, + 0 + ], + "9658": [ + 815, + 6, + 1, + 0, + 0 + ], + "11157": [ + 823, + 0, + 3, + 0, + 0 + ], + "10099": [ + 831, + 0, + 2, + 0, + 0 + ], + "12189": [ + 831, + 0, + 3, + 0, + 0 + ], + "10127": [ + 831, + 0, + 3, + 0, + 0 + ], + "11213": [ + 831, + 1, + 3, + 0, + 0 + ], + "10097": [ + 831, + 1, + 2, + 0, + 0 + ], + "11780": [ + 831, + 1, + 2, + 0, + 0 + ], + "11202": [ + 831, + 2, + 1, + 0, + 0 + ], + "10129": [ + 831, + 2, + 2, + 0, + 0 + ], + "11771": [ + 831, + 2, + 2, + 0, + 0 + ], + "10113": [ + 831, + 3, + 2, + 0, + 0 + ], + "10133": [ + 831, + 3, + 1, + 0, + 0 + ], + "11215": [ + 831, + 3, + 3, + 10129, + 1 + ], + "10121": [ + 831, + 3, + 1, + 0, + 0 + ], + "11779": [ + 831, + 4, + 3, + 0, + 0 + ], + "10115": [ + 831, + 4, + 1, + 0, + 0 + ], + "12151": [ + 831, + 4, + 2, + 0, + 0 + ], + "11168": [ + 831, + 5, + 3, + 10133, + 0 + ], + "11203": [ + 831, + 6, + 1, + 0, + 0 + ], + "12198": [ + 839, + 0, + 3, + 0, + 0 + ], + "10324": [ + 839, + 0, + 2, + 0, + 0 + ], + "10372": [ + 839, + 1, + 2, + 0, + 0 + ], + "10332": [ + 839, + 1, + 3, + 0, + 0 + ], + "10344": [ + 839, + 2, + 2, + 0, + 0 + ], + "10346": [ + 839, + 2, + 3, + 0, + 0 + ], + "10374": [ + 839, + 2, + 1, + 0, + 0 + ], + "11159": [ + 839, + 2, + 2, + 0, + 0 + ], + "11161": [ + 839, + 3, + 2, + 0, + 0 + ], + "11607": [ + 839, + 3, + 1, + 10346, + 2 + ], + "11193": [ + 839, + 3, + 2, + 10374, + 0 + ], + "10680": [ + 839, + 4, + 1, + 0, + 0 + ], + "10356": [ + 839, + 4, + 1, + 11607, + 0 + ], + "10334": [ + 839, + 4, + 1, + 0, + 0 + ], + "10370": [ + 839, + 5, + 2, + 10356, + 0 + ], + "10340": [ + 839, + 5, + 3, + 0, + 0 + ], + "10350": [ + 839, + 6, + 1, + 0, + 0 + ], + "10464": [ + 845, + 0, + 3, + 0, + 0 + ], + "10474": [ + 845, + 0, + 3, + 0, + 0 + ], + "10480": [ + 845, + 0, + 2, + 0, + 0 + ], + "10466": [ + 845, + 1, + 3, + 0, + 0 + ], + "10472": [ + 845, + 1, + 3, + 0, + 0 + ], + "11170": [ + 845, + 1, + 2, + 0, + 0 + ], + "10482": [ + 845, + 2, + 1, + 0, + 0 + ], + "10478": [ + 845, + 2, + 1, + 0, + 0 + ], + "10934": [ + 845, + 2, + 2, + 0, + 0 + ], + "10494": [ + 845, + 2, + 1, + 0, + 0 + ], + "10470": [ + 845, + 3, + 2, + 0, + 0 + ], + "10486": [ + 845, + 3, + 1, + 0, + 0 + ], + "10488": [ + 845, + 4, + 2, + 0, + 0 + ], + "10492": [ + 845, + 4, + 1, + 10478, + 0 + ], + "10484": [ + 845, + 4, + 2, + 0, + 0 + ], + "10496": [ + 845, + 5, + 3, + 10486, + 0 + ], + "10498": [ + 845, + 6, + 1, + 0, + 0 + ], + "10545": [ + 851, + 0, + 2, + 0, + 0 + ], + "10531": [ + 851, + 0, + 3, + 0, + 0 + ], + "10529": [ + 851, + 1, + 3, + 0, + 0 + ], + "11434": [ + 851, + 1, + 3, + 0, + 0 + ], + "10537": [ + 851, + 1, + 2, + 0, + 0 + ], + "11433": [ + 851, + 2, + 2, + 0, + 0 + ], + "10551": [ + 851, + 2, + 1, + 0, + 0 + ], + "10573": [ + 851, + 2, + 1, + 0, + 0 + ], + "10547": [ + 851, + 2, + 2, + 0, + 0 + ], + "10561": [ + 851, + 3, + 1, + 0, + 0 + ], + "12121": [ + 851, + 3, + 2, + 10573, + 0 + ], + "11431": [ + 851, + 3, + 1, + 0, + 0 + ], + "10734": [ + 851, + 4, + 2, + 0, + 0 + ], + "10571": [ + 851, + 4, + 1, + 0, + 0 + ], + "10563": [ + 851, + 4, + 3, + 0, + 0 + ], + "10559": [ + 851, + 5, + 2, + 0, + 0 + ], + "10541": [ + 851, + 5, + 3, + 0, + 0 + ], + "10577": [ + 851, + 6, + 1, + 10571, + 0 + ], + "10647": [ + 855, + 0, + 2, + 0, + 0 + ], + "10651": [ + 855, + 0, + 3, + 0, + 0 + ], + "11612": [ + 855, + 0, + 2, + 0, + 0 + ], + "11269": [ + 855, + 1, + 3, + 0, + 0 + ], + "11611": [ + 855, + 1, + 2, + 0, + 0 + ], + "10665": [ + 855, + 2, + 1, + 0, + 0 + ], + "10661": [ + 855, + 2, + 3, + 0, + 0 + ], + "11610": [ + 855, + 2, + 2, + 11612, + 1 + ], + "11204": [ + 855, + 2, + 1, + 0, + 0 + ], + "11207": [ + 855, + 3, + 1, + 0, + 0 + ], + "11372": [ + 855, + 3, + 1, + 0, + 0 + ], + "10643": [ + 855, + 3, + 1, + 0, + 0 + ], + "10669": [ + 855, + 3, + 3, + 0, + 0 + ], + "10633": [ + 855, + 4, + 2, + 0, + 0 + ], + "10677": [ + 855, + 5, + 3, + 0, + 0 + ], + "11211": [ + 855, + 5, + 2, + 0, + 0 + ], + "11222": [ + 855, + 6, + 1, + 0, + 0 + ], + "10938": [ + 865, + 0, + 3, + 0, + 0 + ], + "10936": [ + 865, + 0, + 3, + 0, + 0 + ], + "10960": [ + 865, + 0, + 2, + 0, + 0 + ], + "11181": [ + 865, + 1, + 2, + 0, + 0 + ], + "10940": [ + 865, + 2, + 2, + 0, + 0 + ], + "10978": [ + 865, + 2, + 3, + 0, + 0 + ], + "10948": [ + 865, + 2, + 1, + 0, + 0 + ], + "11182": [ + 865, + 3, + 2, + 0, + 0 + ], + "10970": [ + 865, + 3, + 2, + 0, + 0 + ], + "12120": [ + 865, + 3, + 1, + 0, + 0 + ], + "10984": [ + 865, + 4, + 3, + 0, + 0 + ], + "10980": [ + 865, + 4, + 1, + 0, + 0 + ], + "10964": [ + 865, + 4, + 2, + 12120, + 0 + ], + "10982": [ + 865, + 5, + 2, + 11182, + 1 + ], + "10962": [ + 865, + 5, + 1, + 0, + 0 + ], + "10986": [ + 865, + 6, + 1, + 10984, + 2 + ], + "10994": [ + 867, + 0, + 3, + 0, + 0 + ], + "10992": [ + 867, + 0, + 3, + 0, + 0 + ], + "11206": [ + 867, + 0, + 2, + 0, + 0 + ], + "11713": [ + 867, + 1, + 2, + 0, + 0 + ], + "11020": [ + 867, + 1, + 2, + 0, + 0 + ], + "11190": [ + 867, + 1, + 2, + 0, + 0 + ], + "11014": [ + 867, + 1, + 2, + 0, + 0 + ], + "11198": [ + 867, + 2, + 3, + 0, + 0 + ], + "11024": [ + 867, + 3, + 3, + 0, + 0 + ], + "11201": [ + 867, + 3, + 1, + 0, + 0 + ], + "11188": [ + 867, + 4, + 2, + 0, + 0 + ], + "11189": [ + 867, + 4, + 1, + 11201, + 0 + ], + "11034": [ + 867, + 4, + 2, + 0, + 0 + ], + "11199": [ + 867, + 5, + 2, + 11189, + 0 + ], + "11042": [ + 867, + 5, + 1, + 0, + 0 + ], + "11044": [ + 867, + 6, + 1, + 0, + 0 + ], + "11100": [ + 871, + 0, + 2, + 0, + 0 + ], + "11110": [ + 871, + 0, + 2, + 0, + 0 + ], + "11104": [ + 871, + 0, + 3, + 0, + 0 + ], + "11214": [ + 871, + 1, + 2, + 0, + 0 + ], + "11112": [ + 871, + 1, + 2, + 0, + 0 + ], + "11420": [ + 871, + 1, + 2, + 0, + 0 + ], + "11134": [ + 871, + 2, + 3, + 0, + 0 + ], + "11366": [ + 871, + 3, + 1, + 11112, + 1 + ], + "11124": [ + 871, + 3, + 3, + 0, + 0 + ], + "11142": [ + 871, + 4, + 3, + 0, + 0 + ], + "11122": [ + 871, + 4, + 2, + 0, + 0 + ], + "11419": [ + 871, + 4, + 1, + 0, + 0 + ], + "11150": [ + 871, + 5, + 3, + 0, + 0 + ], + "11200": [ + 871, + 5, + 2, + 0, + 0 + ], + "11152": [ + 871, + 6, + 1, + 0, + 0 + ] + }, + "tabs": { + "182": [ + 8, + 0 + ], + "261": [ + 64, + 0 + ], + "398": [ + 32, + 0 + ], + "746": [ + 1, + 0 + ], + "752": [ + 1024, + 0 + ], + "760": [ + 16, + 0 + ], + "799": [ + 128, + 0 + ], + "811": [ + 4, + 0 + ], + "831": [ + 2, + 0 + ], + "871": [ + 256, + 0 + ], + "181": [ + 8, + 1 + ], + "263": [ + 64, + 1 + ], + "399": [ + 32, + 1 + ], + "750": [ + 1024, + 1 + ], + "807": [ + 4, + 1 + ], + "813": [ + 16, + 1 + ], + "815": [ + 1, + 1 + ], + "839": [ + 2, + 1 + ], + "851": [ + 128, + 1 + ], + "867": [ + 256, + 1 + ], + "183": [ + 8, + 2 + ], + "262": [ + 64, + 2 + ], + "400": [ + 32, + 2 + ], + "748": [ + 1024, + 2 + ], + "795": [ + 16, + 2 + ], + "809": [ + 4, + 2 + ], + "823": [ + 128, + 2 + ], + "845": [ + 1, + 2 + ], + "855": [ + 2, + 2 + ], + "865": [ + 256, + 2 + ] + } +} diff --git a/tests/clientless/test_adventure_class_rotation.py b/tests/clientless/test_adventure_class_rotation.py new file mode 100644 index 0000000000..140298663a --- /dev/null +++ b/tests/clientless/test_adventure_class_rotation.py @@ -0,0 +1,58 @@ +"""Exercise production spell conditions, resources, procs and stable solo choices.""" +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(' bool Allowed(');b=s.index('{',a);e=b+1;depth=1 +while depth:depth+=(s[e]=='{')-(s[e]=='}');e+=1 +method=s[a:e] +code=r''' +#include "AdventureRotationPolicy.h" +#include +#include +using uint32=unsigned; +constexpr unsigned CLASS_SHAMAN=7,POWER_HOLY_POWER=1; +struct Aura{unsigned stacks=7,charges=7;unsigned GetStackAmount(){return stacks;}unsigned GetCharges(){return charges;}}aura; +struct Unit{ + std::setauras;float hp=100;bool casting=false; + bool HasAura(unsigned id){return auras.count(id);}bool GetAuraApplicationOfRankedSpell(unsigned id,unsigned){return auras.count(id);} + float GetHealthPct(){return hp;}bool HasUnitState(unsigned){return casting;} +}; +constexpr unsigned UNIT_STATE_CASTING=1; +struct Player:Unit{ + unsigned combo=0,holy=0,power=0,max=100,klass=1,spec=0; + unsigned GetComboPoints(){return combo;}unsigned GetPower(unsigned type){return type==POWER_HOLY_POWER?holy:power;} + unsigned GetPowerType(){return 2;}unsigned GetMaxPower(unsigned){return max;}unsigned getClass(){return klass;}unsigned GetGUID(){return 1;} + Aura*GetAura(unsigned id){return HasAura(id)?&aura:nullptr;} +}; +unsigned GetPlayerSpec(Player*p){return p->spec;} +struct Fighter{Player*me; +'''+method+r''' +}; +int main(){ + using namespace AdventureRotationPolicy;Player p;Unit target;Fighter fighter{&p}; + for(unsigned klass:{1,2,3,4,5,6,7,8,9,11}){ + std::setchoices;for(unsigned seed=0;seed<300;++seed){auto spec=SoloSpec(klass,seed);assert(spec==SoloSpec(klass,seed));choices.insert(spec);assert(!Damage(klass,spec).empty());} + if(klass==2 || klass==5)assert(choices==std::set{2}); + if(klass==1 || klass==4 || klass==7 || klass==8 || klass==11)assert((choices==std::set{0,1})); + if(klass==3 || klass==6)assert((choices==std::set{1,2})); + if(klass==9)assert((choices==std::set{0,1,2})); + } + assert(fighter.Allowed({45477,Condition::MissingAura,false,55095},&target));target.auras.insert(55095);assert(!fighter.Allowed({45477,Condition::MissingAura,false,55095},&target)); + Action finisher{2098,Condition::Always,false,0,5};p.combo=4;assert(!fighter.Allowed(finisher,&target));p.combo=5;assert(fighter.Allowed(finisher,&target)); + Action proc{11366,Condition::Always,false,0,0,48108};assert(!fighter.Allowed(proc,&target));p.auras.insert(48108);assert(fighter.Allowed(proc,&target)); + assert(!fighter.Allowed({5308,Condition::Execute},&target));target.hp=20;assert(fighter.Allowed({5308,Condition::Execute},&target)); + p.holy=2;assert(!fighter.Allowed({85256},&target));p.holy=3;assert(fighter.Allowed({85256},&target));p.holy=0;p.auras.insert(90174);assert(fighter.Allowed({85256},&target)); + p.hp=34;assert(fighter.Allowed({871,Condition::Emergency,true},&p));p.hp=35;assert(!fighter.Allowed({871,Condition::Emergency,true},&p)); + p.power=69;assert(!fighter.Allowed({78,Condition::Resource},&target));p.power=70;assert(fighter.Allowed({78,Condition::Resource},&target)); + p.klass=7;p.spec=0;assert(!fighter.Allowed({8042},&target));p.auras.insert(324);assert(fighter.Allowed({8042},&target));aura.stacks=9;aura.charges=6;assert(!fighter.Allowed({8042},&target)); + p.spec=1;p.auras.insert(53817);aura.stacks=4;assert(!fighter.Allowed({403},&target));aura.stacks=5;assert(fighter.Allowed({403},&target)); + p.auras.insert(67483);assert(!fighter.Allowed({2912},&target));p.auras.erase(67483);assert(fighter.Allowed({2912},&target)); + assert(Ranged(3,1) && Ranged(5,2) && !Ranged(1,2) && !Ranged(11,1)); + for(unsigned klass:{2,5,7,11})for(float hp:{20.f,60.f,85.f})assert(!Healing(klass,klass==5?1:2,hp).empty()); +} +''' +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 class rotations: persistent PvE solo pools, native rank aura IDs, combo/holy power/proc/execute/resource gates, Eclipse and healer coverage passed') diff --git a/tests/clientless/test_adventure_combat_reactions.py b/tests/clientless/test_adventure_combat_reactions.py index 2c40dc4ea4..eaf45aec77 100644 --- a/tests/clientless/test_adventure_combat_reactions.py +++ b/tests/clientless/test_adventure_combat_reactions.py @@ -20,6 +20,8 @@ code=r''' #include #include #include +#include +constexpr int UNIT_STATE_CASTING=1; using uint32=unsigned;using uint8=unsigned char; #define TC_GAME_API #define TC_LOG_INFO(...) ((void)0) @@ -33,14 +35,14 @@ struct Position{ float GetExactDist(Position const& b)const{return std::sqrt((x-b.x)*(x-b.x)+(y-b.y)*(y-b.y)+(z-b.z)*(z-b.z));} float GetExactDist(Position const* b)const{return GetExactDist(*b);} }; -struct Map{float GetHeight(int,float,float,float,bool,float){return 0;}}map; +struct Map{bool IsDungeon(){return false;}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;} + float GetOrientation(){return 0;}Unit*GetVictim(){return nullptr;}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,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;}}; @@ -54,12 +56,14 @@ struct Session{Player* owner=nullptr;int releases=0,reclaims=0;void HandleRepopR std::vector escapeCreatures; struct Group{ObjectGuid leader{26};ObjectGuid GetLeaderGUID(){return leader;}}group; struct Player:Unit{ + bool IsResurrectRequested(){return false;}void ResurrectUsingRequestData(){}void ClearResurrectRequestData(){} + bool IsWithinLOSInMap(Unit*){return los;}bool IsFlying(){return false;}bool IsFalling(){return false;}bool HasUnitState(int){return false;}void GetPartyMembers(std::list&){}bool IsInMap(Unit*){return true;} bool grouped=false;float health=100;std::set 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;}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;} + 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;}unsigned getClass(){return 1;}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;} @@ -90,16 +94,26 @@ struct 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; +struct Profile{unsigned Class=0,Specialization=0,SoloSpecialization=0;std::string SourcePath;}; +struct Lua{Profile*GetAdventureProfile(unsigned){return nullptr;}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;}};') # Forward declaration needed in Creature's adapter. code=code.replace('struct Creature;','struct Creature;struct Player;',1) +code+=r''' +struct Encounter{bool Death(Player*){return false;}bool Update(Player*,unsigned){return false;}}encounter;auto sAdventureEncounterMgr=&encounter; +struct Generator{bool PrepareCombat(Player*,std::string&){return true;}}generator;auto sAdventureGeneratorMgr=&generator; +namespace AdventureRotationPolicy{unsigned SoloSpec(unsigned,unsigned){return 0;}} +bool AdventureTank(Unit*){return false;} +struct PlayerAI{static unsigned GetPlayerSpec(Player*){return 0;}}; +''' code+=header+r''' -class AdventureCombatMgr::Fighter{public:int ticks=0;void Tick(Unit*,uint32){++ticks;}}; +class AdventureCombatMgr::Fighter{public:bool Ranged(){return false;}int ticks=0;void Tick(Unit*,uint32){++ticks;}}; +bool AdventureCombatMgr::GroupHeal(Player*,Record&){return false;} +bool AdventureCombatMgr::GroupTarget(Player*,Record&){return false;} bool AdventureCombatMgr::Reachable(Player*p,Unit*)const{return p->los;} Unit* AdventureCombatMgr::PvPTarget(Player*)const{return nullptr;} Creature* AdventureCombatMgr::Proactive(Player*)const{return nullptr;} diff --git a/tests/clientless/test_adventure_encounter.py b/tests/clientless/test_adventure_encounter.py new file mode 100644 index 0000000000..02a01c6ab5 --- /dev/null +++ b/tests/clientless/test_adventure_encounter.py @@ -0,0 +1,83 @@ +"""Run production mixed-group instance/wipe lifecycle with native-world adapters.""" +from pathlib import Path +import subprocess,tempfile +root=Path(__file__).resolve().parents[2] +s=(root/'src/server/game/World/AdventureEncounterMgr.cpp').read_text() +def function(sig): + a=s.index(sig);b=s.index('{',a);e=b+1;depth=1 + while depth:depth+=(s[e]=='{')-(s[e]=='}');e+=1 + return s[a:e] +header=(root/'src/server/game/World/AdventureEncounterMgr.h').read_text() +header='\n'.join(l for l in header.splitlines() if not l.startswith('#include')).replace('private:','public:') +code=r''' +#include +#include +#include +#include +#include +#include +#include +#include +using uint32=unsigned;using uint8=unsigned char; +#define TC_GAME_API +struct ObjectGuid{unsigned value=1;unsigned GetCounter(){return value;}}; +struct Position{float x=0,y=0,z=0,o=0;void Relocate(float a,float b,float c,float d=0){x=a;y=b;z=c;o=d;}void Relocate(Position const*p){*this=*p;}float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}float GetOrientation()const{return o;}float GetExactDist(Position const*p)const{return std::hypot(x-p->x,y-p->y);}}; +struct Session{bool clientless=true;bool IsClientless(){return clientless;}}; +struct Unit:Position{bool alive=true;unsigned map=100;bool IsAlive(){return alive;}unsigned GetMapId(){return map;}}; +struct Player; +struct Ref{Player*p;Ref*nextRef=nullptr;Player*GetSource(){return p;}Ref*next(){return nextRef;}}; +struct Group{Ref*first=nullptr;Ref*GetFirstMember(){return first;}}group; +struct Map{bool dungeon=true;bool IsDungeon(){return dungeon;}}inside,outside{false}; +constexpr unsigned PLAYER_FLAGS=0,PLAYER_FLAGS_GHOST=1,CMSG_REPOP_REQUEST=2; +struct WorldPacket{WorldPacket(int,int){}templateWorldPacket&operator<<(T){return *this;}}; +struct Corpse:Position{unsigned GetMapId(){return 100;}unsigned GetInstanceId(){return 10;}}; +struct Player:Unit{ + ObjectGuid guid;Session session;Group*groupPtr=&group;bool combat=false,ghost=false,requested=false,teleporting=false;unsigned instance=10,teleports=0,revives=0;std::listcompanions; + struct PlayerSession:Session{Player*owner=nullptr;void HandleRepopRequestOpcode(WorldPacket&){owner->ghost=true;owner->map=0;owner->Relocate(-50,0,0);}}playerSession; + Player(){playerSession.owner=this;}ObjectGuid GetGUID(){return guid;}Group*GetGroup(){return groupPtr;}PlayerSession*GetSession(){return &playerSession;} + Corpse*GetCorpse(){return nullptr;}Map*GetMap(){return map==100?&inside:&outside;}unsigned GetInstanceId(){return instance;} + bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleporting;}bool IsInMap(Unit*u){return map==u->map;} + bool IsResurrectRequested(){return requested;}void ResurrectUsingRequestData(){alive=true;ghost=false;requested=false;++revives;} + bool HasFlag(unsigned,unsigned){return ghost;}void GetPartyMembers(std::list&v){v=companions;} + bool TeleportTo(unsigned m,float x,float y,float z,float o){map=m;Relocate(x,y,z,o);++teleports;return true;} + void ResurrectPlayer(float hp,bool sickness){assert(hp==.5f && !sickness);alive=true;ghost=false;++revives;}void SpawnCorpseBones(){} +}; +struct Trigger{unsigned target_mapId;float target_X,target_Y,target_Z,target_Orientation;}; +struct MapStore{Map*LookupEntry(unsigned){return &inside;}}sMapStore; +struct Objects{Trigger entrance{100,1,0,0,0},exit{0,10,0,0,0};Trigger const*GetMapEntranceTrigger(unsigned){return &entrance;}Trigger const*GetGoBackTrigger(unsigned){return &exit;}}objects;auto sObjectMgr=&objects; +enum class ClientlessTravelState{Moving,Arrived,Stuck,PathRejected};struct Status{ClientlessTravelState State=ClientlessTravelState::Arrived;Position Target;}; +struct ClientlessPathDiagnostics{bool Complete=true,Suspicious=false;}; +struct Travel{bool present=false,path=true,accept=true;unsigned starts=0;Status status;Status*GetStatus(unsigned){return present?&status:nullptr;}void CancelTravel(unsigned){present=false;}bool AnalyzePath(Player*,Position const&,ClientlessPathDiagnostics&){return path;}bool StartTravel(Player*,Position const&p){++starts;if(accept){present=true;status.State=ClientlessTravelState::Moving;status.Target=p;}return accept;}}travel;auto sClientlessTravelMgr=&travel; +struct Behavior{unsigned actual=0;void EnterPlayerInstance(Player*){++actual;}}behavior;auto sAdventureBehaviorMgr=&behavior; +'''+header+r''' +AdventureEncounterMgr* AdventureEncounterMgr::instance(){static AdventureEncounterMgr m;return &m;} +bool AdventureEncounterMgr::AvoidGroundDamage(Player*){return false;} +bool AdventureEncounterMgr::FollowIntoInstance(Player*){return false;} +''' +for name in ('bool AdventureEncounterMgr::MixedGroup(', 'bool AdventureEncounterMgr::Active(', 'void AdventureEncounterMgr::Register(', 'void AdventureEncounterMgr::Offline(', 'bool AdventureEncounterMgr::Update(', 'bool AdventureEncounterMgr::Death('):code+=function(name)+'\n' +code+=r''' +int main(){ + AdventureEncounterMgr mgr;Player bot,human,other;human.playerSession.clientless=false;human.guid.value=2;other.guid.value=3; + Ref a{&bot},b{&human},c{&other};a.nextRef=&b;b.nextRef=&c;group.first=&a; + assert(mgr.MixedGroup(&bot) && mgr.Active(&bot));human.playerSession.clientless=true;assert(!mgr.Active(&bot));assert(!mgr.Update(&bot,500));assert(mgr._records.empty());human.playerSession.clientless=false; + assert(!mgr.Update(&bot,500));assert(mgr._records.count(1));assert(behavior.actual==1); + unsigned hook=0;mgr.Register(100,[&](Player*,unsigned){++hook;return true;});assert(mgr.Update(&bot,500) && hook==1);mgr.Register(100,{});mgr._behaviors.clear(); + bot.alive=false;bot.Relocate(35,0,0);bot.companions={&human};assert(mgr.Death(&bot));assert(!bot.ghost && bot.teleports==0); // waits while ally lives + human.alive=false;other.alive=false;bot.companions.clear();assert(mgr.Death(&bot));assert(bot.ghost && bot.map==0); + travel.path=false;assert(mgr.Death(&bot));assert(bot.map==0 && bot.x==10 && bot.teleports==1); // failed route permits entrance fallback + assert(mgr.Death(&bot));assert(bot.alive && !bot.ghost && bot.map==100 && bot.revives==1); + assert(mgr.Update(&bot,500));assert(travel.starts==0); // gather other dead bots first + other.alive=true;other.map=100;assert(mgr.Update(&bot,500));assert(travel.starts==1 && travel.status.Target.x==35); // returns to death point + human.alive=true;assert(!mgr.Update(&bot,500));assert(!mgr._records.at(1).Returning); // follows living player instead + bot.alive=false;bot.requested=true;assert(mgr.Death(&bot));assert(bot.alive && !bot.requested && bot.revives==2); + bot.groupPtr=nullptr;assert(mgr.Update(&bot,500));assert(bot.map==0 && bot.x==10); // kick exits safely + assert(!mgr.Update(&bot,500));assert(mgr._records.empty()); + bot.groupPtr=&group;bot.map=100;bot.alive=false;bot.requested=false;assert(mgr.Death(&bot));assert(mgr._records.count(1)); // login already dead still establishes context + mgr.Offline(&bot);assert(mgr._records.empty()); +} +''' +with tempfile.TemporaryDirectory() as d: + p=Path(d);(p/'test.cpp').write_text(code) + subprocess.run(['g++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) +print('Production encounters: bot-only exclusion, mixed mode, extension hook, wait/revive, wipe fallback/reentry/gather/return, living-player priority, kick exit and offline cleanup passed') diff --git a/tests/clientless/test_adventure_equipment_maintenance.py b/tests/clientless/test_adventure_equipment_maintenance.py index 8730535c94..ab5327d111 100644 --- a/tests/clientless/test_adventure_equipment_maintenance.py +++ b/tests/clientless/test_adventure_equipment_maintenance.py @@ -11,13 +11,13 @@ code=r''' #include #include using uint32=unsigned;using Archetype=AdventureGeneratorPolicy::Archetype; -enum{EQUIP_ERR_OK=0,ITEM_FLAG_CONJURED=1,HOLIDAY_NONE=0,ITEM_CLASS_ARMOR=4, +enum{CLASS_WARRIOR=1,INVTYPE_2HWEAPON=17,EQUIP_ERR_OK=0,ITEM_FLAG_CONJURED=1,HOLIDAY_NONE=0,ITEM_CLASS_ARMOR=4, EQUIPMENT_SLOT_NECK=1,EQUIPMENT_SLOT_BACK=14,EQUIPMENT_SLOT_FINGER1=10,EQUIPMENT_SLOT_FINGER2=11,EQUIPMENT_SLOT_TRINKET1=12,EQUIPMENT_SLOT_TRINKET2=13,EQUIPMENT_SLOT_OFFHAND=16, - ITEM_MOD_STRENGTH=4,ITEM_MOD_AGILITY=3,ITEM_MOD_INTELLECT=5,ITEM_MOD_ATTACK_POWER=38,ITEM_MOD_RANGED_ATTACK_POWER=39,ITEM_MOD_SPIRIT=6,ITEM_MOD_MANA=0,ITEM_MOD_MANA_REGENERATION=43,ITEM_MOD_SPELL_POWER=45,ITEM_MOD_SPELL_PENETRATION=47,MAX_ITEM_PROTO_STATS=1}; -struct Player{unsigned level=60;bool usable=true;unsigned getLevel(){return level;}templateunsigned CanUseItem(T*){return usable?0:1;}}; + ITEM_MOD_DODGE_RATING=13,ITEM_MOD_PARRY_RATING=14,ITEM_MOD_MASTERY_RATING=49,ITEM_MOD_STAMINA=7,ITEM_MOD_RESILIENCE_RATING=35,ITEM_MOD_STRENGTH=4,ITEM_MOD_AGILITY=3,ITEM_MOD_INTELLECT=5,ITEM_MOD_ATTACK_POWER=38,ITEM_MOD_RANGED_ATTACK_POWER=39,ITEM_MOD_SPIRIT=6,ITEM_MOD_MANA=0,ITEM_MOD_MANA_REGENERATION=43,ITEM_MOD_SPELL_POWER=45,ITEM_MOD_SPELL_PENETRATION=47,MAX_ITEM_PROTO_STATS=1}; +struct Player{unsigned level=60;bool usable=true;unsigned getLevel(){return level;}unsigned CalculateTalentsPoints(){return level>=70?41:26;}templateunsigned CanUseItem(T*){return usable?0:1;}}; struct Effect{unsigned SpellID;}; struct ItemTemplate{ - bool fits=true;unsigned quality=3,ilevel=80,cls=4,sub=4,flags=0,holiday=0,map=0,duration=0,rep=0,area=0,prop=0,suffix=0;int req=60,stat=4;float value=10;std::vectorEffects; + unsigned type=1;unsigned GetInventoryType()const{return type;}bool fits=true;unsigned quality=3,ilevel=80,cls=4,sub=4,flags=0,holiday=0,map=0,duration=0,rep=0,area=0,prop=0,suffix=0;int req=60,stat=4;float value=10;std::vectorEffects; int GetRequiredLevel()const{return req;}unsigned GetQuality()const{return quality;}unsigned GetBaseItemLevel()const{return ilevel;} unsigned GetDuration()const{return duration;}unsigned GetFlags()const{return flags;}unsigned GetRequiredReputationFaction()const{return rep;}unsigned GetArea()const{return area;}unsigned GetMap()const{return map;}unsigned GetHolidayID()const{return holiday;}unsigned GetRandomProperty()const{return prop;}unsigned GetRandomSuffix()const{return suffix;} unsigned GetClass()const{return cls;}unsigned GetSubClass()const{return sub;}int GetItemStatType(unsigned)const{return stat;}float GetStatValue(unsigned,Player*)const{return value;} @@ -27,6 +27,7 @@ bool UsefulSpell(unsigned id,Archetype const&){return id==42;} '''+s[a:e]+r''' int main(){Player p;ItemTemplate item;auto a=AdventureGeneratorPolicy::Archetypes()[0]; auto check=[&]{return EquipmentScore(&p,&item,4,a);};assert(check()==100); + a.Spec=1;item.type=17;assert(!std::isfinite(check()));item.type=1;assert(std::isfinite(check()));a.Spec=0; item.sub=1;assert(!std::isfinite(check()));item.sub=4; // wearable cloth is wrong for this warrior item.stat=ITEM_MOD_INTELLECT;assert(!std::isfinite(check()));item.stat=4; item.Effects={{0xFFFFFFFFu}};assert(!std::isfinite(check()));item.Effects={{42}};assert(std::isfinite(check())); @@ -36,7 +37,12 @@ int main(){Player p;ItemTemplate item;auto a=AdventureGeneratorPolicy::Archetype item.map=719;assert(!std::isfinite(check()));item.map=0; item.holiday=1;assert(!std::isfinite(check()));item.holiday=0; p.usable=false;assert(!std::isfinite(check()));p.usable=true; - item.fits=false;assert(!std::isfinite(check())); + item.fits=false;assert(!std::isfinite(check()));item.fits=true; + p.level=85;item.req=85;item.ilevel=416;item.quality=3;assert(!std::isfinite(check())); + item.quality=4;assert(std::isfinite(check()));item.ilevel=346;assert(!std::isfinite(check()));item.ilevel=416; + item.quality=5;assert(!std::isfinite(check()));item.quality=4; + a.Spec=2;item.stat=ITEM_MOD_DODGE_RATING;assert(check()==426); + } ''' # Both birth candidate ranking and retention of already equipped items consume this exact rule. diff --git a/tests/clientless/test_adventure_ground_avoidance.py b/tests/clientless/test_adventure_ground_avoidance.py new file mode 100644 index 0000000000..22d1051c2a --- /dev/null +++ b/tests/clientless/test_adventure_ground_avoidance.py @@ -0,0 +1,47 @@ +"""Execute generic harmful-ground detection and safe-path choice from production.""" +from pathlib import Path +import subprocess,tempfile +root=Path(__file__).resolve().parents[2] +s=(root/'src/server/game/World/AdventureEncounterMgr.cpp').read_text();a=s.index('bool AdventureEncounterMgr::AvoidGroundDamage(');b=s.index('{',a);e=b+1;depth=1 +while depth:depth+=(s[e]=='{')-(s[e]=='}');e+=1 +code=r''' +#include +#include +#include +#include +using uint32=unsigned; +constexpr unsigned GRID_MAP_TYPE_MASK_DYNAMICOBJECT=1,SPELL_EFFECT_SCHOOL_DAMAGE=1,SPELL_AURA_PERIODIC_DAMAGE=2,SPELL_AURA_PERIODIC_DAMAGE_PERCENT=3; +constexpr float INVALID_HEIGHT=-100000; +struct Position{float m_positionX=0,m_positionY=0,m_positionZ=0,o=0;Position()=default;Position(float x,float y,float z,float d):m_positionX(x),m_positionY(y),m_positionZ(z),o(d){}float GetPositionX()const{return m_positionX;}float GetPositionY()const{return m_positionY;}float GetPositionZ()const{return m_positionZ;}float GetOrientation()const{return o;}float GetExactDist2d(Position const*p)const{return std::hypot(m_positionX-p->m_positionX,m_positionY-p->m_positionY);}}; +struct Dynamic;struct Unit{}; +struct WorldObject:Position{virtual Dynamic*ToDynObject(){return nullptr;}}; +struct Effect{unsigned Effect=0,ApplyAuraName=0;};struct Spell{std::vectorEffects;}; +struct Dynamic:WorldObject{bool hostile=true,phase=true;float radius=6;Unit caster;Spell spell{{{0,SPELL_AURA_PERIODIC_DAMAGE}}};Dynamic*ToDynObject()override{return this;}Unit*GetCaster(){return &caster;}Spell*GetSpellInfo(){return &spell;}float GetRadius(){return radius;}}; +struct Guid{unsigned GetCounter(){return 1;}}; +struct Map{bool invalid=false;float GetHeight(unsigned,float,float,float,bool,float){return invalid?INVALID_HEIGHT:0;}}map; +std::vector areaObjects; +struct Player:Position{unsigned stops=0;Guid GetGUID(){return {};}Map*GetMap(){return ↦}unsigned GetPhaseShift(){return 169;}bool IsInPhase(Dynamic*d){return d->phase;}bool IsHostileTo(Unit*u){for(auto obj:areaObjects)if(auto d=obj->ToDynObject())if(&d->caster==u)return d->hostile;return false;}void CastStop(){++stops;}}; +namespace Trinity{templatestruct WorldObjectListSearcher{std::vector&objects;Check✓WorldObjectListSearcher(Player*,std::vector&v,Check&c,unsigned):objects(v),check(c){}};} +namespace Cell{templatevoid VisitAllObjects(Player*,T&search,float){for(auto obj:areaObjects)if(search.check(obj))search.objects.push_back(obj);}} +enum class ClientlessTravelState{Moving,Arrived};struct Status{ClientlessTravelState State=ClientlessTravelState::Moving;Position Target;}; +struct ClientlessPathDiagnostics{bool Complete=true,Suspicious=false;float PathLength=0;}; +struct Travel{bool present=false,path=true;unsigned starts=0;Status status;Status*GetStatus(unsigned){return present?&status:nullptr;}bool AnalyzePath(Player*p,Position const&point,ClientlessPathDiagnostics&d){d.PathLength=p->GetExactDist2d(&point);return path;}bool StartTravel(Player*,Position const&point){++starts;present=true;status.Target=point;return true;}}travel;auto sClientlessTravelMgr=&travel; +struct AdventureEncounterMgr{bool AvoidGroundDamage(Player*);}; +'''+s[a:e]+r''' +int main(){ + AdventureEncounterMgr mgr;Player p;Dynamic hostile,friendly;areaObjects={&hostile,&friendly};friendly.hostile=false;friendly.m_positionX=10; + assert(mgr.AvoidGroundDamage(&p));assert(travel.starts==1 && p.stops==1 && travel.status.Target.GetExactDist2d(&hostile)>=8); + assert(mgr.AvoidGroundDamage(&p));assert(travel.starts==1 && p.stops==1); // existing safe destination retained + p.m_positionX=20;assert(!mgr.AvoidGroundDamage(&p)); + p.m_positionX=0;travel.present=false;travel.path=false;assert(!mgr.AvoidGroundDamage(&p));assert(travel.starts==1); + travel.path=true;map.invalid=true;assert(!mgr.AvoidGroundDamage(&p));map.invalid=false; + hostile.phase=false;assert(!mgr.AvoidGroundDamage(&p));hostile.phase=true; + hostile.m_positionZ=8;assert(!mgr.AvoidGroundDamage(&p));hostile.m_positionZ=0; + hostile.spell.Effects.clear();assert(!mgr.AvoidGroundDamage(&p)); +} +''' +with tempfile.TemporaryDirectory() as d: + p=Path(d);(p/'test.cpp').write_text(code) + subprocess.run(['g++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) +print('Production ground avoidance: harmful/friendly, phase169, height, reachable safe endpoint, rejected paths and reuse without cast/movement thrashing passed') diff --git a/tests/clientless/test_adventure_intent_state.py b/tests/clientless/test_adventure_intent_state.py index 0f014e1d7e..1917541f96 100644 --- a/tests/clientless/test_adventure_intent_state.py +++ b/tests/clientless/test_adventure_intent_state.py @@ -31,6 +31,7 @@ struct Player { } player; namespace ObjectAccessor{Player*FindPlayer(Guid){return nullptr;}} struct BotGroups{bool BotLed(Player*){return false;}bool FollowMember(Player*){return false;}}botGroups;auto sAdventureGroupMgr=&botGroups; +struct Encounters{bool active=false;bool Active(Player*){return active;}}encounters;auto sAdventureEncounterMgr=&encounters; bool unsafe=false;bool Unsafe(Player*){return unsafe;} bool busy=false;bool Busy(uint32){return busy;} time_t now=100;int hour=5; @@ -66,12 +67,14 @@ public: 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 EnterPlayerInstance(Player*); 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;} void Begin(Player*,Record&){++begun;} void Fallback(Player*,Record&){++fallbacks;} void ResetMicro(Player*,Record&r){++resets;r.Micro.clear();} void Micro(Player*,Record&){++micros;} + void FinishInstance(Player*,Record&r){r.Instance.Active=false;} bool UpdateInstance(Player*,Record&,time_t){return false;} bool UpdateLeisure(Player*,Record&,uint32){++leisureUpdates;return true;} bool UpdateSocial(Player*,Record&,uint32){return true;} @@ -79,9 +82,15 @@ public: void Queue(Player*,Record&,std::string const&,std::string const&,std::string const&,bool=false){++queues;} }; ''' +start=s.index('void AdventureBehaviorMgr::EnterPlayerInstance(');end=s.index('void AdventureBehaviorMgr::RefreshDay(',start) +code+=s[start:end] code+=s[a:b]+r''' int main(){ AdventureBehaviorMgr mgr;auto& r=mgr._records.at(26); + r.Instance.Active=true;mgr.EnterPlayerInstance(&player);assert(!r.Instance.Active); + encounters.active=true;r.Schedule=true;r.Deadline=1;r.Plan[6]={"profession","mine"};hour=6; + mgr.Update(&player,1000);assert(mgr.switches==0 && mgr.micros==0 && r.Hour==5); + encounters.active=false;r.Schedule=false;r.Deadline=1000;hour=5; // A completed travel at the exact location opens micro actions. mgr.Update(&player,1000);assert(r.State==AdventureBehaviorMgr::Phase::OnSite&&mgr.micros==1); // Best-effort graph progress must not pretend an area has been reached. diff --git a/tests/clientless/test_adventure_manager_inventory.py b/tests/clientless/test_adventure_manager_inventory.py index cba6d1f265..c7c09f940e 100644 --- a/tests/clientless/test_adventure_manager_inventory.py +++ b/tests/clientless/test_adventure_manager_inventory.py @@ -15,9 +15,10 @@ using uint8=unsigned char;using uint32=unsigned; constexpr unsigned ITEM_CLASS_CONTAINER=1,ITEM_CLASS_WEAPON=2,ITEM_SUBCLASS_WEAPON_FISHING_POLE=3; constexpr unsigned INVENTORY_SLOT_ITEM_START=0,INVENTORY_SLOT_ITEM_END=4,INVENTORY_SLOT_BAG_START=4,INVENTORY_SLOT_BAG_END=6,INVENTORY_SLOT_BAG_0=255; struct Proto{unsigned cls=0,sub=0;unsigned GetClass(){return cls;}unsigned GetSubClass(){return sub;}}; -struct Item{unsigned entry,count=1;Proto proto;Proto*GetTemplate(){return &proto;}unsigned GetEntry(){return entry;}unsigned GetCount(){return count;}}; +struct Guid{unsigned value=50;unsigned GetCounter(){return value;}}; +struct Item{Guid GetGUID(){return {entry};}unsigned entry,count=1;Proto proto;Proto*GetTemplate(){return &proto;}unsigned GetEntry(){return entry;}unsigned GetCount(){return count;}}; struct Bag{unsigned GetBagSize(){return 8;}}; -struct Guid{unsigned GetCounter(){return 50;}}; + struct Player{ unsigned free=0,destroys=0;std::map,Item>items;Bag bag; unsigned GetFreeInventorySpace(){return free;}Guid GetGUID(){return {};} @@ -29,16 +30,17 @@ struct Player{ struct Profile{std::unordered_setKeepItems{2901};unsigned CompanionLeashItem=37460;}profile; struct Lua{Profile*GetAdventureProfile(unsigned){return &profile;}}lua;auto sClientlessLuaEngine=&lua; struct Supply{std::unordered_mapInventoryAllowance(Player*){return {{100,20},{101,20}};}}supply;auto sAdventureSupplyMgr=&supply; +struct Generator{std::unordered_setProtectedEquipment(Player*){return {555};}}generator;auto sAdventureGeneratorMgr=&generator; struct AdventurePopulationMgr{void CleanInventory(Player*);}; '''+s[a:e]+r''' int main(){ Player p;AdventurePopulationMgr mgr; p.items={{ {255,0},Item{100,15}},{{255,1},Item{999,1}},{{255,2},Item{2901,1}},{{255,3},Item{37460,1}}, {{4,0},Item{100,10}},{{4,1},Item{100,20}},{{4,2},Item{101,20}},{{4,3},Item{6256,1,{ITEM_CLASS_WEAPON,ITEM_SUBCLASS_WEAPON_FISHING_POLE}}}, - {{4,4},Item{54443,1,{ITEM_CLASS_CONTAINER,0}}},{{4,5},Item{777,1,{ITEM_CLASS_WEAPON,0}}},{{0,10},Item{888,1}}}; - p.free=1;mgr.CleanInventory(&p);assert(!p.destroys && p.items.size()==11); + {{4,4},Item{54443,1,{ITEM_CLASS_CONTAINER,0}}},{{4,5},Item{777,1,{ITEM_CLASS_WEAPON,0}}},{{4,6},Item{555,1,{ITEM_CLASS_WEAPON,0}}},{{0,10},Item{888,1}}}; + p.free=1;mgr.CleanInventory(&p);assert(!p.destroys && p.items.size()==12); p.free=0;mgr.CleanInventory(&p); - assert(!p.GetItemByPos(255,1));assert(!p.GetItemByPos(4,1));assert(!p.GetItemByPos(4,5)); + assert(p.GetItemByPos(4,6));assert(!p.GetItemByPos(255,1));assert(!p.GetItemByPos(4,1));assert(!p.GetItemByPos(4,5)); assert(p.GetItemByPos(255,0)->count==15 && p.GetItemByPos(4,0)->count==5); for(auto pos:{std::pair{255,2},std::pair{255,3},std::pair{4,2},std::pair{4,3},std::pair{4,4},std::pair{0,10}})assert(p.GetItemByPos(pos.first,pos.second)); } diff --git a/tests/clientless/test_adventure_talent_templates.py b/tests/clientless/test_adventure_talent_templates.py new file mode 100644 index 0000000000..c3c1fae850 --- /dev/null +++ b/tests/clientless/test_adventure_talent_templates.py @@ -0,0 +1,46 @@ +"""Exercise real PvE templates against 4.3.4 DBC rank, tree and dependency gates.""" +from pathlib import Path +import json,subprocess,tempfile +root=Path(__file__).resolve().parents[2] +data=json.loads((Path(__file__).parent/'fixtures/cata_pve_talents.json').read_text()) +rows=','.join('{'+k+',{'+','.join(map(str,v))+'}}' for k,v in data['talents'].items()) +tabs=','.join('{'+k+',{'+','.join(map(str,v))+'}}' for k,v in data['tabs'].items()) +code=r''' +#include "AdventureTalentPolicy.h" +#include "AdventureRotationPolicy.h" +#include +#include +struct Talent{unsigned tab,tier,ranks,dependency,dependencyRank;}; +struct Tab{unsigned mask,order;}; +std::map talents={ROWS};std::map tabs={TABS}; +int main(){ + for(unsigned klass:{1,2,3,4,5,6,7,8,9,11})for(unsigned spec=0;spec<3;++spec)for(bool tank:{false,true}){ + auto build=AdventureTalentPolicy::Build(klass,spec,tank);if(build.empty())continue; + unsigned primary=0;for(auto const& t:tabs)if(t.second.mask==(1u<<(klass-1)) && t.second.order==spec)primary=t.first; + assert(primary);unsigned total=0; + for(auto t:build){assert(talents.count(t.Id));auto d=talents.at(t.Id);assert(t.Ranks<=d.ranks);assert(tabs.at(d.tab).mask==(1u<<(klass-1)));total+=t.Ranks;} + assert(total==41); + for(unsigned budget=1;budget<=41;++budget){ + std::map known;unsigned left=budget; + auto learn=[&](unsigned id,unsigned rank){ + auto d=talents.at(id);if(known[id]>rank || rank>=d.ranks)return false; + auto cost=rank+1-known[id];if(cost>left)return false; + if(d.dependency && known[d.dependency]<=d.dependencyRank)return false; + unsigned spent=0,main=0;for(auto p:known){if(talents.at(p.first).tab==d.tab)spent+=p.second;if(talents.at(p.first).tab==primary)main+=p.second;} + if(spent +#include +#include +using uint32=unsigned;constexpr int MAX_SPELL_EFFECTS=3,MECHANIC_NONE=0; +struct WorldObject{}; +struct SpellEffectInfo{ + void const*_spellInfo;unsigned _effIndex,Effect,ApplyAuraName,AuraPeriod,DieSides; + float RealPointsPerLevel,BasePoints,PointsPerComboPoint,Amplitude,DamageMultiplier,BonusMultiplier; + int MiscValue,MiscValueB,Mechanic;void const*TargetARadiusEntry;void const*TargetBRadiusEntry; + unsigned ChainTarget,ItemType,TriggerSpell;void*ImplicitTargetConditions; + struct{float Coefficient,Variance,ComboPointsCoefficient;}Scaling; +'''+constructor+r''' + bool IsEffect()const{return Effect!=0;} + int CalcBaseValue(WorldObject const*,void const*)const{assert(_spellInfo && IsEffect());return int(BasePoints);} +}; +struct SpellInfo{SpellEffectInfo Effects[3];unsigned MaxAffectedTargets=0;}; +struct SpellValue{int EffectBasePoints[3];unsigned CustomBasePointsMask,MaxAffectedTargets;float RadiusMod,AuraStackAmount,CriticalChance;SpellValue(SpellInfo const*,WorldObject const*);}; +'''+s[a:e]+r''' +int main(){ + alignas(SpellEffectInfo) unsigned char memory[sizeof(SpellEffectInfo)];memset(memory,0xA5,sizeof(memory)); + auto empty=new(memory)SpellEffectInfo; + assert(!empty->_spellInfo && !empty->Effect && empty->Scaling.Coefficient==0 && empty->Scaling.Variance==0 && empty->Scaling.ComboPointsCoefficient==0); + SpellInfo spell;spell.Effects[0]._spellInfo=&spell;spell.Effects[0].Effect=1;spell.Effects[0].BasePoints=7; + spell.Effects[1]._spellInfo=&spell;spell.Effects[1].Effect=2;spell.Effects[1].BasePoints=12; + // Exact state produced by the 56342 native correction. + spell.Effects[2]=SpellEffectInfo();SpellValue value(&spell,nullptr); + assert(value.EffectBasePoints[0]==7 && value.EffectBasePoints[1]==12 && value.EffectBasePoints[2]==0); + spell.Effects[0]=SpellEffectInfo();spell.Effects[1]=SpellEffectInfo();SpellValue allEmpty(&spell,nullptr); + for(auto points:allEmpty.EffectBasePoints)assert(points==0); +} +''' +with tempfile.TemporaryDirectory() as d: + p=Path(d);(p/'test.cpp').write_text(code) + subprocess.run(['g++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True) + subprocess.run([str(p/'test')],check=True) +print('Native empty spell effects: poisoned-memory scaling initialization, Lock and Load cleared slot, active effect values and all-empty spell values passed')