diff --git a/doc/clientless/SESSION-RESUME.md b/doc/clientless/SESSION-RESUME.md index 2353bec08c..888db3aa0c 100644 --- a/doc/clientless/SESSION-RESUME.md +++ b/doc/clientless/SESSION-RESUME.md @@ -1,3 +1,94 @@ +# Aktuell: Automatische Botgruppen, Raid und Gruppenbildung + +Nutzer beauftragte Umsetzung der gemeinsam bestätigten Planung. Vollständige +Doku doc/clientless/bot-groups.md. Vorherige uncommittete PvP-/Fluchtarbeit +wird zusammen mit den Gruppenänderungen auf Nutzerauftrag lokal committet; kein Push. Neue AdventureGroupMgr/Policy, +native Einladungen und Erst-Einlader-Führung, lokale Gruppen und Kontakt in +Hauptstädten via sucheNachGruppe (Spielerchat wird nicht ausgewertet). +Vorbereitungsintent groupformation vergleicht NÄCHSTEN Intent, nie Ziele; +Sammelpunkt ist Gruppenbildungs-Location des Führers. Instanz5/Raid20 inklusive +Führer, andere Intents3/social10. Offene Einladungen werden mitgezählt. +Anführer bestimmt Intent/Ziel/Frist; eigene Tagespläne bleiben erhalten. +AFK während Bildung ausgesetzt, Übergang zu geplanter Aktivität kein Austritt. +Kein Warten auf Vollständigkeit/Anwesenheit. Rollen später. Botführer darf PvP +beginnen, Mitglieder assistieren; Gruppen fliehen weiterhin nie. +Spieler-Invite an gruppierten Bot erzeugt generische GUID-JoinRequest-Queue, +Botführer kann Gegeneinladung anbieten und übergibt nach Beitritt nativ Führung. +Native provisorische Gruppenführer ignorieren eigene Einladung (Regressiontest). +Raidbildung wählt native25-normal Schwierigkeit; Katalog verlangt>=20 Kapazität +mit nativem Downscaling für alte20/40-Raids; Zehner-only-Ziele ausgeschlossen. +Neue CPC route_source-Snapshots mit nativem catch-up, gemeinsame Flugsegmentspur, +native Gruppeninstanzen statt alter Solo-Sperre. Raid/Dungeon nach nativen Maps. +Migration world2026_10_28_01 zweimal auf Test-DB angewendet, bestehende Tage erhalten. +Zusätzlicher Diagnosebefehl adventure prepare [s]. +Alle66 Tests bestanden; abschließende Interaktions- und Plan-Regressionsfälle +zusätzlich erneut bestanden. Debug-j10 erfolgreich, abschließend installiert. +Aktueller Testserver122595 läuft; frische Ready-Meldung, Prozess und Port8085 +bestätigt. Vier Testbots online, Arthoran26 weiterhin offline. +Nach Anmeldung bildete Lenara31 nativ Gruppe mit Aelion29 (Log Invite accepted1, +beide grouped1/leader31); nach Stop-Intentwechsel nativ wieder aufgelöst. +Dabei gefunden/behoben: Gruppentimer darf Behavior.Update bei Botgruppen nicht +auf1Hz mit falschem diff drosseln; Regressiontest sichert normale World-Ticks. +Stop-Wanted sperrt sofort weitere Rekrutierung/Annahme, ohne veralteten Intent. +Diagnose interaction leave: sicheres natives Verlassen/Testaufräumen. +Bots29/31 abschließend beide enabled0/grouped0 in Goldhain bestätigt und gespeichert; +vor Login nativ tele name435 zur sicheren Bereitstellung abseits Flugstart. +30/32 normale soziale Intents. Keine temporären Profiländerungen/Overrides. +Persönlicher Plan wird beim Austritt wiederhergestellt; übernommene Ziele ändern +nicht die persönliche Austrittssignatur (besonders manuelle Aufträge). +Belege: checkpoints/2026-10-28-bot-groups/, /tmp/vcore-groups-tests-final.log. +Kein vollständiger Live-Gruppenlauf oder Raid abgenommen. + +--- + +# Aktuell: Solo-Fluchtlogik ergänzt + +Nutzer hat Flucht vor deutlich stärkeren Gegnern und bei HP<20% beauftragt, +mit absoluter Ausnahme für Gruppenmitglieder. Implementierung CombatMgr/Policy: ++6 Level, gleich-/höherstufige Elite/Bosse, tatsächliche Angreifer; alle Arten. +Gruppenbeitritt bricht Flucht ab. Alle2s 24 MMAP-Fluchtpunkte, Abstand vergrößern, +weitere sichtbare Feinde meiden; kein Pfad => stationäre Nahkampfverteidigung. +Ende erst außerhalb nativem Kampf UND50Y Abstand zur letzten Bedrohung. +Bestehende Tränke, Intentfristen und Todesablauf erhalten. Details combat-reactions.md. +Vorherige uncommittete PvP-Änderungen erhalten. Alle63 Tests bestanden inkl. +erweiterter produktiver Flucht-Zustandsmaschine (/tmp/vcore-flee-tests.log). +Debug-j10 erfolgreich gebaut und installiert. Testserver105886 mit frischer +Ready-Meldung und Port8085 geprüft. Vier Testbots erneut angemeldet;29/31 +nach Profilinitialisierung erneut gestoppt (früher Stop während Loading wird +vom Profilstart überschrieben). Aelion begann kurz nativen Flug Goldhain->SW; +danach nativer tele name435 nach Goldhain, ebenso Lenara. Beide Status +endgültig enabled0/stopped bestätigt, saveall. Aelion am nahen taxi_168, +Lenara am Goldshire-Telepunkt. Keine Live-Fluchtabnahme. +Kein Commit/Push. + +--- + +# Aktuell: Offensive Fraktionsbegegnungen + +Nutzer beauftragte PvP-Erstangriffe gegen gegnerische Bots/Spieler/NPCs: +50 Yards, maximal +/-5 Level inklusive, Persönlichkeit und Intent maßgeblich. +Umsetzung in AdventureCombatMgr/Policy; Details combat-reactions.md. +Alle30s Entscheidung mit adventurousness*danger_tolerance*Intentgewicht; +Gruppen keine autonomen Erstangriffe, Ruhe/Instanz/deaktiviert friedlich. +Native PvP/Phase/LOS/MMAP-Regeln, HP60/Mana30 und keine Elite/Boss-NPCs. +PvE-Zielwahl schließt Fraktions-NPCs aus (kein Umgehen der PvP-Regeln). +Selbstverteidigung unverändert; eigenständige Fluchtlogik bislang NICHT vorhanden. +Neuer Produktionstest test_adventure_pvp.py. Debug-j10 erfolgreich gebaut und +installiert; alle63 Tests bestanden (/tmp/vcore-pvp-tests.log). Testserver100594 +mit frischer Ready-Meldung und Port8085 verifiziert. Bots29..32 nach sauberem +Logout/Neustart wieder online;29/31 autonome Intents wieder gestoppt,29 steht +nun am nahen taxi_582 in Goldhain (kurze initiale Reise vor Stop),31 am Node. +Arthoran26 bleibt offline. Live-PvP nicht ausgeführt/abgenommen. + +Vorheriger begrenzter Goldhain-Test: Lenara Emote-Antwort und Einladung +abgelehnt, Aelion Einladung angenommen im Server.log belegt. Folgen nicht +anhand dieser Logs bestätigt. Aelion29/Lenara31 am goldshire_center, autonome +Intents für den Test gestoppt. Arthoran26 weiterhin offline. Horde30/32 anderswo. +Keine neuen Charaktere, Migrationen oder Profiländerungen für PvP erforderlich. +Kein Commit/Push beauftragt für diese Änderungen. + +--- + # Aktuell: Automatische AdventureBot-Erzeugung abgeschlossen Stand28.10.2026 auf feature/clientless-player-core. Nutzer beauftragte einen diff --git a/doc/clientless/bot-groups.md b/doc/clientless/bot-groups.md new file mode 100644 index 0000000000..5735f33286 --- /dev/null +++ b/doc/clientless/bot-groups.md @@ -0,0 +1,118 @@ +# Autonomous AdventureBot groups + +Bot groups use native invitations, party membership, leadership, raid conversion, +instance bindings and persistence. The first inviter becomes leader. Human-led +groups retain the existing interaction/follow behavior. A human invitation to an +already grouped bot acts as a join request: a bounded GUID-only CPC notification +lets the bot leader offer a reverse native invitation, subject to personality, +faction and capacity. Once the player joins, native leadership is transferred to +the human and bots switch to the existing player-led behavior. This does not +parse player chat or forcibly remove any member. Role assignment, healer +or tank AI, and replacement of absent players remain separate work. + +## Recruitment and preparation + +Adventure, compatible professions, social, instance and raid intents recruit. +Local encounters require phase, visibility, LOS and25-yard proximity. A lone bot +may request admission to an existing matching group; its leader sends the native +invitation. A leader may recruit individual bots. Recruitment and acceptance are +personality dependent, with20–40 seconds between searches,2–4 seconds for native +invite responses (provisional native leaders never answer their own invite) and a10-minute pair cooldown. Dead/combat/fleeing/transit bots do +not recruit. Same faction and an inclusive five-level spread are required. +Profession groups need a common primary profession. + +`groupformation` prepares the next hour's intent in a capital. During this phase, +matching NEXT intents are required, not matching original destinations. The +leader's existing formation location becomes the meeting point; members inherit +it and subsequently inherit the leader's activity target and deadline. Their own +stored day plans remain intact and are restored on departure. Departure decisions +track the personal plan, so inheriting a different group destination is not +misinterpreted as an intent change. Unexpired manual +activities are restored too; expired manual activities end. New day generation inserts +preparation before instance/raid blocks, and probabilistically before other +eligible blocks; backwards processing prevents preparations preparing another +preparation. Existing stored days are preserved. + +The faction-separated native custom channel `sucheNachGruppe` carries bounded +recruitment and response messages across capitals. The manager pairs structured +bot plans; it never interprets player chat. Opposite-faction channel traffic is +not used. Capital zones include racial capitals, Shattrath and Dalaran. +Announcements currently use German RP text. + +Recruitment capacity includes the leader and pending invitations: instance5, +raid20, social10, adventure/profession3. Groups larger than five use native raid +membership. Full groups stop searching. These are intent-specific recruitment +limits; when the leader chooses a smaller activity, excess bot members depart +at a safe point before recruitment continues. Native map admission limits and existing/permanent lockouts remain +in force. Raid preparation requests the native25-player normal difficulty; the catalog +uses native downscaling and requires capacity for at least20 members, excluding +ten-player-only raid maps. Legacy20/40-player raids remain possible. +No role guarantees are implied. + +Preparation never waits for a full group or for members to arrive. The leader's +next intent starts on its ordinary deadline. Late members catch up. Formation +suppresses the AFK/inactivity departure timer and refreshes its baseline; the +planned formation-to-activity transition is exempt from intent-change departure. +Later changes retain the existing loyalty-based departure behavior, deferred in +combat/instances/transit. Native removal handles leadership and party dissolution. + +## Shared travel and instances + +Followers run their AdventureBehavior controller with the leader's effective +intent/location/deadline, keeping their personal daily schedule. After arrival, +non-instance followers use native checked local following instead of independently +performing unrelated micro-actions. Leaders continue their own activities. + +Generic CPC travel actions accept `route_source=`. +The executor copies the leader's route edges and final local destination, keeping +execution/transport state separate. Late arrivals plan a native approach to the +shared route's previous reached node and replay the remaining edges. A bounded +15-second wait handles follower actions processed before the leader plans. If no +matching macro snapshot exists (including a local-only leader leg), the ordinary +native catch-up planner runs; this is not a synthetic transfer or an absolute +formation guarantee. Individual permissions/path failures still use native +travel recovery. `TRAVEL_SHARED_ROUTE` provides evidence of actual copying. + +Free-flight members with the same target consume the leader's published flight +segments. Each member mounts normally and rechecks its own collision corridor. +A shared trail survives leader arrival/new planning through shared ownership. +Waiting for additional published points holds flight position; an aborted or +blocked trail uses existing checked breadcrumb return. No position, speed or +flight-license cheat is introduced. `FLIGHT_SHARED_TRAIL` marks attachment. + +`raid` selects only native raid-map entrances; `instance` excludes them. These +are verified from native map data through the existing entrance catalog rather +than name matching. Both use the existing remainder-of-block visit and final +five-minute outside rest, individual0–50% maximum-health loss, save/restart and +native portal return. Native group binding supplies the shared instance copy; +unrelated shared solo bindings remain rejected. Members do not use separate +synthetic private copies. + +Only the group leader initiates personality/intent-driven PvP. Other bots assist +through the existing damage executor. All group members retain the absolute rule against tactical fleeing, +regardless of health or level. + +## Operations and verification + +Apply `sql/custom/world/2026_10_28_01_adventure_groups.sql`; it is idempotent and +preserves edited weights/current day plans. No character migration is needed. +`clientless adventure reload` reloads catalog data. + +Diagnostics: `clientless interaction leave` performs a safe native +departure/cancels a pending invite for test cleanup. `clientless interaction ` includes next intent, formation, +capacity, bot leadership and own plan location. `clientless adventure plan ` +shows stored day slots. `clientless adventure prepare + [seconds]` stages a manual preparation, with a chosen normal +next destination, lasting30–3600 seconds and followed by a one-hour activity. +It uses normal navigation, invitations and execution, and can start incomplete. +Manual preparations are runtime test state, not restart-persisted day plans. + +Automated fixtures execute production recruitment, provisional-leader invite +handling, full world-tick behavior clocks in bot groups, player admission and +leadership, inheritance, instance +lifecycle, map-type filtering, route adoption and flight-trail geometry. They +cover different original destinations, singleton admission, native inviter +leadership, caps/pending reservations, raid conversion, protected transition, +unchanged personal plans, preparation deadlines, shared edges/free flight, +collision and abort behavior. Full native live group trips,20-member raids, +real AFK duration and visual gameplay remain acceptance tests for the user. diff --git a/doc/clientless/checkpoints/2026-10-28-bot-groups/final-console.txt b/doc/clientless/checkpoints/2026-10-28-bot-groups/final-console.txt new file mode 100644 index 0000000000..14b9a99b34 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-28-bot-groups/final-console.txt @@ -0,0 +1,1281 @@ +TrinityCore rev. 04aa87fcfc13+ 2026-10-28 11:30:47 +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 2 ms +Initialize data stores... +>> Initialized 167 DBC data stores in 672 ms +>> Initialized 5 DB2 data stores in 189 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 9 ms. +Loading SpellInfo store... +>> Loaded SpellInfo store in 112 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 1 ms +>> Loaded SpellInfo custom attributes in 70 ms +Loading SpellInfo diminishing infos... +>> Loaded SpellInfo diminishing infos in 5 ms +Loading SpellInfo immunity infos... +>> Loaded SpellInfo immunity infos in 15 ms +Loading GameObject models... +>> Loaded 3255 GameObject models in 2 ms +Loading Script Names... +>> Loaded 4057 ScriptNames in 295 ms +Loading Instance Template... +>> Loaded 91 instance templates in 0 ms +Loading instances... +>> Loaded instances in 3 ms +Loading character cache store... +Loaded character infos for 19 characters in 0 ms +Loading Broadcast texts... +>> Loaded 73039 broadcast texts in 215 ms +>> Loaded 73039 broadcast text locales in 1291 ms +Loading Localization strings... +>> Loaded 29790 creature locale strings in 335 ms +>> Loaded 19670 gameobject_template_locale strings in 198 ms +>> Loaded 14986 Quest locale strings in 902 ms +>> Loaded 1946 PageText locale strings in 57 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 1497 ms +Loading Account Roles and Permissions... +>> Loaded 666 permission definitions, 650 linked permissions and 4 default permi +ssions in 230 ms +Loading Page Texts... +>> Loaded 1962 page texts in 13 ms +Loading Game Object Templates... +>> Loaded 29501 game object templates in 153 ms +Loading Game Object template addons... +>> Loaded 29495 game object template addons in 35 ms +Loading Transport templates... +>> Loaded 43 transport templates in 9 ms +Loading Transport animations and rotations... +Loading Transport spawns... +>> Spawned 0 continent transports in 1 ms +Loading Spell Rank Data... +>> Loaded 174 spell rank records in 1 ms +Loading Spell Required Data... +>> Loaded 22 spell required records in 0 ms +Loading Spell Group types... +>> Loaded 550 spell group definitions in 1 ms +Loading Spell Learn Skills... +>> Loaded 137 Spell Learn Skills from DBC in 7 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 9 ms +Loading Spell Proc conditions and data... +>> Loaded 1556 spell proc conditions and data in 4 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 9 ms +Loading Aggro Spells Definitions... +>> Loaded 44 SpellThreatEntries in 0 ms +Loading Spell Group Stack Rules... +>> Loaded 41 spell group stack rules in 1 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 85 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 21 ms +Loading Disables +>> Loaded 5211 disables in 8 ms +Loading Items... +>> Loaded 54296 item templates in 38 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 29 ms +Loading Creature templates... +>> Loaded 64891 creature template models in 72 ms +>> Loaded 46633 creature definitions in 559 ms +Loading Equipment templates... +>> Loaded 16167 equipment templates in 21 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 1 ms +Loading Creature Reward OnKill Data... +>> Loaded 2343 creature award reputation definitions in 6 ms +Loading Reputation Spillover Data... +>> Loaded 28 reputation_spillover_template in 0 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 1 ms +>> Loaded 0 npc damage by class game table entries in 2 ms +Loading Creature Base Stats... +>> Loaded 400 creature base stats in 6 ms +Loading Spawn Group Templates... +>> Loaded 98 spawn group templates in 0 ms +Loading Creature Data... +Table `creature` has creature (GUID: 396679 Entry: 32491) with disallowed `unit_ +flags` 32768, removing incorrect flag. +>> Loaded 221565 creatures in 1158 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 11 ms +Loading Creature Addon Data... +>> Loaded 52002 creature addons in 91 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 373 ms +Loading Spawn Group Data... +>> Loaded 94612 spawn group members in 57 ms +Loading instance spawn groups... +>> Loaded 27 instance spawn groups in 0 ms +Loading GameObject Addon Data... +>> Loaded 24493 gameobject addons in 34 ms +Loading GameObject Quest Items... +>> Loaded 1352 gameobject quest items in 2 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 1 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 342 ms +Checking Quest Disables +>> Checked 5124 quest disables in 1 ms +Loading Quest POI +>> Loaded 13941 quest POI definitions in 174 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 0 ms +Loading Creature Start Quest Data... +>> Loaded 8725 quest relations from creature_queststarter in 9 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 2 ms +Loading Creatures Pooling Data... +>> Loaded 835 creatures in pools in 2 ms +Loading Gameobject Pooling Data... +>> Loaded 7404 gameobject in pools in 11 ms +Loading Mother Pooling Data... +>> Loaded 1218 pools in mother pools in 2 ms +Starting objects pooling system... +Loading Quest Pooling Data... +>> Loaded 56 daily, 5 weekly and 0 monthly quest pools in 1 ms +Loading Game Event Data... +>> Loaded 156 holiday dates in 0 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 1 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 0 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 0 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 0 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 0 ms +Loading SpellArea Data... +>> Loaded 872 spell area requirements in 3 ms +Loading AreaTrigger definitions... +>> Loaded 316 area trigger teleport definitions in 1 ms +Loading Access Requirements... +>> Loaded 152 access requirement definitions in 1 ms +Loading Quest Area Triggers... +>> Loaded 49 quest trigger points in 0 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 1 ms +Loading Dungeon boss data... +>> Loaded 617 instance encounters in 2 ms +Loading LFG rewards... +>> Loaded 20 lfg dungeon rewards in 0 ms +Loading Graveyard-zone links... +>> Loaded 855 graveyard-zone links in 2 ms +Loading Graveyard Orientations... +>> Loaded 1016 graveyard orientations in 1 ms +Loading spell pet auras... +>> Loaded 34 spell pet auras in 0 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 0 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 2 ms +Loading Player Create Cast Spell Data... +>> Loaded 63 player create cast spells in 0 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 4 ms +Loading Player Create XP Data... +>> Loaded 84 xp for level definitions in 1 ms +Loading Exploration BaseXP Data... +>> Loaded 80 BaseXP definitions in 0 ms +Loading Pet Name Parts... +>> Loaded 313 pet name parts in 0 ms +Loading the max pet number... +>> Loaded the max pet number: 28 in 1 ms +Loading pet level stats... +>> Loaded 2720 level pet stats definitions in 4 ms +Loading Player level dependent mail rewards... +>> Loaded 24 level dependent mail rewards in 0 ms +Loading creature loot templates... +>> Loaded 528193 creature loot templates in 653 ms +Loading fishing loot templates... +>> Loaded 266 fishing loot templates in 3 ms +Loading gameobject loot templates... +>> Loaded 19834 gameobject loot templates in 29 ms +Loading item loot templates... +>> Loaded 5031 item loot templates in 15 ms +Loading mail loot templates... +>> Loaded 90 mail loot templates in 1 ms +Loading milling loot templates... +>> Loaded 51 milling loot templates in 3 ms +Loading pickpocketing loot templates... +>> Loaded 10981 pickpocketing loot templates in 20 ms +Loading skinning loot templates... +>> Loaded 3390 skinning loot templates in 9 ms +Loading disenchanting loot templates... +>> Loaded 180 disenchanting loot templates in 1 ms +Loading prospecting loot templates... +>> Loaded 43 prospecting loot templates in 3 ms +Loading spell loot templates... +>> Loaded 292 spell loot templates in 3 ms +Loading reference loot templates... +>> Loaded refence loot templates in 47 ms +Loading Skill Discovery Table... +>> Loaded 347 skill discovery definitions in 4 ms. +Loading Skill Extra Item Table... +>> Loaded 225 spell specialization definitions in 0 ms +Loading Skill Perfection Data Table... +>> Loaded 72 spell perfection definitions in 1 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 0 ms +>> Calculated 9 archaeology branch chances in 0 ms +Loading Achievements... +>> Loaded 47 achievement references in 1 ms. +Loading Achievement Criteria Lists... +>> Loaded 9803 achievement criteria and 952 guild achievement crieteria in 4 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 0 ms +Loading Guilds... +Loading guilds definitions... +>> Loaded 1 guild definitions in 1 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 0 ms +Loading guild event logs... +>> Loaded 8 guild event logs in 1 ms +Loading guild bank event logs... +>> Loaded 22 guild bank event logs in 1 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 1 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 2 ms +Loading GameTeleports... +>> Loaded 1531 GameTeleports in 6 ms +Loading Trainers... +>> Loaded 88 Trainers in 8 ms +Loading Gossip menu... +>> Loaded 8225 gossip_menu IDs in 9 ms +Loading Gossip menu options... +>> Loaded 5837 gossip_menu_option entries in 17 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 2 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 68 ms +Loading Waypoints... +>> Loaded 110626 waypoints in 174 ms +Loading Waypoint Addons... +>> Loaded 639 waypoint addon data in 2 ms +Loading SmartAI Waypoints... +>> Loaded 586 SmartAI waypoint paths (total 7064 waypoints) in 9 ms +Loading Creature Formations... +>> Loaded 2517 creatures in formations in 6 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 3 ms +>> Loaded 41 saved world state values 0 ms +Loading Persistend World Variables... +>> Loaded 9 world variables in 1 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 0 ms. +Loading Phase Area definitions... +>> Loaded 25 phase areas in 1 ms. +Loading Conditions... +>> Loaded 15912 conditions in 40 ms +Loading faction change achievement pairs... +>> Loaded 124 faction change achievement pairs in 0 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 0 ms +Loading faction change item pairs... +>> Loaded 752 faction change item pairs in 1 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 0 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 0 ms +Loading event_scripts... +>> Loaded 461 script definitions in 1 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 8 ms +Loading Creature Texts... +>> Loaded 16797 creature texts for 3997 creatures in 50 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 3 ms +>> Loaded spline chain data for 85 chains, consisting of 272 splines with 1414 w +aypoints in 3 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 149 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 17 ms +>> Loaded 13506 SmartAI scripts in 164 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 0 ms +Loading Item loot... +>> Loaded 0 stored item loots +>> Loaded 0 stored item money +Initialize query data... +>> Initialized query cache data in 969 ms +Initialize commands... +Initialize game time and timers +Mail timer set to: 600, 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 0 ms +Starting Battlefield System +>> Loaded 2 battlefields in 1 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 11 seconds +TC>TC>TrinityCore rev. 04aa87fcfc13+ 2026-10-28 11:30:47 +0100 (feature/clientle +ss-player-core branch) (Unix, Debug, Static) (worldserver-daemon) ready... +tele name Aelion 435 +TC>tele name Lenara 435 +TC>You are teleporting Aelion (offline) to Goldshire. +TC> You are teleporting Lenara (offline) to Goldshire. +TC> clientless generate login 29 +TC>clientless generate login 30 +TC>clientless generate login 31 +TC>clientless generate login 32 +TC>Registered clientless session for character GUID 29 (account 6), state=Loadin +g. +Created clientless player session for character GUID 29 (account 6). +ClientlessPlayerMgr started character GUID 29 (account 6). +TC> Registered clientless session for character GUID 30 (account 6), state=Loadi +ng. +Created clientless player session for character GUID 30 (account 6). +ClientlessPlayerMgr started character GUID 30 (account 6). +TC> Registered clientless session for character GUID 31 (account 6), state=Loadi +ng. +Created clientless player session for character GUID 31 (account 6). +ClientlessPlayerMgr started character GUID 31 (account 6). +TC> Registered clientless session for character GUID 32 (account 6), state=Loadi +ng. +Created clientless player session for character GUID 32 (account 6). +ClientlessPlayerMgr started character GUID 32 (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 31 (account 6) reached state=Online. +Initialized transport GameObject grids: map=1 instance=0 grids=13 candidates=17 +Clientless player character GUID 30 (account 6) reached state=Online. +Clientless player character GUID 29 (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=31 name='Lenara' source='generated:31' home= +'elwynn' canGroundMount=0 mounts=0 mountMinDistance=60.0 mountSpeedRate=1.60 per +sonality=8 regionAffinity=1 activityAffinity=7 +PROFILE_PROFESSIONS guid=31 primary=2 +ClientlessLua[31]: adventure profile controller ready for Lenara +ClientlessLua[31]: executed generic adventure script '/home/wow_admin/wow_cata_s +erver/source/TrinityCore/src/server/scripts/Custom/clientless/adventure/generic. +lua'. +ADVENTURE_CATALOG locations=2973 actions=84 lines=76 +INTENT_TRAVEL guid=31 intent='social' location='poi_npc_280710' area=0 +[AdventureBot][Profile] loaded guid=30 name='Zanzan' source='generated:30' home= +'durotar' canGroundMount=0 mounts=0 mountMinDistance=60.0 mountSpeedRate=1.60 pe +rsonality=8 regionAffinity=1 activityAffinity=7 +PROFILE_PROFESSIONS guid=30 primary=2 +ClientlessLua[30]: adventure profile controller ready for Zanzan +ClientlessLua[30]: executed generic adventure script '/home/wow_admin/wow_cata_s +erver/source/TrinityCore/src/server/scripts/Custom/clientless/adventure/generic. +lua'. +INTENT_TRAVEL guid=30 intent='social' location='poi_tele_823' area=0 +[AdventureBot][Profile] loaded guid=29 name='Aelion' source='generated:29' home= +'teldrassil' canGroundMount=0 mounts=0 mountMinDistance=60.0 mountSpeedRate=1.60 + personality=8 regionAffinity=1 activityAffinity=7 +PROFILE_PROFESSIONS guid=29 primary=2 +ClientlessLua[29]: adventure profile controller ready for Aelion +ClientlessLua[29]: executed generic adventure script '/home/wow_admin/wow_cata_s +erver/source/TrinityCore/src/server/scripts/Custom/clientless/adventure/generic. +lua'. +INTENT_TRAVEL guid=29 intent='social' location='poi_guard_111' area=0 +clientless interaction 29 leave +TC>clientless adventure stop 29 +TC>clientless interaction 31 leave +TC>clientless adventure stop 31 +TC>Clientless player character GUID 32 (account 6) reached state=Online. +TC> Behavior stop requested at a safe point. +TC> TC> Behavior stop requested at a safe point. +TC> [AdventureBot] loaded 3966 clientless world points. +[AdventureBot][Profile] loaded guid=32 name='Aldren' source='generated:32' home= +'silverpineforest' canGroundMount=0 mounts=0 mountMinDistance=60.0 mountSpeedRat +e=1.60 personality=8 regionAffinity=1 activityAffinity=7 +PROFILE_PROFESSIONS guid=32 primary=2 +ClientlessLua[32]: adventure profile controller ready for Aldren +ClientlessLua[32]: executed generic adventure script '/home/wow_admin/wow_cata_s +erver/source/TrinityCore/src/server/scripts/Custom/clientless/adventure/generic. +lua'. +INTENT_TRAVEL guid=32 intent='social' location='poi_npc_321922' area=0 +INTENT_ARRIVED guid=30 intent='social' location='poi_tele_823' +MICRO_START guid=30 intent='social' location='poi_tele_823' action='rest' durati +onMs=54000 +INTENT_ARRIVED guid=32 intent='social' location='poi_npc_321922' +SOCIAL_START guid=32 action='social_order' target='GUID Full: 0xf130b1b8000005c6 + Type: Creature Entry: 45496 Low: 1478' approach=1 +MICRO_START guid=32 intent='social' location='poi_npc_321922' action='social_ord +er' durationMs=28000 +SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature Entry: + 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_order' cue=0 target='GUID Full: 0xf130b1b80000 +05c6 Type: Creature Entry: 45496 Low: 1478' +SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature Entry: + 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_order' cue=2 target='GUID Full: 0xf130b1b80000 +05c6 Type: Creature Entry: 45496 Low: 1478' +SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature Entry: + 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_order' cue=4 target='GUID Full: 0xf130b1b80000 +05c6 Type: Creature Entry: 45496 Low: 1478' +SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature Entry: + 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_order' cue=10 target='GUID Full: 0xf130b1b8000 +005c6 Type: Creature Entry: 45496 Low: 1478' +SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature Entry: + 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_order' cue=7 target='GUID Full: 0xf130b1b80000 +05c6 Type: Creature Entry: 45496 Low: 1478' +SOCIAL_END guid=32 action='social_order' elapsedMs=25000 +SOCIAL_START guid=32 action='social_greet' target='GUID Full: 0xf130b1b8000005c6 + Type: Creature Entry: 45496 Low: 1478' approach=0 +MICRO_START guid=32 intent='social' location='poi_npc_321922' action='social_gre +et' durationMs=10000 +SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature Entry: + 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_greet' cue=0 target='GUID Full: 0xf130b1b80000 +05c6 Type: Creature Entry: 45496 Low: 1478' +SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature Entry: + 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_greet' cue=8 target='GUID Full: 0xf130b1b80000 +05c6 Type: Creature Entry: 45496 Low: 1478' +SOCIAL_END guid=32 action='social_greet' elapsedMs=9000 +SOCIAL_START guid=32 action='social_npc' target='GUID Full: 0xf130b1b8000005c6 T +ype: Creature Entry: 45496 Low: 1478' approach=0 +MICRO_START guid=32 intent='social' location='poi_npc_321922' action='social_npc +' durationMs=21000 +SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature Entry: + 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_npc' cue=0 target='GUID Full: 0xf130b1b8000005 +c6 Type: Creature Entry: 45496 Low: 1478' +SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature Entry: + 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_npc' cue=1 target='GUID Full: 0xf130b1b8000005 +c6 Type: Creature Entry: 45496 Low: 1478' +SOCIAL_START guid=30 action='campfire_rest' target='GUID Full: 0x000000000000000 +0 Type: None Low: 0' approach=0 +MICRO_START guid=30 intent='social' location='poi_tele_823' action='campfire_res +t' durationMs=53000 +SOCIAL_FIRE_CAST guid=30 spell=818 +SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature Entry: + 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_npc' cue=4 target='GUID Full: 0xf130b1b8000005 +c6 Type: Creature Entry: 45496 Low: 1478' +SOCIAL_FIRE_CONFIRMED guid=30 spell=818 object='GUID Full: 0xf11074580000064c Ty +pe: Gameobject Entry: 29784 Low: 1612' +SOCIAL_CUE guid=30 action='campfire_rest' cue=14 target='GUID Full: 0xf110745800 +00064c Type: Gameobject Entry: 29784 Low: 1612' +clientless adventure stop 29 +TC>clientless adventure stop 31 +TC>clientless adventure status 29 +TC>clientless adventure status 31 +TC>clientless interaction 29 +TC>clientless interaction 31 +TC>saveall +TC>Behavior stop requested at a safe point. +TC> Behavior stop requested at a safe point. +TC> Adventure GUID 29: enabled=0 schedule=0 phase=0 intent='' location='' micro= +'' deadline=0 detail='stopped' +TC> Adventure GUID 31: enabled=0 schedule=0 phase=0 intent='' location='' micro= +'' deadline=0 detail='stopped' +TC> Bot group intent='' next='' forming=0 botLed=0 capacity=3 meeting='' +Interactions guid=29 grouped=0 leader=0 partner=0 intent='' schedule='' + leaderIdleSeconds=0 afkSeconds=0 sociability=0.89 loyalty=0.54 +TC> Bot group intent='' next='' forming=0 botLed=0 capacity=3 meeting='' +Interactions guid=31 grouped=0 leader=0 partner=0 intent='' schedule='' + leaderIdleSeconds=0 afkSeconds=0 sociability=0.78 loyalty=0.74 +TC> All players saved. +TC> SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature En +try: 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_npc' cue=5 target='GUID Full: 0xf130b1b8000005 +c6 Type: Creature Entry: 45496 Low: 1478' +SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature Entry: + 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_npc' cue=1 target='GUID Full: 0xf130b1b8000005 +c6 Type: Creature Entry: 45496 Low: 1478' +SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature Entry: + 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_npc' cue=7 target='GUID Full: 0xf130b1b8000005 +c6 Type: Creature Entry: 45496 Low: 1478' +SOCIAL_END guid=32 action='social_npc' elapsedMs=29000 +MICRO_START guid=32 intent='social' location='poi_npc_321922' action='laugh' dur +ationMs=11000 +SOCIAL_CUE guid=30 action='campfire_rest' cue=10 target='GUID Full: 0xf110745800 +00064c Type: Gameobject Entry: 29784 Low: 1612' +MICRO_START guid=32 intent='social' location='poi_npc_321922' action='eat' durat +ionMs=31000 +SOCIAL_CUE guid=30 action='campfire_rest' cue=14 target='GUID Full: 0xf110745800 +00064c Type: Gameobject Entry: 29784 Low: 1612' +MICRO_START guid=32 intent='social' location='poi_npc_321922' action='rest' dura +tionMs=56000 +SOCIAL_END guid=30 action='campfire_rest' elapsedMs=67000 +SOCIAL_START guid=30 action='social_react' target='GUID Full: 0xf1300ce100000765 + Type: Creature Entry: 3297 Low: 1893' approach=1 +MICRO_START guid=30 intent='social' location='poi_tele_823' action='social_react +' durationMs=7000 +SOCIAL_FRONT guid=30 target='GUID Full: 0xf1300ce100000765 Type: Creature Entry: + 3297 Low: 1893' offset=-0.000 distance=2.21 +SOCIAL_CUE guid=30 action='social_react' cue=5 target='GUID Full: 0xf1300ce10000 +0765 Type: Creature Entry: 3297 Low: 1893' +SOCIAL_FRONT guid=30 target='GUID Full: 0xf1300ce100000765 Type: Creature Entry: + 3297 Low: 1893' offset=-0.000 distance=2.21 +SOCIAL_CUE guid=30 action='social_react' cue=4 target='GUID Full: 0xf1300ce10000 +0765 Type: Creature Entry: 3297 Low: 1893' +SOCIAL_END guid=30 action='social_react' elapsedMs=14000 +MICRO_START guid=30 intent='social' location='poi_tele_823' action='drink' durat +ionMs=20000 +MoveSplineInitArgs::Validate: expression '_checkPathLengths()' failed for GUID: +359043 Entry: 5840 +MICRO_START guid=30 intent='social' location='poi_tele_823' action='eat' duratio +nMs=23000 +SOCIAL_START guid=32 action='social_order' target='GUID Full: 0xf130b1b8000005c6 + Type: Creature Entry: 45496 Low: 1478' approach=0 +MICRO_START guid=32 intent='social' location='poi_npc_321922' action='social_ord +er' durationMs=24000 +SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature Entry: + 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_order' cue=0 target='GUID Full: 0xf130b1b80000 +05c6 Type: Creature Entry: 45496 Low: 1478' +SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature Entry: + 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_order' cue=2 target='GUID Full: 0xf130b1b80000 +05c6 Type: Creature Entry: 45496 Low: 1478' +SOCIAL_START guid=30 action='speak' target='GUID Full: 0xf13029b400000766 Type: +Creature Entry: 10676 Low: 1894' approach=1 +MICRO_START guid=30 intent='social' location='poi_tele_823' action='speak' durat +ionMs=16000 +SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature Entry: + 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_order' cue=4 target='GUID Full: 0xf130b1b80000 +05c6 Type: Creature Entry: 45496 Low: 1478' +SOCIAL_FRONT guid=30 target='GUID Full: 0xf13029b400000766 Type: Creature Entry: + 10676 Low: 1894' offset=0.000 distance=2.21 +SOCIAL_CUE guid=30 action='speak' cue=0 target='GUID Full: 0xf13029b400000766 Ty +pe: Creature Entry: 10676 Low: 1894' +MoveSplineInitArgs::Validate: expression '_checkPathLengths()' failed for GUID: +359043 Entry: 5840 +SOCIAL_FRONT guid=30 target='GUID Full: 0xf13029b400000766 Type: Creature Entry: + 10676 Low: 1894' offset=0.000 distance=2.21 +SOCIAL_CUE guid=30 action='speak' cue=1 target='GUID Full: 0xf13029b400000766 Ty +pe: Creature Entry: 10676 Low: 1894' +SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature Entry: + 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_order' cue=10 target='GUID Full: 0xf130b1b8000 +005c6 Type: Creature Entry: 45496 Low: 1478' +SOCIAL_FRONT guid=30 target='GUID Full: 0xf13029b400000766 Type: Creature Entry: + 10676 Low: 1894' offset=0.000 distance=2.21 +SOCIAL_CUE guid=30 action='speak' cue=4 target='GUID Full: 0xf13029b400000766 Ty +pe: Creature Entry: 10676 Low: 1894' +SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature Entry: + 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_order' cue=7 target='GUID Full: 0xf130b1b80000 +05c6 Type: Creature Entry: 45496 Low: 1478' +SOCIAL_END guid=32 action='social_order' elapsedMs=25000 +SOCIAL_FRONT guid=30 target='GUID Full: 0xf13029b400000766 Type: Creature Entry: + 10676 Low: 1894' offset=0.000 distance=2.21 +SOCIAL_CUE guid=30 action='speak' cue=5 target='GUID Full: 0xf13029b400000766 Ty +pe: Creature Entry: 10676 Low: 1894' +MICRO_START guid=32 intent='social' location='poi_npc_321922' action='drink' dur +ationMs=27000 +SOCIAL_FRONT guid=30 target='GUID Full: 0xf13029b400000766 Type: Creature Entry: + 10676 Low: 1894' offset=0.000 distance=2.21 +SOCIAL_CUE guid=30 action='speak' cue=1 target='GUID Full: 0xf13029b400000766 Ty +pe: Creature Entry: 10676 Low: 1894' +SOCIAL_FRONT guid=30 target='GUID Full: 0xf13029b400000766 Type: Creature Entry: + 10676 Low: 1894' offset=0.000 distance=2.21 +SOCIAL_CUE guid=30 action='speak' cue=7 target='GUID Full: 0xf13029b400000766 Ty +pe: Creature Entry: 10676 Low: 1894' +SOCIAL_END guid=30 action='speak' elapsedMs=30000 +SOCIAL_START guid=30 action='social_listen' target='GUID Full: 0xf1300ce10000076 +5 Type: Creature Entry: 3297 Low: 1893' approach=1 +MICRO_START guid=30 intent='social' location='poi_tele_823' action='social_liste +n' durationMs=12000 +SOCIAL_FRONT guid=30 target='GUID Full: 0xf1300ce100000765 Type: Creature Entry: + 3297 Low: 1893' offset=-0.000 distance=2.21 +SOCIAL_CUE guid=30 action='social_listen' cue=4 target='GUID Full: 0xf1300ce1000 +00765 Type: Creature Entry: 3297 Low: 1893' +SOCIAL_FRONT guid=30 target='GUID Full: 0xf1300ce100000765 Type: Creature Entry: + 3297 Low: 1893' offset=-0.000 distance=2.21 +SOCIAL_CUE guid=30 action='social_listen' cue=6 target='GUID Full: 0xf1300ce1000 +00765 Type: Creature Entry: 3297 Low: 1893' +SOCIAL_FRONT guid=30 target='GUID Full: 0xf1300ce100000765 Type: Creature Entry: + 3297 Low: 1893' offset=-0.000 distance=2.21 +SOCIAL_CUE guid=30 action='social_listen' cue=4 target='GUID Full: 0xf1300ce1000 +00765 Type: Creature Entry: 3297 Low: 1893' +SOCIAL_START guid=32 action='social_npc' target='GUID Full: 0xf130b1b8000005c6 T +ype: Creature Entry: 45496 Low: 1478' approach=0 +MICRO_START guid=32 intent='social' location='poi_npc_321922' action='social_npc +' durationMs=26000 +SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature Entry: + 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_npc' cue=0 target='GUID Full: 0xf130b1b8000005 +c6 Type: Creature Entry: 45496 Low: 1478' +SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature Entry: + 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_npc' cue=1 target='GUID Full: 0xf130b1b8000005 +c6 Type: Creature Entry: 45496 Low: 1478' +SOCIAL_END guid=30 action='social_listen' elapsedMs=20000 +MICRO_START guid=30 intent='social' location='poi_tele_823' action='look' durati +onMs=11000 +SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature Entry: + 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_npc' cue=4 target='GUID Full: 0xf130b1b8000005 +c6 Type: Creature Entry: 45496 Low: 1478' +SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature Entry: + 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_npc' cue=5 target='GUID Full: 0xf130b1b8000005 +c6 Type: Creature Entry: 45496 Low: 1478' +MICRO_START guid=30 intent='social' location='poi_tele_823' action='laugh' durat +ionMs=21000 +SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature Entry: + 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_npc' cue=1 target='GUID Full: 0xf130b1b8000005 +c6 Type: Creature Entry: 45496 Low: 1478' +SOCIAL_FRONT guid=32 target='GUID Full: 0xf130b1b8000005c6 Type: Creature Entry: + 45496 Low: 1478' offset=0.000 distance=2.20 +SOCIAL_CUE guid=32 action='social_npc' cue=7 target='GUID Full: 0xf130b1b8000005 +c6 Type: Creature Entry: 45496 Low: 1478' +SOCIAL_END guid=32 action='social_npc' elapsedMs=29000 +MICRO_START guid=32 intent='social' location='poi_npc_321922' action='drink' dur +ationMs=17000 +SOCIAL_START guid=30 action='social_farewell' target='GUID Full: 0xf1300ce100000 +765 Type: Creature Entry: 3297 Low: 1893' approach=0 +MICRO_START guid=30 intent='social' location='poi_tele_823' action='social_farew +ell' durationMs=5000 +SOCIAL_FRONT guid=30 target='GUID Full: 0xf1300ce100000765 Type: Creature Entry: + 3297 Low: 1893' offset=-0.000 distance=2.21 +SOCIAL_CUE guid=30 action='social_farewell' cue=7 target='GUID Full: 0xf1300ce10 +0000765 Type: Creature Entry: 3297 Low: 1893' +clientless adventure status 29 +TC>clientless adventure status 31 +TC>clientless interaction 29 +TC>clientless interaction 31 +TC>saveall +TC>Adventure GUID 29: enabled=0 schedule=0 phase=0 intent='' location='' micro=' +' deadline=0 detail='stopped' +TC> Adventure GUID 31: enabled=0 schedule=0 phase=0 intent='' location='' micro= +'' deadline=0 detail='stopped' +TC> Bot group intent='' next='' forming=0 botLed=0 capacity=3 meeting='' +Interactions guid=29 grouped=0 leader=0 partner=0 intent='' schedule='' + leaderIdleSeconds=0 afkSeconds=0 sociability=0.89 loyalty=0.54 +TC> Bot group intent='' next='' forming=0 botLed=0 capacity=3 meeting='' +Interactions guid=31 grouped=0 leader=0 partner=0 intent='' schedule='' + leaderIdleSeconds=0 afkSeconds=0 sociability=0.78 loyalty=0.74 +TC> All players saved. +TC> SOCIAL_END guid=30 action='social_farewell' elapsedMs=5000 diff --git a/doc/clientless/checkpoints/2026-10-28-bot-groups/first-native-group.txt b/doc/clientless/checkpoints/2026-10-28-bot-groups/first-native-group.txt new file mode 100644 index 0000000000..78abb31c34 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-28-bot-groups/first-native-group.txt @@ -0,0 +1,6 @@ +TrinityCore rev. 04aa87fcfc13+ 2026-10-28 11:30:47 +0100 (feature/clientless-player-core branch) (Unix, Debug, Static) (worldserver-daemon) ready... +GROUP_BOT_INVITE leader=31 target=29 intent='social' forming=0 +INTERACTION_INVITE guid=29 leader=31 accepted=1 +INTERACTION_GROUP_LEAVE guid=31 reason='intent-change' +Interactions guid=29 grouped=1 leader=31 partner=0 intent='social' schedule='soc +Interactions guid=31 grouped=1 leader=31 partner=0 intent='social' schedule='soc diff --git a/doc/clientless/checkpoints/2026-10-28-bot-groups/summary.txt b/doc/clientless/checkpoints/2026-10-28-bot-groups/summary.txt new file mode 100644 index 0000000000..3e5624938e --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-28-bot-groups/summary.txt @@ -0,0 +1,3 @@ +66 clientless test scripts passed; final interaction-manager regression rechecked. Native Lenara31 -> Aelion29 invitation accepted, native grouped1/leader31 observed, later normal intent-change departure. Timer defect discovered during smoke and fixed before final deployment. Full group route/raid/visual acceptance remains open. +Latest production plan-signature regression rechecked: different inherited destinations do not trigger personal intent-change departure; restoration returns to the original manual destination. +Final corrected Debug build installed; worldserver122595 fresh ready and port8085 verified. All four bots online;29/31 confirmed enabled0/grouped0 in Goldshire,30/32 original social intents,26offline. No temporary profile changes or overrides. diff --git a/doc/clientless/checkpoints/2026-10-28-bot-groups/tests.log b/doc/clientless/checkpoints/2026-10-28-bot-groups/tests.log new file mode 100644 index 0000000000..fdd3c9d216 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-28-bot-groups/tests.log @@ -0,0 +1,204 @@ + +TEST tests/clientless/test_adventure_area_node.py +Production area destination excludes outside and unreachable graph nodes. + +TEST tests/clientless/test_adventure_behavior_policy.py +24-hour generation, shared intents with changing destinations, broad location classification and repetition penalties passed. + +TEST tests/clientless/test_adventure_combat_exercise.py +Production exercise: stance/charge/melee/casts, native validation delegation, bounded lifetime, invalid targets and unchanged native phases passed. + +TEST tests/clientless/test_adventure_combat_preparation.py +Production talent learner: native tiers/dependencies/class gates, all 26 legal level60 points, no capstone bypass and repeat safety passed. + +TEST tests/clientless/test_adventure_combat_reactions.py +Production combat focus, damage ordering, risk filters, corpse radius/delay, flight/group override, blocked escape, danger avoidance and malus-free persisted fallback passed. + +TEST tests/clientless/test_adventure_economy.py +Economy: native service roles, friendly/phase/LOS/bounds gates, no bot service targets and coherent performative cues passed. + +TEST tests/clientless/test_adventure_economy_catalog.py +Economy catalog: eleven actions, three faction-bound service anchors, native flag gates and idempotent preservation passed. + +TEST tests/clientless/test_adventure_exploration.py +Exploration: visible native props only, no chests/traps/furniture, phase/map/LOS/range gates and contextual preparation passed. + +TEST tests/clientless/test_adventure_exploration_catalog.py +Exploration catalog: ten actions, shared scenes, default wander replacement, explicit category eligibility and preserved edits passed. + +TEST tests/clientless/test_adventure_generator_equipment.py +Production equipment affinity: mana-only/direct/triggered procs, caster/physical stat effects and bounded cycles passed + +TEST tests/clientless/test_adventure_generator_ownership.py +Production generator commands: batch bounds, dedicated ownership, persistent/online/removing retirement protection and retired login refusal passed + +TEST tests/clientless/test_adventure_generator_policy.py +Production generator: 6144 cultural names and 61440 real Lua profiles, profession coherence, personality ranges, escaping and equipment/armor gates passed + +TEST tests/clientless/test_adventure_graph_handover.py +Production graph handover waits for ship/taxi/flight/teleport and stops at safe road/node boundaries. + +TEST tests/clientless/test_adventure_group_plan.py +Production group plans: leader meeting/goal/deadline inheritance, untouched personal day, manual preparation and instance-state guards passed. + +TEST tests/clientless/test_adventure_groups.py +Production recruitment: capitals, different goals, native inviter leadership, joining existing groups, capacity/reservations, raid conversion, planned transition and local contact passed. + +TEST tests/clientless/test_adventure_instance.py +Production instance lifecycle: time budget, true entry, denied/shared entry, native rear exit, restart, expired copy, late arrival, HP and return replay passed. + +TEST tests/clientless/test_adventure_instance_autoresume.py +Instance startup script: no schema/no visits stays idle; saved visitors resume with the Adventure controller and native accounts. + +TEST tests/clientless/test_adventure_instance_begin.py +Production entrance travel uses the existing location planner, requires mapping and retains normal social/area travel. + +TEST tests/clientless/test_adventure_instance_catalog.py +Instance migrations: outside entrances only, native graph preserved, disabled/missing transitions excluded, edits and saved visits preserved. + +TEST tests/clientless/test_adventure_instance_persistence.py +Production instance persistence: phases, original deadline, instance ID, transactional HP marker, failed commit and intent release passed. + +TEST tests/clientless/test_adventure_intent_state.py +Production intent arrival, failed/partial travel, deadlines and safe schedule handover passed. + +TEST tests/clientless/test_adventure_interactions.py +/tmp/tmp_vzcr4vj/test.cpp: In member function ‘void AdventureInteractionMgr::Leave(Player*, Record&, const char*)’: +/tmp/tmp_vzcr4vj/test.cpp:142:69: warning: unused parameter ‘reason’ [-Wunused-parameter] + 142 | void AdventureInteractionMgr::Leave(Player* p,Record& r,char const* reason) + | ~~~~~~~~~~~~^~~~~~ +production interaction manager: invitations, factions, reservations, follow, AFK and deferred departure passed + +TEST tests/clientless/test_adventure_leisure.py +Leisure: substantial curved MMAP routes, mirrored return, interrupted return, rejected corridors, actual arrival, companion follow, riding, flight and fishing passed. + +TEST tests/clientless/test_adventure_leisure_catalog.py +Leisure migration: native city anchor, shared scene references, pet-walk mapping and preservation of edited weights passed. + +TEST tests/clientless/test_adventure_location_import.py +Locations: actual Debug SMALLINT reads retain continent IDs; explicit location eligibility and edited weight overrides passed. + +TEST tests/clientless/test_adventure_micro_reset.py +Micro cleanup ends physical local movement, preserves transit and does not stand up dead players. + +TEST tests/clientless/test_adventure_profession_catalog.py +Profession catalog: native workstation roles, no workshop/resource inference, idempotence and preserved edits passed. + +TEST tests/clientless/test_adventure_profession_profile.py +Production profession parser, real Lua profiles, primary restrictions and secondary permissions passed. + +TEST tests/clientless/test_adventure_profession_sightline.py +Native workstation working-surface sightline and blocked geometry rejection passed. + +TEST tests/clientless/test_adventure_profession_targets.py +Production profession targets: native locks, phase/spawn/loot, stations, corpses, history and trainers passed. + +TEST tests/clientless/test_adventure_pvp.py +Production PvP selector: level boundaries, radius, factions, native legality, navigation, intent, personality and resources passed. + +TEST tests/clientless/test_adventure_repair_catalog.py +Repair action catalog: proper native service requirement, economy-only intent and preserved administrator weights passed. + +TEST tests/clientless/test_adventure_rest.py +Recovery: authored places, bounds, arrival, occupancy, full rest duration, failure, reload and interruption passed. + +TEST tests/clientless/test_adventure_rest_fire.py +Rest fire: shared scene mapping, additive migration, preserved weights and authored creation-place policy passed. + +TEST tests/clientless/test_adventure_rest_space.py +Sleep footprint: furniture, body extent, overhead collision, ledges, stairs, missing heights, rotation and scale passed. + +TEST tests/clientless/test_adventure_rest_targets.py +Native sleep targets: chair exclusion, phase, despawn, floor search, indoors, water, storey, occupancy, bed/surface preference passed. + +TEST tests/clientless/test_adventure_social.py +Production social flow: approach before talk, coherent greeting/talk/listen/farewell, lost targets, native chair confirmation and real fire confirmation passed. + +TEST tests/clientless/test_adventure_social_fire.py +Shared fire: native detection/provisioning, buildings/city/parent/zone restrictions, existing fires and outdoor/water checks passed. + +TEST tests/clientless/test_adventure_supplies.py +Production supplies: exact10% boundary, bandage-before-food, native lockout fallback, recovery completion, combat potions, mana-only drinking and daily/post-combat top-ups passed. + +TEST tests/clientless/test_adventure_supply_catalog.py +Supply migrations: all five roles, level60 netherweave tier, editable/disabled catalog preservation and persistent daily stamp passed. + +TEST tests/clientless/test_adventure_supply_items.py +Production native-item delegation: usable bandage tiers, native cooldown/lockout gates, real inventory consumption, self targeting and level60 secondary caps passed. + +TEST tests/clientless/test_adventure_training_catalog.py +Training catalog: real dummy requirement, leisure-only intent, bounded duration and preserved administrator weights passed. + +TEST tests/clientless/test_adventure_walk.py +Production travel gait reaches native spline, survives repaths, prevents mounting and preserves legacy running defaults. + +TEST tests/clientless/test_clientless_character_factory.py +Production native factory: character/profile atomicity, failed preparation/commit, cache publication order, account/security/name/cap gates passed + +TEST tests/clientless/test_clientless_companion.py +Native companion: provisioning, delayed summon, ownership, real item target, leash confirmation/timeout, reuse and cleanup passed. + +TEST tests/clientless/test_clientless_damage_notifications.py +Production damage handoff: 16 concurrent map-worker writers, ordered GUID-only events, clientless-only/positive damage gates and post-health notification passed. + +TEST tests/clientless/test_clientless_near_teleport_observer.py +Production near-teleport observer packet includes actual mover GUID and destination; clientless timestamp refreshed, regular client timestamp preserved. + +TEST tests/clientless/test_clientless_skill_preparation.py +Production offline skill preparation: native tier caps, no downgrading, rank/level/race/tier gates and no modification of online players passed + +TEST tests/clientless/test_clientless_social_notifications.py +Production social/helper queues: concurrent GUID-only delivery, bounded queues, human/self/zero-damage gates, pet attribution and lethal-cleanup ordering passed + +TEST tests/clientless/test_default_phase.py +Native PhaseShift: implicit/default 169 visibility, genuine custom-phase separation and reference updates passed. + +TEST tests/clientless/test_flight_area_query.py +Native terrain query map/phase, area/zone ordering and cache passed. + +TEST tests/clientless/test_flight_handoff.py +Flight handoff before spline end, terminal landing, return corners, collision stop and actual distance passed. + +TEST tests/clientless/test_flight_policy.py +Flight native eligibility, license/profile/zone policy, RP costs and fail-safe return decisions passed. + +TEST tests/clientless/test_follow_spline_velocity.py +Native follow: actual player/creature spline velocity, client gait and independent follower speeds passed. + +TEST tests/clientless/test_free_flight.py +Flight geometry passed: takeoff/landing, smoothed peak, ceiling, lateral/climbing avoidance, missing terrain and blocked corridors. + +TEST tests/clientless/test_free_flight_planner.py +ground planner regressions and dynamic flight/node/location/virtual-exit integration passed + +TEST tests/clientless/test_ground_exit_continuation.py +optional exits: repeated probes, retained route, delayed success, forward resume, flight fallback passed + +TEST tests/clientless/test_local_spline_segments.py +Production local spline clipping passes native path-length validation, tiny remainders and 1000 sampled paths. + +TEST tests/clientless/test_lua_reload.py +Lua reload regression tests passed (production reload/bindings, real Lua 5.4). + +TEST tests/clientless/test_player_pure_max_skill.py +Native pure skill cap: learned skills return stored cap; missing/deleted/zero-rank skills return0. + +TEST tests/clientless/test_portal_transfer.py +Portal near/far ACK and denied same-map transfer regressions passed. + +TEST tests/clientless/test_portal_use.py +Interactive portal native click, near/far/global ACK, denial, timeout and target checks passed. +Native portal destination discovery and unsupported/ambiguous spell rejection passed. +Production clientless ACK completion sequence increments only after completed near/far transfers. + +TEST tests/clientless/test_shared_group_route.py +Production generic shared routes: same edges/taxi/free flight, independent execution state, final local goal, late catch-up, permissions and stale-goal rejection passed. + +TEST tests/clientless/test_taxi_import_disabled.py +Taxi importer preserves disabled rows and their reasons. + +TEST tests/clientless/test_transport.py +Transport dock, frame, recorder, cross-map copy and ride regressions passed + +TEST tests/clientless/test_transport_grid_loading.py +Core transport grid initialization regressions passed (no CPC dependency). diff --git a/doc/clientless/checkpoints/2026-10-28-bot-groups/vcore-groups-db.py b/doc/clientless/checkpoints/2026-10-28-bot-groups/vcore-groups-db.py new file mode 100644 index 0000000000..ccaca74e60 --- /dev/null +++ b/doc/clientless/checkpoints/2026-10-28-bot-groups/vcore-groups-db.py @@ -0,0 +1,19 @@ +from pathlib import Path +import os,re,subprocess,sys +root=Path('/home/wow_admin/wow_cata_server/source/TrinityCore') +conf=Path('/home/wow_admin/wow_cata_server/server/debug/etc/worldserver.conf').read_text() +def query(kind,sql): + h,port,u,p,db=re.search(r'^'+kind+r'DatabaseInfo\s*=\s*"([^"]+)"',conf,re.M).group(1).split(';') + env=os.environ.copy();env['MYSQL_PWD']=p + r=subprocess.run(['mysql','--protocol=TCP','--host='+h,'--port='+port,'--user='+u,'--database='+db,'--batch','--connect-timeout=10'],input=sql,text=True,capture_output=True,env=env) + if r.returncode:raise SystemExit(r.stderr.replace(p,'[redacted]')) + return r.stdout + +mode=sys.argv[1] +if mode=='inspect': + print(query('World',"SHOW CREATE TABLE vcore_adventure_day; SELECT zone_id,area_id,COUNT(*) n FROM vcore_adventure_location GROUP BY zone_id,area_id ORDER BY n DESC LIMIT 20;")) +elif mode=='apply': + print(query('World',(root/'sql/custom/world/2026_10_28_01_adventure_groups.sql').read_text())) +elif mode=='state': + print(query('Character',"SELECT guid,name,online,map,instance_id FROM characters WHERE guid IN(26,29,30,31,32); SELECT guid,leaderGuid,groupType FROM groups; SELECT guid,memberGuid FROM group_member ORDER BY guid,memberGuid;")) +else:raise SystemExit('unknown mode') diff --git a/doc/clientless/combat-reactions.md b/doc/clientless/combat-reactions.md index d9dd96b0a9..cbab51ec38 100644 --- a/doc/clientless/combat-reactions.md +++ b/doc/clientless/combat-reactions.md @@ -187,3 +187,55 @@ Geisterlauf und nativeLeichenaufnahme. NachLogout lebendHP4340 ohneLeiche oder Aura15007 gespeichert. KeineUhrabkürzung, keinNormalreisebeleg durchTestpositionierung. Belege: checkpoints/2026-10-27-combat/. Mehrfachangreifer undNeustart während Leichenlauf produktiv mitAdaptern geprüft, nochkeine entsprechendeLiveabnahme. + +## Offensive faction encounters + +Autonomous solo bots and bot group leaders with an active intent can seek opposing players +(including clientless bots) and Horde/Alliance NPCs within 50 yards. Levels must +be within five levels inclusive in either direction. Native attack permission, +hostility, visibility/phase, LOS and a complete MMAP approach remain required; +this does not enable PvP flags or bypass sanctuary rules. Player-controlled pets +are not independent offensive targets. Elite/world-boss NPCs remain excluded. +Health must be at least 60%, mana users at least 30% mana. + +Every 30 seconds the nearest reachable eligible opponent receives one decision: +probability = adventurousness * danger_tolerance * intent weight. Weights: +adventure 1, leisure .35, social .2, profession .15, economy .1; rest, instance, +unknown or disabled intents 0. Missing traits default to 0. Group members retain leader-directed support; only the bot leader may initiate +a PvP encounter. Player-led bots never initiate independently. +The PvE path/quarry selector excludes faction NPCs so it cannot bypass these +personality/level gates. Actual damage still takes precedence via the existing +ordered self-defence path, regardless of offensive level boundaries. + +`COMBAT_PVP_INITIATE` records the initiating bot and target; ordinary combat +start/end logs continue. Solo tactical flight is described below. Automated fixtures verify the production selector, +not live PvP gameplay or class rotations beyond the existing warrior executor. + + +## Solo flight + +Grouped bots never flee, including at critical health or against bosses. +Joining a group cancels flight on the next combat update and restores ordinary +combat support. Ungrouped bots flee from actual attackers at least six levels +higher, equal/higher-level elites/bosses, or below 20% health during combat. +A nearby strong creature alone does not trigger flight. Training alone does not +trigger flight; real damage still interrupts training through self-defence. + +The combat manager owns flight, interrupts the current activity and retains the +original intent deadline. Every two seconds it tests 24 terrain-adjusted local +escape candidates, increasing minimum distance to current/last-known attackers. +Complete, non-suspicious MMAP paths up to60 yards are required. Known visible +hostiles within12 yards of the proposed corridor are avoided. This is a local +heuristic, not a guarantee against unseen enemies or the previously deferred +MMAP obstacle limitations. Existing native combat potions remain available. +If no suitable path starts, the bot stays and defends itself in melee, retrying +other directions on the next evaluation. No teleports, forced combat reset, +invulnerability or synthetic movement are used. + +Flight persists until native combat ends AND at least50 yards separate the bot +from the last known threat positions. Normal self-care then runs before the +current intent resumes, with30 seconds of offensive cooldown. Death clears the +flight state and uses the existing corpse recovery. Logs: +COMBAT_FLEE_START/PATH/BLOCKED/END/CANCEL. Production state-machine fixtures cover +level/health thresholds, group override and joining mid-flight, a blocked path, +nearby hostile avoidance and safe completion. Live gameplay remains unverified. diff --git a/doc/clientless/interactions.md b/doc/clientless/interactions.md index 2f92b41c6d..7545ecda70 100644 --- a/doc/clientless/interactions.md +++ b/doc/clientless/interactions.md @@ -41,7 +41,9 @@ An actual native invitation is answered after 2–4 seconds in a safe state. Acceptance probability clamps sociability plus an intent adjustment to [0,1]: rest −0.50, profession/economy −0.25, adventure/instance +0.20, other +0.10. Inviter must be alive and friendly. Bots do not accept invitations from other -clientless players yet; autonomous bot grouping is explicitly deferred. +unreserved clientless players. Reserved native bot invitations, autonomous +groups, raid/preparation intents and human admission to bot groups are described +in [bot-groups.md](bot-groups.md). Invitations are declined inside dungeons. Normal faction/group restrictions run before the invitation reaches the bot; none are bypassed. diff --git a/sql/custom/world/2026_10_28_01_adventure_groups.sql b/sql/custom/world/2026_10_28_01_adventure_groups.sql new file mode 100644 index 0000000000..5044ac03a1 --- /dev/null +++ b/sql/custom/world/2026_10_28_01_adventure_groups.sql @@ -0,0 +1,8 @@ +-- Category weights only. Native map types distinguish dungeon from raid entrances; +-- formation locations must additionally belong to a capital-city zone. +INSERT IGNORE INTO vcore_adventure_location_intent(category,intent,weight) + SELECT category,'groupformation',1 FROM vcore_adventure_location_intent + WHERE intent='social' AND weight>0; +INSERT IGNORE INTO vcore_adventure_location_intent(category,intent,weight) + VALUES ('instance_entrance','raid',5); +-- Existing daily plans remain intact. Newly generated days add preparation hours. diff --git a/src/server/game/Handlers/GroupHandler.cpp b/src/server/game/Handlers/GroupHandler.cpp index de4c88e555..1f8ef0ea3d 100644 --- a/src/server/game/Handlers/GroupHandler.cpp +++ b/src/server/game/Handlers/GroupHandler.cpp @@ -21,6 +21,7 @@ #include "DatabaseEnv.h" #include "Group.h" #include "GroupMgr.h" +#include "ClientlessPlayerMgr.h" #include "Log.h" #include "ObjectAccessor.h" #include "ObjectMgr.h" @@ -136,6 +137,7 @@ void WorldSession::HandlePartyInviteOpcode(WorldPackets::Party::PartyInviteClien // player already in another group or invited if (group2 || invitedPlayer->GetGroupInvite()) { + if(group2)sClientlessPlayerMgr->GroupJoinRequest(invitedPlayer,invitingPlayer); SendPartyResult(PARTY_OP_INVITE, packet.TargetName, ERR_ALREADY_IN_GROUP_S); if (group2) diff --git a/src/server/game/World/AdventureBehaviorMgr.cpp b/src/server/game/World/AdventureBehaviorMgr.cpp index ce8ab59381..c7124421de 100644 --- a/src/server/game/World/AdventureBehaviorMgr.cpp +++ b/src/server/game/World/AdventureBehaviorMgr.cpp @@ -1,5 +1,7 @@ #include "AdventureBehaviorMgr.h" #include "AdventureCombatMgr.h" +#include "AdventureGroupPolicy.h" +#include "Group.h" #include "AdventureLocationMgr.h" #include "AdventureEconomyPolicy.h" #include "AdventureRestSpace.h" @@ -26,6 +28,7 @@ #include "World.h" #include "WorldSession.h" #include "ObjectMgr.h" +#include "ObjectAccessor.h" #include "InstanceSaveMgr.h" #include "Opcodes.h" #include "WorldPacket.h" @@ -133,7 +136,7 @@ namespace bool Known(std::string const & intent) { - return intent == "social" || intent == "adventure" || intent == "profession" || intent == "rest" || intent == "leisure" || intent == "economy" || intent == "instance"; + return intent == "social" || intent == "adventure" || intent == "profession" || intent == "rest" || intent == "leisure" || intent == "economy" || intent == "instance" || intent == "raid" || intent == "groupformation"; } bool Busy(uint32 guid) { @@ -272,19 +275,21 @@ void AdventureBehaviorMgr::Generate(Player * player, Record & record, uint32 dat { std::vector < double > weights { - Affinity(profile->ActivityAffinity, "social", 1) * Affinity(profile->Personality, "sociability", 0.5f) + Affinity(profile->ActivityAffinity, "tavern", 0.5f) * Affinity(profile->Personality, "tavern_affinity", 0.5f), Affinity(profile->ActivityAffinity, "adventure", 1) * Affinity(profile->Personality, "adventurousness", 0.5f), Affinity(profile->ActivityAffinity, "profession", 0.5f), Affinity(profile->ActivityAffinity, "rest", 0.5f), Affinity(profile->ActivityAffinity, "leisure", 0.5f), Affinity(profile->ActivityAffinity, "economy", 0.5f), Affinity(profile->ActivityAffinity, "instance", 0.5f) + Affinity(profile->ActivityAffinity, "social", 1) * Affinity(profile->Personality, "sociability", 0.5f) + Affinity(profile->ActivityAffinity, "tavern", 0.5f) * Affinity(profile->Personality, "tavern_affinity", 0.5f), Affinity(profile->ActivityAffinity, "adventure", 1) * Affinity(profile->Personality, "adventurousness", 0.5f), Affinity(profile->ActivityAffinity, "profession", 0.5f), Affinity(profile->ActivityAffinity, "rest", 0.5f), Affinity(profile->ActivityAffinity, "leisure", 0.5f), Affinity(profile->ActivityAffinity, "economy", 0.5f), Affinity(profile->ActivityAffinity, "instance", 0.5f), Affinity(profile->ActivityAffinity, "raid", 0.25f) }; auto i = Choose(weights, record.Random); - return std::array < std::string, 7 > + return std::array < std::string, 8 > { - "social", "adventure", "profession", "rest", "leisure", "economy", "instance" + "social", "adventure", "profession", "rest", "leisure", "economy", "instance", "raid" } - [std::min(i, std::size_t(6))]; + [std::min(i, std::size_t(7))]; }; record.Plan = AdventureBehaviorPolicy::Generate(record.Random, Affinity(profile->Personality, "wanderlust", 0.3f), pick,[&](std::string const & intent) { return PickLocation(player, record, intent); }); + AdventureGroupPolicy::Prepare(record.Plan,record.Random,Affinity(profile->Personality,"sociability",0.5f), + [&](){return PickLocation(player,record,"groupformation");}); auto tx = WorldDatabase.BeginTransaction(); for (unsigned h = 0; h < 24; ++ h) { @@ -333,6 +338,8 @@ void AdventureBehaviorMgr::Queue(Player * player, Record & r, std::string const a.Name = action; a.Parameters[key] = value; a.Parameters["hard_destination"] = hard ? "true" : "false"; + if(auto group=player->GetGroup())if(group->GetLeaderGUID()!=player->GetGUID() && sAdventureGroupMgr->BotLed(player)) + a.Parameters["route_source"]=std::to_string(group->GetLeaderGUID().GetCounter()); r.Queued = sClientlessPlayerMgr->QueueAction(player->GetGUID().GetCounter(), std::move(a)); } @@ -396,7 +403,7 @@ void AdventureBehaviorMgr::Begin(Player * player, Record & r) intent.Parameters["location"] = r.Location; sClientlessPlayerMgr->SetIntent(player->GetGUID().GetCounter(), std::move(intent)); r.Publishing = false; - if (r.Intent=="instance") + if ((r.Intent=="instance" || r.Intent=="raid")) { if (!ResolveInstance(player,r)) {Fallback(player,r);return;} Queue(player,r,"travel_to_location","id",r.Location,true); @@ -1261,7 +1268,18 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff) else r.Deadline = deadline; } } - else if (r.Deadline && now >= r.Deadline) SetWanted(player, r, "", "", 0); + else if (r.Deadline && now >= r.Deadline) + { + if(r.Intent=="groupformation" && !r.ManualNextIntent.empty()) + { + auto intent=r.ManualNextIntent,location=r.ManualNextLocation; + r.ManualNextIntent.clear();r.ManualNextLocation.clear(); + SetWanted(player,r,intent,location,now+3600); + } + else SetWanted(player,r,"","",0); + } + if(auto group=player->GetGroup())if(sAdventureGroupMgr->BotLed(player) && group->GetLeaderGUID()!=player->GetGUID()) + if(auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID()))AdoptGroup(player,leader); if (advanceLeisure()) return; if (Unsafe(player)) { @@ -1329,7 +1347,9 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff) } if (r.State == Phase::OnSite) { - if (r.Intent=="instance") {UpdateInstance(player,r,now);return;} + if ((r.Intent=="instance" || r.Intent=="raid")) {UpdateInstance(player,r,now);return;} + if(r.Intent=="groupformation")return; + if(sAdventureGroupMgr->FollowMember(player))return; if (r.Recovery.Active) { if (UpdateRecovery(player,r,1000)) return; @@ -1455,6 +1475,17 @@ bool AdventureBehaviorMgr::Command(ChatHandler * handler, char const * args) handler->SendSysMessage("Schedule mode updated; changes use a safe graph handover."); return true; } + if(op=="prepare") + { + std::string next,meeting;uint32 seconds=3600; + in>>next>>meeting;if(in>>seconds)seconds=std::clamp(seconds,30u,3600u); + auto location=sAdventureLocationMgr->Find(meeting); + if(!AdventureGroupPolicy::Eligible(next) || !location || !Allowed(player,*location,"groupformation"))return false; + auto destination=PickLocation(player,r,next);if(destination.empty())return false; + r.Schedule=false;r.Enabled=true;r.ManualNextIntent=next;r.ManualNextLocation=destination; + SetWanted(player,r,"groupformation",meeting,GameTime::GetGameTime()+seconds); + handler->SendSysMessage("Manual preparation started; next activity begins at the deadline even if the group is incomplete.");return true; + } if (op == "intent") { std::string intent, location; @@ -1464,23 +1495,23 @@ bool AdventureBehaviorMgr::Command(ChatHandler * handler, char const * args) auto l = sAdventureLocationMgr->Find(location); if (! Known(intent) || ! l || ! Allowed(player, * l, intent)) { - handler->SendSysMessage("Use social/adventure/profession/rest/leisure and a matching enabled location."); + handler->SendSysMessage("Use social/adventure/profession/rest/leisure/economy/instance/raid/groupformation and a matching enabled location."); return false; } r.Schedule = false; - r.Enabled = true; + r.Enabled = true;r.ManualNextIntent.clear();r.ManualNextLocation.clear(); SetWanted(player, r, intent, location, GameTime::GetGameTime() + seconds); handler->SendSysMessage("Intent requested; travel and micro actions are autonomous."); return true; } if (op == "stop") { - r.Schedule = false; + r.Schedule = false;r.ManualNextIntent.clear();r.ManualNextLocation.clear(); SetWanted(player, r, "", "", 0); handler->SendSysMessage("Behavior stop requested at a safe point."); return true; } - handler->SendSysMessage("Usage: clientless adventure import|repairimport|reload|status |plan |schedule on/off|intent [seconds]|micro |stop "); + handler->SendSysMessage("Usage: clientless adventure import|repairimport|reload|status |plan |schedule on/off|intent [seconds]|prepare [seconds]|micro |stop "); return false; } @@ -1519,7 +1550,8 @@ bool AdventureBehaviorMgr::RestoreInstance(Player* player, Record& r) v.Map=f[3].GetUInt32();v.InstanceId=f[4].GetUInt32();v.BlockEnd=time_t(f[5].GetUInt64()); v.PrepareUntil=time_t(f[6].GetUInt64());v.Stage=AdventureInstancePolicy::Phase(f[7].GetUInt8()); v.LossPercent=f[8].GetUInt8();v.InjuryApplied=f[9].GetUInt8()!=0;r.Schedule=f[10].GetUInt8()!=0; - r.Intent=r.WantedIntent="instance";r.Location=r.WantedLocation=v.Location;r.Deadline=v.BlockEnd; + auto restoredMap=sMapStore.LookupEntry(v.Map); + r.Intent=r.WantedIntent=restoredMap && restoredMap->IsRaid()?"raid":"instance";r.Location=r.WantedLocation=v.Location;r.Deadline=v.BlockEnd; r.State=v.Stage==AdventureInstancePolicy::Phase::Travel?Phase::Idle:Phase::OnSite; r.Dirty=v.Stage==AdventureInstancePolicy::Phase::Travel; r.Detail="resuming saved instance visit"; @@ -1561,8 +1593,8 @@ bool AdventureBehaviorMgr::ResolveInstance(Player* player, Record& r) sClientlessTravelGraphMgr->EnsureLoaded(); if (!source || !map || !map->IsDungeon() || !outside || outside->MapType!=MAP_COMMON || !sClientlessTravelGraphMgr->FindNode("portal_at_"+std::to_string(entry)+"_source") || - !sClientlessTravelGraphMgr->FindNode("portal_at_"+std::to_string(entry)+"_target") || player->GetGroup()) - {r.Detail="solo instance entrance is not a valid native graph transition";return false;} + !sClientlessTravelGraphMgr->FindNode("portal_at_"+std::to_string(entry)+"_target")) + {r.Detail="instance entrance is not a valid native graph transition";return false;} auto validExit=[&](uint32 id) { auto at=sAreaTriggerStore.LookupEntry(id);auto dest=sObjectMgr->GetAreaTrigger(id); @@ -1661,7 +1693,7 @@ bool AdventureBehaviorMgr::UpdateInstance(Player* player, Record& r, time_t now) bool(CharacterDatabase.PQuery("SELECT guid FROM character_instance WHERE instance=%u AND guid<>%u LIMIT 1",id,player->GetGUID().GetCounter())) || bool(CharacterDatabase.PQuery("SELECT guid FROM group_instance WHERE instance=%u LIMIT 1",id)); } - if (player->GetGroup() || shared || player->GetMapId()!=uint32(entry->ContinentID) || !player->IsInAreaTriggerRadius(entry)) + if ((!player->GetGroup() && shared) || player->GetMapId()!=uint32(entry->ContinentID) || !player->IsInAreaTriggerRadius(entry)) { r.Detail="native instance entrance not reached or private solo binding unavailable"; v.Stage=Stage::Rest;SaveInstance(player,r);return true; @@ -1682,7 +1714,7 @@ bool AdventureBehaviorMgr::UpdateInstance(Player* player, Record& r, time_t now) } if (v.Stage==Stage::Inside) { - if (!v.Abort && !player->GetGroup() && nowGetMapId()==v.Map && player->GetInstanceId()==v.InstanceId) {r.Detail="waiting just inside private instance entrance";return true;} v.Stage=Stage::Returning; @@ -1776,7 +1808,13 @@ std::string AdventureBehaviorMgr::InteractionSchedule(Player* player) auto& r=it->second; auto local=Local(GameTime::GetGameTime()); if(r.Schedule && r.Date!=DateOf(local)){Generate(player,r,DateOf(local));r.Hour=-1;} - if(!r.Schedule)return (r.Deadline && GameTime::GetGameTime()>=r.Deadline?"expired:":"")+r.Intent+":"+r.Location; + if(!r.Schedule) + { + auto deadline=r.GroupOverride?r.PersonalDeadline:r.Deadline; + auto const& intent=r.GroupOverride?r.PersonalIntent:r.WantedIntent; + auto const& location=r.GroupOverride?r.PersonalLocation:r.WantedLocation; + return (deadline && GameTime::GetGameTime()>=deadline?"expired:":"")+intent+":"+location; + } auto const& slot=r.Plan[Local(GameTime::GetGameTime()).tm_hour]; return slot.Intent+":"+slot.Location; } @@ -1787,3 +1825,53 @@ void AdventureBehaviorMgr::InterruptForInteraction(Player* player) auto it=_records.find(player->GetGUID().GetCounter()); if(it!=_records.end())it->second.Detail="social/group interaction; original deadline retained"; } + +AdventureGroupPlan AdventureBehaviorMgr::GroupPlan(Player* player) +{ + AdventureGroupPlan plan; + auto it=_records.find(player->GetGUID().GetCounter());if(it==_records.end())return plan; + auto& r=it->second;auto now=GameTime::GetGameTime();auto local=Local(now); + if(r.Schedule && r.Date!=DateOf(local))Generate(player,r,DateOf(local)); + plan.Enabled=r.Enabled && !(r.Dirty && r.WantedIntent.empty());plan.Intent=r.Intent;plan.Location=r.Location;plan.Deadline=r.Deadline; + if(r.Schedule) + { + plan.Intent=r.Plan[local.tm_hour].Intent; + plan.Location=r.Plan[local.tm_hour].Location; + if(local.tm_hour<23){plan.NextIntent=r.Plan[local.tm_hour+1].Intent;plan.NextLocation=r.Plan[local.tm_hour+1].Location;} + } + if(!r.Schedule){plan.NextIntent=r.ManualNextIntent;plan.NextLocation=r.ManualNextLocation;} + plan.Forming=plan.Enabled && plan.Intent=="groupformation"; + return plan; +} +bool AdventureBehaviorMgr::AdoptGroup(Player* player,Player* leader) +{ + auto a=_records.find(player->GetGUID().GetCounter()),b=_records.find(leader->GetGUID().GetCounter()); + if(a==_records.end() || b==_records.end() || !b->second.Enabled || (b->second.Dirty && b->second.WantedIntent.empty()))return false; + auto& r=a->second;auto const& source=b->second; + if(!r.GroupOverride) + { + r.GroupOverride=true;r.PersonalIntent=r.WantedIntent;r.PersonalLocation=r.WantedLocation;r.PersonalDeadline=r.Deadline; + } + if(r.Instance.Active && r.Instance.Stage!=AdventureInstancePolicy::Phase::Travel)return true; + if(r.WantedIntent!=source.Intent || r.WantedLocation!=source.Location) + SetWanted(player,r,source.Intent,source.Location,source.Deadline); + else r.Deadline=source.Deadline; + return true; +} + +void AdventureBehaviorMgr::ResumePersonalPlan(Player* player) +{ + auto it=_records.find(player->GetGUID().GetCounter());if(it==_records.end())return; + auto& r=it->second;if(!r.GroupOverride)return;r.GroupOverride=false; + auto now=GameTime::GetGameTime(); + if(r.Schedule) + { + auto local=Local(now);if(r.Date!=DateOf(local))Generate(player,r,DateOf(local)); + auto const& slot=r.Plan[local.tm_hour]; + std::tm end=local;end.tm_min=0;end.tm_sec=0;++end.tm_hour;end.tm_isdst=-1; + r.Hour=local.tm_hour;SetWanted(player,r,slot.Intent,slot.Location,mktime(&end)); + } + else if(!r.PersonalDeadline || nowCancelTravel(p->GetGUID().GetCounter()); if(training && std::all_of(p->getAttackers().begin(),p->getAttackers().end(),[](Unit* u) {auto c=u->ToCreature();return c && c->GetScriptName()=="npc_training_dummy";}))p->CombatStop(true); // Only the bounded dummy scene may end its own combat. + r.Fleeing=false;r.ThreatPositions.clear(); r.AI.reset();r.Target.Clear();r.Training=false;r.Defending=false;r.UnreachableMs=0;r.Cooldown=10000; } void AdventureCombatMgr::Stop(Player* p) @@ -148,6 +152,42 @@ void AdventureCombatMgr::Stop(Player* p) auto it=_records.find(p->GetGUID().GetCounter());if(it!=_records.end())Finish(p,it->second); } void AdventureCombatMgr::Offline(Player* p){_records.erase(p->GetGUID().GetCounter());} +Unit* AdventureCombatMgr::PvPTarget(Player* p) const +{ + auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter()); + if(!profile)return nullptr; + auto trait=[&](char const* key) + {auto it=profile->Personality.find(key);return it==profile->Personality.end()?0.0f:it->second;}; + auto chance=AdventureCombatPolicy::PvPWillingness(sAdventureBehaviorMgr->CombatIntent(p), + trait("adventurousness"),trait("danger_tolerance")); + if(chance<=0 || p->GetHealthPct()GetPowerType()==POWER_MANA && p->GetMaxPower(POWER_MANA) && + 100.0f*p->GetPower(POWER_MANA)/p->GetMaxPower(POWER_MANA)IsHostileTo(target) || + !AdventureCombatPolicy::PvPLevel(p->getLevel(),target->getLevel()) || + p->GetExactDist(target)>AdventureCombatPolicy::PvPRadius || + (best && p->GetExactDist(target)>=p->GetExactDist(best)))return; + if(auto player=target->ToPlayer()) + {if(player->GetTeam()==p->GetTeam())return;} + else + { + auto creature=target->ToCreature(); + auto faction=target->GetFactionTemplateEntry(); + auto enemy=p->GetTeam()==ALLIANCE?FACTION_GROUP_MASK_HORDE:FACTION_GROUP_MASK_ALLIANCE; + if(!creature || creature->GetControllingPlayer() || !faction || !(faction->FactionGroup & enemy) || + creature->isElite() || creature->isWorldBoss())return; + } + if(Reachable(p,target))best=target; + }; + std::vector players;p->GetPlayerListInGrid(players,AdventureCombatPolicy::PvPRadius); + for(auto target:players)consider(target); + std::vector creatures;p->GetCreatureListWithEntryInGrid(creatures,0,AdventureCombatPolicy::PvPRadius); + for(auto target:creatures)consider(target); + return best && frand(0,1)CombatIntent(p); @@ -158,6 +198,8 @@ Creature* AdventureCombatMgr::Proactive(Player* p) const Creature* best=nullptr;std::vector all;p->GetCreatureListWithEntryInGrid(all,0,AdventureCombatPolicy::SearchRadius); for(auto c:all) { + auto faction=c->GetFactionTemplateEntry(); + if(faction && (faction->FactionGroup & (FACTION_GROUP_MASK_ALLIANCE|FACTION_GROUP_MASK_HORDE)))continue; if(best && p->GetExactDist(c)>=p->GetExactDist(best))continue; if(intent=="adventure") { @@ -174,11 +216,114 @@ Creature* AdventureCombatMgr::Proactive(Player* p) const } return best; } +bool AdventureCombatMgr::Flee(Player* p,Record& r) +{ + if(p->GetGroup()) + { + if(r.Fleeing) + { + sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());p->StopMoving(); + r.Fleeing=false;r.ThreatPositions.clear(); + TC_LOG_INFO("server.adventurebot","COMBAT_FLEE_CANCEL guid=%u reason='group'",p->GetGUID().GetCounter()); + } + return false; + } + std::vector threats; + auto add=[&](Unit* u) + { + if(u && u!=p && u->IsAlive() && u->IsInWorld() && p->IsInPhase(u) && + p->CanSeeOrDetect(u) && std::find(threats.begin(),threats.end(),u)==threats.end())threats.push_back(u); + }; + for(auto u:p->getAttackers())add(u); + for(auto id:r.Damagers)add(ObjectAccessor::GetUnit(*p,id)); + if(r.Defending || (p->IsInCombat() && !r.Training))add(ObjectAccessor::GetUnit(*p,r.Target)); + bool danger=false; + for(auto u:threats) + { + auto c=u->ToCreature(); + if(AdventureCombatPolicy::ShouldFlee(false,p->GetHealthPct(),p->getLevel(),u->getLevel(), + c && c->isElite(),c && c->isWorldBoss()))danger=true; + } + if(!r.Fleeing && (!danger || r.Training))return false; + if(!r.Fleeing) + { + r.Fleeing=true;r.NextEscape=0; + sAdventureBehaviorMgr->InterruptForCombat(p); + sClientlessLuaEngine->DismountGroundMount(p,"combat-flee"); + p->AttackStop();p->CastStop();p->StopMoving();p->GetMotionMaster()->Clear(); + TC_LOG_INFO("server.adventurebot","COMBAT_FLEE_START guid=%u",p->GetGUID().GetCounter()); + } + // Keep last known threat positions when attackers vanish from visibility. + if(!threats.empty()) + { + r.ThreatPositions.clear(); + for(auto u:threats)r.ThreatPositions.push_back(u->GetPosition()); + } + auto clearance=[&](Position const& point) + { + float distance=1000; + for(auto const& threat:r.ThreatPositions)distance=std::min(distance,point.GetExactDist(threat)); + return distance; + }; + if(!p->IsInCombat() && clearance(p->GetPosition())>=50) + { + r.Fleeing=false;r.ThreatPositions.clear();r.Damagers.clear(); + Finish(p,r);r.Cooldown=30000; + TC_LOG_INFO("server.adventurebot","COMBAT_FLEE_END guid=%u",p->GetGUID().GetCounter()); + return true; + } + auto now=GameTime::GetGameTime(); + if(now creatures;p->GetCreatureListWithEntryInGrid(creatures,0,80); + std::vector players;p->GetPlayerListInGrid(players,80); + std::vector nearby; + for(auto c:creatures)nearby.push_back(c); + for(auto other:players)nearby.push_back(other); + Position best;bool found=false;float score=-1e30f; + float current=clearance(p->GetPosition()); + for(float radius:{18.0f,30.0f})for(unsigned i=0;i<12;++i) + { + float angle=i*6.2831853f/12; + float x=p->GetPositionX()+radius*std::cos(angle),y=p->GetPositionY()+radius*std::sin(angle); + float z=p->GetMap()->GetHeight(p->GetPhaseShift(),x,y,p->GetPositionZ()+5,true,15); + if(z<=INVALID_HEIGHT)continue; + Position end;end.Relocate(x,y,z); + float distance=clearance(end); + if(distanceIsAlive() || !p->IsInPhase(u) || !p->CanSeeOrDetect(u) || !p->IsHostileTo(u) || + std::find(threats.begin(),threats.end(),u)!=threats.end())continue; + if(AdventureCombatPolicy::SegmentDistance(u->GetPositionX(),u->GetPositionY(),u->GetPositionZ(), + p->GetPositionX(),p->GetPositionY(),p->GetPositionZ(),x,y,z)<12){unsafe=true;break;} + } + if(unsafe)continue; + ClientlessPathDiagnostics path; + if(!sClientlessTravelMgr->AnalyzePath(p,end,path) || !path.Complete || path.Suspicious || path.PathLength>60)continue; + float value=distance-0.15f*path.PathLength; + if(value>score){score=value;best=end;found=true;} + } + sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter()); + p->AttackStop();p->CastStop(); + if(found && sClientlessTravelMgr->StartTravel(p,best)) + TC_LOG_INFO("server.adventurebot","COMBAT_FLEE_PATH guid=%u clearance=%.2f",p->GetGUID().GetCounter(),clearance(best)); + else + { + p->StopMoving(); + auto target=ObjectAccessor::GetUnit(*p,r.Target); + if(r.AI && Valid(p,target,false) && p->IsWithinMeleeRange(target))r.AI->Tick(target,500); + TC_LOG_INFO("server.adventurebot","COMBAT_FLEE_BLOCKED guid=%u",p->GetGUID().GetCounter()); + } + return true; +} bool AdventureCombatMgr::Death(Player* p,Record& r) { if(!r.Dead) { if(r.AI)Finish(p,r); + r.Fleeing=false;r.ThreatPositions.clear(); sAdventureBehaviorMgr->InterruptForCombat(p);r.Damagers.clear();r.Dead=true;r.Suspended=true; TC_LOG_INFO("server.adventurebot","DEATH_RELEASE guid=%u",p->GetGUID().GetCounter()); } @@ -250,8 +395,8 @@ bool AdventureCombatMgr::Update(Player* p,uint32 diff) if(!p->IsInWorld())return false; auto& r=_records[p->GetGUID().GetCounter()]; r.Cooldown=r.Cooldown>diff?r.Cooldown-diff:0; - if(p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsCharmed())return r.AI || !p->IsAlive(); - if(r.Timer>diff){r.Timer-=diff;return r.AI || !p->IsAlive() || r.Dead;} + if(p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsCharmed())return r.Fleeing || r.AI || !p->IsAlive(); + if(r.Timer>diff){r.Timer-=diff;return r.Fleeing || r.AI || !p->IsAlive() || r.Dead;} r.Timer=500; if(!p->IsAlive())return Death(p,r); if(r.Dead){r.Dead=false;r.Suspended=false;r.NextCorpseSearch=0;r.FailedCorpsePoints.clear();r.Cooldown=30000;r.Timer=0;return false;} @@ -264,8 +409,21 @@ bool AdventureCombatMgr::Update(Player* p,uint32 diff) if(r.AI)Finish(p,r); Start(p,r,c,false,true,0);break; } - if(!r.AI && !p->GetGroup() && !p->IsInCombat() && !r.Cooldown) - if(auto c=Proactive(p))Start(p,r,c,false,false,0); + if(Flee(p,r))return true; + if(!r.AI && (!p->GetGroup() || p->GetGroup()->GetLeaderGUID()==p->GetGUID()) && !p->IsInCombat() && !r.Cooldown) + { + auto now=GameTime::GetGameTime(); + if(now>=r.NextPvP) + { + r.NextPvP=now+30; + if(auto target=PvPTarget(p)) + { + TC_LOG_INFO("server.adventurebot","COMBAT_PVP_INITIATE guid=%u target='%s'",p->GetGUID().GetCounter(),target->GetGUID().ToString().c_str()); + Start(p,r,target,false,false,0); + } + } + if(!r.AI && !p->GetGroup())if(auto c=Proactive(p))Start(p,r,c,false,false,0); + } if(!r.AI)return false; auto c=ObjectAccessor::GetUnit(*p,r.Target); if(!Valid(p,c,r.Training) || (r.Until && GameTime::GetGameTime()>=r.Until) || p->GetExactDist(c)>80) @@ -295,7 +453,7 @@ bool AdventureCombatMgr::Update(Player* p,uint32 diff) bool AdventureCombatMgr::Busy(Player* player) const { auto it=_records.find(player->GetGUID().GetCounter()); - return it!=_records.end() && (it->second.AI || it->second.Dead || !it->second.Damagers.empty()); + return it!=_records.end() && (it->second.Fleeing || it->second.AI || it->second.Dead || !it->second.Damagers.empty()); } bool AdventureCombatMgr::Assist(Player* p,Unit* target) diff --git a/src/server/game/World/AdventureCombatMgr.h b/src/server/game/World/AdventureCombatMgr.h index ffa8050682..ed38ace5db 100644 --- a/src/server/game/World/AdventureCombatMgr.h +++ b/src/server/game/World/AdventureCombatMgr.h @@ -32,9 +32,9 @@ private: std::shared_ptr AI; 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; + bool Fleeing=false,Training=false,Defending=false,Suspended=false,Dead=false,CemeteryPending=false; + time_t Until=0,NextCorpseSearch=0,NextPvP=0,NextEscape=0; + std::vector FailedCorpsePoints,ThreatPositions; uint32 Timer=0,UnreachableMs=0,Cooldown=0; }; bool Start(Player* player,Record& record,Unit* target,bool training,bool defending,time_t until); @@ -42,7 +42,9 @@ private: bool Safe(Player* player,Creature* target) const; bool Reachable(Player* player,Unit* target) const; Creature* Dummy(Player* player) const; + Unit* PvPTarget(Player* player) const; Creature* Proactive(Player* player) const; + bool Flee(Player* player,Record& record); bool Death(Player* player,Record& record); void Finish(Player* player,Record& record); std::unordered_map _records; diff --git a/src/server/game/World/AdventureCombatPolicy.h b/src/server/game/World/AdventureCombatPolicy.h index b9dbef73df..5c53a4a370 100644 --- a/src/server/game/World/AdventureCombatPolicy.h +++ b/src/server/game/World/AdventureCombatPolicy.h @@ -2,6 +2,8 @@ #define VCORE_ADVENTURE_COMBAT_POLICY_H #include #include +#include +#include namespace AdventureCombatPolicy { // Conservative initial tuning. Self-defence deliberately bypasses this filter. @@ -13,6 +15,21 @@ namespace AdventureCombatPolicy return enemy<=level+MaxLevelAdvantage && health>=MinimumHealth && !elite && !boss && nearbyHostiles<=1 && (!manaUser || mana>=MinimumMana); } + inline bool ShouldFlee(bool grouped,float health,unsigned level,unsigned enemy,bool elite,bool boss) + { + return !grouped && (health<20 || (enemy>level && enemy-level>=6) || + ((elite || boss) && enemy>=level)); + } + constexpr float PvPRadius=50; + inline bool PvPLevel(unsigned level,unsigned enemy) + {return level>enemy?level-enemy<=5:enemy-level<=5;} + inline float PvPWillingness(std::string const& intent,float adventure,float risk) + { + float weight=intent=="adventure"?1.0f:intent=="leisure"?0.35f: + intent=="social"?0.2f:intent=="profession"?0.15f:intent=="economy"?0.1f:0.0f; + return weight*std::max(0.0f,std::min(1.0f,adventure))* + std::max(0.0f,std::min(1.0f,risk)); + } inline bool CemeteryFallback(std::int64_t now,std::int64_t released,bool noPath) {return noPath && released>0 && now>=released && now-released>=300;} inline float SegmentDistance(float x,float y,float z,float ax,float ay,float az,float bx,float by,float bz) diff --git a/src/server/game/World/AdventureGroupMgr.cpp b/src/server/game/World/AdventureGroupMgr.cpp new file mode 100644 index 0000000000..1efe72a3ae --- /dev/null +++ b/src/server/game/World/AdventureGroupMgr.cpp @@ -0,0 +1,273 @@ +#include "AdventureGroupMgr.h" +#include "AdventureGroupPolicy.h" +#include "AdventureBehaviorMgr.h" +#include "AdventureCombatMgr.h" +#include "AdventureLocationMgr.h" +#include "ClientlessLuaEngine.h" +#include "ClientlessPlayerMgr.h" +#include "ClientlessTravelMgr.h" +#include "ClientlessTravelGraphMgr.h" +#include "Channel.h" +#include "ChannelMgr.h" +#include "Chat.h" +#include "GameTime.h" +#include "Group.h" +#include "ObjectAccessor.h" +#include "Opcodes.h" +#include "PartyPackets.h" +#include "Player.h" +#include "Map.h" +#include "Random.h" +#include "WorldSession.h" +#include "Log.h" +#include + +namespace +{ +float Sociability(Player* p) +{ + auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter()); + if(profile){auto it=profile->Personality.find("sociability");if(it!=profile->Personality.end())return std::clamp(it->second,0.0f,1.0f);} + return .5f; +} +bool Ready(Player* p) +{return p && p->IsInWorld() && p->IsAlive() && !p->IsInCombat() && !sAdventureCombatMgr->Busy(p) && + !p->IsBeingTeleported() && !p->GetTransport() && !p->IsInFlight() && !p->IsFlying() && !p->IsCharmed() && !p->GetMap()->IsDungeon();} +std::string Activity(AdventureGroupPlan const& plan){return plan.Forming?plan.NextIntent:plan.Intent;} +} +AdventureGroupMgr* AdventureGroupMgr::instance(){static AdventureGroupMgr mgr;return &mgr;} +bool AdventureGroupMgr::BotLed(Player* p) const +{ + auto group=p->GetGroup();if(!group)return false; + auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID()); + return leader && leader->GetSession()->IsClientless() && sClientlessLuaEngine->GetAdventureProfile(leader->GetGUID().GetCounter()); +} +bool AdventureGroupMgr::Forming(Player* p) const +{ + if(auto group=p->GetGroup())if(auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID())) + return sAdventureBehaviorMgr->GroupPlan(leader).Forming; + return sAdventureBehaviorMgr->GroupPlan(p).Forming; +} +bool AdventureGroupMgr::PlannedTransition(Player* p) +{ + if(!BotLed(p))return false; + auto it=_records.find(p->GetGUID()); + if(it==_records.end() || it->second.FormationIntent.empty())return false; + auto plan=sAdventureBehaviorMgr->GroupPlan(p); + if(plan.Intent!=it->second.FormationIntent) + { + if(plan.Forming)if(auto group=p->GetGroup())if(auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID())) + return sAdventureBehaviorMgr->GroupPlan(leader).Intent==it->second.FormationIntent; + return false; + } + it->second.FormationIntent.clear();return true; +} +void AdventureGroupMgr::Speak(Player* p,std::string const& message) +{ + if(!AdventureGroupPolicy::Capital(p->GetZoneId()))return; + auto manager=ChannelMgr::forTeam(p->GetTeam());if(!manager)return; + auto channel=manager->GetJoinChannel(0,"sucheNachGruppe");if(!channel)return; + if(std::find(p->GetJoinedChannels().begin(),p->GetJoinedChannels().end(),channel)==p->GetJoinedChannels().end())channel->JoinChannel(p,""); + channel->Say(p->GetGUID(),message,p->GetTeam()==ALLIANCE?LANG_COMMON:LANG_ORCISH); +} +void AdventureGroupMgr::Offline(Player* p) +{ + // Native player logout owns channel membership cleanup. + _records.erase(p->GetGUID()); +} +bool AdventureGroupMgr::Accept(Player* invited,Player* inviter) +{ + if(!Ready(invited) || !Ready(inviter) || invited->GetGroup() || !inviter->GetSession()->IsClientless() || + invited->GetTeam()!=inviter->GetTeam())return false; + auto it=_records.find(invited->GetGUID()); + if(it==_records.end() || it->second.Inviter!=inviter->GetGUID() || it->second.InviteUntilGroupPlan(inviter),b=sAdventureBehaviorMgr->GroupPlan(invited); + auto intent=Activity(a); + if(!a.Enabled || !b.Enabled || a.Forming!=b.Forming || + !AdventureGroupPolicy::Compatible(intent,Activity(b),inviter->getLevel(),invited->getLevel()))return false; + auto group=inviter->GetGroup(); + return (!group || (group->GetLeaderGUID()==inviter->GetGUID() && group->GetMembersCount()GetGUID()]; + if(PlayerLeadership(p))return; + if(!Ready(p))return; + if(EnforceCapacity(p))return; + auto plan=sAdventureBehaviorMgr->GroupPlan(p);if(!plan.Enabled)return; + auto intent=Activity(plan);if(!AdventureGroupPolicy::Eligible(intent))return; + auto group=p->GetGroup(); + if(group && BotLed(p))if(auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID())) + {auto leaderPlan=sAdventureBehaviorMgr->GroupPlan(leader);if(leaderPlan.Forming)r.FormationIntent=Activity(leaderPlan);} + if(group && group->GetLeaderGUID()!=p->GetGUID())return; + if(intent=="raid" && p->GetRaidDifficulty()!=RAID_DIFFICULTY_25MAN_NORMAL) + { + WorldPacket mode(MSG_SET_RAID_DIFFICULTY,4);mode<GetSession()->HandleSetRaidDifficultyOpcode(mode); + } + auto capacity=AdventureGroupPolicy::Capacity(intent); + if(group && capacity>5 && !group->isRaidGroup())group->ConvertToRaid(); + if(group && capacity<=5 && group->isRaidGroup() && group->GetMembersCount()<=5)group->ConvertToGroup(); + unsigned pending=0; + for(auto const& peer:_records)if(peer.second.Inviter==p->GetGUID() && peer.second.InviteUntil>now) + { + auto invited=ObjectAccessor::FindPlayer(peer.first); + if(invited && invited->GetGroupInvite())++pending; + } + if((group?group->GetMembersCount():1)+pending>=capacity || p->GetGroupInvite() || nowGetZoneId()))return; + if(now>=r.SpeakAt) + { + r.SpeakAt=now+urand(120,240); + auto place=sAdventureLocationMgr->Find(plan.Location); + std::string activity=intent=="instance"?"einen Instanzbesuch":intent=="raid"?"einen Schlachtzug":intent=="profession"?"gemeinsame Berufsarbeit":intent=="social"?"eine gesellige Runde":"ein Abenteuer"; + Speak(p,"Ich suche Gefaehrten fuer "+activity+" in der naechsten Stunde. Treffpunkt: "+(place?place->Name:std::string("hier in der Stadt"))+"."); + } + } + if(frand(0,1)>Sociability(p))return; + // Snapshot GUIDs: callbacks/native invitations may update manager records. + std::vector candidates;for(auto const& entry:_records)candidates.push_back(entry.first); + std::sort(candidates.begin(),candidates.end()); + for(auto guid:candidates) + { + if(guid==p->GetGUID())continue; + auto other=ObjectAccessor::FindPlayer(guid); + if(!Ready(other) || other->GetGroupInvite() || other->GetTeam()!=p->GetTeam())continue; + auto peerPlan=sAdventureBehaviorMgr->GroupPlan(other); + if(!peerPlan.Enabled || peerPlan.Forming!=plan.Forming || + !AdventureGroupPolicy::Compatible(intent,Activity(peerPlan),p->getLevel(),other->getLevel()))continue; + if(intent=="profession") + { + auto a=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter()); + auto b=sClientlessLuaEngine->GetAdventureProfile(other->GetGUID().GetCounter()); + bool compatible=false; + if(a && b)for(auto const& profession:a->PrimaryProfessions)if(b->PrimaryProfessions.count(profession))compatible=true; + if(!compatible)continue; + } + if(auto existing=other->GetGroup()) + { + if(group || existing->GetLeaderGUID()!=other->GetGUID() || + existing->GetMembersCount()>=AdventureGroupPolicy::Capacity(intent))continue; + if(plan.Forming) + {if(!AdventureGroupPolicy::Capital(other->GetZoneId()))continue;} + else if(!p->IsInMap(other) || !p->IsInPhase(other) || !p->CanSeeOrDetect(other) || p->GetExactDist(other)>25 || !p->IsWithinLOSInMap(other))continue; + if(r.Partners[guid]>now || _records[guid].Partners[p->GetGUID()]>now || r.InviteUntil>now)continue; + // Structured join request: the existing native leader sends the + // invitation, so requesting membership never transfers leadership. + Invite(other,p,intent,plan.Forming);return; + } + auto& peer=_records[guid]; + if(peer.InviteUntil>now || r.Partners[guid]>now || peer.Partners[p->GetGUID()]>now)continue; + if(plan.Forming) + {if(!AdventureGroupPolicy::Capital(other->GetZoneId()))continue;} + else if(!p->IsInMap(other) || !p->IsInPhase(other) || !p->CanSeeOrDetect(other) || + p->GetExactDist(other)>25 || !p->IsWithinLOSInMap(other))continue; + Invite(p,other,intent,plan.Forming); + break; + } +} +void AdventureGroupMgr::Report(ChatHandler* h,Player* p) const +{ + auto plan=sAdventureBehaviorMgr->GroupPlan(p); + h->PSendSysMessage("Bot group intent='%s' next='%s' forming=%u botLed=%u capacity=%u meeting='%s'",plan.Intent.c_str(),plan.NextIntent.c_str(),plan.Forming,BotLed(p),AdventureGroupPolicy::Capacity(Activity(plan)),plan.Location.c_str()); +} + +bool AdventureGroupMgr::FollowMember(Player* p) +{ + auto group=p->GetGroup(); + if(!group || !BotLed(p) || group->GetLeaderGUID()==p->GetGUID())return false; + auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID());if(!leader)return true; + auto& r=_records[p->GetGUID()];auto now=GameTime::GetGameTime(); + if(!p->IsInMap(leader) || p->GetExactDist(leader)>=100) + { + if(now>=r.SearchAt) + { + r.SearchAt=now+15;std::string node;float distance=0; + if(sClientlessTravelGraphMgr->FindNearestNodeToPosition(leader->GetMapId(),leader->GetPositionX(),leader->GetPositionY(),leader->GetPositionZ(),node,distance) && distance<1000) + {ClientlessAction action;action.Name="travel_to_node";action.Parameters["node"]=node;sClientlessPlayerMgr->QueueAction(p->GetGUID().GetCounter(),std::move(action));} + } + return true; + } + if(!p->IsInPhase(leader) || !p->CanSeeOrDetect(leader))return true; + if(p->GetExactDist(leader)>5) + { + auto nav=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter()); + if(!nav || nav->State!=ClientlessTravelState::Moving || nav->Target.GetExactDist(leader)>5) + { + Position end;leader->GetNearPoint(p,end.m_positionX,end.m_positionY,end.m_positionZ,3,leader->GetOrientation()+3.14159265f); + ClientlessPathDiagnostics path; + if(sClientlessTravelMgr->AnalyzePath(p,end,path) && path.Complete && !path.Suspicious && path.PathLength<150)sClientlessTravelMgr->StartTravel(p,end); + } + } + else sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter()); + return true; +} + +void AdventureGroupMgr::Invite(Player* p,Player* other,std::string const& intent,bool forming) +{ + auto now=GameTime::GetGameTime();auto& r=_records[p->GetGUID()];auto& peer=_records[other->GetGUID()]; + unsigned pending=0; + for(auto const& entry:_records)if(entry.second.Inviter==p->GetGUID() && entry.second.InviteUntil>now) + if(auto target=ObjectAccessor::FindPlayer(entry.first))if(target->GetGroupInvite())++pending; + auto group=p->GetGroup(); + if((group?group->GetMembersCount():1)+pending>=AdventureGroupPolicy::Capacity(intent))return; + peer.Inviter=p->GetGUID();peer.InviteUntil=now+30; + if(forming){peer.FormationIntent=intent;r.FormationIntent=intent;} + peer.Partners[p->GetGUID()]=now+600;r.Partners[other->GetGUID()]=now+600; + WorldPackets::Party::PartyInviteClient packet{WorldPacket(CMSG_PARTY_INVITE)}; + packet.TargetName=other->GetName();packet.TargetGUID=other->GetGUID(); + p->GetSession()->HandlePartyInviteOpcode(packet); + if(forming && other->GetGroupInvite())Speak(other,"Ich wuerde mich euch anschliessen."); + else if(other->GetGroupInvite()){p->SetFacingToObject(other);p->HandleEmoteCommand(EMOTE_ONESHOT_WAVE);} + TC_LOG_INFO("server.adventurebot","GROUP_BOT_INVITE leader=%u target=%u intent='%s' forming=%u",p->GetGUID().GetCounter(),other->GetGUID().GetCounter(),intent.c_str(),forming); +} + +void AdventureGroupMgr::RequestPlayerAdmission(Player* member,Player* requester) +{ + if(!requester || requester->GetSession()->IsClientless() || requester->GetGroup() || requester->GetGroupInvite() || !BotLed(member))return; + auto group=member->GetGroup();auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID()); + if(!Ready(leader) || !Ready(requester) || leader->GetTeam()!=requester->GetTeam())return; + auto now=GameTime::GetGameTime();auto& r=_records[leader->GetGUID()]; + if(r.Partners[requester->GetGUID()]>now)return; + auto plan=sAdventureBehaviorMgr->GroupPlan(leader); + if(group->GetMembersCount()>=AdventureGroupPolicy::Capacity(Activity(plan)) || frand(0,1)>Sociability(leader))return; + Invite(leader,requester,Activity(plan),false); +} +bool AdventureGroupMgr::PlayerLeadership(Player* leader) +{ + auto group=leader->GetGroup();if(!group || group->GetLeaderGUID()!=leader->GetGUID())return false; + for(auto member=group->GetFirstMember();member;member=member->next()) + { + auto player=member->GetSource(); + if(!player || player->GetSession()->IsClientless())continue; + group->ChangeLeader(player->GetGUID());group->SendUpdate(); + TC_LOG_INFO("server.adventurebot","GROUP_PLAYER_LEADER old=%u leader=%u",leader->GetGUID().GetCounter(),player->GetGUID().GetCounter()); + return true; + } + return false; +} + +bool AdventureGroupMgr::EnforceCapacity(Player* p) +{ + auto group=p->GetGroup();if(!group || !BotLed(p) || group->GetLeaderGUID()==p->GetGUID())return false; + auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID());if(!leader)return false; + auto plan=sAdventureBehaviorMgr->GroupPlan(leader);auto intent=Activity(plan); + if(!AdventureGroupPolicy::Eligible(intent))return false; + auto capacity=AdventureGroupPolicy::Capacity(intent);if(group->GetMembersCount()<=capacity)return false; + unsigned rank=1; + for(auto const& member:group->GetMemberSlots()) + { + if(member.guid==group->GetLeaderGUID())continue; + ++rank; + if(member.guid!=p->GetGUID())continue; + if(rank<=capacity)return false; + p->HandleEmoteCommand(EMOTE_ONESHOT_WAVE); + group->RemoveMember(p->GetGUID(),GROUP_REMOVEMETHOD_LEAVE); + TC_LOG_INFO("server.adventurebot","GROUP_CAPACITY_LEAVE guid=%u capacity=%u",p->GetGUID().GetCounter(),capacity); + return true; + } + return false; +} diff --git a/src/server/game/World/AdventureGroupMgr.h b/src/server/game/World/AdventureGroupMgr.h new file mode 100644 index 0000000000..aebd10dff8 --- /dev/null +++ b/src/server/game/World/AdventureGroupMgr.h @@ -0,0 +1,45 @@ +#ifndef VCORE_ADVENTURE_GROUP_MGR_H +#define VCORE_ADVENTURE_GROUP_MGR_H +#include "Common.h" +#include "ObjectGuid.h" +#include +#include +#include +class Player; +class ChatHandler; +struct AdventureGroupPlan +{ + bool Enabled=false,Forming=false; + std::string Intent,Location,NextIntent,NextLocation; + time_t Deadline=0; +}; +class TC_GAME_API AdventureGroupMgr +{ +public: + static AdventureGroupMgr* instance(); + void Update(Player* player); + void RequestPlayerAdmission(Player* member,Player* requester); + bool EnforceCapacity(Player* player); + bool PlayerLeadership(Player* leader); + void Offline(Player* player); + bool Accept(Player* invited,Player* inviter); + bool FollowMember(Player* player); + bool BotLed(Player* player) const; + bool Forming(Player* player) const; + bool PlannedTransition(Player* player); + void Report(ChatHandler* handler,Player* player) const; +private: + struct Record + { + time_t SearchAt=0,SpeakAt=0; + ObjectGuid Inviter; + std::string FormationIntent; + time_t InviteUntil=0; + std::unordered_map Partners; + }; + void Invite(Player* leader,Player* member,std::string const& intent,bool forming); + void Speak(Player* player,std::string const& message); + std::unordered_map _records; +}; +#define sAdventureGroupMgr AdventureGroupMgr::instance() +#endif diff --git a/src/server/game/World/AdventureGroupPolicy.h b/src/server/game/World/AdventureGroupPolicy.h new file mode 100644 index 0000000000..3eb893fe38 --- /dev/null +++ b/src/server/game/World/AdventureGroupPolicy.h @@ -0,0 +1,30 @@ +#ifndef VCORE_ADVENTURE_GROUP_POLICY_H +#define VCORE_ADVENTURE_GROUP_POLICY_H +#include +#include +namespace AdventureGroupPolicy +{ +inline bool Capital(unsigned zone) +{switch(zone){case 1519:case 1537:case 1657:case 1637:case 1638:case 1497:case 3487:case 3557:case 3703:case 4395:return true;default:return false;}} +inline bool Eligible(std::string const& intent) +{return intent=="instance" || intent=="raid" || intent=="adventure" || intent=="profession" || intent=="social";} +inline unsigned Capacity(std::string const& intent) +{return intent=="raid"?20:intent=="instance"?5:intent=="social"?10:3;} +inline bool Compatible(std::string const& a,std::string const& b,unsigned level,unsigned other) +{return Eligible(a) && a==b && (level>other?level-other<=5:other-level<=5);} +template +void Prepare(Day& plan,std::mt19937& random,float sociability,Meeting meeting) +{ + // Work backwards: never replace the very activity a previous preparation + // hour was already created for. + for(unsigned h=23;h>0;--h) + { + auto const& next=plan[h]; + if(!Eligible(next.Intent) || next.Intent==plan[h-1].Intent)continue; + if(next.Intent!="instance" && next.Intent!="raid" && std::uniform_real_distribution(0,1)(random)>sociability)continue; + auto location=meeting();if(!location.empty())plan[h-1]={"groupformation",location}; + } +} + +} +#endif diff --git a/src/server/game/World/AdventureInteractionMgr.cpp b/src/server/game/World/AdventureInteractionMgr.cpp index 9f80200dcc..557a214084 100644 --- a/src/server/game/World/AdventureInteractionMgr.cpp +++ b/src/server/game/World/AdventureInteractionMgr.cpp @@ -1,5 +1,6 @@ #include "AdventureInteractionMgr.h" #include "AdventureInteractionPolicy.h" +#include "AdventureGroupMgr.h" #include "AdventureBehaviorMgr.h" #include "AdventureCombatMgr.h" #include "ClientlessLuaEngine.h" @@ -73,6 +74,7 @@ void AdventureInteractionMgr::Leave(Player* p,Record& r,char const* reason) TC_LOG_INFO("server.adventurebot","INTERACTION_GROUP_LEAVE guid=%u reason='%s'",p->GetGUID().GetCounter(),reason); p->GetGroup()->RemoveMember(p->GetGUID(),GROUP_REMOVEMETHOD_LEAVE); sAdventureBehaviorMgr->InterruptForInteraction(p); + sAdventureBehaviorMgr->ResumePersonalPlan(p); r.Grouped=false;r.Leader.Clear();r.Invite.Clear();r.Partner.Clear();r.SceneUntil=0; } void AdventureInteractionMgr::Emote(Player* p,Player* source,uint32 emote) @@ -98,15 +100,16 @@ void AdventureInteractionMgr::Reply(Player* p,Player* other,uint32 emote) } void AdventureInteractionMgr::CombatHelp(Player* p,ObjectGuid helper) { _records[p->GetGUID().GetCounter()].Helper=helper; } -void AdventureInteractionMgr::Offline(Player* p){_records.erase(p->GetGUID().GetCounter());} +void AdventureInteractionMgr::Offline(Player* p){sAdventureGroupMgr->Offline(p);_records.erase(p->GetGUID().GetCounter());} bool AdventureInteractionMgr::Update(Player* p,uint32 diff) { auto& r=_records[p->GetGUID().GetCounter()];auto now=GameTime::GetGameTime(); - if(r.Timer>diff){r.Timer-=diff;return r.Grouped || r.SceneUntil>now;} + if(r.Timer>diff){r.Timer-=diff;return (r.Grouped && !sAdventureGroupMgr->BotLed(p)) || r.SceneUntil>now;} r.Timer=1000; bool unsafe=!p->IsAlive() || p->IsInCombat() || sAdventureCombatMgr->Busy(p) || p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsCharmed(); if(unsafe){r.Partner.Clear();r.SceneUntil=0;return p->GetGroup()!=nullptr;} + sAdventureGroupMgr->Update(p); for(auto it=r.Encounters.begin();it!=r.Encounters.end();)if(now>=it->second)it=r.Encounters.erase(it);else ++it; if(r.Helper) { @@ -114,7 +117,7 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff) if(Visible(p,helper,25) && p->IsFriendlyTo(helper)) p->Say(German(p)?"Danke für die Hilfe!":"Thank you for the help!",p->GetTeam()==ALLIANCE?LANG_COMMON:LANG_ORCISH,helper); } - if(auto invite=p->GetGroupInvite()) + if(auto invite=p->GetGroupInvite();invite && invite->GetLeaderGUID()!=p->GetGUID()) { auto leader=invite->GetLeaderGUID(); if(r.Invite!=leader){r.Invite=leader;r.InviteAt=now+urand(2,4);} @@ -122,7 +125,8 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff) { auto inviter=ObjectAccessor::FindPlayer(leader); bool accept=inviter && inviter->IsAlive() && p->IsFriendlyTo(inviter) && !p->GetMap()->IsDungeon() && - !inviter->GetSession()->IsClientless() && frand(0,1)InteractionIntent(p),Sociability(p)); + (inviter->GetSession()->IsClientless()?sAdventureGroupMgr->Accept(p,inviter): + 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); @@ -146,11 +150,13 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff) { r.Grouped=true;r.Schedule=sAdventureBehaviorMgr->InteractionSchedule(p); r.Partner.Clear();r.SceneUntil=0; - sAdventureBehaviorMgr->InterruptForInteraction(p); + if(!sAdventureGroupMgr->BotLed(p) || group->GetLeaderGUID()!=p->GetGUID())sAdventureBehaviorMgr->InterruptForInteraction(p); PartySay(p,"Ich begleite euch eine Weile.","I'll accompany you for a while."); } if(r.Leader!=group->GetLeaderGUID()) { + if(leader==p && sAdventureGroupMgr->BotLed(p) && !p->GetMap()->IsDungeon())sAdventureBehaviorMgr->ResumePersonalPlan(p); + if(r.Leader && !sAdventureGroupMgr->BotLed(p))sAdventureBehaviorMgr->InterruptForInteraction(p); r.Leader=group->GetLeaderGUID();r.LastActivity=now;r.AfkSince=0; if(leader){r.LeaderPosition.Relocate(leader);r.LeaderMap=leader->GetMapId();r.Selection=leader->GetTarget();} } @@ -163,12 +169,14 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff) r.LeaderPosition.Relocate(leader);r.LeaderMap=leader->GetMapId();r.Selection=leader->GetTarget(); if(leader->isAFK()){if(!r.AfkSince)r.AfkSince=now;}else r.AfkSince=0; } - if(AdventureInteractionPolicy::CanLeave(p->IsAlive(),p->IsInCombat(),p->GetMap()->IsDungeon(),p->GetTransport() || p->IsInFlight())) + bool forming=sAdventureGroupMgr->Forming(p); + if(forming){r.LastActivity=now;r.AfkSince=0;r.Schedule=sAdventureBehaviorMgr->InteractionSchedule(p);} + if(!forming && AdventureInteractionPolicy::CanLeave(p->IsAlive(),p->IsInCombat(),p->GetMap()->IsDungeon(),p->GetTransport() || p->IsInFlight() || p->IsFlying())) { if(r.Schedule!=sAdventureBehaviorMgr->InteractionSchedule(p)) { r.Schedule=sAdventureBehaviorMgr->InteractionSchedule(p); - if(frand(0,1)>=Loyalty(p)){Leave(p,r,"intent-change");return false;} + if(!sAdventureGroupMgr->PlannedTransition(p) && frand(0,1)>=Loyalty(p)){Leave(p,r,"intent-change");return false;} } if(AdventureInteractionPolicy::LeaderExpired(now,r.AfkSince?r.AfkSince:r.LastActivity)){Leave(p,r,"leader-afk");return false;} } @@ -181,6 +189,7 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff) if(!target)for(auto attacker:ally->getAttackers())if(attacker->GetVictim()==ally){target=attacker;break;} if(target && sAdventureCombatMgr->Assist(p,target))return true; } + if(sAdventureGroupMgr->BotLed(p))return false; if(!leader || leader==p || !leader->IsAlive())return true; auto execution=sClientlessActionExecutorMgr->GetStatus(p->GetGUID().GetCounter()); if(execution && (execution->State==ClientlessActionExecutionState::Moving || execution->State==ClientlessActionExecutionState::SelectingTarget))return true; @@ -214,7 +223,7 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff) else if(p->GetExactDist(leader)<=5)sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter()); return true; } - if(r.Grouped){r.Grouped=false;r.Leader.Clear();sAdventureBehaviorMgr->InterruptForInteraction(p);} + if(r.Grouped){r.Grouped=false;r.Leader.Clear();sAdventureBehaviorMgr->InterruptForInteraction(p);sAdventureBehaviorMgr->ResumePersonalPlan(p);} auto intent=sAdventureBehaviorMgr->InteractionIntent(p); if(now>=r.NextEncounter && now>=r.NextSocial && !p->GetMap()->IsDungeon() && (intent=="social" || intent=="leisure" || intent=="rest")) { @@ -242,6 +251,7 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff) void AdventureInteractionMgr::Report(ChatHandler* handler,Player* p) const { + sAdventureGroupMgr->Report(handler,p); auto it=_records.find(p->GetGUID().GetCounter()); if(it==_records.end()){handler->SendSysMessage("No interaction state yet.");return;} auto const& r=it->second;auto now=GameTime::GetGameTime(); @@ -252,3 +262,12 @@ void AdventureInteractionMgr::Report(ChatHandler* handler,Player* p) const static_cast(r.LastActivity && now>=r.LastActivity?now-r.LastActivity:0), static_cast(r.AfkSince && now>=r.AfkSince?now-r.AfkSince:0),Sociability(p),Loyalty(p)); } + +bool AdventureInteractionMgr::RequestLeave(Player* p) +{ + if(!AdventureInteractionPolicy::CanLeave(p->IsAlive(),p->IsInCombat(),p->GetMap()->IsDungeon(), + p->GetTransport() || p->IsInFlight() || p->IsFlying()))return false; + if(p->GetGroupInvite())p->UninviteFromGroup(); + if(p->GetGroup())Leave(p,_records[p->GetGUID().GetCounter()],"manual-test"); + return true; +} diff --git a/src/server/game/World/AdventureInteractionMgr.h b/src/server/game/World/AdventureInteractionMgr.h index 11a9fd215f..f78f6814b9 100644 --- a/src/server/game/World/AdventureInteractionMgr.h +++ b/src/server/game/World/AdventureInteractionMgr.h @@ -12,6 +12,7 @@ class TC_GAME_API AdventureInteractionMgr { public: static AdventureInteractionMgr* instance(); + bool RequestLeave(Player* player); bool Update(Player* player,uint32 diff); void Emote(Player* player,Player* source,uint32 emote); void Offline(Player* player); diff --git a/src/server/game/World/AdventureLocationMgr.cpp b/src/server/game/World/AdventureLocationMgr.cpp index 96d4f633a1..40b9b5a048 100644 --- a/src/server/game/World/AdventureLocationMgr.cpp +++ b/src/server/game/World/AdventureLocationMgr.cpp @@ -1,5 +1,6 @@ #include "AdventureLocationMgr.h" #include "AdventureBehaviorPolicy.h" +#include "AdventureGroupPolicy.h" #include "Chat.h" #include "DatabaseEnv.h" #include "DBCStores.h" @@ -53,6 +54,12 @@ bool AdventureLocationMgr::Reload() } while (rows->NextRow()); } + if (WorldDatabase.Query("SHOW TABLES LIKE 'vcore_adventure_instance'")) + if(auto rows=WorldDatabase.Query("SELECT i.location_key,a.target_map FROM vcore_adventure_instance i JOIN areatrigger_teleport a ON a.ID=i.entry_trigger WHERE i.enabled=1")) do + { + auto f=rows->Fetch(); + for(auto& l:_locations)if(l.Point.Id==f[0].GetString())l.InstanceMap=f[1].GetUInt16(); + }while(rows->NextRow()); if (auto rows = WorldDatabase.Query("SELECT category,intent,weight FROM vcore_adventure_location_intent WHERE weight>=0")) do { auto f = rows->Fetch(); @@ -110,6 +117,18 @@ bool AdventureLocationMgr::IsWorkstation(uint32 entry,std::string const& role) c float AdventureLocationMgr::Weight(AdventureLocation const & location, std::string const & intent) const { + if(intent=="instance" || intent=="raid") + { + auto map=sMapStore.LookupEntry(location.InstanceMap); + if(!map || !map->IsDungeon() || map->IsRaid()!=(intent=="raid"))return 0; + if(intent=="raid") + { + Difficulty difficulty=RAID_DIFFICULTY_25MAN_NORMAL; + auto data=sDBCManager.GetDownscaledMapDifficultyData(location.InstanceMap,difficulty); + if(!data || data->maxPlayers<20)return 0; + } + } + if(intent=="groupformation" && !AdventureGroupPolicy::Capital(location.ZoneId?location.ZoneId:location.AreaId))return 0; auto type = _types.find(location.Point.Category); if (type != _types.end()) { diff --git a/src/server/game/World/AdventureLocationMgr.h b/src/server/game/World/AdventureLocationMgr.h index 573cd6880c..643a0cb309 100644 --- a/src/server/game/World/AdventureLocationMgr.h +++ b/src/server/game/World/AdventureLocationMgr.h @@ -11,7 +11,7 @@ struct AdventureLocation { ClientlessWorldPoint Point; std::string Name, Region; - uint32 AreaId = 0, ZoneId = 0; + uint32 AreaId = 0, ZoneId = 0, InstanceMap = 0; bool IsArea = false; uint8 Faction = 0; }; diff --git a/src/server/game/World/ClientlessActionExecutorMgr.cpp b/src/server/game/World/ClientlessActionExecutorMgr.cpp index 14f4045bcb..5c4e892449 100644 --- a/src/server/game/World/ClientlessActionExecutorMgr.cpp +++ b/src/server/game/World/ClientlessActionExecutorMgr.cpp @@ -1,3 +1,5 @@ +#include "Group.h" +#include "ObjectAccessor.h" #include "ClientlessActionExecutorMgr.h" #include "ClientlessTransportMgr.h" #include "ClientlessFlightMgr.h" @@ -256,6 +258,15 @@ bool ClientlessActionExecutorMgr::StartAction(Player* player, ClientlessAction c if (action.Name == "move_to_point") return StartMoveToPoint(player, action, record); + record.PendingShared.reset();record.SharedWaitMs=0; + if ((action.Name=="travel_to_node" || action.Name=="travel_to_location") && action.Parameters.count("route_source")) + { + if(AdoptRoute(player,action,record))return true; + record.PendingShared=std::make_shared(action);record.Active=true; + record.Status.State=ClientlessActionExecutionState::SelectingTarget; + record.Status.Detail="waiting for group leader route";return true; + } + if (action.Name == "travel_to_node") return StartTravelToNode(player, action, record); @@ -2814,6 +2825,17 @@ void ClientlessActionExecutorMgr::Update(uint32 diff) if (!player->IsInWorld()) continue; + if(record.Active && record.PendingShared) + { + record.SharedWaitMs+=std::min(diff,30000u); + auto action=*record.PendingShared; + if(AdoptRoute(player,action,record)){record.PendingShared.reset();continue;} + if(record.SharedWaitMs<15000)continue; + // A local leader leg has no macro route; late arrivals may also + // need their own native catch-up route before joining the convoy. + action.Parameters.erase("route_source");record.PendingShared.reset(); + StartAction(player,action,record);continue; + } if (record.Active) { record.GroundExitCheckMs = diff >= record.GroundExitCheckMs ? 0 : record.GroundExitCheckMs - diff; @@ -2828,3 +2850,40 @@ void ClientlessActionExecutorMgr::Update(uint32 diff) StartAction(player, action, record); } } + +bool ClientlessActionExecutorMgr::AdoptRoute(Player* player,ClientlessAction const& action,Record& record) +{ + auto arg=action.Parameters.find("route_source");if(arg==action.Parameters.end())return false; + uint32 sourceGuid=uint32(std::strtoul(arg->second.c_str(),nullptr,10)); + auto group=player->GetGroup(); + if(!group || group->GetLeaderGUID().GetCounter()!=sourceGuid || sourceGuid==player->GetGUID().GetCounter())return false; + auto source=_records.find(sourceGuid); + if(source==_records.end() || source->second.Route.empty())return false; + // Copy values: inserting/updating executor records must never leave a pointer + // into another character's unordered-map record alive across execution. + auto route=source->second.Route; + auto const snapshot=source->second; + auto goal=action.Parameters.find(action.Name=="travel_to_location"?"id":"node"); + if(goal==action.Parameters.end() || goal->second!=(action.Name=="travel_to_location"?snapshot.FinalLocationId:snapshot.PlannedDestinationNode))return false; + auto start=std::min(snapshot.RouteStep?snapshot.RouteStep-1:0,route.size()-1); + auto anchor=route[start]; + std::vector joined; + Position point;point.Relocate(anchor.X,anchor.Y,anchor.Z); + if(player->GetMapId()==anchor.MapId && player->GetExactDist(point)<100) + {anchor.EdgeType="start";anchor.EdgeDatabaseId=0;joined.push_back(anchor);} + else + { + bool complete=false;std::string reached,error; + if(!sClientlessTravelGraphMgr->PlanAdaptiveRoute(player,anchor.NodeKey,500,300,joined,complete,reached,error) || !complete)return false; + } + joined.insert(joined.end(),route.begin()+start+1,route.end()); + record.Route=std::move(joined);record.RouteStep=0; + record.FinalLocalLegPending=snapshot.FinalLocalLegPending || !snapshot.FinalLocationId.empty(); + record.FinalMapId=snapshot.FinalMapId;record.FinalX=snapshot.FinalX;record.FinalY=snapshot.FinalY; + record.FinalZ=snapshot.FinalZ;record.FinalO=snapshot.FinalO;record.FinalLocationId=snapshot.FinalLocationId; + record.FinalDatabaseId=snapshot.FinalDatabaseId;record.PlannedDestinationNode=snapshot.PlannedDestinationNode; + record.RouteComplete=snapshot.RouteComplete;record.AdaptiveReachedNode=snapshot.AdaptiveReachedNode; + record.HardDestination=snapshot.HardDestination;record.Active=true; + TC_LOG_INFO("server.adventurebot.navigation","TRAVEL_SHARED_ROUTE guid=%u leader=%u steps=%u",player->GetGUID().GetCounter(),sourceGuid,uint32(record.Route.size())); + return StartRouteStep(player,player->GetGUID().GetCounter(),record); +} diff --git a/src/server/game/World/ClientlessActionExecutorMgr.h b/src/server/game/World/ClientlessActionExecutorMgr.h index 086814365d..5a9d6cbb39 100644 --- a/src/server/game/World/ClientlessActionExecutorMgr.h +++ b/src/server/game/World/ClientlessActionExecutorMgr.h @@ -8,6 +8,7 @@ #include "Common.h" #include "ClientlessTravelGraphMgr.h" #include +#include #include #include #include @@ -51,6 +52,8 @@ private: { ClientlessActionExecutionStatus Status; bool Active = false; + std::shared_ptr PendingShared; + uint32 SharedWaitMs=0; bool StopAtNode = false; std::vector Route; @@ -110,6 +113,7 @@ private: std::string AdaptiveReachedNode; }; + bool AdoptRoute(Player* player,ClientlessAction const& action,Record& record); bool StartAction(Player* player, ClientlessAction const& action, Record& record); bool StartApproach(Player* player, ClientlessAction const& action, Record& record); bool StartMoveToPoint(Player* player, ClientlessAction const& action, Record& record); diff --git a/src/server/game/World/ClientlessFlightMgr.cpp b/src/server/game/World/ClientlessFlightMgr.cpp index 31b3f17327..b66b0fde76 100644 --- a/src/server/game/World/ClientlessFlightMgr.cpp +++ b/src/server/game/World/ClientlessFlightMgr.cpp @@ -16,6 +16,7 @@ #include "MoveSpline.h" #include "PhasingHandler.h" #include "Player.h" +#include "Group.h" #include "PathGenerator.h" #include "Random.h" #include "SpellInfo.h" @@ -139,6 +140,8 @@ ClientlessFlightStatus const * ClientlessFlightMgr::GetStatus(uint32 guid) const void ClientlessFlightMgr::Clear(uint32 guid) { + auto it=_records.find(guid); + if(it!=_records.end() && it->second.Status.State!=ClientlessFlightState::Arrived)it->second.Published->Aborted=true; _records.erase(guid); } @@ -270,6 +273,16 @@ bool ClientlessFlightMgr::Start(Player * player, Position const & target, std::s record.Status.State = ClientlessFlightState::Mounting; record.Status.MountSpell = spell; record.Status.Detail = "waiting for native flying mount"; + if(auto group=player->GetGroup())if(group->GetLeaderGUID()!=player->GetGUID()) + { + auto source=_records.find(group->GetLeaderGUID().GetCounter()); + if(source!=_records.end() && source->second.Status.MapId==player->GetMapId() && + source->second.Status.Target.GetExactDist(target)<1 && !source->second.Published->Aborted) + { + record.Following=source->second.Published;record.Status.Landing=source->second.Status.Landing; + TC_LOG_INFO("server.adventurebot.navigation","FLIGHT_SHARED_TRAIL guid=%u leader=%u",player->GetGUID().GetCounter(),group->GetLeaderGUID().GetCounter()); + } + } _records[player->GetGUID().GetCounter()] = std::move(record); if (player->CastSpell(player, spell, false) != SPELL_CAST_OK) { @@ -283,6 +296,7 @@ bool ClientlessFlightMgr::Start(Player * player, Position const & target, std::s void ClientlessFlightMgr::Fail(Player * player, Record & record, std::string const & reason) { + record.Published->Aborted=true; player->StopMoving(); record.Moving = false; if (record.Status.State == ClientlessFlightState::Mounting) player->InterruptNonMeleeSpells(false); @@ -338,6 +352,13 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff back, Corridor(current, back, goal, record.Settings, sample, ray, true), true, false }; } + else if(record.Following) + { + auto shared=FollowTrail(current,goal,record.Settings,*record.Following,record.SharedIndex,sample,ray); + if(shared.Waiting)return true; + next=shared.Step; + if(handoff && next.Valid && Distance(next.Next,PointOf(record.Segment))<1)return true; + } else { auto const memory = record.Memory; @@ -368,6 +389,7 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff Fail(player, record, "flight spline rejected"); return false; } + record.Published->Points.push_back(next.Next); record.Moving = true; record.SegmentElapsed = 0; ++ record.Status.Segments; diff --git a/src/server/game/World/ClientlessFlightMgr.h b/src/server/game/World/ClientlessFlightMgr.h index d76b515579..59868fc42b 100644 --- a/src/server/game/World/ClientlessFlightMgr.h +++ b/src/server/game/World/ClientlessFlightMgr.h @@ -36,6 +36,9 @@ class TC_GAME_API ClientlessFlightMgr bool Command(ChatHandler * handler, char const * args); private : struct Record { + std::shared_ptr Published=std::make_shared(); + std::shared_ptr Following; + std::size_t SharedIndex=0; ClientlessFlightStatus Status; ClientlessFlightPath::Settings Settings; ClientlessFlightPath::Memory Memory; diff --git a/src/server/game/World/ClientlessFlightPath.h b/src/server/game/World/ClientlessFlightPath.h index 8eb90dd04c..ef01e1fe31 100644 --- a/src/server/game/World/ClientlessFlightPath.h +++ b/src/server/game/World/ClientlessFlightPath.h @@ -5,6 +5,7 @@ #include #include #include +#include // Geometry-only receding-horizon navigation. Server adapters supply phased // terrain/ceiling and static+dynamic collision queries. No Player shortcuts. namespace ClientlessFlightPath @@ -209,6 +210,22 @@ namespace ClientlessFlightPath if (best.Valid) memory.Departed = true; return best; } + struct SharedTrail + { + std::vector Points; + bool Aborted=false; + }; + struct SharedResult {Result Step;bool Waiting=false;}; + inline SharedResult FollowTrail(Point const& current,Point const& goal,Settings const& settings, + SharedTrail const& trail,std::size_t& index,Sample const& sample,Ray const& ray) + { + if(trail.Aborted)return {}; + while(indexSharedTargetDamage(player,helper,target); } + void OnGroupJoinRequest(Player* player,Player* requester) override + {sAdventureGroupMgr->RequestPlayerAdmission(player,requester);} void OnTextEmote(Player* player, Player* source, uint32 emote) override { sAdventureInteractionMgr->Emote(player,source,emote); } void OnDamageTaken(Player* player, Unit* attacker, uint32 damage) override diff --git a/src/server/game/World/ClientlessPlayerMgr.cpp b/src/server/game/World/ClientlessPlayerMgr.cpp index 32f3678679..e832a62d52 100644 --- a/src/server/game/World/ClientlessPlayerMgr.cpp +++ b/src/server/game/World/ClientlessPlayerMgr.cpp @@ -157,7 +157,8 @@ void ClientlessPlayerMgr::Update(uint32 diff) decltype(_damageNotices) damageNotices; decltype(_emoteNotices) emoteNotices; decltype(_helpNotices) helpNotices; - { std::lock_guard lock(_damageMutex); damageNotices.swap(_damageNotices); emoteNotices.swap(_emoteNotices); helpNotices.swap(_helpNotices); } + decltype(_groupJoinNotices) groupJoinNotices; + { std::lock_guard lock(_damageMutex); damageNotices.swap(_damageNotices); emoteNotices.swap(_emoteNotices); helpNotices.swap(_helpNotices); groupJoinNotices.swap(_groupJoinNotices); } // CLIENTLESS_PLAYER_CORE_C8_2_TRAVEL_ROBUSTNESS sClientlessTravelMgr->Update(diff); @@ -208,6 +209,9 @@ void ClientlessPlayerMgr::Update(uint32 diff) auto helpers=helpNotices.find(characterGuid); if(helpers!=helpNotices.end())for(auto const& notice:helpers->second) record.Controller->OnSharedTargetDamage(player,notice.Helper,notice.Target); + auto joins=groupJoinNotices.find(characterGuid); + if(joins!=groupJoinNotices.end())for(auto const& source:joins->second) + record.Controller->OnGroupJoinRequest(player,ObjectAccessor::FindPlayer(source)); record.Controller->Update(player, diff); } } @@ -268,3 +272,11 @@ void ClientlessPlayerMgr::SharedTargetDamage(Unit* victim,Unit* attacker,uint32 if(queue.size()<8)queue.push_back({helper->GetGUID(),victim->GetGUID()}); } } + +void ClientlessPlayerMgr::GroupJoinRequest(Player* target,Player* requester) +{ + if(!target || !requester || target==requester || !target->GetSession()->IsClientless() || requester->GetSession()->IsClientless())return; + std::lock_guard lock(_damageMutex); + auto& queue=_groupJoinNotices[target->GetGUID().GetCounter()]; + if(queue.size()<8)queue.push_back(requester->GetGUID()); +} diff --git a/src/server/game/World/ClientlessPlayerMgr.h b/src/server/game/World/ClientlessPlayerMgr.h index af824f1982..af8a055a35 100644 --- a/src/server/game/World/ClientlessPlayerMgr.h +++ b/src/server/game/World/ClientlessPlayerMgr.h @@ -39,6 +39,7 @@ public: virtual void OnPlayerLogout(Player*) { } // Notification after health was reduced, never the mutable pre-damage event. virtual void OnDamageTaken(Player*, Unit*, uint32) { } + virtual void OnGroupJoinRequest(Player*,Player*) { } virtual void OnTextEmote(Player*, Player*, uint32) { } virtual void OnSharedTargetDamage(Player*, ObjectGuid, ObjectGuid) { } }; @@ -54,6 +55,7 @@ public: bool IsManaged(uint32 characterGuid) const; void DamageTaken(Player* player, Unit* attacker, uint32 damage); + void GroupJoinRequest(Player* target,Player* requester); void TextEmote(Player* source, Player* target, uint32 emote); void SharedTargetDamage(Unit* victim,Unit* attacker,uint32 damage); void MarkActivity(Player* player); @@ -89,6 +91,7 @@ private: std::unordered_map> _helpNotices; struct EmoteNotice { ObjectGuid Source; uint32 Emote; }; std::unordered_map> _emoteNotices; + std::unordered_map> _groupJoinNotices; std::unordered_map _activity; std::mutex _damageMutex; std::unordered_map> _damageNotices; diff --git a/src/server/scripts/Custom/viano_bot_commands.cpp b/src/server/scripts/Custom/viano_bot_commands.cpp index 1cba74641b..cd057067f6 100644 --- a/src/server/scripts/Custom/viano_bot_commands.cpp +++ b/src/server/scripts/Custom/viano_bot_commands.cpp @@ -72,6 +72,9 @@ namespace auto session=sWorld->FindClientlessSession(guid);auto player=session?session->GetPlayer():nullptr; if(!player || !sClientlessPlayerMgr->IsManaged(guid)) {handler->SendSysMessage("Usage: clientless interaction ");return false;} + std::string op;input>>op; + if(op=="leave")return sAdventureInteractionMgr->RequestLeave(player); + if(!op.empty())return false; sAdventureInteractionMgr->Report(handler,player);return true; } } diff --git a/tests/clientless/test_adventure_combat_reactions.py b/tests/clientless/test_adventure_combat_reactions.py index fa484d1fb5..2c40dc4ea4 100644 --- a/tests/clientless/test_adventure_combat_reactions.py +++ b/tests/clientless/test_adventure_combat_reactions.py @@ -19,6 +19,7 @@ code=r''' #include #include #include +#include using uint32=unsigned;using uint8=unsigned char; #define TC_GAME_API #define TC_LOG_INFO(...) ((void)0) @@ -36,11 +37,13 @@ struct Map{float GetHeight(int,float,float,float,bool,float){return 0;}}map; struct Creature;struct Player; struct Unit:Position{ ObjectGuid guid;bool valid=true,alive=true,phase=true,visible=true,inWorld=true; + virtual unsigned getLevel(){return 60;} virtual Creature* ToCreature(){return nullptr;}virtual Player* ToPlayer(){return nullptr;} virtual bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}virtual ObjectGuid GetGUID(){return guid;} + Position const& GetPosition()const{return *this;} void GetNearPoint(Player*,float&a,float&b,float&c,float,float){a=x;b=y;c=z;}float GetAngle(Player*){return 0;} }; -struct Creature:Unit{bool controlled=false,evading=false,boss=false;unsigned level=60;std::string script="mob";Creature(unsigned n){guid={n};}ObjectGuid GetGUID(){return guid;}Creature* ToCreature()override{return this;}bool GetControllingPlayer(){return controlled;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsEvadingAttacks(){return evading;}bool isWorldBoss(){return boss;}unsigned getLevel(){return level;}std::string GetScriptName(){return script;}}; +struct Creature:Unit{bool controlled=false,evading=false,boss=false,elite=false;bool isElite(){return elite;}unsigned level=60;std::string script="mob";Creature(unsigned n){guid={n};}ObjectGuid GetGUID(){return guid;}Creature* ToCreature()override{return this;}bool GetControllingPlayer(){return controlled;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsEvadingAttacks(){return evading;}bool isWorldBoss(){return boss;}unsigned getLevel(){return level;}std::string GetScriptName(){return script;}}; struct Player; struct Corpse:Position{ time_t released=100;int saves=0;ObjectGuid GetGUID(){return {700};}time_t GetGhostTime(){return released;}int GetType(){return 0;} @@ -48,13 +51,22 @@ struct Corpse:Position{ }corpse; struct WorldPacket{int opcode;WorldPacket(int o,int):opcode(o){}templateWorldPacket&operator<<(T){return *this;}}; struct Session{Player* owner=nullptr;int releases=0,reclaims=0;void HandleRepopRequestOpcode(WorldPacket&);void HandleReclaimCorpseOpcode(WorldPacket&);}; +std::vector escapeCreatures; +struct Group{ObjectGuid leader{26};ObjectGuid GetLeaderGUID(){return leader;}}group; struct Player:Unit{ + bool grouped=false;float health=100;std::set attackers; bool alive=true,ghost=false,teleport=false,combat=false,melee=true,los=true;unsigned delay=30;int saves=0,revives=0,bones=0,graveyards=0;bool sickness=false;int durability=75; - Session session;Player(){session.owner=this;guid={26};}ObjectGuid GetGUID(){return guid;}Player* ToPlayer()override{return this;}bool GetGroup(){return false;}bool IsAlive(){return alive;}bool IsInWorld(){return true;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool GetTransport(){return false;}bool IsInFlight(){return false;}bool IsCharmed(){return false;} + Session session;Player(){session.owner=this;guid={26};}ObjectGuid GetGUID(){return guid;}Player* ToPlayer()override{return this;}Group* GetGroup(){return grouped?&group:nullptr;}bool IsAlive(){return alive;}bool IsInWorld(){return true;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool GetTransport(){return false;}bool IsInFlight(){return false;}bool IsCharmed(){return false;} unsigned GetMapId(){return 0;}unsigned GetInstanceId(){return 0;}bool InBattleground(){return false;}bool InArena(){return false;}bool HasFlag(int,int){return ghost;}Session*GetSession(){return &session;}Corpse*GetCorpse(){return &corpse;} uint32 GetCorpseReclaimDelay(bool){return delay;}Map*GetMap(){return ↦}int GetPhaseShift(){return 0;} bool IsInPhase(Unit*c){return c->phase;}bool CanSeeOrDetect(Unit*c){return c->visible;}bool IsValidAttackTarget(Unit*c){return c->valid;}unsigned getLevel(){return 60;}bool IsWithinMeleeRange(Unit*){return melee;}float GetCombatReach(){return 1;}float GetAngle(Player*){return 0;} void SaveToDB(){++saves;}void RepopAtGraveyard(){++graveyards;teleport=true;Relocate(-100,0,0);}void ResurrectPlayer(float hp,bool sick){assert(hp==0.5f);sickness=sick;alive=true;ghost=false;++revives;}void SpawnCorpseBones(){++bones;} + bool IsHostileTo(Unit*){return true;}float GetHealthPct(){return health;} + auto const& getAttackers(){return attackers;} + void GetCreatureListWithEntryInGrid(std::vector&v,int,float){v=escapeCreatures;} + void GetPlayerListInGrid(std::vector&,float){} + void AttackStop(){}void CastStop(){}void SetStandState(int){} + struct Motion{void Clear(){}}motion;Motion*GetMotionMaster(){return &motion;} void StopMoving(){} }; bool Corpse::IsWithinDistInMap(Player*p,float r,bool){return GetExactDist(p)<=r;} @@ -72,11 +84,14 @@ struct Travel{ TravelStatus status;bool present=false,path=true,nearOnly=false;int starts=0,analyses=0; auto GetStatus(uint32)->TravelStatus*{return present?&status:nullptr;} void CancelTravel(uint32){present=false;} - bool AnalyzePath(Player*,Position const&end,ClientlessPathDiagnostics& d){++analyses;d.PathLength=std::fabs(end.x+100);return path && (!nearOnly || end.x<-15);} + bool AnalyzePath(Player*p,Position const&end,ClientlessPathDiagnostics& d){++analyses;d.PathLength=p->GetExactDist(end);return path && (!nearOnly || end.x<-15);} bool StartTravel(Player*,Position const&end){++starts;present=true;status.State=ClientlessTravelState::Moving;status.Target=end;return true;} }travel;auto sClientlessTravelMgr=&travel; struct Behavior{int interrupts=0;void InterruptForCombat(Player*){++interrupts;}}behavior;auto sAdventureBehaviorMgr=&behavior; ''' +code+=r''' +struct Lua{void DismountGroundMount(Player*,char const*){}}lua;auto sClientlessLuaEngine=&lua; +''' # production Update uses direct Position members and creature near point. code=code.replace('float x=0,y=0,z=0;','union{float x=0;float m_positionX;};union{float y=0;float m_positionY;};union{float z=0;float m_positionZ;};',1) code=code.replace('bool GetControllingPlayer(){return controlled;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsEvadingAttacks(){return evading;}bool isWorldBoss(){return boss;}unsigned getLevel(){return level;}std::string GetScriptName(){return script;}};', 'bool GetControllingPlayer(){return controlled;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsEvadingAttacks(){return evading;}bool isWorldBoss(){return boss;}unsigned getLevel(){return level;}std::string GetScriptName(){return script;}float GetAngle(Player*){return 0;}void GetNearPoint(Player*,float&a,float&b,float&c,float,float){a=x;b=y;c=z;}};') @@ -86,11 +101,12 @@ code+=header+r''' class AdventureCombatMgr::Fighter{public:int ticks=0;void Tick(Unit*,uint32){++ticks;}}; bool AdventureCombatMgr::Reachable(Player*p,Unit*)const{return p->los;} +Unit* AdventureCombatMgr::PvPTarget(Player*)const{return nullptr;} Creature* AdventureCombatMgr::Proactive(Player*)const{return nullptr;} bool AdventureCombatMgr::Start(Player*,Record&r,Unit*c,bool train,bool defense,time_t until){r.AI=std::make_shared();r.Target=c->GetGUID();r.Training=train;r.Defending=defense;r.Until=until;return true;} void AdventureCombatMgr::Finish(Player*,Record&r){r.AI.reset();r.Target.Clear();r.Defending=false;r.Training=false;r.Cooldown=10000;} ''' -for name in ('bool AdventureCombatMgr::Valid(', 'void AdventureCombatMgr::DamageTaken(', 'bool AdventureCombatMgr::Death(', 'bool AdventureCombatMgr::Update('):code+='\n'+function(name) +for name in ('bool AdventureCombatMgr::Valid(', 'void AdventureCombatMgr::DamageTaken(', 'bool AdventureCombatMgr::Flee(', 'bool AdventureCombatMgr::Death(', 'bool AdventureCombatMgr::Update('):code+='\n'+function(name) code+=r''' int main(){ using namespace AdventureCombatPolicy; @@ -147,10 +163,41 @@ int main(){ AdventureCombatMgr::Record resumed;resumed.Dead=true;GameTime::now=400;mgr.Death(&p,resumed); assert(p.graveyards==1 && p.teleport && !p.alive && resumed.CemeteryPending); p.teleport=false;mgr.Death(&p,resumed);assert(p.alive && !p.sickness && p.revives==1 && p.bones==1 && p.durability==75); + // Flight boundaries, actual threats, group override and blocked fallback. + assert(!ShouldFlee(false,20,60,65,false,false)); + assert(ShouldFlee(false,19,60,60,false,false)); + assert(ShouldFlee(false,100,60,66,false,false)); + assert(ShouldFlee(false,100,60,60,true,false)); + assert(!ShouldFlee(false,100,60,59,true,false)); + assert(!ShouldFlee(true,1,60,90,true,true)); + p.alive=true;p.ghost=false;p.Relocate(0,0,0);p.combat=true;p.health=100; + r={};a.alive=true;a.valid=true;a.level=66;a.Relocate(1,0,0);p.attackers={&a};travel={};GameTime::now=1000; + mgr.Start(&p,r,&a,false,true,0); + assert(mgr.Flee(&p,r)&&r.Fleeing&&travel.starts==1); + assert(travel.status.Target.GetExactDist(a)>p.GetExactDist(a)+3); + // Reject the direct escape corridor if an additional hostile blocks it. + Creature obstacle(100);obstacle.Relocate(-15,0,0);escapeCreatures={&obstacle}; + GameTime::now=1002;mgr.Flee(&p,r); + assert(SegmentDistance(-15,0,0,0,0,0,travel.status.Target.x,travel.status.Target.y,travel.status.Target.z)>=12); + escapeCreatures.clear(); + // Group join aborts immediately, even while critically injured. + p.grouped=true;p.health=1;assert(!mgr.Flee(&p,r)&&!r.Fleeing&&!travel.present); + assert(!mgr.Flee(&p,r));p.grouped=false;p.health=100; + // Blocked paths preserve native self-defence; no teleport/resurrection. + travel={};travel.path=false;GameTime::now=1004; + int ticks=r.AI->ticks;assert(mgr.Flee(&p,r)&&r.Fleeing&&travel.starts==0&&r.AI->ticks>ticks); + int oldRevives=p.revives,oldGraves=p.graveyards; + // Ending native combat alone is insufficient while still close to danger. + p.combat=false;p.attackers.clear();r.Defending=false; + GameTime::now=1006;assert(mgr.Flee(&p,r)&&r.Fleeing); + p.Relocate(-60,0,0);GameTime::now=1008;assert(mgr.Flee(&p,r)&&!r.Fleeing&&!r.AI); + assert(p.revives==oldRevives&&p.graveyards==oldGraves); + // No aggression means no flight solely from low health. + r={};p.health=1;assert(!mgr.Flee(&p,r)); } ''' with tempfile.TemporaryDirectory() as d: p=Path(d);(p/'test.cpp').write_text(code) subprocess.run(['g++','-std=c++17','-I'+str(root/'src/server/game/World'),str(p/'test.cpp'),'-o',str(p/'test')],check=True) subprocess.run([str(p/'test')],check=True) -print('Production combat focus, damage ordering, risk filters, corpse radius/delay, alternate paths and malus-free persisted fallback passed.') +print('Production combat focus, damage ordering, risk filters, corpse radius/delay, flight/group override, blocked escape, danger avoidance and malus-free persisted fallback passed.') diff --git a/tests/clientless/test_adventure_group_plan.py b/tests/clientless/test_adventure_group_plan.py new file mode 100644 index 0000000000..602dae1b52 --- /dev/null +++ b/tests/clientless/test_adventure_group_plan.py @@ -0,0 +1,62 @@ +"""Execute production leader-plan inheritance without modifying members' own days.""" +from pathlib import Path +import subprocess,tempfile +root=Path(__file__).resolve().parents[2] +s=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text() +sig='std::string AdventureBehaviorMgr::InteractionSchedule(' +start=s.index(sig);begin=s.index('{',start);end=begin+1;depth=1 +while depth:depth+=(s[end]=='{')-(s[end]=='}');end+=1 +schedule=s[start:end] +a=s.index('AdventureGroupPlan AdventureBehaviorMgr::GroupPlan(') +code=r''' +#include "AdventureBehaviorPolicy.h" +#include "AdventureInstancePolicy.h" +#include +#include +#include +using uint32=unsigned; +struct Guid{unsigned id;unsigned GetCounter(){return id;}}; +struct Player{Guid guid;Guid GetGUID(){return guid;}}; +struct AdventureGroupPlan{bool Enabled=false,Forming=false;std::string Intent,Location,NextIntent,NextLocation;time_t Deadline=0;}; +namespace GameTime{time_t GetGameTime(){return 100;}} +std::tm Local(time_t){std::tm tm{};tm.tm_hour=5;return tm;}unsigned DateOf(std::tm){return 1;} +struct AdventureBehaviorMgr{ + struct Record{bool Enabled=true,Schedule=true,GroupOverride=false,Dirty=false;int Hour=-1;unsigned Date=1;std::string Intent,Location,WantedIntent,WantedLocation,ManualNextIntent,ManualNextLocation;std::string PersonalIntent,PersonalLocation;time_t PersonalDeadline=0;time_t Deadline=3600;AdventureBehaviorPolicy::Day Plan;AdventureInstancePolicy::Visit Instance;}; + std::unordered_map_records;int changes=0; + void Generate(Player*,Record&r,unsigned date){r.Date=date;} + void SetWanted(Player*,Record&r,std::string const&i,std::string const&l,time_t end){++changes;r.WantedIntent=i;r.WantedLocation=l;r.Deadline=end;} + std::string InteractionSchedule(Player*);void ResumePersonalPlan(Player*);AdventureGroupPlan GroupPlan(Player*);bool AdoptGroup(Player*,Player*); +}; +'''+schedule+'\n'+s[a:]+r''' +int main(){ + AdventureBehaviorMgr mgr;Player leader{{29}},member{{31}};auto& a=mgr._records[29];auto& b=mgr._records[31]; + a.Intent="groupformation";a.Location="leader-capital";a.Plan[5]={"groupformation","leader-capital"};a.Plan[6]={"instance","deadmines"}; + b.Intent="groupformation";b.Location="other-capital";b.Plan[5]={"groupformation","other-capital"};b.Plan[6]={"instance","shadowfang"}; + auto plan=mgr.GroupPlan(&member);assert(plan.Forming&&plan.NextIntent=="instance"&&plan.NextLocation=="shadowfang"); + assert(mgr.AdoptGroup(&member,&leader));assert(b.WantedLocation=="leader-capital"&&b.Deadline==a.Deadline&&b.Schedule); + assert(b.Plan[6].Location=="shadowfang"); // own future plan remains available after departure + a.Intent="instance";a.Location="deadmines";a.Deadline=7200; + assert(mgr.AdoptGroup(&member,&leader));assert(b.WantedIntent=="instance"&&b.WantedLocation=="deadmines"&&b.Deadline==7200); + b.Instance.Active=true;b.Instance.Stage=AdventureInstancePolicy::Phase::Inside;a.Location="other"; + int changes=mgr.changes;assert(mgr.AdoptGroup(&member,&leader)&&mgr.changes==changes); + b.Instance.Stage=AdventureInstancePolicy::Phase::Travel;mgr.AdoptGroup(&member,&leader);assert(b.WantedLocation=="other"); + a.Enabled=false;assert(!mgr.AdoptGroup(&member,&leader)); + b.Instance.Active=false;mgr.ResumePersonalPlan(&member); + assert(!b.GroupOverride&&b.WantedIntent=="groupformation"&&b.WantedLocation=="other-capital"); + b.Schedule=false;b.Intent="groupformation";b.ManualNextIntent="raid";b.ManualNextLocation="molten-core"; + plan=mgr.GroupPlan(&member);assert(plan.Forming&&plan.NextIntent=="raid"&&plan.NextLocation=="molten-core"); + // Different goals are inherited without inventing a personal intent change. + b.GroupOverride=false;b.Instance.Active=false;b.Intent=b.WantedIntent="social"; + b.Location=b.WantedLocation="personal-place";b.Deadline=2000; + a.Enabled=true;a.Intent="social";a.Location="leader-place"; + assert(mgr.AdoptGroup(&member,&leader));b.Location="leader-place"; + assert(mgr.InteractionSchedule(&member)=="social:personal-place"); + mgr.ResumePersonalPlan(&member); + assert(b.WantedLocation=="personal-place"&&mgr.InteractionSchedule(&member)=="social:personal-place"); +} +''' +with tempfile.TemporaryDirectory() as d: + p=Path(d);(p/'plan.cpp').write_text(code) + subprocess.run(['g++','-std=c++17','-I'+str(root/'src/server/game/World'),str(p/'plan.cpp'),'-o',str(p/'plan')],check=True) + subprocess.run([str(p/'plan')],check=True) +print('Production group plans: leader meeting/goal/deadline inheritance, untouched personal day, manual preparation and instance-state guards passed.') diff --git a/tests/clientless/test_adventure_groups.py b/tests/clientless/test_adventure_groups.py new file mode 100644 index 0000000000..0384a31f04 --- /dev/null +++ b/tests/clientless/test_adventure_groups.py @@ -0,0 +1,121 @@ +"""Execute production bot recruitment with native-party/channel adapters.""" +from pathlib import Path +import subprocess,tempfile +root=Path(__file__).resolve().parents[2] +s=(root/'src/server/game/World/AdventureGroupMgr.cpp').read_text() +def fun(sig): + a=s.index(sig);b=s.index('{',a);e=b+1;n=1 + while n:n+=(s[e]=='{')-(s[e]=='}');e+=1 + return s[a:e] +header=(root/'src/server/game/World/AdventureGroupMgr.h').read_text() +header='\n'.join(l for l in header.splitlines() if not l.startswith('#include')).replace('private:','public:') +code=r''' +#include "AdventureGroupPolicy.h" +#include "AdventureBehaviorPolicy.h" +#include +#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;}bool operator==(ObjectGuid b)const{return id==b.id;}bool operator!=(ObjectGuid b)const{return id!=b.id;}bool operator<(ObjectGuid b)const{return idstruct hash{size_t operator()(ObjectGuid g)const{return g.id;}};} +struct Player;struct ChatHandler; +struct Map{bool IsDungeon(){return false;}}map; +struct Member{Player*player=nullptr;Member*nextMember=nullptr;Player*GetSource(){return player;}Member*next(){return nextMember;}}; +constexpr int GROUP_REMOVEMETHOD_LEAVE=1; +struct Group{struct Slot{ObjectGuid guid;};std::vectorslots;auto const&GetMemberSlots(){return slots;}void RemoveMember(ObjectGuid,int);Member*members=nullptr;Member*GetFirstMember(){return members;}void ChangeLeader(ObjectGuid g){leader=g;}void SendUpdate(){}ObjectGuid leader;unsigned count=2;bool raid=false;ObjectGuid GetLeaderGUID(){return leader;}unsigned GetMembersCount(){return count;}bool isRaidGroup(){return raid;}void ConvertToRaid(){raid=true;}void ConvertToGroup(){raid=false;}}; +struct WorldPacket{unsigned value=0;WorldPacket(int,int=0){}WorldPacket&operator<<(unsigned n){value=n;return *this;}};constexpr int MSG_SET_RAID_DIFFICULTY=2,RAID_DIFFICULTY_25MAN_NORMAL=1;constexpr int CMSG_PARTY_INVITE=1,EMOTE_ONESHOT_WAVE=1; +namespace WorldPackets{namespace Party{struct PartyInviteClient{PartyInviteClient(WorldPacket&&){}std::string TargetName;ObjectGuid TargetGUID;};}} +struct Session{Player*owner;bool clientless=true;bool IsClientless(){return clientless;}void HandlePartyInviteOpcode(WorldPackets::Party::PartyInviteClient&);void HandleSetRaidDifficultyOpcode(WorldPacket&);}; +struct Player{ObjectGuid guid;Group*group=nullptr,*invite=nullptr;Session session{this};bool combat=false;unsigned zone=1519,level=60,team=0;float distance=10; + unsigned difficulty=0;unsigned GetRaidDifficulty(){return difficulty;}ObjectGuid GetGUID(){return guid;}bool IsInWorld(){return true;}bool IsAlive(){return true;}bool IsInCombat(){return combat;} + bool IsBeingTeleported(){return false;}bool GetTransport(){return false;}bool IsFlying(){return false;}bool IsInFlight(){return false;}bool IsCharmed(){return false;}Map*GetMap(){return ↦} + unsigned GetZoneId(){return zone;}unsigned getLevel(){return level;}unsigned GetTeam(){return team;} + Group*GetGroup(){return group;}Group*GetGroupInvite(){return invite;}Session*GetSession(){return &session;}std::string GetName(){return std::to_string(guid.id);} + bool IsInMap(Player*o){return zone==o->zone;}bool IsInPhase(Player*){return true;}bool CanSeeOrDetect(Player*){return true;} + float GetExactDist(Player*){return distance;}bool IsWithinLOSInMap(Player*){return true;} + void SetFacingToObject(Player*){}void HandleEmoteCommand(int){} +}; +namespace ObjectAccessor{std::unordered_map players;Player*FindPlayer(ObjectGuid g){auto it=players.find(g);return it==players.end()?nullptr:it->second;}} +void Session::HandleSetRaidDifficultyOpcode(WorldPacket&p){owner->difficulty=p.value;} +void Group::RemoveMember(ObjectGuid g,int){--count;ObjectAccessor::FindPlayer(g)->group=nullptr;} +Group pending;int invites=0; +void Session::HandlePartyInviteOpcode(WorldPackets::Party::PartyInviteClient&p){++invites;pending.leader=owner->guid;ObjectAccessor::FindPlayer(p.TargetGUID)->invite=owner->group?owner->group:&pending;} +struct Profile{std::unordered_map Personality{{"sociability",1}};std::unordered_setPrimaryProfessions{"skinning"};}profile; +struct Lua{Profile*GetAdventureProfile(unsigned){return &profile;}}lua;auto sClientlessLuaEngine=&lua; +struct Combat{bool Busy(Player*){return false;}}combat;auto sAdventureCombatMgr=&combat; +namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}} +float frand(float,float){return 0;}unsigned urand(unsigned a,unsigned){return a;} +'''+header+r''' +struct Location{std::string Name="Stormwind";}location;struct Locations{Location*Find(std::string const&){return &location;}}locations;auto sAdventureLocationMgr=&locations; +struct Behavior{std::unordered_map plans;AdventureGroupPlan GroupPlan(Player*p){return plans[p->guid.id];}}behavior;auto sAdventureBehaviorMgr=&behavior; +int messages=0; +void AdventureGroupMgr::Speak(Player*,std::string const&){++messages;} +''' +code+=fun('float Sociability(')+'\n'+fun('bool Ready(')+'\n'+fun('std::string Activity(') +for name in ['bool AdventureGroupMgr::BotLed(', 'bool AdventureGroupMgr::Forming(', 'bool AdventureGroupMgr::PlannedTransition(', 'bool AdventureGroupMgr::Accept(', 'void AdventureGroupMgr::Update(', 'void AdventureGroupMgr::Invite(', 'void AdventureGroupMgr::RequestPlayerAdmission(', 'bool AdventureGroupMgr::PlayerLeadership(', 'bool AdventureGroupMgr::EnforceCapacity(']:code+='\n'+fun(name) +code+=r''' +int main(){ + using namespace AdventureGroupPolicy; + AdventureBehaviorPolicy::Day day;std::mt19937 random(42); + for(unsigned i=0;i<24;++i)day[i]={i%2?"instance":"social",std::to_string(i)}; + Prepare(day,random,1,[]{return std::string("capital");}); + for(unsigned h=0;h<24;++h)if(day[h].Intent=="groupformation")assert(h<23&&Eligible(day[h+1].Intent)&&day[h].Location=="capital"); + + assert(Capacity("instance")==5&&Capacity("raid")==20&&Capacity("social")>5); + assert(Compatible("instance","instance",60,65)&&!Compatible("instance","raid",60,60)); + assert(!Compatible("raid","raid",60,66)); + AdventureGroupMgr mgr;Player a,b;a.guid={29};b.guid={31};b.zone=1537;b.distance=10000; + ObjectAccessor::players={{a.guid,&a},{b.guid,&b}}; + behavior.plans[29]={true,true,"groupformation","stormwind","instance","deadmines",3600}; + behavior.plans[31]={true,true,"groupformation","ironforge","instance","shadowfang",3600}; + mgr._records[a.guid]={};mgr._records[b.guid]={}; + // Remote capital recruitment accepts different original targets/meeting points. + mgr.Update(&a);assert(invites==1&&b.invite&&b.invite->leader==a.guid&&messages>=1); + assert(mgr.Accept(&b,&a)); + behavior.plans[31].NextIntent="raid";assert(!mgr.Accept(&b,&a));behavior.plans[31].NextIntent="instance"; + b.team=1;assert(!mgr.Accept(&b,&a));b.team=0; + b.combat=true;assert(!mgr.Accept(&b,&a));b.combat=false; + // Native party cap and pending reservations prevent sixth members. + Group party;party.leader=a.guid;party.count=5;a.group=&party; + assert(!mgr.Accept(&b,&a));b.invite=nullptr;mgr._records[b.guid].InviteUntil=0; + mgr._records[a.guid].SearchAt=0;mgr.Update(&a);assert(invites==1); + // Existing group recruits and lone bot requests membership without replacing leader. + party.count=3;mgr._records[a.guid].Partners.clear();mgr._records[b.guid].Partners.clear(); + mgr._records[b.guid].SearchAt=0;mgr.Update(&b);assert(invites==2&&b.invite==&party&&party.leader==a.guid); + assert(mgr.Accept(&b,&a)); + // Intent-hour transition is exempt exactly once, no permanent exit exemption. + b.group=&party;behavior.plans[31].Intent="instance";behavior.plans[31].Forming=false; + assert(mgr.PlannedTransition(&b));assert(!mgr.PlannedTransition(&b)); + // Raid native conversion and twentieth-member boundary. + b.group=nullptr;b.invite=nullptr;a.group=&party;party.count=19;behavior.plans[29].NextIntent="raid"; + behavior.plans[31]={true,true,"groupformation","ironforge","raid","otherraid",3600}; + mgr._records[a.guid].SearchAt=0;mgr._records[a.guid].Partners.clear();mgr._records[b.guid].Partners.clear();mgr._records[b.guid].InviteUntil=0; + mgr.Update(&a);assert(a.difficulty==RAID_DIFFICULTY_25MAN_NORMAL&&party.raid&&invites==3&&mgr.Accept(&b,&a)); + party.count=20;assert(!mgr.Accept(&b,&a)); + // Solo recruitment outside formation requires actual local contact. + a.group=nullptr;b.invite=nullptr;behavior.plans[29]={true,false,"social","city",{}, {},3600};behavior.plans[31]=behavior.plans[29]; + mgr._records.clear();mgr._records[a.guid]={};mgr._records[b.guid]={};mgr.Update(&a);assert(invites==3); + b.zone=a.zone;b.distance=10;mgr._records[a.guid].SearchAt=0;mgr.Update(&a);assert(invites==4); + // When a large raid changes to a smaller activity, excess bots leave safely. + party.count=5;party.slots={{{29}},{{40}},{{41}},{{31}},{{42}}};party.leader=a.guid;b.group=&party; + behavior.plans[29].Intent="adventure";assert(mgr.EnforceCapacity(&b)&&!b.group&&party.count==4); + behavior.plans[29].Intent="social"; + // Explicit human invite to a grouped bot offers admission, then native leadership. + Player human;human.guid={21};human.session.clientless=false;ObjectAccessor::players[human.guid]=&human; + a.group=&party;party.leader=a.guid;party.count=2; + mgr.RequestPlayerAdmission(&a,&human);assert(invites==5&&human.invite==&party); + human.group=&party;Member humanSlot;humanSlot.player=&human;party.members=&humanSlot; + mgr.Update(&a);assert(party.leader==human.guid&&!mgr.BotLed(&a)); +} +''' +with tempfile.TemporaryDirectory() as d: + p=Path(d);(p/'groups.cpp').write_text(code) + subprocess.run(['g++','-std=c++17','-I'+str(root/'src/server/game/World'),str(p/'groups.cpp'),'-o',str(p/'groups')],check=True) + subprocess.run([str(p/'groups')],check=True) +print('Production recruitment: capitals, different goals, native inviter leadership, joining existing groups, capacity/reservations, raid conversion, planned transition and local contact passed.') diff --git a/tests/clientless/test_adventure_instance.py b/tests/clientless/test_adventure_instance.py index 95c7dd9290..77d2ba27fc 100644 --- a/tests/clientless/test_adventure_instance.py +++ b/tests/clientless/test_adventure_instance.py @@ -101,9 +101,9 @@ int main(){ // Late arrival never enters, nor applies a fake injury. r=fresh();r.State=AdventureBehaviorMgr::Phase::Travel;int calls=p.entryCalls; assert(mgr.UpdateInstance(&p,r,3350)&&r.Instance.Stage==Stage::Rest&&p.entryCalls==calls&&p.damages==damages); - // Shared or grouped copies are not accepted for this solo performance. + // Native groups may enter their group copy; unrelated solo shared binds remain rejected. r=fresh();r.Instance.Stage=Stage::Entering;p.group=true; - assert(mgr.UpdateInstance(&p,r,100)&&p.entryCalls==calls);p.group=false; + assert(mgr.UpdateInstance(&p,r,100)&&p.entryCalls==calls+1);++calls;p.group=false;p.map=0;p.instance=0;p.x=0; r=fresh();r.Instance.Stage=Stage::Entering;p.bind.save=&save;CharacterDatabase.shared=true; assert(mgr.UpdateInstance(&p,r,100)&&p.entryCalls==calls);CharacterDatabase.shared=false;p.bind.save=nullptr; // A denied exit remains pending and cannot wound a player still inside. diff --git a/tests/clientless/test_adventure_instance_persistence.py b/tests/clientless/test_adventure_instance_persistence.py index e5ce0c9e7d..0bc0dcc688 100644 --- a/tests/clientless/test_adventure_instance_persistence.py +++ b/tests/clientless/test_adventure_instance_persistence.py @@ -10,6 +10,7 @@ def function(signature): return s[a:n] code=r''' #include "AdventureInstancePolicy.h" +struct MapEntry{bool IsRaid(){return false;}}mapEntry;struct Maps{MapEntry*LookupEntry(unsigned){return &mapEntry;}}sMapStore; #include #include #include diff --git a/tests/clientless/test_adventure_intent_state.py b/tests/clientless/test_adventure_intent_state.py index 579bbc6ba9..0f014e1d7e 100644 --- a/tests/clientless/test_adventure_intent_state.py +++ b/tests/clientless/test_adventure_intent_state.py @@ -15,12 +15,13 @@ using uint32=uint32_t;using uint64=uint64_t; #define TC_LOG_INFO(...) ((void)0) constexpr int SPELL_VISUAL_KIT_DRINK=438,UNIT_NPC_EMOTESTATE=0; struct Position {float x=0;void Relocate(void const*){}}; -struct Guid{uint32 GetCounter(){return 26;}}; +struct Guid{uint32 GetCounter(){return 26;}bool operator!=(Guid)const{return false;}}; +struct Group{Guid GetLeaderGUID(){return {};}}; struct Object{} object; struct Map{Object* GetGameObject(int){return &object;}} map; struct Player { bool combat=false;uint32 area=87,zone=12,mapId=0; - Guid GetGUID(){return {};}bool IsFlying(){return false;}bool IsInWorld(){return true;}bool IsInCombat(){return combat;} + Group*GetGroup(){return nullptr;}Guid GetGUID(){return {};}bool IsFlying(){return false;}bool IsInWorld(){return true;}bool IsInCombat(){return combat;} uint32 GetMapId(){return mapId;}uint32 GetAreaId(){return area;}uint32 GetZoneId(){return zone;} float GetPositionX(){return 0;}float GetPositionY(){return 0;}float GetPositionZ(){return 0;} float GetExactDist(Position const&){return 0;}float GetExactDist(float,float,float){return 0;}float GetExactDist(Object*){return 0;} @@ -28,6 +29,8 @@ struct Player { bool IsWithinLOSInMap(Object*){return true;}void SetFacingToObject(Object*){} Map* GetMap(){return ↦} } player; +namespace ObjectAccessor{Player*FindPlayer(Guid){return nullptr;}} +struct BotGroups{bool BotLed(Player*){return false;}bool FollowMember(Player*){return false;}}botGroups;auto sAdventureGroupMgr=&botGroups; bool unsafe=false;bool Unsafe(Player*){return unsafe;} bool busy=false;bool Busy(uint32){return busy;} time_t now=100;int hour=5; @@ -51,7 +54,7 @@ public: struct Record{ bool Enabled=true,Schedule=false,Queued=false,Dirty=false; Phase State=Phase::Travel; - std::string Intent="social",Location="tavern",WantedIntent="social",WantedLocation="tavern",Detail,Micro; + std::string Intent="social",Location="tavern",WantedIntent="social",WantedLocation="tavern",Detail,Micro,ManualNextIntent,ManualNextLocation; time_t Deadline=1000;uint32 Date=20261026,Timer=0,MicroTimer=0,GestureTimer=0,RetryTimer=0;int Hour=5; struct{bool Active=false;}Social; struct{bool Active=false;}Recovery; @@ -61,6 +64,7 @@ public: }; std::unordered_map _records{{26,{}}}; int begun=0,fallbacks=0,micros=0,resets=0,switches=0,queues=0,leisureUpdates=0; + bool AdoptGroup(Player*,Player*){return false;} void Update(Player*,uint32); void Generate(Player*,Record&,uint32){} void SetWanted(Player*,Record& r,std::string const&i,std::string const&l,time_t d){++switches;r.WantedIntent=i;r.WantedLocation=l;r.Deadline=d;r.Dirty=true;r.State=Phase::Idle;} @@ -103,6 +107,11 @@ int main(){ // Leashed companion distance is serviced at world cadence, not the 1 s planning clock. r.Timer=1000;r.Leisure.Active=true;mgr.Update(&player,50); assert(mgr.leisureUpdates==1&&r.Timer==950); + // Preparation ends on its deadline without waiting for a full/assembled party. + r.Leisure.Active=false;r.Timer=0;r.Intent="groupformation";r.Deadline=now; + r.ManualNextIntent="social";r.ManualNextLocation="leader-goal"; + mgr.Update(&player,1000); + assert(r.WantedIntent=="social"&&r.WantedLocation=="leader-goal"&&r.Deadline==now+3600&&r.ManualNextIntent.empty()); } ''' with tempfile.TemporaryDirectory(prefix='vcore-adventure-state-') as folder: diff --git a/tests/clientless/test_adventure_interactions.py b/tests/clientless/test_adventure_interactions.py index 933f5dafe9..068ede218e 100644 --- a/tests/clientless/test_adventure_interactions.py +++ b/tests/clientless/test_adventure_interactions.py @@ -40,9 +40,9 @@ struct Player:Unit { ObjectGuid guid,selection;bool alive=true,inWorld=true,combat=false,teleport=false,transport=false,flight=false,charmed=false,casting=false,afk=false,visible=true,phase=true,los=true; unsigned mapid=0,team=ALLIANCE;Session session;Group*group=nullptr,*invite=nullptr;Map map;int gestures=0,says=0,posture=0;uint32 lastEmote=0; explicit Player(unsigned id=26){guid.id=id;session.owner=this;} - ObjectGuid GetGUID(){return guid;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool GetTransport(){return transport;}bool IsInFlight(){return flight;}bool IsCharmed(){return charmed;}bool HasUnitState(int){return casting;} + ObjectGuid GetGUID(){return guid;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool GetTransport(){return transport;}bool IsFlying(){return false;}bool IsInFlight(){return flight;}bool IsCharmed(){return charmed;}bool HasUnitState(int){return casting;} bool IsInMap(Player*p){return mapid==p->mapid;}bool IsInPhase(Player*p){return phase && p->phase;}bool CanSeeOrDetect(Player*p){return p->visible;}bool IsWithinLOSInMap(Player*p){return los && p->los;}bool IsFriendlyTo(Player*p){return team==p->team;} - Map*GetMap(){return ↦}unsigned GetMapId(){return mapid;}Session*GetSession(){return &session;}Group*GetGroup(){return group;}Group*GetGroupInvite(){return invite;}unsigned GetTeam(){return team;}ObjectGuid GetTarget(){return selection;}bool isAFK(){return afk;} + Map*GetMap(){return ↦}unsigned GetMapId(){return mapid;}Session*GetSession(){return &session;}Group*GetGroup(){return group;}void UninviteFromGroup(){invite=nullptr;}Group*GetGroupInvite(){return invite;}unsigned GetTeam(){return team;}ObjectGuid GetTarget(){return selection;}bool isAFK(){return afk;} void SetStandState(int s){posture=s;}void SetFacingToObject(Player*){}void HandleEmoteCommand(uint32 e){++gestures;lastEmote=e;}void Say(char const*,int,Player*){++says;} void GetNearPoint(Player*,float&x,float&y,float&z,float,float){x=m_positionX;y=m_positionY;z=m_positionZ;} std::vector people;void GetPlayerListInGrid(std::vector&out,float){out=people;} @@ -56,7 +56,7 @@ unsigned urand(unsigned a,unsigned){return a;}float roll=0;float frand(float,flo namespace ObjectAccessor {std::unordered_map 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 Behavior {int interrupts=0;std::string intent="social",schedule="social:inn";std::string InteractionIntent(Player*){return intent;}std::string InteractionSchedule(Player*){return schedule;}void ResumePersonalPlan(Player*){}void InterruptForInteraction(Player*){++interrupts;}}behavior;auto sAdventureBehaviorMgr=&behavior; struct Combat {bool busy=false;int assists=0;bool Busy(Player*){return busy;}bool Assist(Player*,Unit*){++assists;return true;}}combat;auto sAdventureCombatMgr=&combat; struct ClientlessAction{std::string Name;std::unordered_map Parameters;}; struct Core {time_t activity=0;int queued=0;time_t LastActivity(ObjectGuid){return activity;}bool QueueAction(unsigned,ClientlessAction){++queued;return true;}}core;auto sClientlessPlayerMgr=&core; @@ -66,6 +66,7 @@ struct Travel {Nav nav;bool active=false,valid=true;int starts=0,cancels=0;Nav*G enum class ClientlessActionExecutionState{Moving,SelectingTarget,Completed};struct Execution {ClientlessActionExecutionState State=ClientlessActionExecutionState::Completed;}; struct Executor {Execution status;Execution*GetStatus(unsigned){return &status;}}executor;auto sClientlessActionExecutorMgr=&executor; struct Graph {void EnsureLoaded(){}bool FindNearestNodeToPosition(unsigned,float,float,float,std::string&node,float&distance){node="entrance";distance=10;return true;}}graph;auto sClientlessTravelGraphMgr=&graph; +struct BotGroups{void Offline(Player*){}void Update(Player*){}bool Accept(Player*,Player*){return false;}bool botLed=false;bool BotLed(Player*){return botLed;}bool Forming(Player*){return false;}bool PlannedTransition(Player*){return false;}void Report(ChatHandler*,Player*){}}botGroups;auto sAdventureGroupMgr=&botGroups; '''+header+'\n'+source+r''' int main(){ using namespace AdventureInteractionPolicy; @@ -102,12 +103,26 @@ int main(){ // Bot encounters reserve both participants; cooldown prevents new loops. other.team=ALLIANCE;bot.people={&other};mgr._records.clear();tick(2200);assert(mgr._records[26].Partner==other.guid && mgr._records[27].Partner==bot.guid); tick(2201);GameTime::now=2203;mgr.Update(&other,1000);assert(other.gestures==1);tick(2205);GameTime::now=2205;mgr.Update(&other,1000);tick(2265);assert(!mgr._records[26].Partner); - // Bot-origin invitations are declined; autonomous bot grouping is deferred. + // A native provisional leader also holds an invite; never answer his own invitation. + mgr._records.clear();group.leader=bot.guid;bot.invite=&group;tick(2270);tick(2275); + assert(bot.invite==&group);bot.invite=nullptr; + // Unreserved bot-origin invitations are declined. mgr._records.clear();group.leader=other.guid;bot.invite=&group;tick(2300);tick(2302);assert(!bot.group && declines==1); // Missing leader map uses the existing graph executor, without teleporting. group.leader=leader.guid;bot.group=&group;leader.mapid=1;tick(2400);assert(core.queued==1);executor.status.State=ClientlessActionExecutionState::Moving;tick(2416);assert(core.queued==1); // Pending help is acknowledged only outside combat, after caller's self-care. mgr.CombatHelp(&bot,leader.guid);leader.mapid=0;executor.status.State=ClientlessActionExecutionState::Completed;bot.combat=true;auto says=bot.says;tick(2417);assert(bot.says==says);bot.combat=false;tick(2418);assert(bot.says==says+1); + // A newly joined bot follower cancels its previous scene without stopping the leader. + bot.group=&group;group.leader=other.guid;botGroups.botLed=true;mgr._records.clear(); + GameTime::now=3000;int beforeInterrupts=behavior.interrupts;mgr.Update(&bot,1000); + assert(behavior.interrupts==beforeInterrupts+1); + // Bot-led groups must let the behavior clock advance every world tick. + bot.group=&group;group.leader=bot.guid;botGroups.botLed=true; + auto& rec=mgr._records[26];rec.Grouped=true;rec.SceneUntil=0;rec.Timer=1000; + assert(!mgr.Update(&bot,50)&&rec.Timer==950);botGroups.botLed=false; + // Explicit diagnostic leave uses ordinary safe-state guards. + bot.combat=true;assert(!mgr.RequestLeave(&bot)&&bot.group);bot.combat=false; + assert(mgr.RequestLeave(&bot)&&!bot.group); mgr.Offline(&bot);assert(!mgr._records.count(26)); } ''' diff --git a/tests/clientless/test_adventure_location_import.py b/tests/clientless/test_adventure_location_import.py index b08014c27d..8a3342700d 100644 --- a/tests/clientless/test_adventure_location_import.py +++ b/tests/clientless/test_adventure_location_import.py @@ -10,6 +10,7 @@ def function(text,sig): return text[a:e] code=r''' #include "AdventureEconomyPolicy.h" +#include "AdventureGroupPolicy.h" #include #include #include @@ -26,7 +27,9 @@ struct Field{ ''' code+=function(field,'uint8 Field::GetUInt8() const')+'\n'+function(field,'uint16 Field::GetUInt16() const')+'\n'+function(field,'uint32 Field::GetUInt32() const') code+=r''' -struct AdventureLocation{struct{uint32 MapId=0;std::string Category;}Point;bool IsArea=false;}; +struct AdventureLocation{struct{uint32 MapId=0;std::string Category;}Point;bool IsArea=false;uint32 InstanceMap=0,ZoneId=0,AreaId=0;}; +struct MapEntry{bool raid=false;bool IsDungeon(){return true;}bool IsRaid(){return raid;}}dungeon,raid{true};struct Maps{MapEntry*LookupEntry(unsigned id){return id==36?&dungeon:(id==409||id==999)?&raid:nullptr;}}sMapStore; +using Difficulty=unsigned;constexpr unsigned RAID_DIFFICULTY_25MAN_NORMAL=1;struct MapDifficulty{unsigned maxPlayers=40;}largeRaid,smallRaid{10};struct DBC{MapDifficulty*GetDownscaledMapDifficultyData(unsigned id,Difficulty&){return id==999?&smallRaid:&largeRaid;}}sDBCManager; struct AdventureLocationMgr{std::unordered_map> _types;float Weight(AdventureLocation const&,std::string const&)const;}; ''' code+=function(source,'float AdventureLocationMgr::Weight(') @@ -56,6 +59,14 @@ int main(){ for(auto category:{"zone","settlement","tavern","bank","service","landmark","wilderness","mine","workshop"}){ place.Point.Category=category;assert(mgr.Weight(place,"economy")==0);mgr._types[category]["economy"]=4;assert(mgr.Weight(place,"economy")==4);assert(mgr.Weight(place,"social")==0); } + place.Point.Category="instance_entrance";mgr._types[place.Point.Category]["instance"]=5;mgr._types[place.Point.Category]["raid"]=5; + place.InstanceMap=36;assert(mgr.Weight(place,"instance")==5&&mgr.Weight(place,"raid")==0); + place.InstanceMap=409;assert(mgr.Weight(place,"instance")==0&&mgr.Weight(place,"raid")==5); + place.InstanceMap=999;assert(mgr.Weight(place,"raid")==0); + place.InstanceMap=0;assert(mgr.Weight(place,"instance")==0&&mgr.Weight(place,"raid")==0); + place.Point.Category="landmark";mgr._types["landmark"]["groupformation"]=1; + place.ZoneId=12;assert(mgr.Weight(place,"groupformation")==0); + place.ZoneId=1519;assert(mgr.Weight(place,"groupformation")==1); // Edited weights, including an explicit zero, override economy defaults. place.Point.Category="settlement";mgr._types["settlement"]["economy"]=0; assert(mgr.Weight(place,"economy")==0);mgr._types["settlement"]["economy"]=2.75; diff --git a/tests/clientless/test_adventure_pvp.py b/tests/clientless/test_adventure_pvp.py new file mode 100644 index 0000000000..82ca451203 --- /dev/null +++ b/tests/clientless/test_adventure_pvp.py @@ -0,0 +1,67 @@ +"""Run the production PvP selector against faction, legality and navigation fixtures.""" +from pathlib import Path +import subprocess, tempfile +root=Path(__file__).resolve().parents[2] +s=(root/'src/server/game/World/AdventureCombatMgr.cpp').read_text() +a=s.index('Unit* AdventureCombatMgr::PvPTarget');b=s.index('Creature* AdventureCombatMgr::Proactive',a) +code=r''' +#include "AdventureCombatPolicy.h" +#include +#include +#include +#include +constexpr int ALLIANCE=0,POWER_MANA=0,FACTION_GROUP_MASK_HORDE=4,FACTION_GROUP_MASK_ALLIANCE=2; +struct Player;struct Creature; +struct Faction{unsigned FactionGroup=4;}; +struct Guid{int GetCounter(){return 29;}}; +struct Unit{unsigned level=60;float distance=10;bool valid=true,hostile=true,reachable=true;Faction faction; + virtual Player* ToPlayer(){return nullptr;}virtual Creature* ToCreature(){return nullptr;} + unsigned getLevel(){return level;}Faction*GetFactionTemplateEntry(){return &faction;}}; +struct Creature:Unit{bool controlled=false,elite=false,boss=false;Creature*ToCreature()override{return this;} + bool GetControllingPlayer(){return controlled;}bool isElite(){return elite;}bool isWorldBoss(){return boss;}}; +std::vector players;std::vector creatures; +struct Player:Unit{int team=1,powerType=1;float health=100;unsigned mana=100; + Player*ToPlayer()override{return this;}int GetTeam(){return team;}Guid GetGUID(){return {};} + float GetHealthPct(){return health;}int GetPowerType(){return powerType;}unsigned GetMaxPower(int){return 100;}unsigned GetPower(int){return mana;} + bool IsHostileTo(Unit*t){return t->hostile;}float GetExactDist(Unit*t){return t->distance;} + void GetPlayerListInGrid(std::vector&v,float){v=players;}void GetCreatureListWithEntryInGrid(std::vector&v,int,float){v=creatures;}}; +struct Profile{std::unordered_mapPersonality{{"adventurousness",1},{"danger_tolerance",1}};}profile; +struct Lua{Profile*GetAdventureProfile(int){return &profile;}}lua; +struct Behavior{std::string intent="adventure";std::string CombatIntent(Player*){return intent;}}behavior; +auto sClientlessLuaEngine=&lua;auto sAdventureBehaviorMgr=&behavior; +float roll=0;float frand(float,float){return roll;} +struct AdventureCombatMgr{bool Valid(Player*,Unit*t,bool)const{return t->valid;}bool Reachable(Player*,Unit*t)const{return t->reachable;} + Unit*PvPTarget(Player*)const;}; +'''+s[a:b]+r''' +int main(){ + using namespace AdventureCombatPolicy; + assert(PvPLevel(60,55)&&PvPLevel(60,65)&&!PvPLevel(60,54)&&!PvPLevel(60,66)); + assert(PvPLevel(1,6)&&!PvPLevel(1,7)); + assert(PvPWillingness("rest",1,1)==0&&PvPWillingness("",1,1)==0); + assert(PvPWillingness("adventure",0,1)==0); + assert(PvPWillingness("adventure",1,1)>PvPWillingness("economy",1,1)); + Player bot,enemy;bot.team=ALLIANCE;players={&enemy};AdventureCombatMgr mgr; + assert(mgr.PvPTarget(&bot)==&enemy); + for(unsigned level:{54u,66u}){enemy.level=level;assert(!mgr.PvPTarget(&bot));}enemy.level=55; + assert(mgr.PvPTarget(&bot)==&enemy);enemy.level=65;assert(mgr.PvPTarget(&bot)==&enemy); + enemy.distance=50;assert(mgr.PvPTarget(&bot)==&enemy);enemy.distance=50.1;assert(!mgr.PvPTarget(&bot));enemy.distance=10; + enemy.team=ALLIANCE;assert(!mgr.PvPTarget(&bot));enemy.team=1; + enemy.valid=false;assert(!mgr.PvPTarget(&bot));enemy.valid=true; + enemy.reachable=false;assert(!mgr.PvPTarget(&bot));enemy.reachable=true; + enemy.hostile=false;assert(!mgr.PvPTarget(&bot));enemy.hostile=true; + behavior.intent="rest";assert(!mgr.PvPTarget(&bot));behavior.intent="adventure"; + bot.health=59;assert(!mgr.PvPTarget(&bot));bot.health=100; + bot.powerType=POWER_MANA;bot.mana=29;assert(!mgr.PvPTarget(&bot));bot.mana=100; + roll=1;assert(!mgr.PvPTarget(&bot));roll=0; + Creature npc;npc.distance=5;creatures={&npc};assert(mgr.PvPTarget(&bot)==&npc); + npc.faction.FactionGroup=8;assert(mgr.PvPTarget(&bot)==&enemy);npc.faction.FactionGroup=4; + npc.elite=true;assert(mgr.PvPTarget(&bot)==&enemy);npc.elite=false; + npc.controlled=true;assert(mgr.PvPTarget(&bot)==&enemy);npc.controlled=false; + npc.level=54;assert(mgr.PvPTarget(&bot)==&enemy); +} +''' +with tempfile.TemporaryDirectory() as d: + src=Path(d)/'pvp.cpp';src.write_text(code);binary=Path(d)/'pvp' + subprocess.run(['g++','-std=c++17','-I'+str(root/'src/server/game/World'),str(src),'-o',str(binary)],check=True) + subprocess.run([str(binary)],check=True) +print('Production PvP selector: level boundaries, radius, factions, native legality, navigation, intent, personality and resources passed.') diff --git a/tests/clientless/test_clientless_social_notifications.py b/tests/clientless/test_clientless_social_notifications.py index 521e990290..b65f931bac 100644 --- a/tests/clientless/test_clientless_social_notifications.py +++ b/tests/clientless/test_clientless_social_notifications.py @@ -24,21 +24,22 @@ struct Player:Unit {Session session;Player(){owner=this;}Player*ToPlayer()overri struct ClientlessPlayerMgr { struct EmoteNotice{ObjectGuid Source;uint32 Emote;};struct HelpNotice{ObjectGuid Helper,Target;}; std::mutex _damageMutex;std::unordered_map> _emoteNotices;std::unordered_map> _helpNotices; + std::unordered_map> _groupJoinNotices;void GroupJoinRequest(Player*,Player*); void TextEmote(Player*,Player*,uint32);void SharedTargetDamage(Unit*,Unit*,uint32); }; -'''+function('void ClientlessPlayerMgr::TextEmote(')+'\n'+function('void ClientlessPlayerMgr::SharedTargetDamage(')+r''' +'''+function('void ClientlessPlayerMgr::TextEmote(')+'\n'+function('void ClientlessPlayerMgr::SharedTargetDamage(')+'\n'+function('void ClientlessPlayerMgr::GroupJoinRequest(')+r''' int main(){ ClientlessPlayerMgr mgr;std::vector bots(16);std::vector threads; Player helper;helper.guid={77};helper.session.clientless=false; for(unsigned i=0;i +#include #include #include #include @@ -48,7 +49,7 @@ struct Flags{bool fly=true;bool HasFlag(AreaMountFlags){return fly;}}; struct Area{bool fly=true;uint32 World_pvp_ID=0;Flags GetMountFlags()const{return {fly};}} area; struct Areas{bool known=true;auto LookupEntry(uint32){return known?&area:nullptr;}} areas;auto& sAreaTableStore=areas; enum class ClientlessFlightState{Mounting,Flying,Arrived,Failed}; -struct Record{ +struct Record{std::shared_ptrPublished=std::make_shared(); struct{ClientlessFlightState State=ClientlessFlightState::Flying;uint32 MapId=0;std::string Detail;}Status; bool Moving=true,Returning=false;std::vector Breadcrumbs{{}};std::string AbortReason; }; diff --git a/tests/clientless/test_free_flight.py b/tests/clientless/test_free_flight.py index 6d2a45a105..68ff718d69 100644 --- a/tests/clientless/test_free_flight.py +++ b/tests/clientless/test_free_flight.py @@ -58,6 +58,20 @@ int main(){ assert(firstDescent.Valid&&firstDescent.Next.X>660&&firstDescent.Next.Z<60); auto highApproach=simulate({0,0,70},{1,0,0},flat,clear); assert(highApproach.Arrived&&highApproach.Steps<20&&highApproach.Peak<=70); + // Convoy members replay exactly the published segments, retaining their own index. + SharedTrail trail;trail.Points={{20,0,30},{40,0,40}};std::size_t index=0;Settings settings; + auto shared=FollowTrail({0,0,0},{800,0,0},settings,trail,index,flat,clear); + assert(shared.Step.Valid&&shared.Step.Next.X==20&&index==0); + shared=FollowTrail({20,0,30},{800,0,0},settings,trail,index,flat,clear); + assert(shared.Step.Valid&&shared.Step.Next.X==40&&index==1); + shared=FollowTrail({40,0,40},{800,0,0},settings,trail,index,flat,clear); + assert(shared.Waiting&&index==2);trail.Points.push_back({60,0,40}); + shared=FollowTrail({40,0,40},{800,0,0},settings,trail,index,flat,clear); + assert(shared.Step.Valid&&shared.Step.Next.X==60); + + assert(!FollowTrail({40,0,40},{800,0,0},settings,trail,index,flat,blocked).Step.Valid); + trail.Aborted=true;shared=FollowTrail({40,0,40},{800,0,0},settings,trail,index,flat,clear); + assert(!shared.Step.Valid&&!shared.Waiting); std::cout<<"Flight geometry passed: takeoff/landing, smoothed peak, ceiling, lateral/climbing avoidance, missing terrain and blocked corridors.\n"; } ''' diff --git a/tests/clientless/test_ground_exit_continuation.py b/tests/clientless/test_ground_exit_continuation.py index 9623392780..cd0587b5ad 100644 --- a/tests/clientless/test_ground_exit_continuation.py +++ b/tests/clientless/test_ground_exit_continuation.py @@ -28,6 +28,7 @@ preamble = r''' #include #include #include +#include using uint8=uint8_t; using uint32=uint32_t; using uint64=uint64_t; using int32=int32_t; #define TC_GAME_API #define TC_LOG_INFO(...) ((void)0) diff --git a/tests/clientless/test_shared_group_route.py b/tests/clientless/test_shared_group_route.py new file mode 100644 index 0000000000..7e36dab70c --- /dev/null +++ b/tests/clientless/test_shared_group_route.py @@ -0,0 +1,52 @@ +"""Run the generic production route-copy function (no AdventureBot dependency).""" +from pathlib import Path +import subprocess,tempfile +root=Path(__file__).resolve().parents[2] +s=(root/'src/server/game/World/ClientlessActionExecutorMgr.cpp').read_text() +a=s.index('bool ClientlessActionExecutorMgr::AdoptRoute(') +code=r''' +#include +#include +#include +#include +#include +#include +#include +using uint32=unsigned; +#define TC_LOG_INFO(...) ((void)0) +struct Guid{unsigned id=0;unsigned GetCounter(){return id;}}; +struct Position{float x,y,z;void Relocate(float a,float b,float c){x=a;y=b;z=c;}}; +struct Group{Guid leader{29};Guid GetLeaderGUID(){return leader;}}group; +struct Player{bool grouped=true;unsigned map=0;Guid GetGUID(){return {31};}Group*GetGroup(){return grouped?&group:nullptr;}unsigned GetMapId(){return map;}float GetExactDist(Position const&p){return std::hypot(p.x,p.y);}}; +struct ClientlessAction{std::string Name;std::unordered_mapParameters;}; +struct ClientlessTravelRouteStep{std::string NodeKey,EdgeType;unsigned EdgeDatabaseId=0,MapId=0;float X=0,Y=0,Z=0;}; +struct Graph{bool allowed=true;bool PlanAdaptiveRoute(Player*,std::string const&node,int,int,std::vector&r,bool&complete,std::string&,std::string&){complete=true;r={{node,"start"}};return allowed;}}graph;auto sClientlessTravelGraphMgr=&graph; +struct ClientlessActionExecutorMgr{ + struct Record{std::vectorRoute;size_t RouteStep=0;bool FinalLocalLegPending=false,RouteComplete=true,HardDestination=false,Active=false; + unsigned FinalMapId=0,FinalDatabaseId=0;float FinalX=0,FinalY=0,FinalZ=0,FinalO=0;std::string FinalLocationId,PlannedDestinationNode,AdaptiveReachedNode;}; + std::unordered_map_records;int starts=0;bool StartRouteStep(Player*,unsigned,Record&){++starts;return true;} + bool AdoptRoute(Player*,ClientlessAction const&,Record&); +}; +'''+s[a:]+r''' +int main(){ + ClientlessActionExecutorMgr mgr;Player p;ClientlessAction a;a.Name="travel_to_location";a.Parameters={{"id","destination"},{"route_source","29"}}; + auto& lead=mgr._records[29];lead.Route={{"start","start",0,0,0,0,0},{"taxi_end","taxi",18,0,30,0,0},{"flight_end","free_flight",0,0,80,0,0}}; + lead.RouteStep=1;lead.FinalLocationId="destination";lead.FinalDatabaseId=77;lead.FinalX=90;lead.FinalLocalLegPending=true; + ClientlessActionExecutorMgr::Record peer; + assert(mgr.AdoptRoute(&p,a,peer));assert(peer.Route.size()==3&&peer.Route[1].EdgeType=="taxi"&&peer.Route[1].EdgeDatabaseId==18&&peer.Route[2].EdgeType=="free_flight"); + assert(peer.FinalLocationId=="destination"&&peer.FinalLocalLegPending&&peer.FinalX==90&&peer.FinalDatabaseId==77); + assert(peer.RouteStep==0&&peer.Active&&lead.RouteStep==1);peer.Route[1].NodeKey="changed";assert(lead.Route[1].NodeKey=="taxi_end"); + // Late arrival joins the active route at its previous reached node. + lead.RouteStep=2;assert(mgr.AdoptRoute(&p,a,peer));assert(peer.Route.size()==2&&peer.Route[0].NodeKey=="taxi_end"&&peer.Route[0].EdgeType=="start"&&peer.Route[1].EdgeType=="free_flight"); + p.map=1;assert(mgr.AdoptRoute(&p,a,peer));graph.allowed=false;assert(!mgr.AdoptRoute(&p,a,peer));graph.allowed=true;p.map=0; + // No cross-group route copying and no stale destination reuse. + group.leader={32};assert(!mgr.AdoptRoute(&p,a,peer));group.leader={29};p.grouped=false;assert(!mgr.AdoptRoute(&p,a,peer));p.grouped=true; + a.Parameters["id"]="different";assert(!mgr.AdoptRoute(&p,a,peer));a.Parameters["id"]="destination"; + lead.Route.clear();assert(!mgr.AdoptRoute(&p,a,peer)); +} +''' +with tempfile.TemporaryDirectory() as d: + p=Path(d);(p/'route.cpp').write_text(code) + subprocess.run(['g++','-std=c++17',str(p/'route.cpp'),'-o',str(p/'route')],check=True) + subprocess.run([str(p/'route')],check=True) +print('Production generic shared routes: same edges/taxi/free flight, independent execution state, final local goal, late catch-up, permissions and stale-goal rejection passed.')