diff --git a/doc/clientless/SESSION-RESUME.md b/doc/clientless/SESSION-RESUME.md index ce2a136c40..412f3fbcb3 100644 --- a/doc/clientless/SESSION-RESUME.md +++ b/doc/clientless/SESSION-RESUME.md @@ -1,3 +1,51 @@ +# Aktuell: Interaktionen und spielergeführte Gruppen umgesetzt + +Stand 28.10.2026. Nutzer beauftragte Umsetzung nach gemeinsamer Planung. +Branch feature/clientless-player-core; vorher sauber. Nutzer hat den lokalen +Commit dieses Stands beauftragt; kein Push. Neuer AdventureInteractionMgr/Policy, +generische GUID-Queues für Textemotes und tatsächliche Kampfhilfe, native +Einladungsantworten, intent-/persönlichkeitsabhängige Annahme, MMAP-Folgen und +TravelGraph-Handoff, grundlegende Gruppen-Kampfunterstützung. Spieler/Pet- +Selbstverteidigung im bestehenden CombatMgr, keine eigenständigen PvP-Angriffe. +Details und Grenzen in doc/clientless/interactions.md. + +Austritt nur außerhalb Kampf/Tod/Instanz/Transport/Flug. Intent-/Locationwechsel +oder manuelle Blockfrist erlaubt Entscheidung; group_loyalty beeinflusst Bleiben +(Arthoran0.35, Standard0.25). Native AFK oder beobachtete Inaktivität des +Anführers volle600s; Führungswechsel beginnt neues Zeitfenster. Chat/Emotes, +Zauber/Itemanfragen, Bewegung/Blick/Ziel, Kampf/Casts zählen als Aktivität. +Native Gruppenmitgliedschaft bleibt über Neustart; Beobachtungsfristen starten +neu. Rollenprofile, autonome Bot-Gruppen, freie Chatgespräche und eigenständige +PvP-Erstangriffe bleiben ausdrücklich spätere Arbeit. + +Debug-Build-j10 erfolgreich installiert. 57 Testskripte bestanden: vollständiger +56er-Lauf plus neuer Social-Queue-Concurrencytest; der anschließend erweiterte +Combat-Test für echten Spielerfokus zusätzlich erneut erfolgreich. Native +Testserver PID59086, frische Ready-Meldung und Port8085 bestätigt. Arthoran26 +online, Persönlichkeit8Werte, neue Diagnose clientless interaction26 erfolgreich: +sociability0.65/loyalty0.35, keine Gruppe. Echte Kreaturen-Selbstverteidigung +nach Neustart weiterhin beobachtet, kein Beleg für Live-PvP/Gruppenannahme. + +Arthorans unveränderte initiale Profilvorgabe startete bei Login die normale +Goldhain-Reise. clientless adventure stop26 wurde angefordert, letzte Diagnose +noch Switching/waiting for safe player/transport state. Keine Teleportierung, +kein temporärer Profiloverride; normale Herkunftsdateien bleiben erhalten. +Für Live-Abnahme Position und Reise zuerst erneut prüfen. Gruppeneinladungen +werden in Flug-/Transport-/Kampfzuständen bis zur sicheren Lage zurückgestellt. + +Beim sauberen Shutdown der ALTEN, vor Installation laufenden Binärdatei trat +Object.h:448 GetMap ASSERTION FAILED m_currMap auf. AlterProzess49765 endete; +keine fremden Prozesse beendet. Das ist ein beobachteter Shutdownfehler des +Ausgangsstands, Ursache hier nicht untersucht/behoben. Neuer Start erfolgreich. + +Offene Gameplay-Abnahmen: echte Spielereinladungen, zwei gleichzeitige Bots, +PvP/Pet-Verteidigung, native Gruppenportale/kartenübergreifendes Folgen, +zehn echte Minuten AFK. Klassenrotationen außer Krieger und Supportrollen +weiterhin begrenzt. Belege: checkpoints/2026-10-28-interactions/. +Logs /tmp/vcore-interactions-{build-final,install}.log und zugehörige Tests. + +--- + # Aktuell: Selbstversorgung und gemeinsame Nebenberufe abgeschlossen Stand 27.10.2026. Branch feature/clientless-player-core, vorheriger Commit diff --git a/doc/clientless/checkpoints/2026-10-28-interactions/build.txt b/doc/clientless/checkpoints/2026-10-28-interactions/build.txt new file mode 100644 index 0000000000..2820b9f46c --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-28-interactions/build.txt @@ -0,0 +1,22 @@ +[1/10] cd /home/wow_admin/wow_cata_server/source/TrinityCore && /usr/bin/cmake -DBUILDDIR="/home/wow_admin/wow_cata_server/build/debug" -P /home/wow_admin/wow_cata_server/source/TrinityCore/cmake/genrev.cmake /home/wow_admin/wow_cata_server/build/debug +[2/10] Building CXX object src/server/game/CMakeFiles/game.dir/World/AdventureCombatMgr.cpp.o +[3/10] Building CXX object src/server/game/CMakeFiles/game.dir/World/ClientlessLuaEngine.cpp.o +[4/10] Building CXX object src/server/game/CMakeFiles/game.dir/World/AdventureInteractionMgr.cpp.o +[5/10] Building CXX object src/server/game/CMakeFiles/game.dir/World/AdventureSupplyMgr.cpp.o +[6/10] Building CXX object src/server/scripts/CMakeFiles/scripts.dir/Custom/viano_bot_commands.cpp.o +/home/wow_admin/wow_cata_server/source/TrinityCore/src/server/scripts/Custom/viano_bot_commands.cpp: In function ‘bool VianoBotCommands::HandleClientlessLocationAdd(ChatHandler*, const char*)’: +/home/wow_admin/wow_cata_server/source/TrinityCore/src/server/scripts/Custom/viano_bot_commands.cpp:2341:20: warning: unused variable ‘loaded’ [-Wunused-variable] + 2341 | bool const loaded = sClientlessWorldPointMgr->Reload(); + | ^~~~~~ +/home/wow_admin/wow_cata_server/source/TrinityCore/src/server/scripts/Custom/viano_bot_commands.cpp: In function ‘bool VianoBotCommands::HandleClientlessLocationMove(ChatHandler*, const char*)’: +/home/wow_admin/wow_cata_server/source/TrinityCore/src/server/scripts/Custom/viano_bot_commands.cpp:2378:20: warning: unused variable ‘loaded’ [-Wunused-variable] + 2378 | bool const loaded = sClientlessWorldPointMgr->Reload(); + | ^~~~~~ +/home/wow_admin/wow_cata_server/source/TrinityCore/src/server/scripts/Custom/viano_bot_commands.cpp: In function ‘bool VianoBotCommands::HandleClientlessLocationRemove(ChatHandler*, const char*)’: +/home/wow_admin/wow_cata_server/source/TrinityCore/src/server/scripts/Custom/viano_bot_commands.cpp:2402:20: warning: unused variable ‘loaded’ [-Wunused-variable] + 2402 | bool const loaded = sClientlessWorldPointMgr->Reload(); + | ^~~~~~ +[7/10] Building CXX object src/server/game/CMakeFiles/game.dir/World/AdventureBehaviorMgr.cpp.o +[8/10] Linking CXX static library src/server/game/libgame.a +[9/10] Linking CXX static library src/server/scripts/libscripts.a +[10/10] Linking CXX executable src/server/worldserver/worldserver diff --git a/doc/clientless/checkpoints/2026-10-28-interactions/combat.txt b/doc/clientless/checkpoints/2026-10-28-interactions/combat.txt new file mode 100644 index 0000000000..e446f83414 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-28-interactions/combat.txt @@ -0,0 +1 @@ +Production combat focus, damage ordering, risk filters, corpse radius/delay, alternate paths and malus-free persisted fallback passed. diff --git a/doc/clientless/checkpoints/2026-10-28-interactions/interactions.txt b/doc/clientless/checkpoints/2026-10-28-interactions/interactions.txt new file mode 100644 index 0000000000..49a90a3298 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-28-interactions/interactions.txt @@ -0,0 +1,5 @@ +/tmp/tmp2j7tsqhj/test.cpp: In member function ‘void AdventureInteractionMgr::Leave(Player*, Record&, const char*)’: +/tmp/tmp2j7tsqhj/test.cpp:140:69: warning: unused parameter ‘reason’ [-Wunused-parameter] + 140 | void AdventureInteractionMgr::Leave(Player* p,Record& r,char const* reason) + | ~~~~~~~~~~~~^~~~~~ +production interaction manager: invitations, factions, reservations, follow, AFK and deferred departure passed diff --git a/doc/clientless/checkpoints/2026-10-28-interactions/notifications.txt b/doc/clientless/checkpoints/2026-10-28-interactions/notifications.txt new file mode 100644 index 0000000000..8f5d9cf5ba --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-28-interactions/notifications.txt @@ -0,0 +1 @@ +Production social/helper queues: concurrent GUID-only delivery, bounded queues, human/self/zero-damage gates, pet attribution and lethal-cleanup ordering passed diff --git a/doc/clientless/checkpoints/2026-10-28-interactions/runtime.txt b/doc/clientless/checkpoints/2026-10-28-interactions/runtime.txt new file mode 100644 index 0000000000..3504f0ed44 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-28-interactions/runtime.txt @@ -0,0 +1,114 @@ +Validating spell scripts... +Spell `85123` Effect `Index: EFFECT_0 Target: 7` of script `spell_siege_cannon` +did not match dbc effect data - handler bound to hook `OnObjectAreaTargetSelect` + of SpellScript won't be executed +>> Validated 3470 scripts in 12 ms +Loading SmartAI scripts... +>> Loaded SmartAIMgr Helpers in 16 ms +>> Loaded 13506 SmartAI scripts in 167 ms +Loading Calendar data... +>> Loaded 0 calendar events in 1 ms +>> Loaded 0 calendar invites in 0 ms +Loading Petitions... +>> Loaded 0 petitions. +Loading Signatures... +>> Loaded 0 Petition signs! +Loading Summon Properties parameter data... +>> Loaded 78 summon properties parameters in 1 ms +Loading Item loot... +>> Loaded 0 stored item loots +>> Loaded 0 stored item money +Initialize query data... +>> Initialized query cache data in 1019 ms +Initialize commands... +Initialize game time and timers +Mail timer set to: 1260, mail return is called every 1440 minutes +Starting Map System +Starting Game Event system... +Initialize AuctionHouseBot... +Initializing Opcodes... +Starting Arena Season... +Starting Battleground System +>> Loaded 15 battlegrounds in 0 ms +Starting Outdoor PvP System +>> Loaded 5 outdoor PvP definitions in 1 ms +Starting Battlefield System +>> Loaded 2 battlefields in 0 ms +Loading Warden Checks... +Loading Warden Action Overrides... +Deleting expired bans... +Initializing quest reset times... +Calculate random battleground reset time... +Calculate guild limitation(s) reset time... +Calculate next currency reset time... +World initialized in 0 minutes 13 seconds +TC>TC>TrinityCore rev. 39a515dbd13e+ 2026-10-27 23:56:43 +0100 (feature/clientle +ss-player-core branch) (Unix, Debug, Static) (worldserver-daemon) ready... +clientless login 26 +TC>Registered clientless session for character GUID 26 (account 6), state=Loadin +g. +Created clientless player session for character GUID 26 (account 6). +ClientlessPlayerMgr started character GUID 26 (account 6). +Clientless login started for 'Arthoran' (GUID 26, account 6). +TC> Loading saved respawn time of 1790175804 for spawnid (0,323981) - spawn does + not exist, ignoring +Loading saved respawn time of 1790090644 for spawnid (0,396680) - spawn does not + exist, ignoring +Loading saved respawn time of 1790019808 for spawnid (0,396681) - spawn does not + exist, ignoring +Loading saved respawn time of 1790019853 for spawnid (0,396682) - spawn does not + exist, ignoring +Initialized transport GameObject grids: map=0 instance=0 grids=4 candidates=15 +Clientless player character GUID 26 (account 6) reached state=Online. +ClientlessLua initialized, script root '/home/wow_admin/wow_cata_server/source/T +rinityCore/src/server/scripts/Custom/clientless'. +[AdventureBot][Profile] loaded guid=26 name='Arthoran' source='/home/wow_admin/w +ow_cata_server/source/TrinityCore/src/server/scripts/Custom/clientless/adventure +/profiles/arthoran.lua' home='elwynn' canGroundMount=1 mounts=1 mountMinDistance +=45.0 mountSpeedRate=1.60 personality=8 regionAffinity=4 activityAffinity=7 +PROFILE_PROFESSIONS guid=26 primary=2 +ClientlessLua[26]: Arthoran ready; no automatic test journey queued +ClientlessLua[26]: executed adventure override script '/home/wow_admin/wow_cata_ +server/source/TrinityCore/src/server/scripts/Custom/clientless/adventure/overrid +es/arthoran.lua'. +ADVENTURE_CATALOG locations=2973 actions=84 lines=76 +INTENT_TRAVEL guid=26 intent='social' location='goldshire_tavern' area=0 +[AdventureBot] loaded 3966 clientless world points. +[AdventureBot] loaded travel graph: 993 node(s), 1765 directed edge(s). +clientless interaction 26 +TC>Interactions guid=26 grouped=0 leader=0 partner=0 intent='social' schedule='' + leaderIdleSeconds=0 afkSeconds=0 sociability=0.65 loyalty=0.35 +TC> SUPPLY_RESTOCK guid=26 day=20261028 daily=0 afterCombat=1 +COMBAT_START guid=26 target='GUID Full: 0xf130a1fe000006ce Type: Creature Entry: + 41470 Low: 1742' training=0 defending=1 until=0 +COMBAT_CAST guid=26 target='GUID Full: 0x000000000000001a Type: Player Low: 26' +spell=6673 +COMBAT_CAST guid=26 target='GUID Full: 0xf130a1fe000006ce Type: Creature Entry: +41470 Low: 1742' spell=78 +COMBAT_CAST guid=26 target='GUID Full: 0xf130a1fe000006ce Type: Creature Entry: +41470 Low: 1742' spell=12294 +COMBAT_CAST guid=26 target='GUID Full: 0xf130a1fe000006ce Type: Creature Entry: +41470 Low: 1742' spell=772 +COMBAT_END guid=26 target='GUID Full: 0xf130a1fe000006ce Type: Creature Entry: 4 +1470 Low: 1742' training=0 +INTENT_TRAVEL guid=26 intent='social' location='goldshire_tavern' area=0 +SUPPLY_RESTOCK guid=26 day=20261028 daily=0 afterCombat=1 +COMBAT_START guid=26 target='GUID Full: 0xf130a1ff000006d0 Type: Creature Entry: + 41471 Low: 1744' training=0 defending=1 until=0 +COMBAT_CAST guid=26 target='GUID Full: 0xf130a1ff000006d0 Type: Creature Entry: +41471 Low: 1744' spell=12294 +COMBAT_END guid=26 target='GUID Full: 0xf130a1ff000006d0 Type: Creature Entry: 4 +1471 Low: 1744' training=0 +SUPPLY_RESTOCK guid=26 day=20261028 daily=0 afterCombat=1 +INTENT_TRAVEL guid=26 intent='social' location='goldshire_tavern' area=0 +clientless adventure stop 26 +TC>Behavior stop requested at a safe point. +TC> clientless adventure status 26 +TC>clientless interaction 26 +TC>Adventure GUID 26: enabled=1 schedule=0 phase=3 intent='social' location='gol +dshire_tavern' micro='' deadline=0 detail='waiting for safe player/transport sta +te' +TC> Interactions guid=26 grouped=0 leader=0 partner=0 intent='social' schedule=' +' + leaderIdleSeconds=0 afkSeconds=0 sociability=0.65 loyalty=0.35 +TC> diff --git a/doc/clientless/checkpoints/2026-10-28-interactions/startup-console.txt b/doc/clientless/checkpoints/2026-10-28-interactions/startup-console.txt new file mode 100644 index 0000000000..8e71f7bea2 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-28-interactions/startup-console.txt @@ -0,0 +1,968 @@ +TrinityCore rev. 39a515dbd13e+ 2026-10-27 23:56:43 +0100 (feature/clientless-pla +yer-core branch) (Unix, Debug, Static) (worldserver-daemon) + to stop. + + ______ __ +/\__ _\ __ __/\ \__ +\/_/\ \/ _ __ /\_\ ___ /\_\ \, _\ __ __ + \ \ \/\`'__\/\ \ /' _ `\/\ \ \ \/ /\ \/\ \ + \ \ \ \ \/ \ \ \/\ \/\ \ \ \ \ \_\ \ \_\ \ + \ \_\ \_\ \ \_\ \_\ \_\ \_\ \__\\/`____ \ + \/_/\/_/ \/_/\/_/\/_/\/_/\/__/ `/___/> \ + C O R E /\___/ +http://TrinityCore.org \/__/ + +Using configuration file /home/wow_admin/wow_cata_server/server/debug/etc/worlds +erver.conf. +Using SSL version: OpenSSL 3.5.5 27 Jan 2026 (library: OpenSSL 3.5.5 27 Jan 2026 +) +Using Boost version: 1.90.0 +Automatic database updates are disabled for all databases! +Realm running as realm ID 2 region 2 battlegroup 1 +Using World DB: TDB 434.22011 +Using enUS DBC Locale +Will clear `logs` table of entries older than 1209600 seconds every 10 minutes. +Client cache version set to: 22011 +Using DataDir /home/wow_admin/wow_cata_server/data/ +WORLD: MMap data directory is: /home/wow_admin/wow_cata_server/data/mmaps +VMap support included. LineOfSight: 1, getHeight: 1, indoorCheck: 1 +VMap data directory is: /home/wow_admin/wow_cata_server/data/vmaps +Missing name DungeonFinder.LowPopulation.Enable in config file /home/wow_admin/w +ow_cata_server/server/debug/etc/worldserver.conf, add "DungeonFinder.LowPopulati +on.Enable = 0" to this file +Missing name DungeonFinder.LowPopulation.FallbackDelay in config file /home/wow_ +admin/wow_cata_server/server/debug/etc/worldserver.conf, add "DungeonFinder.LowP +opulation.FallbackDelay = 60" to this file +Missing name DungeonFinder.LowPopulation.MinPlayers in config file /home/wow_adm +in/wow_cata_server/server/debug/etc/worldserver.conf, add "DungeonFinder.LowPopu +lation.MinPlayers = 1" to this file +Missing name DungeonFinder.LowPopulation.SoloGroupOutsideTimeout in config file +/home/wow_admin/wow_cata_server/server/debug/etc/worldserver.conf, add "DungeonF +inder.LowPopulation.SoloGroupOutsideTimeout = 300" to this file +Loading Trinity strings... +>> Loaded 991 trinity strings in 7 ms +Initialize data stores... +>> Initialized 167 DBC data stores in 857 ms +>> Initialized 5 DB2 data stores in 200 ms +>> Loading Cinematic Camera files +>> Loaded 0 cinematic waypoint sets in 0 ms +Loading IP Location Database... +Initializing PlayerDump tables... +>> Initialized tables for PlayerDump in 14 ms. +Loading SpellInfo store... +>> Loaded SpellInfo store in 110 ms +Loading SpellInfo corrections... +>> Loaded SpellInfo corrections in 22 ms +Loading SkillLineAbilityMultiMap Data... +>> Loaded 8384 SkillLineAbility MultiMap Data in 3 ms +Loading SpellInfo custom attributes... +>> Loaded 195 spell custom attributes from DB in 2 ms +>> Loaded SpellInfo custom attributes in 76 ms +Loading SpellInfo diminishing infos... +>> Loaded SpellInfo diminishing infos in 5 ms +Loading SpellInfo immunity infos... +>> Loaded SpellInfo immunity infos in 16 ms +Loading GameObject models... +>> Loaded 3255 GameObject models in 3 ms +Loading Script Names... +>> Loaded 4057 ScriptNames in 1376 ms +Loading Instance Template... +>> Loaded 91 instance templates in 0 ms +Loading instances... +>> Loaded instances in 6 ms +Loading character cache store... +Loaded character infos for 13 characters in 0 ms +Loading Broadcast texts... +>> Loaded 73039 broadcast texts in 360 ms +>> Loaded 73039 broadcast text locales in 1637 ms +Loading Localization strings... +>> Loaded 29790 creature locale strings in 405 ms +>> Loaded 19670 gameobject_template_locale strings in 226 ms +>> Loaded 14986 Quest locale strings in 999 ms +>> Loaded 1946 PageText locale strings in 109 ms +>> Loaded 453 points_of_interest locale strings in 4 ms +>> Loaded 0 quest_greeting locales. DB table `quest_greeting_locale` is empty. +>> Localization strings loaded in 1758 ms +Loading Account Roles and Permissions... +>> Loaded 666 permission definitions, 650 linked permissions and 4 default permi +ssions in 343 ms +Loading Page Texts... +>> Loaded 1962 page texts in 13 ms +Loading Game Object Templates... +>> Loaded 29501 game object templates in 156 ms +Loading Game Object template addons... +>> Loaded 29495 game object template addons in 41 ms +Loading Transport templates... +>> Loaded 43 transport templates in 9 ms +Loading Transport animations and rotations... +Loading Transport spawns... +>> Spawned 0 continent transports in 0 ms +Loading Spell Rank Data... +>> Loaded 174 spell rank records in 1 ms +Loading Spell Required Data... +>> Loaded 22 spell required records in 1 ms +Loading Spell Group types... +>> Loaded 550 spell group definitions in 2 ms +Loading Spell Learn Skills... +>> Loaded 137 Spell Learn Skills from DBC in 5 ms +Loading SpellInfo SpellSpecific and AuraState... +>> Loaded SpellInfo SpellSpecific and AuraState in 10 ms +Loading Spell Learn Spells... +The spell 17002 listed in `spell_learn_spell` attempts learning talent spell 248 +67, skipped. +The spell 24866 listed in `spell_learn_spell` attempts learning talent spell 248 +64, skipped. +The spell 33872 listed in `spell_learn_spell` attempts learning talent spell 471 +79, skipped. +The spell 33873 listed in `spell_learn_spell` attempts learning talent spell 471 +80, skipped. +>> Loaded 24 spell learn spells, 512 found in Spell.dbc and 31 from TalentTab.db +c in 7 ms +Loading Spell Proc conditions and data... +>> Loaded 1556 spell proc conditions and data in 5 ms +Generating spell proc data from SpellMap... +Spell Id 36488 has SPELL_ATTR3_CAN_PROC_FROM_PROCS attribute and no restriction +on what spells can cause it to proc and no cooldown. This spell can cause infini +te proc loops. Proc data for this spell was not generated, data in `spell_proc` +table is required for it to function! +Spell Id 39440 has SPELL_ATTR3_CAN_PROC_FROM_PROCS attribute and no restriction +on what spells can cause it to proc and no cooldown. This spell can cause infini +te proc loops. Proc data for this spell was not generated, data in `spell_proc` +table is required for it to function! +Spell Id 47261 has SPELL_ATTR3_CAN_PROC_FROM_PROCS attribute and no restriction +on what spells can cause it to proc and no cooldown. This spell can cause infini +te proc loops. Proc data for this spell was not generated, data in `spell_proc` +table is required for it to function! +Spell Id 47262 has SPELL_ATTR3_CAN_PROC_FROM_PROCS attribute and no restriction +on what spells can cause it to proc and no cooldown. This spell can cause infini +te proc loops. Proc data for this spell was not generated, data in `spell_proc` +table is required for it to function! +Spell Id 67726 has SPELL_ATTR3_CAN_PROC_FROM_PROCS attribute and no restriction +on what spells can cause it to proc and no cooldown. This spell can cause infini +te proc loops. Proc data for this spell was not generated, data in `spell_proc` +table is required for it to function! +Spell Id 67736 has SPELL_ATTR3_CAN_PROC_FROM_PROCS attribute and no restriction +on what spells can cause it to proc and no cooldown. This spell can cause infini +te proc loops. Proc data for this spell was not generated, data in `spell_proc` +table is required for it to function! +Spell Id 67740 has SPELL_ATTR3_CAN_PROC_FROM_PROCS attribute and no restriction +on what spells can cause it to proc and no cooldown. This spell can cause infini +te proc loops. Proc data for this spell was not generated, data in `spell_proc` +table is required for it to function! +Spell Id 67744 has SPELL_ATTR3_CAN_PROC_FROM_PROCS attribute and no restriction +on what spells can cause it to proc and no cooldown. This spell can cause infini +te proc loops. Proc data for this spell was not generated, data in `spell_proc` +table is required for it to function! +Spell Id 85556 has SPELL_ATTR3_CAN_PROC_FROM_PROCS attribute and no restriction +on what spells can cause it to proc and no cooldown. This spell can cause infini +te proc loops. Proc data for this spell was not generated, data in `spell_proc` +table is required for it to function! +>> Generated spell proc data for 1620 spells in 8 ms +Loading Aggro Spells Definitions... +>> Loaded 44 SpellThreatEntries in 2 ms +Loading Spell Group Stack Rules... +>> Loaded 41 spell group stack rules in 2 ms +>> Parsing SPELL_GROUP_STACK_RULE_EXCLUSIVE_SAME_EFFECT stack rules... +>> Parsed 23 SPELL_GROUP_STACK_RULE_EXCLUSIVE_SAME_EFFECT stack rules in 0 ms +Loading NPC Texts... +>> Loaded 8406 npc texts in 92 ms +Loading Enchant Spells Proc datas... +>> Loaded 45 enchant proc data definitions in 1 ms +Loading Item Random Enchantments Table... +>> Loaded 29217 Item Enchantment definitions in 25 ms +Loading Disables +>> Loaded 5211 disables in 10 ms +Loading Items... +>> Loaded 54296 item templates in 39 ms +Loading Item set names... +>> Loaded 622 item addon templates in 2 ms +>> Loaded 21 item script names in 0 ms +Loading Creature Model Based Info Data... +>> Loaded 31775 creature model based info in 33 ms +Loading Creature templates... +>> Loaded 64891 creature template models in 77 ms +>> Loaded 46633 creature definitions in 580 ms +Loading Equipment templates... +>> Loaded 16167 equipment templates in 23 ms +Loading Creature template addons... +Creature (Entry: 47203) has SPELL_AURA_CONTROL_VEHICLE aura 87978 defined in `au +ras` field in `creature_template_addon`. +>> Loaded 46613 creature template addons in 73 ms +Loading Reputation Reward Rates... +>> Loaded 16 reputation_reward_rate in 0 ms +Loading Creature Reward OnKill Data... +>> Loaded 2343 creature award reputation definitions in 7 ms +Loading Reputation Spillover Data... +>> Loaded 28 reputation_spillover_template in 2 ms +Loading Points Of Interest Data... +>> Loaded 603 Points of Interest definitions in 2 ms +Loading Database Game Tables... +>> Loaded 0 npc total health game table entries in 5 ms +>> Loaded 0 npc damage by class game table entries in 4 ms +Loading Creature Base Stats... +>> Loaded 400 creature base stats in 8 ms +Loading Spawn Group Templates... +>> Loaded 98 spawn group templates in 1 ms +Loading Creature Data... +Table `creature` has creature (GUID: 396679 Entry: 32491) with disallowed `unit_ +flags` 32768, removing incorrect flag. +>> Loaded 221565 creatures in 1182 ms +Loading Temporary Summon Data... +>> Loaded 869 temp summons in 4 ms +Loading pet levelup spells... +>> Loaded 263 pet levelup and default spells for 50 families in 8 ms +Loading pet default spells additional to levelup spells... +>> Loaded addition spells for 0 pet spell data entries in 2 ms +Loading summonable creature templates... +>> Loaded 427 summonable creature templates in 10 ms +Loading Creature Addon Data... +>> Loaded 52002 creature addons in 93 ms +Loading Creature Movement Overrides... +>> Loaded 0 creature movement overrides. DB table `creature_movement_override` i +s empty! +Loading Creature Movement Info... +>> Loaded 270 creature movement info override entries in 1 ms +Loading Gameobject Data... +>> Loaded 78938 gameobjects in 392 ms +Loading Spawn Group Data... +>> Loaded 94612 spawn group members in 58 ms +Loading instance spawn groups... +>> Loaded 27 instance spawn groups in 2 ms +Loading GameObject Addon Data... +>> Loaded 24493 gameobject addons in 38 ms +Loading GameObject Quest Items... +>> Loaded 1352 gameobject quest items in 3 ms +Loading Creature Quest Items... +>> Loaded 5361 creature quest items in 8 ms +Loading Creature Sparring Data... +>> Loaded 0 creature sparring templates in 1 ms +Loading Creature Linked Respawn... +>> Loaded 5239 linked respawns in 7 ms +Loading Weather Data... +>> Loaded 36 weather definitions in 0 ms +Loading Quests... +Quest 29507 has `RequiredCurrencyId1` = 393 but `RequiredCurrencyCount1` = 0, qu +est can't be done. +Quest 29283 has `RequiredCurrencyId1` = 416 but `RequiredCurrencyCount1` = 0, qu +est can't be done. +Quest 29281 has `RequiredCurrencyId1` = 416 but `RequiredCurrencyCount1` = 0, qu +est can't be done. +Quest 29279 has `RequiredCurrencyId1` = 416 but `RequiredCurrencyCount1` = 0, qu +est can't be done. +Quest 29214 has `RequiredCurrencyId1` = 416 but `RequiredCurrencyCount1` = 0, qu +est can't be done. +Quest 29199 has `RequiredCurrencyId1` = 416 but `RequiredCurrencyCount1` = 0, qu +est can't be done. +Quest 29181 has `RequiredCurrencyId1` = 416 but `RequiredCurrencyCount1` = 0, qu +est can't be done. +Quest 13478 has `RequiredCurrencyId1` = 392 but `RequiredCurrencyCount1` = 0, qu +est can't be done. +Quest 13476 has `RequiredCurrencyId1` = 392 but `RequiredCurrencyCount1` = 0, qu +est can't be done. +Quest 8371 has `RequiredCurrencyId1` = 392 but `RequiredCurrencyCount1` = 0, que +st can't be done. +Quest 8368 has `RequiredCurrencyId1` = 392 but `RequiredCurrencyCount1` = 0, que +st can't be done. +Quest 8367 has `RequiredCurrencyId1` = 392 but `RequiredCurrencyCount1` = 0, que +st can't be done. +>> Loaded 14991 quests definitions in 347 ms +Checking Quest Disables +>> Checked 5124 quest disables in 0 ms +Loading Quest POI +>> Loaded 13941 quest POI definitions in 189 ms +Loading Quest Money Rewards +>> Loaded 100 quest money reward entries in 1 ms +Loading Quests Starters and Enders... +Loading GO Start Quest Data... +>> Loaded 618 quest relations from gameobject_queststarter in 2 ms +Loading GO End Quest Data... +>> Loaded 517 quest relations from gameobject_questender in 2 ms +Loading Creature Start Quest Data... +>> Loaded 8725 quest relations from creature_queststarter in 10 ms +Loading Creature End Quest Data... +>> Loaded 9136 quest relations from creature_questender in 12 ms +Loading Quests Greetings... +>> Loaded 0 quest greetings. DB table `quest_greeting` is empty. +Loading Objects Pooling Data... +>> Loaded 1721 objects pools in 3 ms +Loading Creatures Pooling Data... +>> Loaded 835 creatures in pools in 2 ms +Loading Gameobject Pooling Data... +>> Loaded 7404 gameobject in pools in 10 ms +Loading Mother Pooling Data... +>> Loaded 1218 pools in mother pools in 3 ms +Starting objects pooling system... +Loading Quest Pooling Data... +>> Loaded 56 daily, 5 weekly and 0 monthly quest pools in 2 ms +Loading Game Event Data... +>> Loaded 156 holiday dates in 1 ms +>> Loaded 78 game events in 1 ms. +Loading Game Event Saves Data... +>> Loaded 0 game event saves in game events. DB table `game_event_save` is empty +. +Loading Game Event Prerequisite Data... +>> Loaded 0 game event prerequisites in game events. DB table `game_event_prereq +uisite` is empty. +Loading Game Event Creature Data... +>> Loaded 2790 creatures in game events in 2 ms. +Loading Game Event GO Data... +>> Loaded 21863 gameobjects in game events in 10 ms. +Loading Game Event Model/Equipment Change Data... +>> Loaded 22 model/equipment changes in game events in 2 ms. +Loading Game Event Quest Data... +>> Loaded 327 quests additions in game events in 1 ms. +Loading Game Event GO Quest Data... +>> Loaded 189 quests additions in game events in 1 ms. +Loading Game Event Quest Condition Data... +>> Loaded 0 quest event conditions in game events. DB table `game_event_quest_co +ndition` is empty. +Loading Game Event Condition Data... +>> Loaded 0 conditions in game events. DB table `game_event_condition` is empty. +Loading Game Event Condition Save Data... +>> Loaded 0 condition saves in game events. DB table `game_event_condition_save` + is empty. +Loading Game Event NPCflag Data... +>> Loaded 0 npcflags in game events. DB table `game_event_npcflag` is empty. +Loading Game Event Seasonal Quest Relations... +>> Loaded 690 quests additions in game events in 1 ms. +Loading Game Event Vendor Additions Data... +>> Loaded 2 vendor additions in game events in 1 ms. +Loading Game Event Battleground Holiday Data... +>> Loaded 8 battleground holidays in game events in 1 ms. +Loading Game Event Pool Data... +>> Loaded 2 pools for game events in 0 ms. +Loading UNIT_NPC_FLAG_SPELLCLICK Data... +>> Loaded 702 spellclick definitions in 3 ms +Loading Vehicle Template Accessories... +>> Loaded 431 Vehicle Template Accessories in 2 ms +Loading Vehicle Accessories... +>> Loaded 0 Vehicle Accessories in 0 ms +Loading Vehicle Seat Addon Data... +>> Loaded 15 Vehicle Seat Addon entries in 1 ms +Loading SpellArea Data... +>> Loaded 872 spell area requirements in 4 ms +Loading AreaTrigger definitions... +>> Loaded 316 area trigger teleport definitions in 2 ms +Loading Access Requirements... +>> Loaded 152 access requirement definitions in 1 ms +Loading Quest Area Triggers... +>> Loaded 49 quest trigger points in 1 ms +Loading Tavern Area Triggers... +>> Loaded 175 tavern triggers in 1 ms +Loading AreaTrigger script names... +>> Loaded 229 areatrigger scripts in 0 ms +Loading LFG entrance positions... +>> Loaded 68 lfg dungeon templates in 2 ms +Loading Dungeon boss data... +>> Loaded 617 instance encounters in 2 ms +Loading LFG rewards... +>> Loaded 20 lfg dungeon rewards in 1 ms +Loading Graveyard-zone links... +>> Loaded 855 graveyard-zone links in 2 ms +Loading Graveyard Orientations... +>> Loaded 1016 graveyard orientations in 2 ms +Loading spell pet auras... +>> Loaded 34 spell pet auras in 1 ms +Loading Spell target coordinates... +>> Loaded 1438 spell teleport coordinates in 4 ms +Loading linked spells... +>> Loaded 413 linked spells in 1 ms +Loading Player Create Data... +>> Loaded 91 player create definitions in 1 ms +Loading Player Create Items Data... +>> Loaded 1 custom player create items in 1 ms +Loading Player Create Skill Data... +>> Loaded player create skills in 1 ms +Loading Player Create Custom Spell Data... +>> Loaded 4507 custom player create spells in 1 ms +Loading Player Create Cast Spell Data... +>> Loaded 63 player create cast spells in 1 ms +Loading Player Create Action Data... +>> Loaded 352 player create actions in 1 ms +Loading Player Create Level Stats Data... +>> Loaded 850 level stats definitions in 5 ms +Loading Player Create XP Data... +>> Loaded 84 xp for level definitions in 1 ms +Loading Exploration BaseXP Data... +>> Loaded 80 BaseXP definitions in 1 ms +Loading Pet Name Parts... +>> Loaded 313 pet name parts in 1 ms +Loading the max pet number... +>> Loaded the max pet number: 28 in 0 ms +Loading pet level stats... +>> Loaded 2720 level pet stats definitions in 5 ms +Loading Player level dependent mail rewards... +>> Loaded 24 level dependent mail rewards in 1 ms +Loading creature loot templates... +>> Loaded 528193 creature loot templates in 634 ms +Loading fishing loot templates... +>> Loaded 266 fishing loot templates in 2 ms +Loading gameobject loot templates... +>> Loaded 19834 gameobject loot templates in 32 ms +Loading item loot templates... +>> Loaded 5031 item loot templates in 16 ms +Loading mail loot templates... +>> Loaded 90 mail loot templates in 0 ms +Loading milling loot templates... +>> Loaded 51 milling loot templates in 4 ms +Loading pickpocketing loot templates... +>> Loaded 10981 pickpocketing loot templates in 20 ms +Loading skinning loot templates... +>> Loaded 3390 skinning loot templates in 10 ms +Loading disenchanting loot templates... +>> Loaded 180 disenchanting loot templates in 1 ms +Loading prospecting loot templates... +>> Loaded 43 prospecting loot templates in 4 ms +Loading spell loot templates... +>> Loaded 292 spell loot templates in 3 ms +Loading reference loot templates... +>> Loaded refence loot templates in 48 ms +Loading Skill Discovery Table... +>> Loaded 347 skill discovery definitions in 3 ms. +Loading Skill Extra Item Table... +>> Loaded 225 spell specialization definitions in 1 ms +Loading Skill Perfection Data Table... +>> Loaded 72 spell perfection definitions in 0 ms. +Loading Skill Fishing base level requirements... +>> Loaded 95 areas for fishing base skill level in 0 ms +Loading Archaeology store... +>> Loaded 180 archaeology sites in 1 ms +>> Loaded 180 archaeology site polygons in 157 ms +>> Loaded 9 archaeology branches in 0 ms +>> Loaded 139 archaeology projects in 1 ms +>> Calculated 9 archaeology branch chances in 0 ms +Loading Achievements... +>> Loaded 47 achievement references in 0 ms. +Loading Achievement Criteria Lists... +>> Loaded 9803 achievement criteria and 952 guild achievement crieteria in 5 ms. +Loading Achievement Criteria Data... +>> Loaded 1291 additional achievement criteria data in 2 ms +Loading Achievement Rewards... +>> Loaded 120 achievement rewards in 1 ms. +Loading Achievement Reward Locales... +>> Loaded 0 achievement reward locale strings. DB table `achievement_reward_loc +ale` is empty. +Loading Completed Achievements... +>> Loaded 77 realm first completed achievements in 1 ms. +Loading Item Auctions... +>> Loaded 0 auction items. DB table `auctionhouse` or `item_instance` is empty! +Loading Auctions... +>> Loaded 0 auctions. DB table `auctionhouse` is empty. +Loading Guild XP for level... +>> Loaded 24 xp for guild level definitions in 1 ms +Loading Guild rewards... +>> Loaded 42 guild reward definitions in 0 ms +Initializing Guild Profession Data Store... +Loading Guild Challenges... +>> Loaded 4 guild challenge definitions in 1 ms +Loading Guilds... +Loading guilds definitions... +>> Loaded 1 guild definitions in 2 ms +Loading guild ranks... +>> Loaded 5 guild ranks in 0 ms +Loading guild members... +>> Loaded 4 guild members in 5 ms +Loading bank tab rights... +>> Loaded 30 bank tab rights in 1 ms +Loading guild event logs... +>> Loaded 8 guild event logs in 2 ms +Loading guild bank event logs... +>> Loaded 22 guild bank event logs in 0 ms +Loading Guild News... +>> Loaded 0 guild event logs. DB table `guild_newslog` is empty. +Loading guild bank tabs... +>> Loaded 6 guild bank tabs in 0 ms +>> Loaded 22 guild bank tab items in 1 ms +>> Validated data of loaded guilds in 0 ms +Loading guild finder guild-related settings... +>> Loaded 0 guild finder guild-related settings. Table `guild_finder_guild_setti +ngs` is empty. +Loading guild finder membership requests... +>> Loaded 0 guild finder membership requests. Table `guild_finder_applicant` is +empty. +Loading ArenaTeams... +>> Loaded 0 arena teams. DB table `arena_team` is empty! +Loading Groups... +>> Loaded 0 group definitions. DB table `groups` is empty! +Loading ReservedNames... +>> Loaded 0 reserved player names. DB table `reserved_name` is empty! +Loading GameObjects for quests... +>> Loaded 2750 GameObjects for quests in 5 ms +Loading BattleMasters... +>> Loaded 171 battlemaster entries in 3 ms +Loading GameTeleports... +>> Loaded 1531 GameTeleports in 5 ms +Loading Trainers... +>> Loaded 88 Trainers in 10 ms +Loading Gossip menu... +>> Loaded 8225 gossip_menu IDs in 10 ms +Loading Gossip menu options... +>> Loaded 5837 gossip_menu_option entries in 18 ms +Loading Creature Template Gossip... +creature_template_gossip has gossip definitions for menu id 55002 but this menu +doesn't exist +creature_template_gossip has gossip definitions for menu id 55002 but this menu +doesn't exist +creature_template_gossip has gossip definitions for menu id 6268 but this menu d +oesn't exist +creature_template_gossip has gossip definitions for menu id 9856 but this menu d +oesn't exist +creature_template_gossip has gossip definitions for menu id 10844 but this menu +doesn't exist +creature_template_gossip has gossip definitions for menu id 10852 but this menu +doesn't exist +creature_template_gossip has gossip definitions for menu id 2522 but this menu d +oesn't exist +creature_template_gossip has gossip definitions for menu id 12720 but this menu +doesn't exist +creature_template_gossip has gossip definitions for menu id 12718 but this menu +doesn't exist +creature_template_gossip has gossip definitions for menu id 13160 but this menu +doesn't exist +>> Loaded 5435 creature template gossip menus in 8 ms +Loading Creature trainers... +>> Loaded 1187 default trainers in 4 ms +Loading Vendors... +Table `(game_event_)npc_vendor` for Vendor (Entry: 44972) have in item list non- +existed item (60822, type 1), ignore +>> Loaded 48278 Vendors in 71 ms +Loading Waypoints... +>> Loaded 110626 waypoints in 177 ms +Loading Waypoint Addons... +>> Loaded 639 waypoint addon data in 2 ms +Loading SmartAI Waypoints... +>> Loaded 586 SmartAI waypoint paths (total 7064 waypoints) in 10 ms +Loading Creature Formations... +>> Loaded 2517 creatures in formations in 5 ms +Loading World State templates... +Table `world_state` contains a world state 3490 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3490 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3491 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3491 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3600 with invalid MapID (1134), map i +gnored +Table `world_state` contains a world state 3600 with invalid MapID (1504), map i +gnored +Table `world_state` contains a world state 3600 with invalid MapID (1505), map i +gnored +Table `world_state` contains a world state 3600 with invalid MapID (1552), map i +gnored +Table `world_state` contains a world state 3600 with invalid MapID (1672), map i +gnored +Table `world_state` contains a world state 3600 with invalid MapID (1825), map i +gnored +Table `world_state` contains a world state 3600 with invalid MapID (1911), map i +gnored +Table `world_state` contains a world state 3600 with invalid MapID (2167), map i +gnored +Table `world_state` contains a world state 3600 with invalid MapID (2373), map i +gnored +Table `world_state` contains a world state 3600 with invalid MapID (2509), map i +gnored +Table `world_state` contains a world state 3600 with invalid MapID (2511), map i +gnored +Table `world_state` contains a world state 3600 with invalid MapID (2547), map i +gnored +Table `world_state` contains a world state 3601 with invalid MapID (1134), map i +gnored +Table `world_state` contains a world state 3601 with invalid MapID (1504), map i +gnored +Table `world_state` contains a world state 3601 with invalid MapID (1505), map i +gnored +Table `world_state` contains a world state 3601 with invalid MapID (1552), map i +gnored +Table `world_state` contains a world state 3601 with invalid MapID (1672), map i +gnored +Table `world_state` contains a world state 3601 with invalid MapID (1825), map i +gnored +Table `world_state` contains a world state 3601 with invalid MapID (1911), map i +gnored +Table `world_state` contains a world state 3601 with invalid MapID (2167), map i +gnored +Table `world_state` contains a world state 3601 with invalid MapID (2373), map i +gnored +Table `world_state` contains a world state 3601 with invalid MapID (2509), map i +gnored +Table `world_state` contains a world state 3601 with invalid MapID (2511), map i +gnored +Table `world_state` contains a world state 3601 with invalid MapID (2547), map i +gnored +Table `world_state` contains a world state 3610 with invalid MapID (1134), map i +gnored +Table `world_state` contains a world state 3610 with invalid MapID (1504), map i +gnored +Table `world_state` contains a world state 3610 with invalid MapID (1505), map i +gnored +Table `world_state` contains a world state 3610 with invalid MapID (1552), map i +gnored +Table `world_state` contains a world state 3610 with invalid MapID (1672), map i +gnored +Table `world_state` contains a world state 3610 with invalid MapID (1825), map i +gnored +Table `world_state` contains a world state 3610 with invalid MapID (1911), map i +gnored +Table `world_state` contains a world state 3610 with invalid MapID (2167), map i +gnored +Table `world_state` contains a world state 3610 with invalid MapID (2373), map i +gnored +Table `world_state` contains a world state 3610 with invalid MapID (2509), map i +gnored +Table `world_state` contains a world state 3610 with invalid MapID (2511), map i +gnored +Table `world_state` contains a world state 3610 with invalid MapID (2547), map i +gnored +Table `world_state` contains a world state 3680 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3680 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3681 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3681 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3698 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3698 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3699 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3699 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3700 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3700 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3701 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3701 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3702 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3702 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3703 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3703 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3704 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3704 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3705 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3705 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3706 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3706 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3710 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3710 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3711 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3711 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3712 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3712 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3713 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3713 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3714 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3714 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3749 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3749 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3750 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3750 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3751 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3751 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3752 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3752 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3753 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3753 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3754 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3754 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3755 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3755 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3756 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3756 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3757 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3757 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3758 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3758 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3759 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3759 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3760 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3760 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3761 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3761 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3762 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3762 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3763 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3763 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3764 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3764 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3765 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3765 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3766 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3766 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3767 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3767 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3768 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3768 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3769 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3769 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3770 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3770 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3771 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3771 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3772 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3772 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3773 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3773 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3781 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3781 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3801 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3801 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3802 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3802 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3803 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 3803 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 3815 with invalid MapID (1544), map i +gnored +Table `world_state` contains a world state 3816 with invalid MapID (1544), map i +gnored +Table `world_state` contains a world state 4022 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 4022 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 4023 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 4023 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 4024 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 4024 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 4025 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 4025 with invalid AreaID (10176), are +a ignored +Table `world_state` contains a world state 4354 with invalid MapID (2118), map i +gnored +Table `world_state` contains a world state 4354 with invalid AreaID (10176), are +a ignored +>> Loaded 526 world state templates 4 ms +>> Loaded 41 saved world state values 24 ms +Loading Persistend World Variables... +>> Loaded 9 world variables in 4 ms +Loading Terrain World Map definitions... +>> Loaded 4 terrain world maps in 0 ms. +Loading Terrain Swap Default definitions... +>> Loaded 4 terrain swap defaults in 1 ms. +Loading Phase Area definitions... +>> Loaded 25 phase areas in 1 ms. +Loading Conditions... +>> Loaded 15912 conditions in 45 ms +Loading faction change achievement pairs... +>> Loaded 124 faction change achievement pairs in 2 ms +Loading faction change spell pairs... +>> Loaded 113 faction change spell pairs in 1 ms +Loading faction change quest pairs... +>> Loaded 250 faction change quest pairs in 1 ms +Loading faction change item pairs... +>> Loaded 752 faction change item pairs in 2 ms +Loading faction change reputation pairs... +>> Loaded 17 faction change reputation pairs in 1 ms +Loading faction change title pairs... +>> Loaded 22 faction change title pairs in 1 ms +Loading GM tickets... +>> Loaded 2 GM tickets in 0 ms +Loading GM surveys... +>> Loaded GM Survey count from database in 1 ms +Loading client addons... +>> Loaded 35 known addons in 0 ms +Returning old mails... +>> No expired mails found. +Loading Autobroadcasts... +>> Loaded 0 autobroadcasts definitions. DB table `autobroadcast` is empty for th +is realm! +Loading spell_scripts... +>> Loaded 143 script definitions in 1 ms +Loading event_scripts... +>> Loaded 461 script definitions in 2 ms +Loading waypoint_scripts... +>> Loaded 422 script definitions in 2 ms +Loading spell script names... +Scriptname: `spell_vcore_dungeon_horn` spell (Id: 120000) does not exist. +Scriptname: `spell_vcore_dungeon_horn` spell (Id: 120001) does not exist. +Scriptname: `spell_vcore_dungeon_horn` spell (Id: 120002) does not exist. +Scriptname: `spell_vcore_dungeon_horn` spell (Id: 120003) does not exist. +>> Loaded 3282 spell script names in 7 ms +Loading Creature Texts... +>> Loaded 16797 creature texts for 3997 creatures in 53 ms +Loading Creature Text Locales... +Loading creature StaticFlags overrides... +>> Loaded 133 creature static flag overrides in 1 ms +Loading Taxi node level definitions... +>> Loaded 274 taxi node level definitions in 1 ms +Initializing Scripts... +Loading Script Waypoints for 54 creature(s)... +>> Loaded 1377 Script Waypoint nodes in 4 ms +>> Loaded spline chain data for 85 chains, consisting of 272 splines with 1414 w +aypoints in 4 ms +Loading C++ scripts +Script named 'world_map_set_faction_worldstates_609' does not have a script name + assigned in database. +Script named 'item_gor_dreks_ointment' does not have a script name assigned in d +atabase. +>> Loaded 4106 C++ scripts in 146 ms +Validating spell scripts... +Spell `85123` Effect `Index: EFFECT_0 Target: 7` of script `spell_siege_cannon` +did not match dbc effect data - handler bound to hook `OnObjectAreaTargetSelect` + of SpellScript won't be executed +>> Validated 3470 scripts in 12 ms +Loading SmartAI scripts... +>> Loaded SmartAIMgr Helpers in 16 ms +>> Loaded 13506 SmartAI scripts in 167 ms +Loading Calendar data... +>> Loaded 0 calendar events in 1 ms +>> Loaded 0 calendar invites in 0 ms +Loading Petitions... +>> Loaded 0 petitions. +Loading Signatures... +>> Loaded 0 Petition signs! +Loading Summon Properties parameter data... +>> Loaded 78 summon properties parameters in 1 ms +Loading Item loot... +>> Loaded 0 stored item loots +>> Loaded 0 stored item money +Initialize query data... +>> Initialized query cache data in 1019 ms +Initialize commands... +Initialize game time and timers +Mail timer set to: 1260, mail return is called every 1440 minutes +Starting Map System +Starting Game Event system... +Initialize AuctionHouseBot... +Initializing Opcodes... +Starting Arena Season... +Starting Battleground System +>> Loaded 15 battlegrounds in 0 ms +Starting Outdoor PvP System +>> Loaded 5 outdoor PvP definitions in 1 ms +Starting Battlefield System +>> Loaded 2 battlefields in 0 ms +Loading Warden Checks... +Loading Warden Action Overrides... +Deleting expired bans... +Initializing quest reset times... +Calculate random battleground reset time... +Calculate guild limitation(s) reset time... +Calculate next currency reset time... +World initialized in 0 minutes 13 seconds +TC>TC>TrinityCore rev. 39a515dbd13e+ 2026-10-27 23:56:43 +0100 (feature/clientle +ss-player-core branch) (Unix, Debug, Static) (worldserver-daemon) ready... +clientless login 26 +TC>Registered clientless session for character GUID 26 (account 6), state=Loadin +g. +Created clientless player session for character GUID 26 (account 6). +ClientlessPlayerMgr started character GUID 26 (account 6). +Clientless login started for 'Arthoran' (GUID 26, account 6). +TC> Loading saved respawn time of 1790175804 for spawnid (0,323981) - spawn does + not exist, ignoring +Loading saved respawn time of 1790090644 for spawnid (0,396680) - spawn does not + exist, ignoring +Loading saved respawn time of 1790019808 for spawnid (0,396681) - spawn does not + exist, ignoring +Loading saved respawn time of 1790019853 for spawnid (0,396682) - spawn does not + exist, ignoring +Initialized transport GameObject grids: map=0 instance=0 grids=4 candidates=15 +Clientless player character GUID 26 (account 6) reached state=Online. +ClientlessLua initialized, script root '/home/wow_admin/wow_cata_server/source/T +rinityCore/src/server/scripts/Custom/clientless'. +[AdventureBot][Profile] loaded guid=26 name='Arthoran' source='/home/wow_admin/w +ow_cata_server/source/TrinityCore/src/server/scripts/Custom/clientless/adventure +/profiles/arthoran.lua' home='elwynn' canGroundMount=1 mounts=1 mountMinDistance +=45.0 mountSpeedRate=1.60 personality=8 regionAffinity=4 activityAffinity=7 +PROFILE_PROFESSIONS guid=26 primary=2 +ClientlessLua[26]: Arthoran ready; no automatic test journey queued +ClientlessLua[26]: executed adventure override script '/home/wow_admin/wow_cata_ +server/source/TrinityCore/src/server/scripts/Custom/clientless/adventure/overrid +es/arthoran.lua'. +ADVENTURE_CATALOG locations=2973 actions=84 lines=76 +INTENT_TRAVEL guid=26 intent='social' location='goldshire_tavern' area=0 +[AdventureBot] loaded 3966 clientless world points. +[AdventureBot] loaded travel graph: 993 node(s), 1765 directed edge(s). +clientless interaction 26 +TC>Interactions guid=26 grouped=0 leader=0 partner=0 intent='social' schedule='' + leaderIdleSeconds=0 afkSeconds=0 sociability=0.65 loyalty=0.35 +TC> SUPPLY_RESTOCK guid=26 day=20261028 daily=0 afterCombat=1 + diff --git a/doc/clientless/checkpoints/2026-10-28-interactions/tests.txt b/doc/clientless/checkpoints/2026-10-28-interactions/tests.txt new file mode 100644 index 0000000000..524798f5b4 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-28-interactions/tests.txt @@ -0,0 +1,57 @@ +test_adventure_area_node.py: PASS +test_adventure_behavior_policy.py: PASS +test_adventure_combat_exercise.py: PASS +test_adventure_combat_preparation.py: PASS +test_adventure_combat_reactions.py: PASS +test_adventure_economy.py: PASS +test_adventure_economy_catalog.py: PASS +test_adventure_exploration.py: PASS +test_adventure_exploration_catalog.py: PASS +test_adventure_graph_handover.py: PASS +test_adventure_instance.py: PASS +test_adventure_instance_autoresume.py: PASS +test_adventure_instance_begin.py: PASS +test_adventure_instance_catalog.py: PASS +test_adventure_instance_persistence.py: PASS +test_adventure_intent_state.py: PASS +test_adventure_interactions.py: PASS +test_adventure_leisure.py: PASS +test_adventure_leisure_catalog.py: PASS +test_adventure_location_import.py: PASS +test_adventure_micro_reset.py: PASS +test_adventure_profession_catalog.py: PASS +test_adventure_profession_profile.py: PASS +test_adventure_profession_sightline.py: PASS +test_adventure_profession_targets.py: PASS +test_adventure_repair_catalog.py: PASS +test_adventure_rest.py: PASS +test_adventure_rest_fire.py: PASS +test_adventure_rest_space.py: PASS +test_adventure_rest_targets.py: PASS +test_adventure_social.py: PASS +test_adventure_social_fire.py: PASS +test_adventure_supplies.py: PASS +test_adventure_supply_catalog.py: PASS +test_adventure_supply_items.py: PASS +test_adventure_training_catalog.py: PASS +test_adventure_walk.py: PASS +test_clientless_companion.py: PASS +test_clientless_damage_notifications.py: PASS +test_clientless_near_teleport_observer.py: PASS +test_default_phase.py: PASS +test_flight_area_query.py: PASS +test_flight_handoff.py: PASS +test_flight_policy.py: PASS +test_follow_spline_velocity.py: PASS +test_free_flight.py: PASS +test_free_flight_planner.py: PASS +test_ground_exit_continuation.py: PASS +test_local_spline_segments.py: PASS +test_lua_reload.py: PASS +test_player_pure_max_skill.py: PASS +test_portal_transfer.py: PASS +test_portal_use.py: PASS +test_taxi_import_disabled.py: PASS +test_transport.py: PASS +test_transport_grid_loading.py: PASS +56/56 passed; failures: [] diff --git a/doc/clientless/checkpoints/2026-10-28-interactions/validation.txt b/doc/clientless/checkpoints/2026-10-28-interactions/validation.txt new file mode 100644 index 0000000000..97b7cbf21e --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-28-interactions/validation.txt @@ -0,0 +1,8 @@ +Debug build -j10 and installation succeeded. Test worldserver59086 freshly ready, port8085 listening. +56/56 full suite passed; additional new social notification concurrency test passed (57 scripts total). +Final expanded combat test with player focus also passed. +Native login26 and interaction diagnostic succeeded; sociability0.65 loyalty0.35. +Real creature defense after restart observed. No live player invitation/PvP/pet/portal/ten-minute AFK claim. +Normal initial Goldshire trip started on login; behavior stop requested at safe point, last observed switching/waiting for safe state. +OLD executable shutdown observed Object.h:448 GetMap ASSERTION FAILED m_currMap; process49765 exited before installation. Cause not investigated; new startup succeeded. +Local commit requested by user; no push. No DB migrations. No temporary profile overrides. diff --git a/doc/clientless/interactions.md b/doc/clientless/interactions.md new file mode 100644 index 0000000000..2f92b41c6d --- /dev/null +++ b/doc/clientless/interactions.md @@ -0,0 +1,103 @@ +# AdventureBot interactions and player-led groups + +Implemented 2026-10-28 on feature/clientless-player-core. No autonomous bot-to-bot +party formation, role assignment, healer/tank AI or proactive PvP aggression. +PlayerAI.cpp supplies spell/attack foundations and charmed-player follow logic; +it does not implement invitation decisions. AdventureBots keep independent +controllers and use the native party invitation response handler. + +## Encounters + +Directed native text emotes greet/hello/wave/bow/salute/smile/thank and +rude/threaten/roar generate GUID-only notices in ClientlessPlayerMgr. Map workers +never call AdventureBot policy directly. Queues are bounded and delivered on the +world thread. Participants must be alive, mutually visible, in the same map, +instance and phase, within 25 yards and in line of sight. + +Replies wait 1–3 seconds, briefly interrupt the current activity, face the other +player and use native animation emotes. Friendly encounters wave/bow; hostility +or hostile emotes produce a negative gesture. There is no automatic attack just +because another player is hostile. Original intent deadlines are retained. +The global reply cooldown is 15 seconds, the same partner has 120 seconds. +Direct chat/free conversation parsing is not implemented. + +Bots in social/leisure/rest can occasionally initiate a five-second encounter +with another available AdventureBot within 12 yards. The lower character GUID +initiates and reserves both participants. The peer must also have a compatible +intent. Sociability weights initiation. Pair cooldown is five minutes; native +animation replies never feed back into incoming text-emote events. Occasional +localized 'Safe travels' accompanies greetings. This is a short greeting, not a +multi-line conversation or synchronized shared activity. + +Real positive damage by another player or their pet against the bot's active +combat target can queue a thank-you. Damage attribution is captured after damage +rejection/duel clamping and before lethal cleanup; no threat-only inference. +The thank-you is deferred until combat and the existing self-care finish, and +requires the helper still to be nearby and friendly. Dummy training is excluded. + +## Invitations and group behavior + +An actual native invitation is answered after 2–4 seconds in a safe state. +Acceptance probability clamps sociability plus an intent adjustment to [0,1]: +rest −0.50, profession/economy −0.25, adventure/instance +0.20, other +0.10. +Inviter must be alive and friendly. Bots do not accept invitations from other +clientless players yet; autonomous bot grouping is explicitly deferred. +Invitations are declined inside dungeons. Normal faction/group restrictions run +before the invitation reaches the bot; none are bypassed. + +Membership suspends autonomous intent execution, clears its queued actions and +retains its deadline. Native membership survives server restart. Bots follow the +current leader with checked local MMAP paths, aiming about three yards behind +and stopping within five yards. Longer/cross-map routes use existing +travel_to_node actions toward a nearby TravelGraph node at the leader's location; +no new teleport shortcut is introduced. Coverage of the graph and native portal +rules determines whether these routes work. An active graph trip is allowed to +finish before local follow resumes. Transport/flight states are not interrupted. +Unreachable destinations are retried conservatively; an active leader does not +cause a timeout merely because the route is missing. + +Nearby (40 yards) group members' actual combat targets or attackers can be +supported with the existing damage executor. Self-defense retains priority and +the first actual damager focus. Assist requires native attack permission, valid +visibility/phase, a verified path, at least 30% HP, and excludes world bosses and +creatures more than two levels above the bot. Other classes currently retain +basic autoattacks; class-specific support roles remain future work. + +## Leaving + +Never voluntarily leave while dead, fighting, in a dungeon/raid, on a transport, +or in flight. Pending conditions are reevaluated when safe: + +- A changed scheduled intent/location permits leaving. Personality + `group_loyalty` is the chance of staying for that new block (default 0.25, + Arthoran 0.35). The choice is made once per observed change, not each tick. + A new date generates/loads the native daily plan even while grouped. + For manual intents, expiry of the original deadline also changes this token. +- The leader being explicitly AFK for ten minutes, or ten minutes of observed + inactivity, permits leaving. Position/orientation, selection, combat, + casting, native chat, text emotes, spell and item requests count as activity. + Sampling cannot detect every conceivable stationary action. Native AFK takes + precedence while set. A new leader starts a fresh observation window; returning + from AFK ends the explicit AFK window. Observation clocks restart after a + server restart, so a restart does not immediately evict a bot. + +Before voluntarily leaving, a localized farewell is sent to the native party +chat. Native RemoveMember performs removal. Removal/disbanding by other players +also resumes autonomous scheduling at the current time, retaining deadlines. + +## Diagnostics and verification + +`clientless interaction 26` reports membership, leader, partner, intent, +schedule observation, idle/AFK seconds, sociability and loyalty. No state changes. + +`tests/clientless/test_adventure_interactions.py` compiles and executes production +manager code with deterministic native-world adapters: invitation acceptance and +rejection, group following/path rejection, leader changes, 599/600-second +boundaries, activity, deferred exits, directed group emotes, hostile replies, +pair reservation, cooldowns, graph handoff and deferred thanks. +`test_adventure_combat_reactions.py` also covers player target permission and +player damage notices while preserving corpse recovery regression checks. + +These tests do not replace live client acceptance. Multi-client player invites, +real PvP/pet defense, cross-map/instance group following, ten real minutes of AFK, +and two simultaneously online AdventureBots require separate gameplay checks. diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp index 9b3eeb0e25..e8c03cbfac 100644 --- a/src/server/game/Entities/Unit/Unit.cpp +++ b/src/server/game/Entities/Unit/Unit.cpp @@ -890,6 +890,9 @@ bool Unit::HasBreakableByDamageCrowdControlAura(Unit* excludeCasterChannel) cons victim->ToCreature()->LowerPlayerDamageReq(health < damage ? health : damage); } + if (damage) + sClientlessPlayerMgr->SharedTargetDamage(victim, attacker, std::min(health, damage)); + if (health <= damage) { if (victim->GetTypeId() == TYPEID_PLAYER && victim != attacker) diff --git a/src/server/game/Handlers/ChatHandler.cpp b/src/server/game/Handlers/ChatHandler.cpp index 9dec043982..78438c88a1 100644 --- a/src/server/game/Handlers/ChatHandler.cpp +++ b/src/server/game/Handlers/ChatHandler.cpp @@ -16,6 +16,7 @@ */ #include "WorldSession.h" +#include "ClientlessPlayerMgr.h" #include #include "AccountMgr.h" @@ -104,6 +105,7 @@ void WorldSession::HandleMessagechatOpcode(WorldPacket& recvData) } Player* sender = GetPlayer(); + sClientlessPlayerMgr->MarkActivity(sender); //TC_LOG_DEBUG("misc", "CHAT: packet received. type %u, lang %u", type, lang); @@ -569,6 +571,7 @@ void WorldSession::HandleMessagechatOpcode(WorldPacket& recvData) void WorldSession::HandleAddonMessagechatOpcode(WorldPacket& recvData) { Player* sender = GetPlayer(); + sClientlessPlayerMgr->MarkActivity(sender); ChatMsg type; switch (recvData.GetOpcode()) @@ -808,6 +811,7 @@ void WorldSession::HandleSendTextEmoteOpcode(WorldPackets::Chat::SendTextEmote& return; } + sClientlessPlayerMgr->MarkActivity(GetPlayer()); sScriptMgr->OnPlayerTextEmote(GetPlayer(), packet.SoundIndex, packet.EmoteID, packet.Target); EmotesTextEntry const* em = sEmotesTextStore.LookupEntry(packet.EmoteID); @@ -848,6 +852,9 @@ void WorldSession::HandleSendTextEmoteOpcode(WorldPackets::Chat::SendTextEmote& GetPlayer()->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_DO_EMOTE, packet.EmoteID, 0, 0, unit); + if (unit && unit->ToPlayer()) + sClientlessPlayerMgr->TextEmote(_player, unit->ToPlayer(), packet.EmoteID); + //Send scripted event call if (unit && unit->GetTypeId() == TYPEID_UNIT && ((Creature*)unit)->AI()) ((Creature*)unit)->AI()->ReceiveEmote(GetPlayer(), packet.EmoteID); diff --git a/src/server/game/Handlers/SpellHandler.cpp b/src/server/game/Handlers/SpellHandler.cpp index f92da7b84f..0407d8d52c 100644 --- a/src/server/game/Handlers/SpellHandler.cpp +++ b/src/server/game/Handlers/SpellHandler.cpp @@ -16,6 +16,7 @@ */ #include "WorldSession.h" +#include "ClientlessPlayerMgr.h" #include "Archaeology.h" #include "Common.h" #include "Config.h" @@ -93,6 +94,7 @@ void WorldSession::HandleClientCastFlags(WorldPacket& recvPacket, uint8 castFlag void WorldSession::HandleUseItemOpcode(WorldPackets::Spells::UseItem& packet) { + sClientlessPlayerMgr->MarkActivity(GetPlayer()); Player* user = _player; // ignore for remote control state @@ -265,6 +267,7 @@ void WorldSession::HandleGameobjectReportUse(WorldPacket& recvPacket) void WorldSession::HandleCastSpellOpcode(WorldPackets::Spells::CastSpell& cast) { + sClientlessPlayerMgr->MarkActivity(GetPlayer()); // ignore for remote control state (for player case) Unit* mover = GetGameClient()->GetActivelyMovedUnit(); if (!mover || (mover != _player && mover->IsPlayer())) diff --git a/src/server/game/World/AdventureBehaviorMgr.cpp b/src/server/game/World/AdventureBehaviorMgr.cpp index 5de1908138..ce8ab59381 100644 --- a/src/server/game/World/AdventureBehaviorMgr.cpp +++ b/src/server/game/World/AdventureBehaviorMgr.cpp @@ -1763,3 +1763,27 @@ std::string AdventureBehaviorMgr::CombatIntent(Player* player) const (it->second.State!=Phase::Travel && it->second.State!=Phase::OnSite)) return {}; return it->second.Intent; } + +std::string AdventureBehaviorMgr::InteractionIntent(Player* player) const +{ + auto it=_records.find(player->GetGUID().GetCounter()); + return it==_records.end()?std::string():it->second.Intent; +} +std::string AdventureBehaviorMgr::InteractionSchedule(Player* player) +{ + auto it=_records.find(player->GetGUID().GetCounter()); + if(it==_records.end())return {}; + auto& r=it->second; + auto local=Local(GameTime::GetGameTime()); + if(r.Schedule && r.Date!=DateOf(local)){Generate(player,r,DateOf(local));r.Hour=-1;} + if(!r.Schedule)return (r.Deadline && GameTime::GetGameTime()>=r.Deadline?"expired:":"")+r.Intent+":"+r.Location; + auto const& slot=r.Plan[Local(GameTime::GetGameTime()).tm_hour]; + return slot.Intent+":"+slot.Location; +} + +void AdventureBehaviorMgr::InterruptForInteraction(Player* player) +{ + InterruptForCombat(player); + auto it=_records.find(player->GetGUID().GetCounter()); + if(it!=_records.end())it->second.Detail="social/group interaction; original deadline retained"; +} diff --git a/src/server/game/World/AdventureBehaviorMgr.h b/src/server/game/World/AdventureBehaviorMgr.h index 45ed8001e6..d879881f9a 100644 --- a/src/server/game/World/AdventureBehaviorMgr.h +++ b/src/server/game/World/AdventureBehaviorMgr.h @@ -30,7 +30,10 @@ public: void Update(Player* player, uint32 diff); bool Command(ChatHandler* handler, char const* args); void InterruptForCombat(Player* player); + void InterruptForInteraction(Player* player); std::string CombatIntent(Player* player) const; + std::string InteractionIntent(Player* player) const; + std::string InteractionSchedule(Player* player); private: enum class Phase {Idle, Travel, OnSite, Switching, Handover}; struct RecoveryScene diff --git a/src/server/game/World/AdventureCombatMgr.cpp b/src/server/game/World/AdventureCombatMgr.cpp index 01cb644712..6ee8481957 100644 --- a/src/server/game/World/AdventureCombatMgr.cpp +++ b/src/server/game/World/AdventureCombatMgr.cpp @@ -1,4 +1,5 @@ #include "AdventureCombatMgr.h" +#include "AdventureInteractionMgr.h" #include "AdventureCombatPolicy.h" #include "AdventureBehaviorMgr.h" #include "ClientlessLuaEngine.h" @@ -30,7 +31,7 @@ class AdventureCombatMgr::Fighter : public PlayerAI public: explicit Fighter(Player* player):PlayerAI(player) { } void UpdateAI(uint32) override { } - void Tick(Creature* target,uint32 diff) + void Tick(Unit* target,uint32 diff) { _rend=_rend>diff?_rend-diff:0;_filler=_filler>diff?_filler-diff:0; me->SetFacingToObject(target); @@ -59,13 +60,16 @@ private: uint32 _rend=0,_filler=0; }; AdventureCombatMgr* AdventureCombatMgr::instance(){static AdventureCombatMgr mgr;return &mgr;} -bool AdventureCombatMgr::Valid(Player* p,Creature* c,bool training) const +bool AdventureCombatMgr::Valid(Player* p,Unit* c,bool training) const { - return c && c->IsAlive() && c->IsInWorld() && p->IsInPhase(c) && p->CanSeeOrDetect(c) && - p->IsValidAttackTarget(c) && !c->GetControllingPlayer() && !c->IsEvadingAttacks() && - (training?(c->GetScriptName()=="npc_training_dummy" && c->getLevel()<=p->getLevel()+2 && !c->isWorldBoss()):c->GetScriptName()!="npc_training_dummy"); + if(!c || !c->IsAlive() || !c->IsInWorld() || !p->IsInPhase(c) || + !p->CanSeeOrDetect(c) || !p->IsValidAttackTarget(c))return false; + if(auto creature=c->ToCreature()) + return !creature->IsEvadingAttacks() && (!training || !creature->GetControllingPlayer()) && + (training?(creature->GetScriptName()=="npc_training_dummy" && creature->getLevel()<=p->getLevel()+2 && !creature->isWorldBoss()):creature->GetScriptName()!="npc_training_dummy"); + return !training && c->ToPlayer(); // Native PvP/sanctuary/duel checks above. } -bool AdventureCombatMgr::Reachable(Player* p,Creature* c) const +bool AdventureCombatMgr::Reachable(Player* p,Unit* c) const { if(!p->IsWithinLOSInMap(c))return false; if(p->IsWithinMeleeRange(c))return true; @@ -75,7 +79,7 @@ bool AdventureCombatMgr::Reachable(Player* p,Creature* c) const } bool AdventureCombatMgr::Safe(Player* p,Creature* c) const { - if(!Valid(p,c,false) || p->GetExactDist(c)>40 || (c->GetVictim() && c->GetVictim()!=p))return false; + if(!Valid(p,c,false) || c->GetControllingPlayer() || p->GetExactDist(c)>40 || (c->GetVictim() && c->GetVictim()!=p))return false; std::vector others;p->GetCreatureListWithEntryInGrid(others,0,35); unsigned hostiles=0; for(auto other:others)if(other->IsAlive() && p->IsInPhase(other) && p->IsHostileTo(other) && @@ -95,7 +99,7 @@ Creature* AdventureCombatMgr::Dummy(Player* p) const return best; } bool AdventureCombatMgr::TrainingAvailable(Player* p) const{return Dummy(p)!=nullptr;} -bool AdventureCombatMgr::Start(Player* p,Record& r,Creature* c,bool training,bool defending,time_t until) +bool AdventureCombatMgr::Start(Player* p,Record& r,Unit* c,bool training,bool defending,time_t until) { if(!Valid(p,c,training) || !p->IsAlive() || p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsCharmed())return false; sAdventureBehaviorMgr->InterruptForCombat(p); @@ -122,8 +126,8 @@ bool AdventureCombatMgr::Attack(Player* p,uint32 spawn) } void AdventureCombatMgr::DamageTaken(Player* p,Unit* attacker,uint32 damage) { - auto c=attacker?attacker->ToCreature():nullptr; - if(!damage || !p->IsAlive() || !c || c->GetControllingPlayer())return; + auto c=attacker; + if(!damage || !p->IsAlive() || !c)return; auto& r=_records[p->GetGUID().GetCounter()]; if(std::find(r.Damagers.begin(),r.Damagers.end(),c->GetGUID())==r.Damagers.end())r.Damagers.push_back(c->GetGUID()); } @@ -131,6 +135,8 @@ void AdventureCombatMgr::Finish(Player* p,Record& r) { TC_LOG_INFO("server.adventurebot","COMBAT_END guid=%u target='%s' training=%u",p->GetGUID().GetCounter(),r.Target.ToString().c_str(),r.Training); bool training=r.Training; + if(!training && r.Helper)sAdventureInteractionMgr->CombatHelp(p,r.Helper); + r.Helper.Clear(); p->AttackStop();p->CastStop();p->StopMoving();p->GetMotionMaster()->Clear();p->SetSelection(ObjectGuid::Empty); sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter()); if(training && std::all_of(p->getAttackers().begin(),p->getAttackers().end(),[](Unit* u) @@ -253,15 +259,15 @@ bool AdventureCombatMgr::Update(Player* p,uint32 diff) // target. Keep all subsequent damagers ordered for after that focus dies. if(!r.Defending)while(!r.Damagers.empty()) { - auto id=r.Damagers.front();r.Damagers.pop_front();auto c=ObjectAccessor::GetCreature(*p,id); + auto id=r.Damagers.front();r.Damagers.pop_front();auto c=ObjectAccessor::GetUnit(*p,id); if(!Valid(p,c,false))continue; if(r.AI)Finish(p,r); Start(p,r,c,false,true,0);break; } - if(!r.AI && !p->IsInCombat() && !r.Cooldown) + if(!r.AI && !p->GetGroup() && !p->IsInCombat() && !r.Cooldown) if(auto c=Proactive(p))Start(p,r,c,false,false,0); if(!r.AI)return false; - auto c=ObjectAccessor::GetCreature(*p,r.Target); + 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)) @@ -291,3 +297,18 @@ bool AdventureCombatMgr::Busy(Player* player) const auto it=_records.find(player->GetGUID().GetCounter()); return it!=_records.end() && (it->second.AI || it->second.Dead || !it->second.Damagers.empty()); } + +bool AdventureCombatMgr::Assist(Player* p,Unit* target) +{ + if(!p->GetGroup() || Busy(p) || !Valid(p,target,false) || p->GetExactDist(target)>40 || + !Reachable(p,target) || p->GetHealthPct()<30)return false; + if(auto c=target->ToCreature()) + if(c->isWorldBoss() || c->getLevel()>p->getLevel()+2)return false; + return Start(p,_records[p->GetGUID().GetCounter()],target,false,false,0); +} + +void AdventureCombatMgr::SharedTargetDamage(Player* p,ObjectGuid helper,ObjectGuid target) +{ + auto it=_records.find(p->GetGUID().GetCounter()); + if(it!=_records.end() && it->second.Target==target && !it->second.Training)it->second.Helper=helper; +} diff --git a/src/server/game/World/AdventureCombatMgr.h b/src/server/game/World/AdventureCombatMgr.h index c88d671e9d..ffa8050682 100644 --- a/src/server/game/World/AdventureCombatMgr.h +++ b/src/server/game/World/AdventureCombatMgr.h @@ -22,23 +22,25 @@ public: bool Train(Player* player,uint32 durationMs,time_t deadline=0); bool Attack(Player* player,uint32 creatureSpawnId); void Stop(Player* player); + bool Assist(Player* player,Unit* target); + void SharedTargetDamage(Player* player,ObjectGuid helper,ObjectGuid target); bool Busy(Player* player) const; private: class Fighter; struct Record { std::shared_ptr AI; - ObjectGuid Target; + ObjectGuid Target,Helper; std::deque Damagers; bool Training=false,Defending=false,Suspended=false,Dead=false,CemeteryPending=false; time_t Until=0,NextCorpseSearch=0; std::vector FailedCorpsePoints; uint32 Timer=0,UnreachableMs=0,Cooldown=0; }; - bool Start(Player* player,Record& record,Creature* target,bool training,bool defending,time_t until); - bool Valid(Player* player,Creature* target,bool training) const; + bool Start(Player* player,Record& record,Unit* target,bool training,bool defending,time_t until); + bool Valid(Player* player,Unit* target,bool training) const; bool Safe(Player* player,Creature* target) const; - bool Reachable(Player* player,Creature* target) const; + bool Reachable(Player* player,Unit* target) const; Creature* Dummy(Player* player) const; Creature* Proactive(Player* player) const; bool Death(Player* player,Record& record); diff --git a/src/server/game/World/AdventureInteractionMgr.cpp b/src/server/game/World/AdventureInteractionMgr.cpp new file mode 100644 index 0000000000..9f80200dcc --- /dev/null +++ b/src/server/game/World/AdventureInteractionMgr.cpp @@ -0,0 +1,254 @@ +#include "AdventureInteractionMgr.h" +#include "AdventureInteractionPolicy.h" +#include "AdventureBehaviorMgr.h" +#include "AdventureCombatMgr.h" +#include "ClientlessLuaEngine.h" +#include "ClientlessPlayerMgr.h" +#include "ClientlessTravelMgr.h" +#include "ClientlessActionExecutorMgr.h" +#include "ClientlessTravelGraphMgr.h" +#include "Chat.h" +#include "Creature.h" +#include "GameTime.h" +#include "Group.h" +#include "Map.h" +#include "MotionMaster.h" +#include "ObjectAccessor.h" +#include "Opcodes.h" +#include "PartyPackets.h" +#include "Player.h" +#include "Random.h" +#include "WorldPacket.h" +#include "WorldSession.h" +#include "Log.h" +#include + +namespace +{ + float Sociability(Player* p) + { + auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter()); + if(profile){auto it=profile->Personality.find("sociability");if(it!=profile->Personality.end())return it->second;} + return 0.5f; + } + float Loyalty(Player* p) + { + auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter()); + if(profile){auto it=profile->Personality.find("group_loyalty");if(it!=profile->Personality.end())return std::max(0.0f,std::min(1.0f,it->second));} + return 0.25f; + } + bool German(Player* p) + { + auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter()); + return profile && profile->Locale=="deDE"; + } + bool HostileEmote(uint32 emote) + {return emote==TEXT_EMOTE_RUDE || emote==TEXT_EMOTE_THREATEN || emote==TEXT_EMOTE_ROAR;} + bool FriendlyEmote(uint32 emote) + { + switch(emote) + { + case TEXT_EMOTE_GREET:case TEXT_EMOTE_HELLO:case TEXT_EMOTE_WAVE:case TEXT_EMOTE_BOW: + case TEXT_EMOTE_SALUTE:case TEXT_EMOTE_SMILE:case TEXT_EMOTE_THANK:return true; + default:return false; + } + } +} +AdventureInteractionMgr* AdventureInteractionMgr::instance(){static AdventureInteractionMgr mgr;return &mgr;} +bool AdventureInteractionMgr::Visible(Player* p,Player* other,float distance) const +{ + return other && other!=p && other->IsAlive() && other->IsInWorld() && p->IsInMap(other) && + p->IsInPhase(other) && p->CanSeeOrDetect(other) && other->CanSeeOrDetect(p) && p->GetExactDist(other)<=distance && p->IsWithinLOSInMap(other); +} +void AdventureInteractionMgr::PartySay(Player* p,char const* german,char const* english) +{ + auto group=p->GetGroup();if(!group)return; + WorldPacket packet; + ChatHandler::BuildChatPacket(packet,CHAT_MSG_PARTY,p->GetTeam()==ALLIANCE?LANG_COMMON:LANG_ORCISH,p,nullptr,German(p)?german:english); + group->BroadcastPacket(&packet,false); +} +void AdventureInteractionMgr::Leave(Player* p,Record& r,char const* reason) +{ + PartySay(p,"Ich muss weiter. Gute Reise!","I must be going. Safe travels!"); + TC_LOG_INFO("server.adventurebot","INTERACTION_GROUP_LEAVE guid=%u reason='%s'",p->GetGUID().GetCounter(),reason); + p->GetGroup()->RemoveMember(p->GetGUID(),GROUP_REMOVEMETHOD_LEAVE); + sAdventureBehaviorMgr->InterruptForInteraction(p); + r.Grouped=false;r.Leader.Clear();r.Invite.Clear();r.Partner.Clear();r.SceneUntil=0; +} +void AdventureInteractionMgr::Emote(Player* p,Player* source,uint32 emote) +{ + if(!Visible(p,source,25) || p->IsInCombat() || !p->IsAlive() || p->GetMap()->IsDungeon() || + p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsCharmed())return; + auto now=GameTime::GetGameTime();auto& r=_records[p->GetGUID().GetCounter()]; + if(r.Partner || nowGetGUID());if(it!=r.Encounters.end() && nowsecond)return; + r.Partner=source->GetGUID();r.Reply=emote;r.ReplyAt=now+urand(1,3);r.SceneUntil=r.ReplyAt+2; + r.NextSocial=now+15;r.Encounters[source->GetGUID()]=now+120; +} +void AdventureInteractionMgr::Reply(Player* p,Player* other,uint32 emote) +{ + p->SetStandState(UNIT_STAND_STATE_STAND);p->SetFacingToObject(other); + // Hostility alone never grants permission to attack. Native attack checks + // remain in combat; these are visual reactions without combat side effects. + bool friendly=p->IsFriendlyTo(other) && !HostileEmote(emote); + p->HandleEmoteCommand(friendly?(emote==TEXT_EMOTE_BOW?EMOTE_ONESHOT_BOW:EMOTE_ONESHOT_WAVE):EMOTE_ONESHOT_NO); + if(emote==TEXT_EMOTE_GREET && friendly && urand(0,2)==0) + p->Say(German(p)?"Gute Reise!":"Safe travels!",p->GetTeam()==ALLIANCE?LANG_COMMON:LANG_ORCISH,other); + TC_LOG_INFO("server.adventurebot","INTERACTION_EMOTE guid=%u other=%u friendly=%u",p->GetGUID().GetCounter(),other->GetGUID().GetCounter(),friendly); +} +void AdventureInteractionMgr::CombatHelp(Player* p,ObjectGuid helper) +{ _records[p->GetGUID().GetCounter()].Helper=helper; } +void AdventureInteractionMgr::Offline(Player* p){_records.erase(p->GetGUID().GetCounter());} +bool AdventureInteractionMgr::Update(Player* p,uint32 diff) +{ + auto& r=_records[p->GetGUID().GetCounter()];auto now=GameTime::GetGameTime(); + if(r.Timer>diff){r.Timer-=diff;return r.Grouped || r.SceneUntil>now;} + r.Timer=1000; + bool unsafe=!p->IsAlive() || p->IsInCombat() || sAdventureCombatMgr->Busy(p) || + p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsCharmed(); + if(unsafe){r.Partner.Clear();r.SceneUntil=0;return p->GetGroup()!=nullptr;} + for(auto it=r.Encounters.begin();it!=r.Encounters.end();)if(now>=it->second)it=r.Encounters.erase(it);else ++it; + if(r.Helper) + { + auto helper=ObjectAccessor::FindPlayer(r.Helper);r.Helper.Clear(); + if(Visible(p,helper,25) && p->IsFriendlyTo(helper)) + p->Say(German(p)?"Danke für die Hilfe!":"Thank you for the help!",p->GetTeam()==ALLIANCE?LANG_COMMON:LANG_ORCISH,helper); + } + if(auto invite=p->GetGroupInvite()) + { + auto leader=invite->GetLeaderGUID(); + if(r.Invite!=leader){r.Invite=leader;r.InviteAt=now+urand(2,4);} + if(now>=r.InviteAt) + { + auto inviter=ObjectAccessor::FindPlayer(leader); + bool accept=inviter && inviter->IsAlive() && p->IsFriendlyTo(inviter) && !p->GetMap()->IsDungeon() && + !inviter->GetSession()->IsClientless() && frand(0,1)InteractionIntent(p),Sociability(p)); + WorldPackets::Party::PartyInviteResponse response{WorldPacket(CMSG_PARTY_INVITE_RESPONSE)};response.Accept=accept; + p->GetSession()->HandlePartyInviteResponseOpcode(response);r.Invite.Clear(); + TC_LOG_INFO("server.adventurebot","INTERACTION_INVITE guid=%u leader=%u accepted=%u",p->GetGUID().GetCounter(),leader.GetCounter(),accept); + } + } + else r.Invite.Clear(); + if(r.Partner) + { + auto other=ObjectAccessor::FindPlayer(r.Partner); + if(!Visible(p,other,25) || now>=r.SceneUntil){r.Partner.Clear();r.SceneUntil=0;} + else if(now>=r.ReplyAt) + { + sAdventureBehaviorMgr->InterruptForInteraction(p);Reply(p,other,r.Reply);r.ReplyAt=r.SceneUntil+1; + } + if(r.Partner)return true; + } + if(auto group=p->GetGroup()) + { + auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID()); + if(!r.Grouped) + { + r.Grouped=true;r.Schedule=sAdventureBehaviorMgr->InteractionSchedule(p); + r.Partner.Clear();r.SceneUntil=0; + sAdventureBehaviorMgr->InterruptForInteraction(p); + PartySay(p,"Ich begleite euch eine Weile.","I'll accompany you for a while."); + } + if(r.Leader!=group->GetLeaderGUID()) + { + r.Leader=group->GetLeaderGUID();r.LastActivity=now;r.AfkSince=0; + if(leader){r.LeaderPosition.Relocate(leader);r.LeaderMap=leader->GetMapId();r.Selection=leader->GetTarget();} + } + if(leader) + { + r.LastActivity=std::max(r.LastActivity,sClientlessPlayerMgr->LastActivity(leader->GetGUID())); + if(r.LeaderMap!=leader->GetMapId() || r.LeaderPosition.GetExactDist(leader)>0.1f || + r.LeaderPosition.GetOrientation()!=leader->GetOrientation() || r.Selection!=leader->GetTarget() || + leader->IsInCombat() || leader->HasUnitState(UNIT_STATE_CASTING))r.LastActivity=now; + r.LeaderPosition.Relocate(leader);r.LeaderMap=leader->GetMapId();r.Selection=leader->GetTarget(); + if(leader->isAFK()){if(!r.AfkSince)r.AfkSince=now;}else r.AfkSince=0; + } + if(AdventureInteractionPolicy::CanLeave(p->IsAlive(),p->IsInCombat(),p->GetMap()->IsDungeon(),p->GetTransport() || p->IsInFlight())) + { + if(r.Schedule!=sAdventureBehaviorMgr->InteractionSchedule(p)) + { + r.Schedule=sAdventureBehaviorMgr->InteractionSchedule(p); + if(frand(0,1)>=Loyalty(p)){Leave(p,r,"intent-change");return false;} + } + if(AdventureInteractionPolicy::LeaderExpired(now,r.AfkSince?r.AfkSince:r.LastActivity)){Leave(p,r,"leader-afk");return false;} + } + // Existing warrior executor supplies damage assistance. Roles/healing + // deliberately remain a later profile-driven extension. + for(auto member=group->GetFirstMember();member;member=member->next()) + { + auto ally=member->GetSource();if(!ally || !ally->IsAlive() || !p->IsInMap(ally) || p->GetExactDist(ally)>40)continue; + auto target=ally->IsInCombat()?ally->GetVictim():nullptr; + if(!target)for(auto attacker:ally->getAttackers())if(attacker->GetVictim()==ally){target=attacker;break;} + if(target && sAdventureCombatMgr->Assist(p,target))return true; + } + if(!leader || leader==p || !leader->IsAlive())return true; + auto execution=sClientlessActionExecutorMgr->GetStatus(p->GetGUID().GetCounter()); + if(execution && (execution->State==ClientlessActionExecutionState::Moving || execution->State==ClientlessActionExecutionState::SelectingTarget))return true; + if(!p->IsInMap(leader) || p->GetExactDist(leader)>=100) + { + if(now>=r.NextRoute) + { + r.NextRoute=now+15; + std::string node;float distance=0; + sClientlessTravelGraphMgr->EnsureLoaded(); + if(sClientlessTravelGraphMgr->FindNearestNodeToPosition(leader->GetMapId(),leader->GetPositionX(),leader->GetPositionY(),leader->GetPositionZ(),node,distance) && distance<1000) + { + ClientlessAction action;action.Name="travel_to_node";action.Parameters["node"]=node; + sClientlessPlayerMgr->QueueAction(p->GetGUID().GetCounter(),std::move(action)); + } + } + return true; + } + if(!p->IsInPhase(leader) || !p->CanSeeOrDetect(leader))return true; + if(p->GetExactDist(leader)>5 && p->GetExactDist(leader)<100) + { + auto nav=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter()); + if(!nav || nav->State!=ClientlessTravelState::Moving || nav->Target.GetExactDist(leader)>5) + { + Position end;leader->GetNearPoint(p,end.m_positionX,end.m_positionY,end.m_positionZ,3,leader->GetOrientation()+3.14159265f); + ClientlessPathDiagnostics path; + if(sClientlessTravelMgr->AnalyzePath(p,end,path) && path.Complete && !path.Suspicious && path.PathLength<150) + sClientlessTravelMgr->StartTravel(p,end); + } + } + else if(p->GetExactDist(leader)<=5)sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter()); + return true; + } + if(r.Grouped){r.Grouped=false;r.Leader.Clear();sAdventureBehaviorMgr->InterruptForInteraction(p);} + auto intent=sAdventureBehaviorMgr->InteractionIntent(p); + if(now>=r.NextEncounter && now>=r.NextSocial && !p->GetMap()->IsDungeon() && (intent=="social" || intent=="leisure" || intent=="rest")) + { + r.NextEncounter=now+urand(30,60); + std::vector people;p->GetPlayerListInGrid(people,12); + for(auto other:people) + { + if(!Visible(p,other,12) || !sClientlessLuaEngine->GetAdventureProfile(other->GetGUID().GetCounter()) || + p->GetGUID().GetCounter()>=other->GetGUID().GetCounter() || other->IsInCombat() || other->GetGroup())continue; + auto& peer=_records[other->GetGUID().GetCounter()]; + auto peerIntent=sAdventureBehaviorMgr->InteractionIntent(other); + if((peerIntent!="social" && peerIntent!="leisure" && peerIntent!="rest") || other->HasUnitState(UNIT_STATE_CASTING))continue; + if(peer.Partner || peer.NextSocial>now || r.Encounters.count(other->GetGUID()) || frand(0,1)>Sociability(p))continue; + // One initiator reserves both participants; animation replies do + // not generate incoming text-emote events, preventing loops. + r.Partner=other->GetGUID();r.Reply=p->IsFriendlyTo(other)?TEXT_EMOTE_GREET:TEXT_EMOTE_THREATEN; + r.ReplyAt=now+1;r.SceneUntil=now+5;r.Encounters[other->GetGUID()]=now+300; + peer.Partner=p->GetGUID();peer.Reply=r.Reply;peer.ReplyAt=now+3;peer.SceneUntil=now+5; + peer.NextSocial=now+60;peer.Encounters[p->GetGUID()]=now+300; + return true; + } + } + return false; +} + +void AdventureInteractionMgr::Report(ChatHandler* handler,Player* p) const +{ + auto it=_records.find(p->GetGUID().GetCounter()); + if(it==_records.end()){handler->SendSysMessage("No interaction state yet.");return;} + auto const& r=it->second;auto now=GameTime::GetGameTime(); + handler->PSendSysMessage("Interactions guid=%u grouped=%u leader=%u partner=%u intent='%s' schedule='%s'", + p->GetGUID().GetCounter(),p->GetGroup()?1:0,r.Leader.GetCounter(),r.Partner.GetCounter(), + sAdventureBehaviorMgr->InteractionIntent(p).c_str(),r.Schedule.c_str()); + handler->PSendSysMessage(" leaderIdleSeconds=%llu afkSeconds=%llu sociability=%.2f loyalty=%.2f", + static_cast(r.LastActivity && now>=r.LastActivity?now-r.LastActivity:0), + static_cast(r.AfkSince && now>=r.AfkSince?now-r.AfkSince:0),Sociability(p),Loyalty(p)); +} diff --git a/src/server/game/World/AdventureInteractionMgr.h b/src/server/game/World/AdventureInteractionMgr.h new file mode 100644 index 0000000000..11a9fd215f --- /dev/null +++ b/src/server/game/World/AdventureInteractionMgr.h @@ -0,0 +1,41 @@ +#ifndef VCORE_ADVENTURE_INTERACTION_MGR_H +#define VCORE_ADVENTURE_INTERACTION_MGR_H +#include "Common.h" +#include "ObjectGuid.h" +#include "Position.h" +#include +#include +#include +class Player; +class ChatHandler; +class TC_GAME_API AdventureInteractionMgr +{ +public: + static AdventureInteractionMgr* instance(); + bool Update(Player* player,uint32 diff); + void Emote(Player* player,Player* source,uint32 emote); + void Offline(Player* player); + void CombatHelp(Player* player,ObjectGuid helper); + void Report(ChatHandler* handler,Player* player) const; +private: + struct Record + { + uint32 Timer=0; + ObjectGuid Leader,Invite,Partner,Selection,Helper; + time_t NextRoute=0,NextEncounter=0; + Position LeaderPosition; + uint32 LeaderMap=0; + time_t InviteAt=0,LastActivity=0,AfkSince=0,ReplyAt=0,SceneUntil=0,NextSocial=0; + uint32 Reply=0; + bool Grouped=false; + std::string Schedule; + std::unordered_map Encounters; + }; + bool Visible(Player* player,Player* other,float distance) const; + void Reply(Player* player,Player* other,uint32 emote); + void Leave(Player* player,Record& record,char const* reason); + void PartySay(Player* player,char const* german,char const* english); + std::unordered_map _records; +}; +#define sAdventureInteractionMgr AdventureInteractionMgr::instance() +#endif diff --git a/src/server/game/World/AdventureInteractionPolicy.h b/src/server/game/World/AdventureInteractionPolicy.h new file mode 100644 index 0000000000..9591aec2f6 --- /dev/null +++ b/src/server/game/World/AdventureInteractionPolicy.h @@ -0,0 +1,20 @@ +#ifndef VCORE_ADVENTURE_INTERACTION_POLICY_H +#define VCORE_ADVENTURE_INTERACTION_POLICY_H +#include +#include +#include +namespace AdventureInteractionPolicy +{ + constexpr std::int64_t LeaderTimeout=600; + inline float Willingness(std::string const& intent,float sociability) + { + float bias=intent=="rest"?-0.5f:intent=="profession"||intent=="economy"?-0.25f: + intent=="adventure"||intent=="instance"?0.2f:0.1f; + return std::max(0.0f,std::min(1.0f,sociability+bias)); + } + inline bool CanLeave(bool alive,bool combat,bool dungeon,bool transport) + {return alive && !combat && !dungeon && !transport;} + inline bool LeaderExpired(std::int64_t now,std::int64_t lastActivity) + {return lastActivity>0 && now>=lastActivity && now-lastActivity>=LeaderTimeout;} +} +#endif diff --git a/src/server/game/World/ClientlessLuaEngine.cpp b/src/server/game/World/ClientlessLuaEngine.cpp index d666e8fca6..ac0360e6ea 100644 --- a/src/server/game/World/ClientlessLuaEngine.cpp +++ b/src/server/game/World/ClientlessLuaEngine.cpp @@ -5,6 +5,7 @@ #include "ClientlessTravelMgr.h" #include "AdventureBehaviorMgr.h" #include "AdventureCombatMgr.h" +#include "AdventureInteractionMgr.h" #include "AdventureSupplyMgr.h" #include "AdventureSocialActions.h" #include "Config.h" @@ -223,12 +224,17 @@ public: void Update(Player* player, uint32 diff) override { if(sAdventureSupplyMgr->Update(player,diff,sAdventureCombatMgr->Busy(player)))return; - if(!sAdventureCombatMgr->Update(player,diff))sAdventureBehaviorMgr->Update(player,diff); + if(!sAdventureCombatMgr->Update(player,diff) && !sAdventureInteractionMgr->Update(player,diff)) + sAdventureBehaviorMgr->Update(player,diff); } + void OnSharedTargetDamage(Player* player,ObjectGuid helper,ObjectGuid target) override + { sAdventureCombatMgr->SharedTargetDamage(player,helper,target); } + void OnTextEmote(Player* player, Player* source, uint32 emote) override + { sAdventureInteractionMgr->Emote(player,source,emote); } void OnDamageTaken(Player* player, Unit* attacker, uint32 damage) override { sAdventureCombatMgr->DamageTaken(player, attacker, damage); } void OnPlayerLogout(Player* player) override - { sAdventureSupplyMgr->Offline(player); sAdventureCombatMgr->Offline(player); sAdventureBehaviorMgr->Offline(player); } + { sAdventureInteractionMgr->Offline(player); sAdventureSupplyMgr->Offline(player); sAdventureCombatMgr->Offline(player); sAdventureBehaviorMgr->Offline(player); } }; } diff --git a/src/server/game/World/ClientlessPlayerMgr.cpp b/src/server/game/World/ClientlessPlayerMgr.cpp index aa728e65e3..32f3678679 100644 --- a/src/server/game/World/ClientlessPlayerMgr.cpp +++ b/src/server/game/World/ClientlessPlayerMgr.cpp @@ -1,4 +1,5 @@ #include "ClientlessPlayerMgr.h" +#include "GameTime.h" #include "ClientlessTravelMgr.h" #include "ClientlessTransportMgr.h" #include "ClientlessPerceptionMgr.h" @@ -154,7 +155,9 @@ void ClientlessPlayerMgr::Update(uint32 diff) // Damage can arrive from different map workers. Hand GUID-only notices to // the world-thread controller, preserving order within each character. decltype(_damageNotices) damageNotices; - { std::lock_guard lock(_damageMutex); damageNotices.swap(_damageNotices); } + decltype(_emoteNotices) emoteNotices; + decltype(_helpNotices) helpNotices; + { std::lock_guard lock(_damageMutex); damageNotices.swap(_damageNotices); emoteNotices.swap(_emoteNotices); helpNotices.swap(_helpNotices); } // CLIENTLESS_PLAYER_CORE_C8_2_TRAVEL_ROBUSTNESS sClientlessTravelMgr->Update(diff); @@ -199,6 +202,12 @@ void ClientlessPlayerMgr::Update(uint32 diff) auto notices=damageNotices.find(characterGuid); if(notices!=damageNotices.end())for(auto const& notice:notices->second) record.Controller->OnDamageTaken(player,ObjectAccessor::GetUnit(*player,notice.Attacker),notice.Damage); + auto emotes=emoteNotices.find(characterGuid); + if(emotes!=emoteNotices.end())for(auto const& notice:emotes->second) + record.Controller->OnTextEmote(player,ObjectAccessor::FindPlayer(notice.Source),notice.Emote); + auto helpers=helpNotices.find(characterGuid); + if(helpers!=helpNotices.end())for(auto const& notice:helpers->second) + record.Controller->OnSharedTargetDamage(player,notice.Helper,notice.Target); record.Controller->Update(player, diff); } } @@ -219,3 +228,43 @@ void ClientlessPlayerMgr::DamageTaken(Player* player, Unit* attacker, uint32 dam std::lock_guard lock(_damageMutex); _damageNotices[player->GetGUID().GetCounter()].push_back({attacker->GetGUID(),damage}); } + +void ClientlessPlayerMgr::TextEmote(Player* source, Player* target, uint32 emote) +{ + if(!source || !target || source==target || !target->GetSession()->IsClientless())return; + std::lock_guard lock(_damageMutex); + auto& queue=_emoteNotices[target->GetGUID().GetCounter()]; + if(queue.size()<8)queue.push_back({source->GetGUID(),emote}); +} + +void ClientlessPlayerMgr::MarkActivity(Player* player) +{ + if(!player)return; + std::lock_guard lock(_damageMutex); + auto now=GameTime::GetGameTime(); + if(_activity.size()>4096) + { + for(auto it=_activity.begin();it!=_activity.end();) + if(now-it->second>600)it=_activity.erase(it);else ++it; + } + _activity[player->GetGUID()]=now; +} +time_t ClientlessPlayerMgr::LastActivity(ObjectGuid player) +{ + std::lock_guard lock(_damageMutex); + auto it=_activity.find(player);return it==_activity.end()?0:it->second; +} + +void ClientlessPlayerMgr::SharedTargetDamage(Unit* victim,Unit* attacker,uint32 damage) +{ + auto helper=attacker?attacker->GetControllingPlayer():nullptr; + if(!damage || !helper || !victim || helper==victim)return; + std::lock_guard lock(_damageMutex); + for(auto unit:victim->getAttackers()) + if(auto player=unit->ToPlayer()) + if(player!=helper && player->GetSession()->IsClientless()) + { + auto& queue=_helpNotices[player->GetGUID().GetCounter()]; + if(queue.size()<8)queue.push_back({helper->GetGUID(),victim->GetGUID()}); + } +} diff --git a/src/server/game/World/ClientlessPlayerMgr.h b/src/server/game/World/ClientlessPlayerMgr.h index 04dd722c3a..af824f1982 100644 --- a/src/server/game/World/ClientlessPlayerMgr.h +++ b/src/server/game/World/ClientlessPlayerMgr.h @@ -39,6 +39,8 @@ public: virtual void OnPlayerLogout(Player*) { } // Notification after health was reduced, never the mutable pre-damage event. virtual void OnDamageTaken(Player*, Unit*, uint32) { } + virtual void OnTextEmote(Player*, Player*, uint32) { } + virtual void OnSharedTargetDamage(Player*, ObjectGuid, ObjectGuid) { } }; class TC_GAME_API ClientlessPlayerMgr @@ -52,6 +54,10 @@ public: bool IsManaged(uint32 characterGuid) const; void DamageTaken(Player* player, Unit* attacker, uint32 damage); + void TextEmote(Player* source, Player* target, uint32 emote); + void SharedTargetDamage(Unit* victim,Unit* attacker,uint32 damage); + void MarkActivity(Player* player); + time_t LastActivity(ObjectGuid player); uint32 GetAccountId(uint32 characterGuid) const; uint32 GetManagedCount() const { return uint32(_records.size()); } @@ -79,6 +85,11 @@ private: }; struct DamageNotice { ObjectGuid Attacker; uint32 Damage; }; + struct HelpNotice { ObjectGuid Helper,Target; }; + std::unordered_map> _helpNotices; + struct EmoteNotice { ObjectGuid Source; uint32 Emote; }; + std::unordered_map> _emoteNotices; + std::unordered_map _activity; std::mutex _damageMutex; std::unordered_map> _damageNotices; std::unordered_map _records; diff --git a/src/server/scripts/Custom/clientless/adventure/profiles/arthoran.lua b/src/server/scripts/Custom/clientless/adventure/profiles/arthoran.lua index d77896dc09..290c2c884d 100644 --- a/src/server/scripts/Custom/clientless/adventure/profiles/arthoran.lua +++ b/src/server/scripts/Custom/clientless/adventure/profiles/arthoran.lua @@ -34,6 +34,7 @@ return { }, personality = { + group_loyalty = 0.35, -- Chance to stay when the scheduled intent changes. sociability = 0.65, adventurousness = 0.70, wanderlust = 0.30, diff --git a/src/server/scripts/Custom/clientless/adventure/profiles/generic.lua b/src/server/scripts/Custom/clientless/adventure/profiles/generic.lua index 00fbff6e74..f232d5ba64 100644 --- a/src/server/scripts/Custom/clientless/adventure/profiles/generic.lua +++ b/src/server/scripts/Custom/clientless/adventure/profiles/generic.lua @@ -18,6 +18,7 @@ return { }, personality = { + group_loyalty = 0.25, -- Chance to stay when the scheduled intent changes. sociability = 0.50, adventurousness = 0.50, wanderlust = 0.50, diff --git a/src/server/scripts/Custom/viano_bot_commands.cpp b/src/server/scripts/Custom/viano_bot_commands.cpp index 3b66b22749..4a842f4769 100644 --- a/src/server/scripts/Custom/viano_bot_commands.cpp +++ b/src/server/scripts/Custom/viano_bot_commands.cpp @@ -20,6 +20,7 @@ #include "ClientlessWorldPointMgr.h" #include "AdventureBehaviorMgr.h" #include "AdventureCombatMgr.h" +#include "AdventureInteractionMgr.h" #include "AdventureSupplyMgr.h" #include "ClientlessTravelGraphMgr.h" #include "ClientlessRouteRecorderMgr.h" @@ -60,6 +61,18 @@ #include #include +namespace +{ + bool InteractionStatus(ChatHandler* handler,char const* args) + { + std::istringstream input(args?args:"");uint32 guid=0;input>>guid; + auto session=sWorld->FindClientlessSession(guid);auto player=session?session->GetPlayer():nullptr; + if(!player || !sClientlessPlayerMgr->IsManaged(guid)) + {handler->SendSysMessage("Usage: clientless interaction ");return false;} + sAdventureInteractionMgr->Report(handler,player);return true; + } +} + namespace ClientlessCombatPreparation { struct Gear {uint8 Slot;uint32 Entry;std::string Enchantments;uint8 RandomType;uint32 RandomId;}; @@ -2504,6 +2517,7 @@ static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*arg static std::vector clientlessCommands = { { "combat", rbac::RBAC_PERM_COMMAND_GM, true, &ClientlessCombatPreparation::Command, "" }, + { "interaction", rbac::RBAC_PERM_COMMAND_GM, true, &InteractionStatus, "" }, { "luareload", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessLuaReload, "" }, { "transport", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessTransport, "" }, { "login", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessLogin, "" }, diff --git a/tests/clientless/test_adventure_combat_reactions.py b/tests/clientless/test_adventure_combat_reactions.py index 5eb0527b70..fa484d1fb5 100644 --- a/tests/clientless/test_adventure_combat_reactions.py +++ b/tests/clientless/test_adventure_combat_reactions.py @@ -33,9 +33,14 @@ struct Position{ float GetExactDist(Position const* b)const{return GetExactDist(*b);} }; struct Map{float GetHeight(int,float,float,float,bool,float){return 0;}}map; -struct Creature; -struct Unit:Position{virtual Creature* ToCreature(){return nullptr;}}; -struct Creature:Unit{ObjectGuid guid;bool valid=true,alive=true,controlled=false,evading=false,boss=false,phase=true,visible=true,inWorld=true;unsigned level=60;std::string script="mob";Creature(unsigned n):guid{n}{}ObjectGuid GetGUID(){return guid;}Creature* ToCreature()override{return this;}bool GetControllingPlayer(){return controlled;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsEvadingAttacks(){return evading;}bool isWorldBoss(){return boss;}unsigned getLevel(){return level;}std::string GetScriptName(){return script;}}; +struct Creature;struct Player; +struct Unit:Position{ + ObjectGuid guid;bool valid=true,alive=true,phase=true,visible=true,inWorld=true; + virtual Creature* ToCreature(){return nullptr;}virtual Player* ToPlayer(){return nullptr;} + virtual bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}virtual ObjectGuid GetGUID(){return guid;} + void GetNearPoint(Player*,float&a,float&b,float&c,float,float){a=x;b=y;c=z;}float GetAngle(Player*){return 0;} +}; +struct Creature:Unit{bool controlled=false,evading=false,boss=false;unsigned level=60;std::string script="mob";Creature(unsigned n){guid={n};}ObjectGuid GetGUID(){return guid;}Creature* ToCreature()override{return this;}bool GetControllingPlayer(){return controlled;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsEvadingAttacks(){return evading;}bool isWorldBoss(){return boss;}unsigned getLevel(){return level;}std::string GetScriptName(){return script;}}; struct Player; struct Corpse:Position{ time_t released=100;int saves=0;ObjectGuid GetGUID(){return {700};}time_t GetGhostTime(){return released;}int GetType(){return 0;} @@ -45,10 +50,10 @@ struct WorldPacket{int opcode;WorldPacket(int o,int):opcode(o){}templatephase;}bool CanSeeOrDetect(Creature*c){return c->visible;}bool IsValidAttackTarget(Creature*c){return c->valid;}unsigned getLevel(){return 60;}bool IsWithinMeleeRange(Creature*){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;}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;} void StopMoving(){} }; @@ -59,7 +64,7 @@ void Session::HandleReclaimCorpseOpcode(WorldPacket&){++reclaims;assert(corpse.I void nearPoint(Creature*,Player*,float&,float&,float&,float,float){} struct FakeCreature:Creature{using Creature::Creature;void GetNearPoint(Player*,float&x,float&y,float&z,float,float){x=this->x;y=this->y;z=this->z;}float GetAngle(Player*){return 0;}}; namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}} -namespace ObjectAccessor{std::unordered_map objects;Creature* GetCreature(Player&,ObjectGuid g){auto i=objects.find(g.id);return i==objects.end()?nullptr:i->second;}} +namespace ObjectAccessor{std::unordered_map objects;Unit* GetUnit(Player&,ObjectGuid g){auto i=objects.find(g.id);return i==objects.end()?nullptr:i->second;}} struct ClientlessPathDiagnostics{bool Complete=true,Suspicious=false;float PathLength=100;}; enum class ClientlessTravelState{Moving,Arrived,Stuck,PathRejected}; struct TravelStatus{ClientlessTravelState State=ClientlessTravelState::Arrived;Position Target;}; @@ -78,11 +83,11 @@ code=code.replace('bool GetControllingPlayer(){return controlled;}bool IsAlive() # Forward declaration needed in Creature's adapter. code=code.replace('struct Creature;','struct Creature;struct Player;',1) code+=header+r''' -class AdventureCombatMgr::Fighter{public:int ticks=0;void Tick(Creature*,uint32){++ticks;}}; +class AdventureCombatMgr::Fighter{public:int ticks=0;void Tick(Unit*,uint32){++ticks;}}; -bool AdventureCombatMgr::Reachable(Player*p,Creature*)const{return p->los;} +bool AdventureCombatMgr::Reachable(Player*p,Unit*)const{return p->los;} Creature* AdventureCombatMgr::Proactive(Player*)const{return nullptr;} -bool AdventureCombatMgr::Start(Player*,Record&r,Creature*c,bool train,bool defense,time_t until){r.AI=std::make_shared();r.Target=c->guid;r.Training=train;r.Defending=defense;r.Until=until;return true;} +bool AdventureCombatMgr::Start(Player*,Record&r,Unit*c,bool train,bool defense,time_t until){r.AI=std::make_shared();r.Target=c->GetGUID();r.Training=train;r.Defending=defense;r.Until=until;return true;} void AdventureCombatMgr::Finish(Player*,Record&r){r.AI.reset();r.Target.Clear();r.Defending=false;r.Training=false;r.Cooldown=10000;} ''' for name in ('bool AdventureCombatMgr::Valid(', 'void AdventureCombatMgr::DamageTaken(', 'bool AdventureCombatMgr::Death(', 'bool AdventureCombatMgr::Update('):code+='\n'+function(name) @@ -101,6 +106,9 @@ int main(){ assert(SegmentDistance(5,3,0,0,0,0,10,0,0)==3); AdventureCombatMgr mgr;Player p;Creature a(1),b(2),c(3);ObjectAccessor::objects={{1,&a},{2,&b},{3,&c}}; + Player enemy;enemy.guid={99};ObjectAccessor::objects[99]=&enemy; + assert(mgr.Valid(&p,&enemy,false));assert(!mgr.Valid(&p,&enemy,true)); + enemy.valid=false;assert(!mgr.Valid(&p,&enemy,false));enemy.valid=true; assert(mgr.Valid(&p,&a,false));assert(!mgr.Valid(&p,&a,true)); Creature dummy(4);dummy.script="npc_training_dummy";assert(mgr.Valid(&p,&dummy,true));assert(!mgr.Valid(&p,&dummy,false)); dummy.level=63;assert(!mgr.Valid(&p,&dummy,true));dummy.level=60; @@ -110,6 +118,7 @@ int main(){ dummy.controlled=true;assert(!mgr.Valid(&p,&dummy,true));dummy.controlled=false; dummy.valid=false;assert(!mgr.Valid(&p,&dummy,true)); auto&r=mgr._records[26]; + mgr.DamageTaken(&p,&enemy,1);assert(r.Damagers.front().id==99);r.Damagers.clear(); mgr.DamageTaken(&p,&a,0);assert(r.Damagers.empty()); mgr.DamageTaken(&p,&a,5);mgr.DamageTaken(&p,&b,5);mgr.Update(&p,500); assert(r.Target.id==1 && r.Defending); // first actual damager @@ -117,6 +126,10 @@ int main(){ a.alive=false;mgr.Update(&p,500);assert(!r.AI);mgr.Update(&p,500);assert(r.Target.id==2); mgr.Finish(&p,r);r.Damagers.clear();mgr.Start(&p,r,&c,true,false,500); mgr.DamageTaken(&p,&b,1);mgr.Update(&p,500);assert(r.Target.id==2 && r.Defending && !r.Training); + // Actual player attackers use the same first-damager focus and native PvP permission. + mgr.Finish(&p,r);r.Damagers.clear();mgr.DamageTaken(&p,&enemy,1);mgr.Update(&p,500); + assert(r.Target.id==99 && r.Defending);mgr.DamageTaken(&p,&c,1);mgr.Update(&p,500);assert(r.Target.id==99); + enemy.valid=false;mgr.Update(&p,500);assert(!r.AI);r.Damagers.clear(); // Release through native handler and save ghost plus corpse timestamp. mgr.Finish(&p,r);p.alive=false;p.ghost=false;r.Dead=false;mgr.Death(&p,r); assert(p.ghost && p.session.releases==1 && p.saves==1 && corpse.saves==1); diff --git a/tests/clientless/test_adventure_interactions.py b/tests/clientless/test_adventure_interactions.py new file mode 100644 index 0000000000..933f5dafe9 --- /dev/null +++ b/tests/clientless/test_adventure_interactions.py @@ -0,0 +1,118 @@ +"""Run production interaction manager against deterministic native-world adapters.""" +from pathlib import Path +import subprocess +import tempfile +root=Path(__file__).resolve().parents[2] +header=(root/'src/server/game/World/AdventureInteractionMgr.h').read_text() +header='\n'.join(line for line in header.splitlines() if not line.startswith('#include')).replace('private:','public:') +source=(root/'src/server/game/World/AdventureInteractionMgr.cpp').read_text() +source='\n'.join(line for line in source.splitlines() if not line.startswith('#include')) +code=r''' +#include "AdventureInteractionPolicy.h" +#include +#include +#include +#include +#include +#include +#include +using uint32=unsigned; +#define TC_GAME_API +#define TC_LOG_INFO(...) ((void)0) +struct ObjectGuid { unsigned id=0;unsigned GetCounter()const{return id;}void Clear(){id=0;}explicit operator bool()const{return id!=0;}bool operator==(ObjectGuid b)const{return id==b.id;}bool operator!=(ObjectGuid b)const{return id!=b.id;}}; +namespace std { template<>struct hash{size_t operator()(ObjectGuid g)const{return g.id;}}; } +struct Position { + float m_positionX=0,m_positionY=0,m_positionZ=0,o=0; + void Relocate(Position const*p){*this=*p;} + float GetPositionX()const{return m_positionX;}float GetPositionY()const{return m_positionY;}float GetPositionZ()const{return m_positionZ;}float GetOrientation()const{return o;} + float GetExactDist(Position const*p)const{return std::hypot(m_positionX-p->m_positionX,m_positionY-p->m_positionY);} +}; +enum {TEXT_EMOTE_RUDE=1,TEXT_EMOTE_THREATEN,TEXT_EMOTE_ROAR,TEXT_EMOTE_GREET,TEXT_EMOTE_HELLO,TEXT_EMOTE_WAVE,TEXT_EMOTE_BOW,TEXT_EMOTE_SALUTE,TEXT_EMOTE_SMILE,TEXT_EMOTE_THANK,EMOTE_ONESHOT_BOW,EMOTE_ONESHOT_WAVE,EMOTE_ONESHOT_NO,UNIT_STAND_STATE_STAND,ALLIANCE,LANG_COMMON,LANG_ORCISH,CHAT_MSG_PARTY,GROUP_REMOVEMETHOD_LEAVE,CMSG_PARTY_INVITE_RESPONSE,UNIT_STATE_CASTING}; +struct WorldPacket { explicit WorldPacket(int=0){} }; +struct Player;struct Group; +namespace WorldPackets {namespace Party {struct PartyInviteResponse{explicit PartyInviteResponse(WorldPacket&&){}bool Accept=false;};}} +struct Session {Player*owner=nullptr;bool clientless=false;bool IsClientless(){return clientless;}void HandlePartyInviteResponseOpcode(WorldPackets::Party::PartyInviteResponse&);}; +struct Map {bool dungeon=false;bool IsDungeon(){return dungeon;}}; +struct Unit:Position {Player*victim=nullptr;std::vector attackers;virtual ~Unit()=default;virtual struct Creature*ToCreature(){return nullptr;}Unit*GetVictim(){return reinterpret_cast(victim);}auto&getAttackers(){return attackers;}}; +struct Creature:Unit{}; +struct Member {Player*player=nullptr;Member*nextMember=nullptr;Player*GetSource(){return player;}Member*next(){return nextMember;}}; +struct Player:Unit { + ObjectGuid guid,selection;bool alive=true,inWorld=true,combat=false,teleport=false,transport=false,flight=false,charmed=false,casting=false,afk=false,visible=true,phase=true,los=true; + unsigned mapid=0,team=ALLIANCE;Session session;Group*group=nullptr,*invite=nullptr;Map map;int gestures=0,says=0,posture=0;uint32 lastEmote=0; + explicit Player(unsigned id=26){guid.id=id;session.owner=this;} + ObjectGuid GetGUID(){return guid;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool GetTransport(){return transport;}bool IsInFlight(){return flight;}bool IsCharmed(){return charmed;}bool HasUnitState(int){return casting;} + bool IsInMap(Player*p){return mapid==p->mapid;}bool IsInPhase(Player*p){return phase && p->phase;}bool CanSeeOrDetect(Player*p){return p->visible;}bool IsWithinLOSInMap(Player*p){return los && p->los;}bool IsFriendlyTo(Player*p){return team==p->team;} + Map*GetMap(){return ↦}unsigned GetMapId(){return mapid;}Session*GetSession(){return &session;}Group*GetGroup(){return group;}Group*GetGroupInvite(){return invite;}unsigned GetTeam(){return team;}ObjectGuid GetTarget(){return selection;}bool isAFK(){return afk;} + void SetStandState(int s){posture=s;}void SetFacingToObject(Player*){}void HandleEmoteCommand(uint32 e){++gestures;lastEmote=e;}void Say(char const*,int,Player*){++says;} + void GetNearPoint(Player*,float&x,float&y,float&z,float,float){x=m_positionX;y=m_positionY;z=m_positionZ;} + std::vector people;void GetPlayerListInGrid(std::vector&out,float){out=people;} +}; +struct Group {ObjectGuid leader;Member first;int removals=0,messages=0;ObjectGuid GetLeaderGUID(){return leader;}Member*GetFirstMember(){return first.player?&first:nullptr;}void BroadcastPacket(WorldPacket*,bool){++messages;}void RemoveMember(ObjectGuid,int){++removals;assert(first.player);first.player->group=nullptr;}}; +int accepts=0,declines=0; +void Session::HandlePartyInviteResponseOpcode(WorldPackets::Party::PartyInviteResponse&p){assert(owner->invite);if(p.Accept){++accepts;owner->group=owner->invite;}else ++declines;owner->invite=nullptr;} +struct ChatHandler{void SendSysMessage(char const*){}templatevoid PSendSysMessage(char const*,Args...){}static void BuildChatPacket(WorldPacket&,int,int,Player*,Player*,char const*){}}; +namespace GameTime {time_t now=100;time_t GetGameTime(){return now;}} +unsigned urand(unsigned a,unsigned){return a;}float roll=0;float frand(float,float){return roll;} +namespace ObjectAccessor {std::unordered_map players;Player*FindPlayer(ObjectGuid id){auto it=players.find(id);return it==players.end()?nullptr:it->second;}} +struct Profile {std::string Locale="deDE";std::unordered_map Personality;}; +struct Lua {std::unordered_map profiles;Profile const*GetAdventureProfile(unsigned id){auto it=profiles.find(id);return it==profiles.end()?nullptr:&it->second;}}lua;auto sClientlessLuaEngine=&lua; +struct Behavior {int interrupts=0;std::string intent="social",schedule="social:inn";std::string InteractionIntent(Player*){return intent;}std::string InteractionSchedule(Player*){return schedule;}void InterruptForInteraction(Player*){++interrupts;}}behavior;auto sAdventureBehaviorMgr=&behavior; +struct Combat {bool busy=false;int assists=0;bool Busy(Player*){return busy;}bool Assist(Player*,Unit*){++assists;return true;}}combat;auto sAdventureCombatMgr=&combat; +struct ClientlessAction{std::string Name;std::unordered_map Parameters;}; +struct Core {time_t activity=0;int queued=0;time_t LastActivity(ObjectGuid){return activity;}bool QueueAction(unsigned,ClientlessAction){++queued;return true;}}core;auto sClientlessPlayerMgr=&core; +enum class ClientlessTravelState{Moving,Arrived};struct Nav {ClientlessTravelState State=ClientlessTravelState::Arrived;Position Target;}; +struct ClientlessPathDiagnostics{bool Complete=true,Suspicious=false;float PathLength=10;}; +struct Travel {Nav nav;bool active=false,valid=true;int starts=0,cancels=0;Nav*GetStatus(unsigned){return active?&nav:nullptr;}bool AnalyzePath(Player*,Position const&,ClientlessPathDiagnostics&){return valid;}void StartTravel(Player*,Position const&p){++starts;nav.Target=p;nav.State=ClientlessTravelState::Moving;active=true;}void CancelTravel(unsigned){++cancels;active=false;}}travel;auto sClientlessTravelMgr=&travel; +enum class ClientlessActionExecutionState{Moving,SelectingTarget,Completed};struct Execution {ClientlessActionExecutionState State=ClientlessActionExecutionState::Completed;}; +struct Executor {Execution status;Execution*GetStatus(unsigned){return &status;}}executor;auto sClientlessActionExecutorMgr=&executor; +struct Graph {void EnsureLoaded(){}bool FindNearestNodeToPosition(unsigned,float,float,float,std::string&node,float&distance){node="entrance";distance=10;return true;}}graph;auto sClientlessTravelGraphMgr=&graph; +'''+header+'\n'+source+r''' +int main(){ + using namespace AdventureInteractionPolicy; + assert(Willingness("adventure",.65f)>Willingness("profession",.65f));assert(Willingness("rest",0)==0); + assert(!LeaderExpired(699,100));assert(LeaderExpired(700,100));assert(!LeaderExpired(99,100)); + assert(CanLeave(true,false,false,false));assert(!CanLeave(true,true,false,false));assert(!CanLeave(true,false,true,false)); + AdventureInteractionMgr mgr;Player bot(26),leader(21),other(27);bot.session.clientless=true;other.session.clientless=true; + ObjectAccessor::players={{bot.guid,&bot},{leader.guid,&leader},{other.guid,&other}}; + lua.profiles[26].Personality["sociability"]=.65f;lua.profiles[26].Personality["group_loyalty"]=0;lua.profiles[27]={}; + Group group;group.leader=leader.guid;group.first.player=⊥ + auto tick=[&](time_t now){GameTime::now=now;return mgr.Update(&bot,1000);}; + // Native invitation response creates actual membership, then suspends intent. + bot.invite=&group;tick(100);assert(!bot.group);tick(102);assert(bot.group && accepts==1); + assert(mgr._records[26].Grouped && behavior.interrupts==1); + // Follow uses verified local MMAP paths; invalid paths never move the player. + leader.m_positionX=20;tick(103);assert(travel.starts==1);travel.active=false;travel.valid=false;leader.m_positionX=30;tick(104);assert(travel.starts==1);travel.valid=true; + // Changing leader resets both AFK and inactivity timers. + mgr._records[26].AfkSince=1;group.leader=other.guid;tick(105);assert(mgr._records[26].AfkSince==0 && mgr._records[26].LastActivity==105); + group.leader=leader.guid;tick(106); + // Chat/spells count as activity even while the leader stands still. + core.activity=650;tick(650);tick(1249);assert(bot.group);tick(1250);assert(!bot.group && group.removals==1); + // Hour intent change is deferred inside instances and during combat. + core.activity=0;bot.group=&group;behavior.schedule="adventure:road";tick(1300);behavior.schedule="economy:city"; + bot.map.dungeon=true;tick(1301);assert(bot.group);bot.map.dungeon=false;bot.combat=true;tick(1302);assert(bot.group);bot.combat=false;tick(1303);assert(!bot.group); + // Native AFK needs a full ten minutes even if manually set; no early exit. + bot.group=&group;tick(1400);leader.afk=true;tick(1401);tick(2000);assert(bot.group);tick(2001);assert(!bot.group);leader.afk=false; + // Grouped bots also answer directed emotes; responses do not create events. + leader.m_positionX=0;bot.group=&group;tick(2100);mgr.Emote(&bot,&leader,TEXT_EMOTE_WAVE);tick(2101);assert(bot.gestures==1);tick(2102);assert(bot.gestures==1);tick(2103);assert(bot.group); + // Friendly greeting versus hostile contact; no combat call is made. + bot.group=nullptr;tick(2120);mgr.Emote(&bot,&other,TEXT_EMOTE_WAVE);tick(2121);assert(bot.lastEmote==EMOTE_ONESHOT_WAVE);tick(2123); + other.team=999;mgr._records[26].Encounters.clear();mgr._records[26].NextSocial=0;mgr.Emote(&bot,&other,TEXT_EMOTE_THREATEN);tick(2124);assert(bot.lastEmote==EMOTE_ONESHOT_NO && combat.assists==0);tick(2126); + // Invisible/distant targets and repeated emotes are ignored. + auto gestures=bot.gestures;other.visible=false;mgr.Emote(&bot,&other,TEXT_EMOTE_WAVE);tick(2130);assert(bot.gestures==gestures);other.visible=true; + // Bot encounters reserve both participants; cooldown prevents new loops. + other.team=ALLIANCE;bot.people={&other};mgr._records.clear();tick(2200);assert(mgr._records[26].Partner==other.guid && mgr._records[27].Partner==bot.guid); + tick(2201);GameTime::now=2203;mgr.Update(&other,1000);assert(other.gestures==1);tick(2205);GameTime::now=2205;mgr.Update(&other,1000);tick(2265);assert(!mgr._records[26].Partner); + // Bot-origin invitations are declined; autonomous bot grouping is deferred. + mgr._records.clear();group.leader=other.guid;bot.invite=&group;tick(2300);tick(2302);assert(!bot.group && declines==1); + // Missing leader map uses the existing graph executor, without teleporting. + group.leader=leader.guid;bot.group=&group;leader.mapid=1;tick(2400);assert(core.queued==1);executor.status.State=ClientlessActionExecutionState::Moving;tick(2416);assert(core.queued==1); + // Pending help is acknowledged only outside combat, after caller's self-care. + mgr.CombatHelp(&bot,leader.guid);leader.mapid=0;executor.status.State=ClientlessActionExecutionState::Completed;bot.combat=true;auto says=bot.says;tick(2417);assert(bot.says==says);bot.combat=false;tick(2418);assert(bot.says==says+1); + mgr.Offline(&bot);assert(!mgr._records.count(26)); +} +''' +with tempfile.TemporaryDirectory() as d: + p=Path(d);(p/'test.cpp').write_text(code) + subprocess.run(['g++','-std=c++17','-Wall','-Wextra','-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 interaction manager: invitations, factions, reservations, follow, AFK and deferred departure passed') diff --git a/tests/clientless/test_clientless_social_notifications.py b/tests/clientless/test_clientless_social_notifications.py new file mode 100644 index 0000000000..521e990290 --- /dev/null +++ b/tests/clientless/test_clientless_social_notifications.py @@ -0,0 +1,56 @@ +"""Exercise production map-worker social/helper queues concurrently.""" +from pathlib import Path +import subprocess +import tempfile +root=Path(__file__).resolve().parents[2] +source=(root/'src/server/game/World/ClientlessPlayerMgr.cpp').read_text() +def function(signature): + a=source.index(signature);b=source.index('{',a);e=b+1;depth=1 + while depth:depth+=(source[e]=='{')-(source[e]=='}');e+=1 + return source[a:e] +code=r''' +#include +#include +#include +#include +#include +#include +using uint32=unsigned; +struct ObjectGuid{unsigned id;unsigned GetCounter()const{return id;}bool operator==(ObjectGuid b)const{return id==b.id;}}; +struct Player; +struct Unit {ObjectGuid guid;Player*owner=nullptr;std::vector attackers;ObjectGuid GetGUID(){return guid;}virtual Player*ToPlayer(){return nullptr;}Player*GetControllingPlayer(){return owner;}auto&getAttackers(){return attackers;}}; +struct Session {bool clientless=true;bool IsClientless(){return clientless;}}; +struct Player:Unit {Session session;Player(){owner=this;}Player*ToPlayer()override{return this;}Session*GetSession(){return &session;}}; +struct ClientlessPlayerMgr { + struct EmoteNotice{ObjectGuid Source;uint32 Emote;};struct HelpNotice{ObjectGuid Helper,Target;}; + std::mutex _damageMutex;std::unordered_map> _emoteNotices;std::unordered_map> _helpNotices; + void TextEmote(Player*,Player*,uint32);void SharedTargetDamage(Unit*,Unit*,uint32); +}; +'''+function('void ClientlessPlayerMgr::TextEmote(')+'\n'+function('void ClientlessPlayerMgr::SharedTargetDamage(')+r''' +int main(){ + ClientlessPlayerMgr mgr;std::vector bots(16);std::vector threads; + Player helper;helper.guid={77};helper.session.clientless=false; + for(unsigned i=0;iSharedTargetDamage(',start) +assert unit.index('GetCommandStatus(CHEAT_GOD)',start)