Implement AdventureBot class rotations, group roles and instance encounters
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s

This commit is contained in:
Nico committed 2026-10-28 22:37:18 +01:00
1 parent d3273c677e
commit a758239da9
33 files changed
+5102 -66

No files matched your search

+69
View File
@@ -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
@@ -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
@@ -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&)::<lambda(uint32, unsigned int)>, (anonymous namespace)::Talents(Player*, const AdventureGeneratorPolicy::Archetype&)::<lambda()> >(const std::vector<AdventureTalentPolicy::Talent, std::allocator<AdventureTalentPolicy::Talent> > &, 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 <AdventureGeneratorMgr::instance()::mgr>, 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 <AdventureGeneratorMgr::instance()::mgr>, player=0x7fffaa8bd000, error="") at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/World/AdventureGeneratorMgr.cpp:601
@@ -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.
@@ -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<char>]’:
/tmp/tmpk5e83d51/test.cpp:22:126: required from ‘void Tx::PAppend(const char*, A ...) [with A = {}]’
22 | struct Tx{std::vector<std::string>sql;template<class...A>void PAppend(char const*f,A...a){sql.push_back(Trinity::StringFormat(f,a...));}};
| ~~~~~~~~~~~~~~~~~~~~~^~~~~~~~
/tmp/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{template<class...A>std::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 []
+146
View File
@@ -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.
+2 -1
View File
@@ -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;
+1 -1
View File
@@ -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);
@@ -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;
@@ -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);
+391 -19
View File
@@ -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 <algorithm>
#include <cmath>
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<Unit*> AdventureAllies(Player* player)
{
std::list<Unit*> 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<Unit*> subgroup;member->GetPartyMembers(subgroup);
result.splice(result.end(),subgroup);
}
result.sort([](Unit* a,Unit* b){return a->GetGUID()<b->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<Creature*> 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()<action.Combo)return false;
if((action.Spell==85256 || action.Spell==53600 || action.Spell==85222) &&
me->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<Creature*> 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)<radius || enemy->GetExactDist(me)<radius))
{delete cast.first;return false;}
}
SpellCastTargets targets;targets.SetUnitTarget(cast.second);
if(info->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<Unit*> 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<<uint8(0);p->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()<tank->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)<p->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<Creature*> 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)<p->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<Fighter>(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<size_t>(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<Fighter>(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());
}
+5 -2
View File
@@ -29,13 +29,14 @@ private:
class Fighter;
struct Record
{
std::shared_ptr<Fighter> AI;
std::shared_ptr<Fighter> AI,Support;
ObjectGuid Target,Helper;
std::deque<ObjectGuid> 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<Position> 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<uint32,Record> _records;
};
@@ -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 <cmath>
#include <vector>
#include <utility>
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<Unit*> 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<<uint8(0);p->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 || score<distance){entrance=area;distance=score;}
}
if(!entrance)return false;
if(p->IsInAreaTriggerRadius(entrance))
{
WorldPacket packet(CMSG_AREATRIGGER,4);packet<<entrance->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<WorldObject*> objects;
auto check=[](WorldObject* object){return object->ToDynObject()!=nullptr;};
Trinity::WorldObjectListSearcher<decltype(check)> searcher(p,objects,check,GRID_MAP_TYPE_MASK_DYNAMICOBJECT);
Cell::VisitAllObjects(p,searcher,60);
struct Danger{Position Center;float Radius;};std::vector<Danger> 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)<d.Radius)return false;return true;};
if(dangers.empty() || safe(*p))return false;
auto current=sClientlessTravelMgr->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.PathLength<shortest)
{shortest=path.PathLength;best=point;}
}
if(shortest==1e30f)return false;
p->CastStop();return sClientlessTravelMgr->StartTravel(p,best);
}
@@ -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 <functional>
#include <unordered_map>
class Player;
// Adventure policy only. DungeonBot CreatureAI and generic CPC stay independent.
class TC_GAME_API AdventureEncounterMgr
{
public:
using Behavior=std::function<bool(Player*,uint32)>;
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<uint32,Record> _records;
std::unordered_map<uint32,Behavior> _behaviors;
};
#define sAdventureEncounterMgr AdventureEncounterMgr::instance()
#endif
+146 -24
View File
@@ -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<uint32,uint32> 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;tree<MAX_TALENT_TABS && p->GetFreeTalentPoints();++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<TalentEntry const*> 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)<std::tie(y->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;rank<MAX_TALENT_RANK && p->GetFreeTalentPoints();++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<float>::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;index<MAX_ITEM_PROTO_STATS;++index)
{
auto type=item->GetItemStatType(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;slot<EQUIPMENT_SLOT_END;++slot)
{
if(missingOnly)
if(auto current=p->GetItemByPos(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<float>::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;bagSlot<INVENTORY_SLOT_ITEM_END;++bagSlot)
consider(p->GetItemByPos(INVENTORY_SLOT_BAG_0,bagSlot));
for(uint8 bag=INVENTORY_SLOT_BAG_START;bag<INVENTORY_SLOT_BAG_END;++bag)
if(auto container=p->GetBagByPos(bag))for(uint8 bagSlot=0;bagSlot<container->GetBagSize();++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<Candidate> 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<uint32> AdventureGeneratorMgr::ProtectedEquipment(Player* player) const
{
std::unordered_set<uint32> result;
auto profile=sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter());
if(!profile || profile->SourcePath.find("generated:")!=0)return result;
std::vector<Item*> items;
for(uint8 slot=INVENTORY_SLOT_ITEM_START;slot<INVENTORY_SLOT_ITEM_END;++slot)
if(auto item=player->GetItemByPos(INVENTORY_SLOT_BAG_0,slot))items.push_back(item);
for(uint8 bag=INVENTORY_SLOT_BAG_START;bag<INVENTORY_SLOT_BAG_END;++bag)
if(auto container=player->GetBagByPos(bag))for(uint8 slot=0;slot<container->GetBagSize();++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<EQUIPMENT_SLOT_END;++slot)
{
float best=-std::numeric_limits<float>::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)
{
@@ -4,6 +4,7 @@
#include "AdventureGeneratorPolicy.h"
#include <string>
#include <functional>
#include <unordered_set>
class Player;
class ChatHandler;
class TC_GAME_API AdventureGeneratorMgr
@@ -17,6 +18,8 @@ public:
using Completion=std::function<void(uint32,std::string)>;
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<uint32> 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={});
@@ -7,6 +7,7 @@
#include <iomanip>
#include <sstream>
#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<unsigned,2> Skills;
char const* Vocation;
char const* RoleOverride = nullptr;
};
inline std::array<Archetype,10> 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 = "<<quote(name)<<", home_region = "<<quote(region)
<<", profile_version = 2, level = "<<level<<", class = "<<a.Class<<", specialization = "<<a.Spec
<<", solo_specialization = "<<AdventureRotationPolicy::SoloSpec(a.Class,seed)
<<", role = "<<quote(Role(a))<<", seed = "<<seed<<", home_zone = "<<homeZone
<<", home_capital = "<<Capital(race)<<", favorite_zone = "<<favoriteZone<<", favorite_region = "<<quote(favoriteRegion)
<<", vocation = "<<quote(a.Vocation)<<", background = "
@@ -229,11 +229,12 @@ void AdventurePopulationMgr::CleanInventory(Player* p)
if(p->GetFreeInventorySpace()>0)return;
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
auto allowance=sAdventureSupplyMgr->InventoryAllowance(p);
auto equipment=sAdventureGeneratorMgr->ProtectedEquipment(p);
auto clean=[&](uint8 bag,uint8 slot)
{
auto item=p->GetItemByPos(bag,slot);if(!item)return;
auto proto=item->GetTemplate();
bool keep=proto->GetClass()==ITEM_CLASS_CONTAINER ||
bool keep=equipment.count(item->GetGUID().GetCounter()) || proto->GetClass()==ITEM_CLASS_CONTAINER ||
(proto->GetClass()==ITEM_CLASS_WEAPON && proto->GetSubClass()==ITEM_SUBCLASS_WEAPON_FISHING_POLE) ||
(profile && (profile->KeepItems.count(item->GetEntry()) || item->GetEntry()==profile->CompanionLeashItem));
if(auto it=allowance.find(item->GetEntry());it!=allowance.end())
@@ -0,0 +1,85 @@
#ifndef VCORE_ADVENTURE_ROTATION_POLICY_H
#define VCORE_ADVENTURE_ROTATION_POLICY_H
#include <vector>
#include <cstdint>
namespace AdventureRotationPolicy
{
enum class Condition { Always, MissingAura, Execute, Injured, Emergency, Resource, LowResource, Interrupt };
struct Action { uint32_t Spell; Condition When = Condition::Always; bool Self = false; uint32_t Aura = 0; unsigned Combo = 0; uint32_t Proc = 0; };
// Stable, shared PvE priorities. Native spell validation handles level,
// resources, procs, cooldowns and learned ranks; no random action selection.
inline unsigned SoloSpec(unsigned klass, uint32_t seed)
{
switch (klass)
{
case 1: case 4: case 7: case 8: case 11: return seed % 2;
case 3: case 6: return 1 + seed % 2;
case 2: case 5: return 2;
case 9: return seed % 3;
default: return 0;
}
}
inline bool Ranged(unsigned klass, unsigned spec)
{ return klass==3 || klass==5 || klass==8 || klass==9 || (klass==7 && spec!=1) || (klass==11 && spec!=1); }
inline std::vector<Action> Damage(unsigned klass, unsigned spec)
{
using C=Condition;
switch (klass)
{
case 1:
if(spec==2)return {{71,C::MissingAura,true},{871,C::Emergency,true},{12975,C::Emergency,true},{2565,C::Injured,true},{23922},{6572},{772,C::MissingAura},{20243},{78,C::Resource}};
if(spec==1)return {{2458,C::MissingAura,true},{23881},{85288},{5308,C::Execute},{86346},{1464,C::Always,false,0,0,46916},{78,C::Resource}};
return {{2457,C::MissingAura,true},{772,C::MissingAura},{12294},{86346},{7384},{5308,C::Execute},{1464},{78,C::Resource}};
case 2:
if(spec==0)return {{20271},{879},{35395}};
if(spec==1)return {{25780,C::MissingAura,true},{498,C::Injured,true},{31850,C::Emergency,true},{31935},{53600},{35395},{20271},{24275,C::Execute}};
return {{85256},{35395},{24275,C::Execute},{20271},{879}};
case 3:
if(spec==2)return {{1978,C::MissingAura},{53301},{3674},{53351,C::Execute},{77767},{3044}};
return {{1978,C::MissingAura},{53209},{53351,C::Execute},{19434},{56641},{3044}};
case 4:
if(spec==0)return {{5171,C::MissingAura,true,0,3},{1943,C::MissingAura,false,0,1},{32645,C::Always,false,0,5},{2098,C::Always,false,0,5},{1329},{1752}};
return {{5171,C::MissingAura,true,0,3},{1943,C::MissingAura,false,0,5},{2098,C::Always,false,0,5},{84617},{1752}};
case 5:
if(spec!=2)return {{585}};
return {{15473,C::MissingAura,true},{34914,C::MissingAura},{589,C::MissingAura},{2944,C::MissingAura},{8092},{32379,C::Execute},{15407},{585}};
case 6:
if(spec==0)return {{48263,C::MissingAura,true},{48792,C::Emergency,true},{45462,C::MissingAura,false,55078},{45477,C::MissingAura,false,55095},{49998},{55050},{47541}};
if(spec==1)return {{48265,C::MissingAura,true},{45477,C::MissingAura,false,55095},{45462,C::MissingAura,false,55078},{49020},{49143},{49184},{47541}};
return {{48265,C::MissingAura,true},{45477,C::MissingAura,false,55095},{45462,C::MissingAura,false,55078},{55090},{85948},{47541}};
case 7:
if(spec==2)return {{403}};
if(spec==1)return {{17364},{60103},{8050,C::MissingAura},{8042},{73899},{403}};
return {{324,C::MissingAura,true},{8050,C::MissingAura},{51505},{8042},{403}};
case 8:
if(spec==1)return {{30482,C::MissingAura,true},{44457,C::MissingAura},{11366,C::Always,false,0,0,48108},{2136},{133}};
return {{6117,C::MissingAura,true},{12042,C::Resource,true},{30451},{5143,C::Always,false,0,0,79808},{44425}};
case 9:
if(spec==1)return {{28176,C::MissingAura,true},{348,C::MissingAura},{172,C::MissingAura},{603,C::MissingAura},{71521},{29722},{686}};
if(spec==2)return {{28176,C::MissingAura,true},{348,C::MissingAura},{17962},{50796},{29722},{17877,C::Execute}};
return {{28176,C::MissingAura,true},{172,C::MissingAura},{30108,C::MissingAura},{980,C::MissingAura},{48181},{1120,C::Execute},{686}};
case 11:
if(spec==2)return {{5176}};
if(spec==1)return {{768,C::MissingAura,true},{5217,C::LowResource,true},{52610,C::MissingAura,true,0,3},{33876,C::MissingAura},{1079,C::MissingAura,false,0,5},{1822,C::MissingAura},{22568,C::Always,false,0,5},{5221},{33876},{1082}};
return {{24858,C::MissingAura,true},{8921,C::MissingAura},{5570,C::MissingAura},{78674},{2912},{5176}};
default:return {};
}
}
inline std::vector<Action> Healing(unsigned klass, unsigned spec, float hp)
{
using C=Condition;
switch(klass)
{
case 2: if(hp<30)return {{633},{20473},{19750},{82326},{635}}; return {{20473},{82326},{635},{19750}};
case 5:
if(spec==0 && hp<40)return {{33206},{17,C::MissingAura},{47540},{2061},{2060}};
if(hp<35)return {{47788},{2061},{2050}};
if(hp<70)return {{33076,C::MissingAura},{47540},{2060},{2050}};
return {{139,C::MissingAura},{2050},{2061}};
case 7: if(hp<35)return {{61295},{8004},{77472},{331}}; return {{61295,C::MissingAura},{77472},{331}};
case 11: if(hp<35)return {{18562},{8936},{5185}}; return {{33763,C::MissingAura},{774,C::MissingAura},{50464},{5185}};
default:return {};
}
}
}
#endif
+1 -1
View File
@@ -244,7 +244,7 @@ bool AdventureSupplyMgr::Update(Player* p,uint32 diff,bool combatReaction)
}
return false; // Native combat and attacks keep running.
}
if(combatReaction)return false;
if(combatReaction){if(r.Recovering)Cancel(p,r);return false;}
if((r.LastDay!=Day(now) || r.AfterCombat) && now>=r.RetryStock)
{
if(!Stock(p,r,Day(now)))
@@ -0,0 +1,58 @@
#ifndef VCORE_ADVENTURE_TALENT_POLICY_H
#define VCORE_ADVENTURE_TALENT_POLICY_H
#include <vector>
#include <cstdint>
namespace AdventureTalentPolicy
{
struct Talent {uint32_t Id;unsigned Ranks;};
// Cataclysm PvE templates, checked against the local 4.3.4 Talent.dbc.
// Source URLs and retrieval date: doc/clientless/class-combat.md.
template<class Learn,class Remaining>
bool Spend(std::vector<Talent> const& build,Learn learn,Remaining remaining)
{
for(unsigned pass=0;pass<50 && remaining();++pass)
{
bool progress=false;
for(auto const& talent:build)
for(unsigned rank=0;rank<talent.Ranks && remaining();++rank)
if(learn(talent.Id,rank))progress=true;
if(!progress)break;
}
return !remaining();
}
inline std::vector<Talent> Build(unsigned klass,unsigned spec,bool tank=false)
{
switch(klass*10+spec)
{
case 10: return {{9664,2},{10134,3},{8176,3},{8184,1},{11416,2},{8192,1},{10138,3},{10741,2},{9662,2},{11223,1},{11418,2},{8208,1},{8214,2},{10520,3},{8194,2},{8222,1},{9606,3},{9608,2},{9644,2},{10464,1},{10480,2}};
case 11: return {{9606,3},{9608,2},{9624,2},{9644,2},{11415,2},{9612,3},{9630,1},{9636,3},{9648,1},{9650,1},{11208,1},{9642,2},{10743,2},{9654,3},{10744,2},{9658,1},{9664,2},{10134,3},{8176,3},{10464,2}};
case 12: return {{10464,3},{10474,3},{10480,2},{10466,1},{10472,3},{11170,2},{10478,1},{10482,1},{10494,1},{10934,2},{10470,2},{10486,1},{10484,2},{10488,2},{10492,1},{10496,3},{10498,1},{10134,3},{11163,2},{8176,3},{9608,2}};
case 20: return {{10127,3},{12189,3},{10097,1},{11213,3},{10129,2},{11202,1},{11771,2},{10113,1},{10121,1},{10133,1},{11215,3},{10115,1},{11779,3},{12151,2},{11168,3},{11203,1},{12198,3},{10651,3},{11612,2},{11611,2}};
case 21: return {{10324,2},{12198,3},{10332,3},{10372,2},{10344,2},{10346,3},{10374,1},{11159,2},{11161,2},{11193,2},{11607,1},{10334,1},{10356,1},{10680,1},{10340,3},{10370,2},{10350,1},{10651,3},{11612,2},{11269,2},{11611,2}};
case 22: return {{10647,2},{10651,3},{11612,2},{11269,3},{11611,2},{10661,3},{10665,1},{11204,1},{11610,2},{10643,1},{10669,3},{11207,1},{11372,1},{10633,2},{10677,3},{11211,1},{11222,1},{10099,2},{10127,3},{11780,2},{10324,2}};
case 31: return {{9380,3},{9390,2},{9396,2},{9398,2},{9402,3},{11225,3},{9408,2},{9412,1},{9416,2},{9404,1},{9418,3},{9422,2},{9426,2},{9428,2},{9430,1},{9490,3},{9492,2},{9512,2},{9432,3}};
case 32: return {{9432,3},{9450,2},{9444,2},{10753,3},{9452,2},{9484,3},{9460,3},{9462,2},{9464,2},{9468,1},{9474,2},{9476,1},{9478,3},{11698,1},{9480,1},{9490,3},{9380,3},{9390,2},{9396,2}};
case 40: return {{269,3},{276,3},{273,3},{277,3},{1721,2},{280,1},{682,3},{283,2},{11209,1},{281,1},{1715,1},{6516,2},{2070,3},{6517,2},{2071,1},{181,2},{244,2},{2244,3},{261,3}};
case 41: return {{181,3},{201,3},{6395,2},{1122,3},{1827,2},{186,3},{203,2},{11171,1},{1825,3},{205,1},{2074,2},{6513,2},{11174,3},{2076,1},{269,2},{276,3},{273,2},{2244,3}};
case 50: return {{8577,3},{8595,3},{10736,2},{8593,2},{8607,2},{11608,1},{8591,1},{8611,1},{11224,2},{11812,2},{8617,3},{11523,2},{8609,3},{8623,1},{8625,2},{8603,1},{9549,3},{9553,2},{9561,2},{9032,3}};
case 51: return {{9549,3},{9553,3},{10746,2},{9561,2},{11669,1},{11765,2},{9577,2},{9593,2},{11666,1},{12184,2},{9573,2},{14738,1},{11667,1},{11755,1},{9595,1},{9597,3},{11668,2},{9601,1},{8577,3},{8595,3},{9032,3}};
case 52: return {{9032,3},{9036,2},{9046,2},{9042,3},{9062,2},{11673,2},{9064,1},{11606,2},{9054,1},{9060,2},{11778,2},{9074,1},{9076,2},{9070,3},{11605,2},{9080,1},{8577,3},{8595,3},{8581,1},{8593,2},{11608,1}};
case 60: return {{1938,3},{2017,3},{1948,2},{7462,2},{12223,2},{2105,2},{7458,3},{7459,1},{1936,2},{1960,2},{7461,2},{1941,1},{1959,3},{2019,1},{2259,3},{7463,2},{1961,1},{2031,3},{1963,3}};
case 61: return {{2022,3},{2031,2},{1971,2},{2048,3},{1981,2},{1993,2},{2044,3},{1979,1},{1980,2},{1992,3},{2223,1},{1999,1},{2029,2},{2284,3},{1989,1},{1938,3},{1939,2},{12223,2},{1932,3}};
case 62: return {{1932,3},{1963,3},{2025,1},{11178,3},{2047,2},{7574,1},{11179,3},{12119,2},{2009,2},{2082,3},{1996,1},{2013,2},{2085,1},{2043,2},{7575,3},{2000,1},{1938,3},{1939,2},{12223,2},{2031,1}};
case 70: return {{563,3},{11218,3},{561,2},{575,2},{1685,3},{574,1},{1641,2},{11767,2},{2049,2},{2051,3},{573,1},{5565,1},{11769,1},{5566,2},{11368,3},{607,1},{611,2},{5568,2},{11177,3},{595,2}};
case 71: return {{607,2},{611,2},{5560,3},{602,3},{605,2},{11216,3},{901,1},{2055,3},{2083,3},{1689,2},{1693,1},{2057,3},{5563,2},{2058,1},{563,2},{11218,3},{561,2},{1685,3}};
case 72: return {{593,3},{5568,1},{11177,3},{583,2},{595,2},{5567,3},{581,2},{591,1},{1696,3},{588,2},{2084,1},{590,1},{7705,2},{15487,1},{2063,3},{2064,1},{563,2},{11218,3},{1685,3},{611,2}};
case 80: return {{9154,3},{9200,3},{9170,3},{9172,2},{10737,2},{9174,1},{9178,3},{9192,2},{9198,2},{9188,2},{10733,1},{11825,2},{9180,2},{9196,1},{9194,2},{10578,1},{9186,1},{10531,3},{10545,2},{11157,3}};
case 81: return {{10531,3},{10545,2},{10529,3},{10537,2},{11434,3},{10547,2},{10551,1},{10573,1},{11433,2},{10561,1},{11431,1},{12121,2},{10563,3},{10571,1},{10734,2},{10541,3},{10559,2},{10577,1},{9200,3},{11157,3}};
case 90: return {{11100,2},{11104,3},{11110,2},{11112,2},{11214,2},{11420,2},{11134,3},{11124,3},{11366,1},{11122,2},{11142,3},{11419,1},{11150,3},{11200,2},{11152,1},{10992,3},{10936,3},{10938,3}};
case 91: return {{10992,3},{10994,3},{11206,1},{11014,2},{11020,2},{11190,2},{11713,2},{11198,3},{11024,3},{11201,1},{11034,2},{11188,2},{11189,1},{11042,1},{11199,2},{11044,1},{11104,3},{10936,3},{10960,2},{11181,2}};
case 92: return {{10936,3},{10938,3},{10960,2},{11181,2},{10940,2},{10948,1},{10978,3},{10970,2},{11182,2},{12120,1},{10964,2},{10980,1},{10984,3},{10962,1},{10982,2},{10986,1},{11104,3},{10992,3},{11206,2},{11020,2}};
case 110: return {{8349,3},{8359,3},{11281,2},{8353,2},{8383,2},{11284,3},{8381,2},{8389,2},{11278,1},{11282,1},{8361,1},{8391,3},{8399,1},{11277,1},{12149,2},{12150,1},{8405,1},{11716,1},{8237,2},{11715,3},{8277,1},{11279,3}};
case 111: if(tank)return {{8295,2},{11716,3},{8305,3},{8761,2},{11285,2},{8293,3},{8299,1},{8301,2},{8323,3},{8325,1},{8313,1},{8758,2},{8759,2},{8319,1},{8343,3},{8347,1},{8237,2},{11715,3},{8277,1},{11279,3}}; return {{8295,2},{9026,2},{11716,3},{8305,3},{8761,2},{11285,2},{8299,1},{8301,2},{8323,3},{8303,1},{8307,2},{8325,1},{8313,1},{8335,2},{8759,2},{8341,2},{8343,3},{8347,1},{8237,2},{11715,3},{8277,1}};
case 112: return {{8227,2},{8237,2},{11699,2},{11715,3},{8245,3},{8277,1},{8249,1},{8269,2},{8255,1},{8762,2},{12146,2},{8263,3},{8279,1},{8763,1},{8265,1},{11280,3},{8271,1},{8359,3},{11281,2},{8353,2},{11284,3}};
default:return {};
}
}
}
#endif
@@ -1,5 +1,6 @@
#include "AdventureProfessionPolicy.h"
#include "ClientlessLuaEngine.h"
#include "AdventureRotationPolicy.h"
#include "ClientlessPlayerMgr.h"
#include "ClientlessActionExecutorMgr.h"
#include "ClientlessTravelMgr.h"
@@ -405,6 +406,8 @@ bool ClientlessLuaEngine::LoadAdventureProfile(Player* player)
lua_getfield(_state,-1,"seed");
if(lua_isinteger(_state,-1)){auto value=lua_tointeger(_state,-1);if(value>=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)));
@@ -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<uint32, float> ZoneAffinity;
File diff suppressed because it is too large. Load diff
@@ -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 <cassert>
#include <set>
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::set<unsigned>auras;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::set<unsigned>choices;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<unsigned>{2});
if(klass==1 || klass==4 || klass==7 || klass==8 || klass==11)assert((choices==std::set<unsigned>{0,1}));
if(klass==3 || klass==6)assert((choices==std::set<unsigned>{1,2}));
if(klass==9)assert((choices==std::set<unsigned>{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')
@@ -20,6 +20,8 @@ code=r'''
#include <algorithm>
#include <ctime>
#include <set>
#include <list>
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<Creature*> 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<Unit*>&){}bool IsInMap(Unit*){return true;}
bool grouped=false;float health=100;std::set<Unit*> attackers;
bool alive=true,ghost=false,teleport=false,combat=false,melee=true,los=true;unsigned delay=30;int saves=0,revives=0,bones=0,graveyards=0;bool sickness=false;int durability=75;
Session session;Player(){session.owner=this;guid={26};}ObjectGuid GetGUID(){return guid;}Player* ToPlayer()override{return this;}Group* GetGroup(){return grouped?&group:nullptr;}bool IsAlive(){return alive;}bool IsInWorld(){return true;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool GetTransport(){return false;}bool IsInFlight(){return false;}bool IsCharmed(){return false;}
unsigned GetMapId(){return 0;}unsigned GetInstanceId(){return 0;}bool InBattleground(){return false;}bool InArena(){return false;}bool HasFlag(int,int){return ghost;}Session*GetSession(){return &session;}Corpse*GetCorpse(){return &corpse;}
uint32 GetCorpseReclaimDelay(bool){return delay;}Map*GetMap(){return &map;}int GetPhaseShift(){return 0;}
bool IsInPhase(Unit*c){return c->phase;}bool CanSeeOrDetect(Unit*c){return c->visible;}bool IsValidAttackTarget(Unit*c){return c->valid;}unsigned getLevel(){return 60;}bool IsWithinMeleeRange(Unit*){return melee;}float GetCombatReach(){return 1;}float GetAngle(Player*){return 0;}
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;}
@@ -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 <cassert>
#include <functional>
#include <unordered_map>
#include <vector>
#include <list>
#include <algorithm>
#include <utility>
#include <cmath>
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){}template<class T>WorldPacket&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::list<Unit*>companions;
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<Unit*>&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')
@@ -11,13 +11,13 @@ code=r'''
#include <cmath>
#include <limits>
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;}template<class T>unsigned 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;}template<class T>unsigned 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::vector<Effect>Effects;
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::vector<Effect>Effects;
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.
@@ -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 <vector>
#include <algorithm>
#include <cmath>
#include <cassert>
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::vector<Effect>Effects;};
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<WorldObject*> areaObjects;
struct Player:Position{unsigned stops=0;Guid GetGUID(){return {};}Map*GetMap(){return &map;}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{template<class Check>struct WorldObjectListSearcher{std::vector<WorldObject*>&objects;Check&check;WorldObjectListSearcher(Player*,std::vector<WorldObject*>&v,Check&c,unsigned):objects(v),check(c){}};}
namespace Cell{template<class T>void 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')
@@ -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.
@@ -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<std::pair<unsigned,unsigned>,Item>items;Bag bag;
unsigned GetFreeInventorySpace(){return free;}Guid GetGUID(){return {};}
@@ -29,16 +30,17 @@ struct Player{
struct Profile{std::unordered_set<unsigned>KeepItems{2901};unsigned CompanionLeashItem=37460;}profile;
struct Lua{Profile*GetAdventureProfile(unsigned){return &profile;}}lua;auto sClientlessLuaEngine=&lua;
struct Supply{std::unordered_map<unsigned,unsigned>InventoryAllowance(Player*){return {{100,20},{101,20}};}}supply;auto sAdventureSupplyMgr=&supply;
struct Generator{std::unordered_set<unsigned>ProtectedEquipment(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));
}
@@ -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 <map>
#include <cassert>
struct Talent{unsigned tab,tier,ranks,dependency,dependencyRank;};
struct Tab{unsigned mask,order;};
std::map<unsigned,Talent> talents={ROWS};std::map<unsigned,Tab> 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<unsigned,unsigned> 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<d.tier*5 || (d.tab!=primary && main<31))return false;
known[id]=rank+1;left-=cost;return true;
};
assert(AdventureTalentPolicy::Spend(build,learn,[&]{return left;}));assert(!left);
if(budget==41)for(auto t:build)assert(known[t.Id]==t.Ranks);
}
}
for(unsigned klass:{1,2,3,4,5,6,7,8,9,11})for(unsigned seed=0;seed<100;++seed){
auto spec=AdventureRotationPolicy::SoloSpec(klass,seed);assert(!AdventureTalentPolicy::Build(klass,spec).empty());
}
}
'''.replace('ROWS',rows).replace('TABS',tabs)
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('Canonical Cata PvE builds: 4.3.4 class/tree/rank/prerequisite gates, all 1..41 point budgets, solo selection coverage passed')
@@ -0,0 +1,45 @@
"""Lock and Load correction clears effect 2: never scale an empty effect."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
h=(root/'src/server/game/Spells/SpellInfo.h').read_text()
a=h.index(' SpellEffectInfo() :');b=h.index('{ }',a)+3;constructor=h[a:b]
s=(root/'src/server/game/Spells/Spell.cpp').read_text();a=s.index('SpellValue::SpellValue(');b=s.index('{',a);e=b+1;depth=1
while depth:depth+=(s[e]=='{')-(s[e]=='}');e+=1
code=r'''
#include <cassert>
#include <cstring>
#include <new>
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')