Implement AdventureBot profiles, daily planning and population management
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s

This commit is contained in:
Nico committed 2026-10-28 18:22:13 +01:00
1 parent d6fea6ee68
commit d3273c677e
61 files changed
+2280 -116

No files matched your search

+120
View File
@@ -1,3 +1,123 @@
# Aktuell: Loader auf Nutzerauftrag für24 Bots aktiviert
Nutzer hat ausdrücklich aktive Testpopulation festgelegt: Allianz10 persistent
+2 temporär, Horde10 persistent+2 temporär. In aktiver Testserverkonfiguration
server/debug/etc/worldserver.conf gesetzt, Loader.Enabled1. Native reload config
und clientless population reload ausgeführt, kein Neustart/Build nötig.
Abschließend nativ bestätigt: alle vier Pools online10/2/10/2, pending0,
manager24, busy0, error leer. PID139425 weiterhin aktiv, Port8085 lauscht.
16 fehlende persistente Bots wurden schrittweise erzeugt/angemeldet;
bestehende temporäre Bots zählen mit.25 fertige Registry-Bots, davon24 online,
ein weiterer Allianz-Temp offline. Levelbereich1..85, Rotation15%/Mindestalter24h
und restliche Einstellungen unverändert. Loader AKTIV LASSEN, NICHT auf die
in den historischen Checkpoints beschriebenen deaktivierten Defaults setzen.
Keine weiteren Codeänderungen/Commits/Pushes für diese Konfigurationsaufgabe.
---
# Aktuell: Profile, Tagesplaner, Botmanager und Loader abgeschlossen
Nutzer beauftragte Umsetzung nach bestätigter Planung. Branch
feature/clientless-player-core war sauber. Änderungen noch NICHT committet;
kein Push. Vollständige Doku: doc/clientless/population.md.
Neue AdventurePopulationMgr/Policy mit vier unabhängigen Sollzahlen:
Allianz/Horde jeweils persistent/temporär. Normale World-Updates statt
Startup-Warten; verzögerte einzelne Anmeldungen, asynchrone Snapshots,
SRP6-Accountanlage, generischer Factory-Preflight und atomischer Save ohne
Future-Warten im Loader. Native Vorbereitung bleibt World-Thread; vorhandene
native Login-/Profil-/Katalogpfade enthalten weiterhin synchrone Reads.
Zufällige temporäre Rotation vor Login, Mindestalter und Chance konfigurierbar;
pro Kandidat einmal pro Prozess geprüft, keine erzwungene Online-Abmeldung.
Erst Ersatz speichern, dann Journal, nativ alten Bot löschen, World-Plan
bereinigen, zuletzt Profil/Supply-/Instanzstate/Journal. Persistente und
verbundene Charaktere geschützt. Accounts nur explizit eigene normale Accounts.
Profile v2 in Character-DB, weiterhin Lua-Sandbox/generated:GUID, keine Dateien
im Sourceprofilbaum. Level1..85/DK>=55, Klasse/Spec/Rolle/Seed, Heimat+Hauptstadt,
levelgerechtes Lieblingsgebiet, Aggression/Heimatbindung plus bisherige
Persönlichkeit. Gewichtete native Bodenmount-Displays/Flugmount- und Petspells,
Anzahlen und Inventar-Whitelist. Erweiterte kulturelle Namensgrammatiken.
Rollen beeinflussen native Spezialisierung/Ausrüstung; Tank-/Heiler-Kampf-AI
und volle Klassenrotationen weiterhin spätere Arbeit. Alte Generatorprofile
wurden beim Laden erweitert, Identität/alte Persönlichkeit/Berufe erhalten.
Keine handgeschriebenen Profile geändert, keine temporären Overrides.
Planer speichert24 vollständige Stunden-Slots vor Ausführung in bestehender
World-Tabelle. Gleiche Folge-Intents/-Locations teilen komplette Blockfrist.
Veraltete/unvollständige Tage werden atomisch ersetzt, gleiche Tage geladen.
Bestehender TINYINT-hour-Lesefehler GetUInt32->GetUInt8 behoben.
Heimatbindung, Neugier und Ziel-/Gebietswiederholungen beeinflussen Gewichte,
Gebietsbänder aus neuem editierbarem Katalog; Profillevel maßgeblich.
Auch neutral markierte Orte in Gegnerhauptstädten sind nun ausgeschlossen.
Manager nur für generierte versionierte Profile: sichere Zustände, Level/XP,
Training/Talente/Riding/fehlende oder unpassende Ausrüstung korrigieren.
Geburt und Erhaltung nutzen dieselbe Armor-/Stat-/Spell-/Level-/Use-Prüfung.
Volle Rucksäcke nativ leeren, angelegte Ausrüstung unberührt, aktuelle
Vorratsstacks/Whitelist/Werkzeuge/Leine/Angel/Taschen geschützt.
WICHTIGER Integrationsfix: alter ClientlessInstanceVisitResume startete alle
Instanzbesucher synchron in OnStartup und umging die neuen Quoten. Jetzt nur
asynchrones verzögertes/gepacetes Wiederanmelden NICHT-generierter Besucher
in OnUpdate. Generator-eigene Instanzbesucher laufen ausschließlich über die
Quoten. Fehlende Generator-Tabelle lässt alte manuelle Wiederaufnahme bestehen.
Nativ bewiesen: bei finalem Start keine Clientless-Anmeldung trotz gespeicherter
Instanzvisite36, während Loader deaktiviert/alle Sollzahlen0 waren.
Migrationen world/characters2026_10_29_00 auf TEST-DB zweimal angewendet
(78 Gebiete). Beide SQL-Dateien wegen Ignore explizit vorgemerkt.
75/75 Tests im abschließenden Lauf erfolgreich; Debug-j10 gebaut/installiert.
Letzter Testserver PID139425: frische Ready nach11s, Prozess und Port8085
bestätigt. Native Tests: beide Fraktionen Level1 und85, vier getrennte Pools,
Stufe60-Schutzkrieger37 mit Tankprofil/Schutzbaum/Waffe+Schild; Level1->5
administrativ verändert und auf1 korrigiert, Planhash unverändert; alter
Testtag ersetzt; gleiche Pläne nach echtem Neustart bytegleich.
Rotation37->38: alter Nativechar/Profil/Plan/Supply-/Instanzstate weg,
Journal leer. Bestehende29..32 und retired27/28 erhalten. Native Inventarhashes
für menschlichen21 und Arthoran26 vor/nach identisch.
Eigene neue normale Accounts7 (Allianz) /8 (Horde). Neue persistente Testbots:
33 Tinkbin Gnom-Krieger1,34 Sylaria Blutelf-Schurkin1,
35 Karggan Zwergen-Priester85,36 Edwin Untoten-Krieger85;
38 Renaarna Worgen-Priesterin85 temporär. Diese fünf abschließend OFFLINE.
Testbot37 Elaisia wurde ausdrücklich für Rotation erzeugt und nativ gelöscht.
36 behält gespeicherte Instanz-Anreise; beim nächsten freigegebenen Login
normaler vorhandener Instanz-/Zeitbudget-Abgleich. Kein automatischer Start
unter deaktiviertem Loader. Arthoran26 bleibt OFFLINE/unverändert.
29 Aelion und31 Lenara wieder ONLINE in Goldhain, Intents gestoppt,
schedule0 und grouped0. Vor Login nativ offline GameTele435 positioniert,
nach Profilinitialisierung per Watcher gestoppt, keine Profiländerung dafür.
30 Zanzan nativ zu GameTele823 SenjinVillage/Durotar zurückgesetzt,32 Aldren
zu GameTele1040 TheDeadField/Silberwald nahe ursprünglichem NPC-Ziel;
beide online mit ursprünglichen Social-Zielen823/poi_npc_321922 angefordert.
Das waren administrative Test-Rückstellungen, kein Reisebeleg.
Aktive Testkonfiguration dauerhaft auf Defaults zurückgesetzt:
Loader.Enabled0, alle vier Sollzahlen0, Min1/Max85, Rotation15%, Mindestalter24h,
Intervall2000ms, Manager300s. Bereits manuell angemeldete vier Bots bleiben
online. Konfigurationsänderung: reload config, dann clientless population reload.
Diagnose population status/reload, generate profile GUID, adventure plan GUID.
Kein Commit/Push dieses neuen Arbeitsstands beauftragt.
Zwei während Tests gefundene native Abstürze behoben: LOGIN_INS_ACCOUNT braucht
CONNECTION_BOTH für asynchrones SRP6-Insert; Security-Abfrage nutzt tatsächliche
AccountID/SecurityLevel-Spalten. Item-Spell-Sentinel4294967295 erreichte
ApplyEquipCooldown-Assert; Generator schließt fehlende Spellmetadaten aus,
nativer Cooldown-Code prüft GetSpellInfo statt Assert. Factory-Shutdown gibt
wartende Offline-Players vor Map-Unload frei. Ein bekannter temporärer Auth-DB-
Ausfall10.0.0.1 wurde durch SIGTERM des Fehlstarts und zweiten Start überstanden;
keine Remote-Infrastrukturänderung. Aktuell Testserver bereit.
Restliche Abnahme: große1000er-Last, visuelle Reisen/Interaktionen und volle
Klassen-/Rollen-Kampflogik. Es wurden bestehende TravelGraph/MMAP-Fallbacks und
fehlende Map719-Terraindaten beobachtet; Navigationsüberarbeitung bleibt auf
Nutzerwunsch vertagt. Nicht als erfolgreich geprüfte Fernreise ausgeben.
Belege: checkpoints/2026-10-28-population/; Logs /tmp/vcore-population-*.log.
DB-Helfer /tmp/vcore-population-db.py (Credentials nur aus lokaler Konfiguration,
keine Secrets ausgeben). Originals- und Dayhash-Vergleiche in Checkpoint.
---
# Aktuell: Automatische Botgruppen, Raid und Gruppenbildung
Nutzer beauftragte Umsetzung der gemeinsam bestätigten Planung. Vollständige
@@ -0,0 +1,11 @@
Clientless.Adventure.Loader.Enabled = 0
Clientless.Adventure.Loader.Alliance.Persistent = 0
Clientless.Adventure.Loader.Alliance.Temporary = 0
Clientless.Adventure.Loader.Horde.Persistent = 0
Clientless.Adventure.Loader.Horde.Temporary = 0
Clientless.Adventure.Loader.IntervalMs = 2000
Clientless.Adventure.Loader.MinLevel = 1
Clientless.Adventure.Loader.MaxLevel = 85
Clientless.Adventure.Loader.RotationPercent = 15
Clientless.Adventure.Loader.RotationMinAgeHours = 24
Clientless.Adventure.Manager.IntervalSeconds = 300
@@ -0,0 +1,20 @@
zones
78
Table Create Table
vcore_adventure_day CREATE TABLE `vcore_adventure_day` (\n `character_guid` int(10) unsigned NOT NULL,\n `local_date` int(10) unsigned NOT NULL,\n `hour_slot` tinyint(3) unsigned NOT NULL,\n `intent` varchar(32) NOT NULL,\n `location_key` varchar(100) NOT NULL,\n PRIMARY KEY (`character_guid`,`local_date`,`hour_slot`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
character_guid name level race class online persistent state profile_bytes
27 Dagri 60 3 1 0 0 retired 871
28 Jarana 60 8 1 0 0 retired 870
29 Aelion 60 4 1 1 0 ready 1761
30 Zanzan 60 8 1 1 0 ready 1749
31 Lenara 60 22 8 1 0 ready 1740
32 Aldren 60 5 6 1 0 ready 1765
33 Tinkbin 1 7 1 0 1 ready 1456
34 Sylaria 1 10 4 0 1 ready 1478
35 Karggan 85 3 5 0 1 ready 1530
36 Edwin 85 5 1 0 1 ready 1543
38 Renaarna 85 22 5 0 0 ready 1497
account_id faction
7 1
8 2
@@ -0,0 +1,4 @@
TrinityCore rev. d6fea6ee6831+ 2026-10-28 15:00:41 +0100 (feature/clientless-player-core branch) (Unix, Debug, Static) (worldserver-daemon)
World initialized in 0 minutes 11 seconds
TrinityCore rev. d6fea6ee6831+ 2026-10-28 15:00:41 +0100 (feature/clientless-player-core branch) (Unix, Debug, Static) (worldserver-daemon) ready...
No clientless sessions auto-started with all four targets zero, despite saved generated instance visit36.
@@ -0,0 +1,25 @@
World initialized in 0 minutes 11 seconds
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'
Interactions guid=29 grouped=0 leader=0 partner=0 intent='' schedule=''
Interactions guid=31 grouped=0 leader=0 partner=0 intent='' schedule=''
TC> Adventure GUID 30: enabled=1 schedule=0 phase=1 intent='social' location='poi_npc_300050' micro='' deadline=1793208319 detail='travelling to poi_npc_300050'
TC> Adventure GUID 32: enabled=1 schedule=0 phase=1 intent='social' location='poi_npc_321922' micro='' deadline=1793208319 detail='waiting for safe player/transport state'
TC> Adventure loader enabled=0 schema=0 busy=0 pools=0/0/0/0 known=0 rotations=0 manager=4 error=''
pool=0 faction=alliance persistent=1 target=0 online=0 pending=0 saved=0
pool=1 faction=alliance persistent=0 target=0 online=0 pending=0 saved=0
pool=2 faction=horde persistent=1 target=0 online=0 pending=0 saved=0
pool=3 faction=horde persistent=0 target=0 online=0 pending=0 saved=0
TC> All players saved.
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'
Interactions guid=29 grouped=0 leader=0 partner=0 intent='' schedule=''
Interactions guid=31 grouped=0 leader=0 partner=0 intent='' schedule=''
TC> Adventure GUID 30: enabled=1 schedule=0 phase=2 intent='social' location='poi_tele_823' micro='social_comment' deadline=1793208765 detail='social scene: comment'
TC>Adventure GUID 32: enabled=1 schedule=0 phase=2 intent='social' location='poi_npc_321922' micro='social_listen' deadline=1793208766 detail='social scene: listen'
TC> Adventure loader enabled=0 schema=0 busy=0 pools=0/0/0/0 known=0 rotations=0 manager=4 error=''
pool=0 faction=alliance persistent=1 target=0 online=0 pending=0 saved=0
pool=1 faction=alliance persistent=0 target=0 online=0 pending=0 saved=0
pool=2 faction=horde persistent=1 target=0 online=0 pending=0 saved=0
pool=3 faction=horde persistent=0 target=0 online=0 pending=0 saved=0
TC> All players saved.
@@ -0,0 +1,5 @@
TC>Adventure loader enabled=1 schema=1 busy=0 pools=10/2/10/2 known=25 rotations=0 manager=24 error=''
pool=0 faction=alliance persistent=1 target=10 online=10 pending=0 saved=10
pool=1 faction=alliance persistent=0 target=2 online=2 pending=0 saved=3
pool=2 faction=horde persistent=1 target=10 online=10 pending=0 saved=10
pool=3 faction=horde persistent=0 target=2 online=2 pending=0 saved=2
@@ -0,0 +1,117 @@
guid name level talentTree persistent state
33 Tinkbin 1 0 0 1 ready
34 Sylaria 1 0 0 1 ready
35 Karggan 85 795 0 1 ready
36 Edwin 85 746 0 1 ready
37 Elaisia 60 845 0 0 ready
guid slot itemEntry
33 0 63278
33 1 27945
33 2 59113
33 4 3733
33 5 65705
33 6 6087
33 7 57417
33 8 60907
33 9 15463
33 10 56714
33 11 59116
33 12 12846
33 13 40483
33 14 60927
33 15 65687
33 16 56691
33 19 54443
33 20 54443
33 21 54443
33 22 54443
34 0 9978
34 1 27945
34 2 59112
34 4 61050
34 5 59091
34 6 56699
34 7 2033
34 8 60929
34 9 60939
34 10 57717
34 11 59114
34 12 12846
34 13 40483
34 14 65721
34 15 56679
34 16 6504
34 17 28979
34 19 54443
34 20 54443
34 21 54443
34 22 54443
35 0 75073
35 1 58699
35 2 75091
35 4 75088
35 5 75086
35 6 75072
35 7 75095
35 8 75098
35 9 75063
35 10 75068
35 11 56432
35 12 58614
35 13 56351
35 14 75065
35 15 63461
35 17 65172
35 19 54443
35 20 54443
35 21 54443
35 22 54443
36 0 75126
36 1 75078
36 2 75119
36 4 75135
36 5 75123
36 6 75136
36 7 75120
36 8 75124
36 9 75122
36 10 58648
36 11 75071
36 12 56132
36 13 73164
36 14 75077
36 15 56342
36 19 54443
36 20 54443
36 21 54443
36 22 54443
37 0 77867
37 1 21809
37 2 21805
37 4 23300
37 5 22197
37 6 13498
37 7 31125
37 8 22205
37 9 77734
37 10 12058
37 11 19873
37 12 19947
37 13 21488
37 14 22337
37 15 19104
37 16 18756
37 19 54443
37 20 54443
37 21 54443
37 22 54443
guid spells
33 24
34 15
35 109
36 110
37 85
guid talent_spells
35 22
36 21
37 13
@@ -0,0 +1,3 @@
guid items inventory_hash
21 26 c684ea37abcbdce68e151d144c823fb43abd94f0ec860e03e35738845e37172a
26 24 e1094ed84b20084243dac5338d88814eb54c3bc4ddd3ed64a1a4bca455013a93
@@ -0,0 +1,3 @@
guid items inventory_hash
21 26 c684ea37abcbdce68e151d144c823fb43abd94f0ec860e03e35738845e37172a
26 24 e1094ed84b20084243dac5338d88814eb54c3bc4ddd3ed64a1a4bca455013a93
@@ -0,0 +1,11 @@
guid name online map position_x position_y position_z
26 Arthoran 0 0 -11683.5 -2317.07 25.1635
29 Aelion 1 0 -9448.55 68.236 56.3225
30 Zanzan 1 1 -826.364 -4879.64 19.5037
31 Lenara 1 0 -9448.55 68.236 56.3225
32 Aldren 1 0 1082.5 1578.14 27.8342
33 Tinkbin 0 0 -5144.9 453.528 393.232
34 Sylaria 0 530 8713 -6649.58 72.7513
35 Karggan 0 0 -8622.47 407.783 102.924
36 Edwin 0 0 -7065.2 -2602.84 256.074
38 Renaarna 0 0 -8733.04 543.374 101.123
@@ -0,0 +1,6 @@
character_guid local_date slots day_hash
33 20261028 24 ae72eb55c08e2d19c7de777e50778181a1ec37e7a7ca26aeb576b1255bcd0571
34 20261028 24 ba3246dc3dbffed593e26672ec4d15dace181d2bd8b121fa9b34e2a96b60a964
35 20261028 24 5fe768de3439ffc9adf00b9694f9bcdd5e91f1a2d1aa46724d04173300203536
36 20261028 24 81c6f2da21e74edda3ff32e25cb3af0764f8ec13461e8a40d6efcf816a749cd0
37 20261028 24 5d0052af0910553f70e9ccf411da0386b58e8911a63d2ff540358cb1fe8d1f3f
@@ -0,0 +1,6 @@
character_guid local_date slots day_hash
33 20261028 24 ae72eb55c08e2d19c7de777e50778181a1ec37e7a7ca26aeb576b1255bcd0571
34 20261028 24 ba3246dc3dbffed593e26672ec4d15dace181d2bd8b121fa9b34e2a96b60a964
35 20261028 24 5fe768de3439ffc9adf00b9694f9bcdd5e91f1a2d1aa46724d04173300203536
36 20261028 24 81c6f2da21e74edda3ff32e25cb3af0764f8ec13461e8a40d6efcf816a749cd0
37 20261028 24 5d0052af0910553f70e9ccf411da0386b58e8911a63d2ff540358cb1fe8d1f3f
@@ -0,0 +1,13 @@
Profile/planner/population implementation complete on feature/clientless-player-core.
75/75 production-code regression scripts passed; final Debug j10 built and installed.
Worldserver139425 ready after11s, port8085 listening. No push/commit of this WIP.
Loader disabled; four targets0, level1..85, rotation15%, minimum age24h.
Aelion29 and Lenara31 manually online/stopped/ungrouped in Goldshire.
Zanzan30 and Aldren32 restored to native Durotar/Silverpine home anchors/social goals.
New33..36 persistent and38 temporary offline. Dedicated37 rotated/deleted.
Native level1/85 both factions, protection60, profile-level repair, day relog/restart,
expired-day replacement and native rotation/profile+day cleanup verified.
Generated saved instance visitor36 does not auto-start under disabled/zero quotas;
legacy unowned recovery remains asynchronous and paced. Existing21/26 inventory hashes match.
Large populations, visual gameplay, complete role AI and navigation improvements await acceptance.
See population.md and SESSION-RESUME.md for contracts, caveats and exact state.
@@ -0,0 +1,167 @@
TEST tests/clientless/test_adventure_area_node.py
Production area destination excludes outside and unreachable graph nodes.
TEST tests/clientless/test_adventure_async_database.py
Async creation DB contracts: prepared connection masks, native security column names, no future waits, successful-save cache publication and world-update-only loader hook passed
TEST tests/clientless/test_adventure_behavior_policy.py
24-hour generation, shared intents with changing destinations, broad location classification and repetition penalties passed.
TEST tests/clientless/test_adventure_combat_exercise.py
Production exercise: stance/charge/melee/casts, native validation delegation, bounded lifetime, invalid targets and unchanged native phases passed.
TEST tests/clientless/test_adventure_combat_preparation.py
Production talent learner: native tiers/dependencies/class gates, all 26 legal level60 points, no capstone bypass and repeat safety passed.
TEST tests/clientless/test_adventure_combat_reactions.py
Production combat focus, damage ordering, risk filters, corpse radius/delay, flight/group override, blocked escape, danger avoidance and malus-free persisted fallback passed.
TEST tests/clientless/test_adventure_day_persistence.py
Production day planner: all 24 TINYINT slots restored across logins, incomplete/old-day replacement, full transaction and curiosity/level weighting passed
TEST tests/clientless/test_adventure_economy.py
Economy: native service roles, friendly/phase/LOS/bounds gates, no bot service targets and coherent performative cues passed.
TEST tests/clientless/test_adventure_economy_catalog.py
Economy catalog: eleven actions, three faction-bound service anchors, native flag gates and idempotent preservation passed.
TEST tests/clientless/test_adventure_equipment_maintenance.py
Production gear maintenance: same birth/retention gate rejects wrong armor/stats, malformed effects, level, temporary, reputation, map, holiday, native-use and slot mismatches passed
TEST tests/clientless/test_adventure_exploration.py
Exploration: visible native props only, no chests/traps/furniture, phase/map/LOS/range gates and contextual preparation passed.
TEST tests/clientless/test_adventure_exploration_catalog.py
Exploration catalog: ten actions, shared scenes, default wander replacement, explicit category eligibility and preserved edits passed.
TEST tests/clientless/test_adventure_generator_equipment.py
Production equipment affinity: mana-only/direct/triggered procs, caster/physical stat effects and bounded cycles passed
TEST tests/clientless/test_adventure_generator_ownership.py
Production generator commands: batch bounds, dedicated ownership, persistent/online/removing retirement protection and retired login refusal passed
TEST tests/clientless/test_adventure_generator_policy.py
Production generator: 6144 cultural names and 61440 real Lua profiles, profession coherence, personality ranges, escaping and equipment/armor gates passed
TEST tests/clientless/test_adventure_graph_handover.py
Production graph handover waits for ship/taxi/flight/teleport and stops at safe road/node boundaries.
TEST tests/clientless/test_adventure_group_plan.py
Production group plans: leader meeting/goal/deadline inheritance, untouched personal day, manual preparation and instance-state guards passed.
TEST tests/clientless/test_adventure_groups.py
Production recruitment: capitals, different goals, native inviter leadership, joining existing groups, capacity/reservations, raid conversion, planned transition and local contact passed.
TEST tests/clientless/test_adventure_instance.py
Production instance lifecycle: time budget, true entry, denied/shared entry, native rear exit, restart, expired copy, late arrival, HP and return replay passed.
TEST tests/clientless/test_adventure_instance_autoresume.py
Deferred instance script: missing schema/visits stays idle; no login work on startup; saved visitors resume with Adventure controllers and native accounts.
TEST tests/clientless/test_adventure_instance_begin.py
Production entrance travel uses the existing location planner, requires mapping and retains normal social/area travel.
TEST tests/clientless/test_adventure_instance_catalog.py
Instance migrations: outside entrances only, native graph preserved, disabled/missing transitions excluded, edits and saved visits preserved.
TEST tests/clientless/test_adventure_instance_loader.py
Production instance recovery: no startup DB/login work, asynchronous delayed/paced manual recovery, generated-bot quota ownership, absent-schema compatibility and existing preparation ticks passed
TEST tests/clientless/test_adventure_instance_persistence.py
Production instance persistence: phases, original deadline, instance ID, transactional HP marker, failed commit and intent release passed.
TEST tests/clientless/test_adventure_intent_state.py
Production intent arrival, failed/partial travel, deadlines and safe schedule handover passed.
TEST tests/clientless/test_adventure_interactions.py
/tmp/tmpwuo364tp/test.cpp: In member function ‘void AdventureInteractionMgr::Leave(Player*, Record&, const char*)’:
/tmp/tmpwuo364tp/test.cpp:142:69: warning: unused parameter ‘reason’ [-Wunused-parameter]
142 | void AdventureInteractionMgr::Leave(Player* p,Record& r,char const* reason)
| ~~~~~~~~~~~~^~~~~~
production interaction manager: invitations, factions, reservations, follow, AFK and deferred departure passed
TEST tests/clientless/test_adventure_leisure.py
Leisure: substantial curved MMAP routes, mirrored return, interrupted return, rejected corridors, actual arrival, companion follow, riding, flight and fishing passed.
TEST tests/clientless/test_adventure_leisure_catalog.py
Leisure migration: native city anchor, shared scene references, pet-walk mapping and preservation of edited weights passed.
TEST tests/clientless/test_adventure_location_import.py
Locations: actual Debug SMALLINT reads retain continent IDs; explicit location eligibility and edited weight overrides passed.
TEST tests/clientless/test_adventure_manager_inventory.py
Production inventory maintenance: only full backpacks, native stack trimming, level-appropriate supplies, tools, leash, fishing pole, containers and untouched equipped slots passed
TEST tests/clientless/test_adventure_micro_reset.py
Micro cleanup ends physical local movement, preserves transit and does not stand up dead players.
TEST tests/clientless/test_adventure_population.py
Production population: four independent faction/persistence targets, fair selection, pending reservations, retries, existing inventory, once-per-boot rotation and connected/persistent protection passed
TEST tests/clientless/test_adventure_profession_catalog.py
Profession catalog: native workstation roles, no workshop/resource inference, idempotence and preserved edits passed.
TEST tests/clientless/test_adventure_profession_profile.py
Production profession parser, real Lua profiles, primary restrictions and secondary permissions passed.
TEST tests/clientless/test_adventure_profession_sightline.py
Native workstation working-surface sightline and blocked geometry rejection passed.
TEST tests/clientless/test_adventure_profession_targets.py
Production profession targets: native locks, phase/spawn/loot, stations, corpses, history and trainers passed.
TEST tests/clientless/test_adventure_profile_selection.py
Production profile selection: real Lua ID dictionaries, exact 9:1 companion weighting, zero/negative/infinite/NaN/oversized weights rejected and legacy companion fallback passed
TEST tests/clientless/test_adventure_pvp.py
Production PvP selector: level boundaries, radius, factions, native legality, navigation, intent, personality and resources passed.
TEST tests/clientless/test_adventure_repair_catalog.py
Repair action catalog: proper native service requirement, economy-only intent and preserved administrator weights passed.
TEST tests/clientless/test_adventure_rest.py
Recovery: authored places, bounds, arrival, occupancy, full rest duration, failure, reload and interruption passed.
TEST tests/clientless/test_adventure_rest_fire.py
Rest fire: shared scene mapping, additive migration, preserved weights and authored creation-place policy passed.
TEST tests/clientless/test_adventure_rest_space.py
Sleep footprint: furniture, body extent, overhead collision, ledges, stairs, missing heights, rotation and scale passed.
TEST tests/clientless/test_adventure_rest_targets.py
Native sleep targets: chair exclusion, phase, despawn, floor search, indoors, water, storey, occupancy, bed/surface preference passed.
TEST tests/clientless/test_adventure_rotation.py
/tmp/tmpn8583ajg/test.cpp: In instantiation of ‘std::string Trinity::StringFormat(const char*, A ...) [with A = {}; std::string = std::__cxx11::basic_string<char>]’:
/tmp/tmpn8583ajg/test.cpp:22:126: required from ‘void Tx::PAppend(const char*, A ...) [with A = {}]’
22 | struct Tx{std::vector<std::string>sql;template<class...A>void PAppend(char const*f,A...a){sql.push_back(Trinity::StringFormat(f,a...));}};
| ~~~~~~~~~~~~~~~~~~~~~^~~~~~~~
/tmp/tmpn8583ajg/test.cpp:51:75: required from here
51 | auto barrier=CharacterDatabase.BeginTransaction();barrier->PAppend("DO 0");
| ~~~~~~~~~~~~~~~~^~~~~~~~
/tmp/tmpn8583ajg/test.cpp:11:113: warning: format not a string literal and no format arguments [-Wformat-security]
11 | namespace Trinity{template<class...A>std::string StringFormat(char const* f,A...args){char b[4096];std::snprintf(b,sizeof(b),f,args...);return b;}}
| ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~
Production rotation: connected/native-online protection, ownership refusal, native delete failure, world cleanup failure and journal-retained retry before successful profile/plan/state removal passed
TEST tests/clientless/test_adventure_social.py
Production social flow: approach before talk, coherent greeting/talk/listen/farewell, lost targets, native chair confirmation and real fire confirmation passed.
TEST tests/clientless/test_adventure_social_fire.py
Shared fire: native detection/provisioning, buildings/city/parent/zone restrictions, existing fires and outdoor/water checks passed.
TEST tests/clientless/test_adventure_supplies.py
Production supplies: exact10% boundary, bandage-before-food, native lockout fallback, recovery completion, combat potions, mana-only drinking and daily/post-combat top-ups passed.
TEST tests/clientless/test_adventure_supply_catalog.py
Supply migrations: all five roles, level60 netherweave tier, editable/disabled catalog preservation and persistent daily stamp passed.
TEST tests/clientless/test_adventure_supply_items.py
Production native-item delegation: usable bandage tiers, native cooldown/lockout gates, real inventory consumption, self targeting and level60 secondary caps passed.
TEST tests/clientless/test_adventure_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_item_equip_cooldown.py
Native item equip cooldown: zero/sentinel/missing spells safely skipped, valid use cooldown preserved and longer/disabled cooldowns untouched passed
TEST tests/clientless/test_local_spline_segments.py
Production local spline clipping passes native path-length validation, tiny remainders and 1000 sampled paths.
TEST tests/clientless/test_lua_reload.py
Lua reload regression tests passed (production reload/bindings, real Lua 5.4).
TEST tests/clientless/test_player_pure_max_skill.py
Native pure skill cap: learned skills return stored cap; missing/deleted/zero-rank skills return0.
TEST tests/clientless/test_portal_transfer.py
Portal near/far ACK and denied same-map transfer regressions passed.
TEST tests/clientless/test_portal_use.py
Interactive portal native click, near/far/global ACK, denial, timeout and target checks passed.
Native portal destination discovery and unsupported/ambiguous spell rejection passed.
Production clientless ACK completion sequence increments only after completed near/far transfers.
TEST tests/clientless/test_shared_group_route.py
Production generic shared routes: same edges/taxi/free flight, independent execution state, final local goal, late catch-up, permissions and stale-goal rejection passed.
TEST tests/clientless/test_taxi_import_disabled.py
Taxi importer preserves disabled rows and their reasons.
TEST tests/clientless/test_transport.py
Transport dock, frame, recorder, cross-map copy and ride regressions passed
TEST tests/clientless/test_transport_grid_loading.py
Core transport grid initialization regressions passed (no CPC dependency).
TOTAL 75 FAILED []
@@ -0,0 +1,41 @@
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=='apply':
for kind,name in [('World','sql/custom/world/2026_10_29_00_adventure_population.sql'),('Character','sql/custom/characters/2026_10_29_00_adventure_population.sql')]:
print(query(kind,(root/name).read_text()))
elif mode=='audit':
print(query('World',"SELECT COUNT(*) AS zones FROM vcore_adventure_zone; SHOW CREATE TABLE vcore_adventure_day;"))
print(query('Character',"SELECT g.character_guid,c.name,c.level,c.race,c.class,c.online,g.persistent,g.state,LENGTH(g.profile_lua) profile_bytes FROM vcore_adventure_generated_bot g JOIN characters c ON c.guid=g.character_guid ORDER BY c.guid; SELECT * FROM vcore_adventure_rotation; SELECT * FROM vcore_adventure_bot_account;"))
elif mode=='originals':
print(query('Character',"SELECT ci.guid,COUNT(*) items,SHA2(GROUP_CONCAT(CONCAT(ci.item,':',ii.itemEntry) ORDER BY ci.item SEPARATOR ','),256) inventory_hash FROM character_inventory ci JOIN item_instance ii ON ii.guid=ci.item WHERE ci.guid IN(21,26) GROUP BY ci.guid;"))
elif mode=='dayhash':
print(query('World',"SELECT character_guid,local_date,COUNT(*) slots,SHA2(GROUP_CONCAT(CONCAT(hour_slot,':',intent,':',location_key) ORDER BY hour_slot SEPARATOR '|'),256) day_hash FROM vcore_adventure_day WHERE character_guid BETWEEN 33 AND 50 GROUP BY character_guid,local_date;"))
elif mode=='details':
print(query('Character',"SELECT c.guid,c.name,c.level,c.talentTree,g.persistent,g.state FROM characters c JOIN vcore_adventure_generated_bot g ON g.character_guid=c.guid WHERE c.guid>=33; SELECT ci.guid,ci.slot,i.itemEntry FROM character_inventory ci JOIN item_instance i ON i.guid=ci.item WHERE ci.guid>=33 AND ci.bag=0 AND ci.slot<23 ORDER BY ci.guid,ci.slot; SELECT s.guid,COUNT(*) spells FROM character_spell s WHERE s.guid>=33 GROUP BY s.guid; SELECT t.guid,COUNT(*) talent_spells FROM character_talent t WHERE t.guid>=33 GROUP BY t.guid;"))
elif mode=='expire37':
row=query('Character',"SELECT c.online FROM characters c JOIN vcore_adventure_generated_bot g ON c.guid=g.character_guid WHERE c.guid=37 AND g.persistent=0 AND g.state='ready';")
if row.strip().splitlines()!=['online','0']:raise SystemExit('Test bot37 must be offline and temporary')
print(query('World',"UPDATE vcore_adventure_day SET local_date=20261027 WHERE character_guid=37; SELECT character_guid,local_date,COUNT(*) slots FROM vcore_adventure_day WHERE character_guid=37 GROUP BY character_guid,local_date;"))
elif mode=='positions':
print(query('Character',"SELECT guid,name,online,map,position_x,position_y,position_z FROM characters WHERE guid IN(26,29,30,31,32,33,34,35,36,38);"))
elif mode=='visits':
print(query('Character',"SELECT character_guid,phase,block_end,location_key FROM vcore_adventure_instance_visit ORDER BY character_guid;"))
elif mode=='homes':
print(query('World',"SELECT id,name,map,position_x,position_y,position_z FROM game_tele WHERE id=823 OR name LIKE '%sepul%' OR name='Brill'; SELECT point_key,map,position_x,position_y,position_z FROM vcore_clientless_world_point WHERE point_key='poi_npc_321922';"))
elif mode=='near_home':
print(query('World',"SELECT id,name,map,position_x,position_y,position_z FROM game_tele WHERE map=0 ORDER BY POW(position_x-1097.57,2)+POW(position_y-1587.29,2) LIMIT 3;"))
elif mode=='zone_names':
print(query('World',"SELECT DISTINCT zone_id,region_key FROM vcore_adventure_location WHERE zone_id IN(210,33,5287,5288,616,4709,493,2817,4755,5144,5145,4815,11,51,3) ORDER BY zone_id;"))
elif mode=='plans':
print(query('World',"SELECT character_guid,local_date,COUNT(*) slots,MIN(hour_slot),MAX(hour_slot),COUNT(DISTINCT location_key) locations FROM vcore_adventure_day GROUP BY character_guid,local_date;"))
else:raise SystemExit('unknown mode')
@@ -0,0 +1,58 @@
BOT_LOADER_LOGIN guid=33 pool=0
Clientless player character GUID 33 (account 7) reached state=Online.
DAY_PLAN guid=33 date=20261028 slots=24
BOT_LOADER_LOGIN guid=34 pool=2
Clientless player character GUID 34 (account 8) reached state=Online.
DAY_PLAN guid=34 date=20261028 slots=24
TC>Adventure loader enabled=1 schema=1 busy=0 pools=1/0/1/0 known=6 rotations=0 manager=2 error=''
pool=0 faction=alliance persistent=1 target=1 online=1 pending=0 saved=1
pool=1 faction=alliance persistent=0 target=0 online=0 pending=0 saved=2
pool=2 faction=horde persistent=1 target=1 online=1 pending=0 saved=1
pool=3 faction=horde persistent=0 target=0 online=0 pending=0 saved=2
TC> Adventure loader enabled=1 schema=1 busy=1 pools=2/0/2/0 known=6 rotations=0 manager=2 error=''
pool=0 faction=alliance persistent=1 target=2 online=1 pending=0 saved=1
pool=1 faction=alliance persistent=0 target=0 online=0 pending=0 saved=2
pool=2 faction=horde persistent=1 target=2 online=1 pending=0 saved=1
pool=3 faction=horde persistent=0 target=0 online=0 pending=0 saved=2
BOT_LOADER_LOGIN guid=35 pool=0
Clientless player character GUID 35 (account 7) reached state=Online.
DAY_PLAN guid=35 date=20261028 slots=24
BOT_LOADER_LOGIN guid=36 pool=2
Clientless player character GUID 36 (account 8) reached state=Online.
DAY_PLAN guid=36 date=20261028 slots=24
TC>Adventure loader enabled=1 schema=1 busy=0 pools=2/0/2/0 known=8 rotations=0 manager=4 error=''
pool=0 faction=alliance persistent=1 target=2 online=2 pending=0 saved=2
pool=1 faction=alliance persistent=0 target=0 online=0 pending=0 saved=2
pool=2 faction=horde persistent=1 target=2 online=2 pending=0 saved=2
pool=3 faction=horde persistent=0 target=0 online=0 pending=0 saved=2
TC>You changed level of Tinkbin to 5.
BOT_LOADER_LOGIN guid=33 pool=0
Clientless player character GUID 33 (account 7) reached state=Online.
TC> Adventure loader enabled=1 schema=1 busy=1 pools=2/2/2/2 known=8 rotations=0 manager=4 error=''
pool=0 faction=alliance persistent=1 target=2 online=2 pending=0 saved=2
pool=1 faction=alliance persistent=0 target=2 online=0 pending=0 saved=2
pool=2 faction=horde persistent=1 target=2 online=2 pending=0 saved=2
pool=3 faction=horde persistent=0 target=2 online=0 pending=0 saved=2
BOT_LOADER_LOGIN guid=29 pool=1
Clientless player character GUID 29 (account 6) reached state=Online.
DAY_PLAN guid=29 date=20261028 slots=24
BOT_LOADER_LOGIN guid=30 pool=3
Clientless player character GUID 30 (account 6) reached state=Online.
DAY_PLAN guid=30 date=20261028 slots=24
BOT_LOADER_LOGIN guid=31 pool=1
Clientless player character GUID 31 (account 6) reached state=Online.
DAY_PLAN guid=31 date=20261028 slots=24
BOT_LOADER_LOGIN guid=32 pool=3
Clientless player character GUID 32 (account 6) reached state=Online.
DAY_PLAN guid=32 date=20261028 slots=24
Generated Elaisia guid=37 race=1 class=1 level=60 archetype=warrior persistent=0 seed=2 home=poi_npc_280710 (offline).
TC>return { identity = { name = "Elaisia", home_region = "elwynn", profile_version = 2, level = 60, class = 1, specialization = 2, role = "tank", seed = 2, home_zone = 12, home_capital = 1519, favorite_zone = 1377, favorite_region = "silithus", vocation = "Wandernder Waffenschmied", background = "Herkunft: Elwynn. Sucht auf Reisen Arbeit, neue Wege und Begegnungen." }, behavior = { enabled = true, daily_schedule = true, initial_intent = 'social', initial_location = "poi_npc_280710", initial_duration_seconds = 3600, locale = 'deDE' }, professions = { primary = { "mining", "blacksmithing" } }, personality = { sociability = 0.786, adventurousness = 0.328, aggression = 0.575, home_attachment = 0.418, wanderlust = 0.713, group_loyalty = 0.603, tavern_affinity = 0.311, wilderness_affinity = 0.457, city_affinity = 0.593, danger_tolerance = 0.496 }, region_affinity = { ["elwynn"] = 2.5 }, zone_affinity = { [12] = 4.0, [1377] = 5.0, [1519] = 4.0, }, activity_affinity = { social = 0.589, adventure = 0.761, profession = 0.483, leisure = 0.267, economy = 0.351, rest = 0.513, instance = 0.643, raid = 0.180 }, inventory = { keep_items = {37460,6256,5956,2901,7005,6217} }, movement = { can_ground_mount = true, ground_mount_speed_rate = 2, ground_mounts = {[2404]=1,[2402]=2,[2409]=1,}, ground_mount_count = 3, can_free_fly = true, flight_mounts = {[32235]=2,[32239]=10,[32240]=6,}, flight_mount_count = 3, companions = {[10673]=1,}, companion_count = 1, companion_leash_item = 37460 } }
Clientless player character GUID 37 (account 7) reached state=Online.
DAY_PLAN guid=37 date=20261028 slots=24
Clientless player character GUID 37 (account 7) reached state=Online.
DAY_PLAN guid=37 date=20261028 slots=24
TC> Adventure loader enabled=0 schema=1 busy=0 pools=2/2/2/2 known=9 rotations=0 manager=8 error=''
pool=0 faction=alliance persistent=1 target=2 online=2 pending=0 saved=2
pool=1 faction=alliance persistent=0 target=2 online=2 pending=0 saved=3
pool=2 faction=horde persistent=1 target=2 online=2 pending=0 saved=2
pool=3 faction=horde persistent=0 target=2 online=2 pending=0 saved=2
@@ -0,0 +1,41 @@
Clientless player character GUID 36 (account 8) reached state=Online.
BOT_LOADER_LOGIN guid=33 pool=0
Clientless player character GUID 33 (account 7) reached state=Online.
BOT_LOADER_LOGIN guid=29 pool=1
Clientless player character GUID 29 (account 6) reached state=Online.
BOT_LOADER_LOGIN guid=34 pool=2
Clientless player character GUID 34 (account 8) reached state=Online.
BOT_LOADER_LOGIN guid=30 pool=3
Clientless player character GUID 30 (account 6) reached state=Online.
BOT_LOADER_LOGIN guid=35 pool=0
Clientless player character GUID 35 (account 7) reached state=Online.
BOT_LOADER_LOGIN guid=31 pool=1
Clientless player character GUID 31 (account 6) reached state=Online.
BOT_LOADER_LOGIN guid=32 pool=3
Clientless player character GUID 32 (account 6) reached state=Online.
TC>Adventure loader enabled=1 schema=1 busy=0 pools=2/2/2/2 known=9 rotations=0 manager=8 error=''
pool=0 faction=alliance persistent=1 target=2 online=2 pending=0 saved=2
pool=1 faction=alliance persistent=0 target=2 online=2 pending=0 saved=3
pool=2 faction=horde persistent=1 target=2 online=2 pending=0 saved=2
pool=3 faction=horde persistent=0 target=2 online=2 pending=0 saved=2
TC> Adventure loader enabled=1 schema=0 busy=1 pools=2/3/2/2 known=9 rotations=0 manager=8 error=''
pool=0 faction=alliance persistent=1 target=2 online=2 pending=0 saved=2
pool=1 faction=alliance persistent=0 target=3 online=2 pending=0 saved=3
pool=2 faction=horde persistent=1 target=2 online=2 pending=0 saved=2
pool=3 faction=horde persistent=0 target=2 online=2 pending=0 saved=2
BOT_ROTATED_REMOVED guid=37
BOT_LOADER_LOGIN guid=38 pool=1
Clientless player character GUID 38 (account 7) reached state=Online.
DAY_PLAN guid=38 date=20261028 slots=24
TC> Adventure loader enabled=0 schema=1 busy=0 pools=0/0/0/0 known=9 rotations=0 manager=9 error=''
pool=0 faction=alliance persistent=1 target=0 online=2 pending=0 saved=2
pool=1 faction=alliance persistent=0 target=0 online=3 pending=0 saved=3
pool=2 faction=horde persistent=1 target=0 online=2 pending=0 saved=2
pool=3 faction=horde persistent=0 target=0 online=2 pending=0 saved=2
TC> Clientless player character GUID 29 (account 6) reached state=Online.
TC>Clientless player character GUID 31 (account 6) reached state=Online.
TC>Adventure loader enabled=0 schema=1 busy=0 pools=0/0/0/0 known=9 rotations=0 manager=4 error=''
pool=0 faction=alliance persistent=1 target=0 online=0 pending=0 saved=2
pool=1 faction=alliance persistent=0 target=0 online=2 pending=0 saved=3
pool=2 faction=horde persistent=1 target=0 online=0 pending=0 saved=2
pool=3 faction=horde persistent=0 target=0 online=2 pending=0 saved=2
+9 -8
View File
@@ -20,7 +20,7 @@ clientless generate retire <guid>
The short create form defaults to level60, any faction/archetype, persistent,
and a time-based seed. When specifying options, supply all five optional values.
Count is1–10 per request, level10–85 (also limited by server maximum), faction is
Count is1–10 per request, level1–85 (also limited by server maximum), faction is
`any`, `alliance` or `horde`, and persistence is `persistent` or `temporary`.
Each character is committed separately. A failed batch reports its actual
completed count and retains already completed characters.
@@ -38,10 +38,10 @@ an error can stop an online refresh partway through. The initial offline create
pipeline is the atomic operation.
Creation leaves bots offline. Explicit login uses the existing AdventureBot Lua
controller. Initially they perform a local social intent for one hour at their
home. Enable the existing daily planner with `clientless adventure schedule
<guid> on` when desired. Rotation and automatic population targeting are not
activated by this change.
controller. New profiles enable the persistent daily planner automatically.
The optional four-pool loader, temporary replacement and profile maintenance are
documented in [population.md](population.md); automatic loading remains disabled
by default.
## Identity and profile
@@ -88,7 +88,7 @@ The AdventureBot layer supplies:
- Native class-trainer spells, using the same read-only `CanTeachSpell` predicate
as ordinary native teaching; weapon/armor proficiencies are race/class/level
checked. Owned weapon skills are raised to their native level cap.
- A solo damage specialization and valid talent allocation through native talent
- A profile specialization/role and valid talent allocation through native talent
prerequisites, tiers, ranks and primary-tree gates. Arms uses the existing
explicit build, other archetypes use a legal tier-ordered baseline. These
generic builds are not claimed to be optimized raid/PvP builds.
@@ -110,8 +110,9 @@ The AdventureBot layer supplies:
- Existing daily/post-combat supply restocking upon login. Consumables are native
items and continue to use the existing actual item/cooldown logic.
No mounts, pets or optimized class rotations are introduced here. Existing
non-warrior combat limitations remain. Being fully trained is distinct from
Weighted native mount/companion catalogs and riding are now generated; see
[population.md](population.md). Optimized class rotations remain outside this
feature. Existing non-warrior combat limitations remain. Being fully trained is distinct from
having class-specific automated combat/support AI.
## Persistence and future rotation
+195
View File
@@ -0,0 +1,195 @@
# AdventureBot profiles, daily plans and population
The generator owns its characters explicitly in `vcore_adventure_generated_bot`.
Lua stays in the character database and is loaded through the existing sandbox
as `generated:<guid>`. Hand-written named profiles retain precedence. Neither
names nor account prefixes establish ownership.
## Profile version 2
`identity` records name, level, numeric native class and specialization index,
`role` (`tank`, `healer`, `damage`), seed, racial home zone/capital, favorite
adventure zone/region and RP origin/vocation/background. Race/class combinations
remain native and names use cultural grammars with native validation.
Generation supports levels 1–85, limited by the configured native maximum.
Death knights require at least 55. Primary profession rights are already in the
profile at low levels, but training waits for native level eligibility. Talents
begin at 10. Warriors/paladins may use protection, death knights blood; priests,
shamans and druids can receive healing specifications. Role metadata and native
specialization/equipment are provided, **not complete tank/healer combat AI**.
Existing class rotation limitations still apply.
Personality includes aggression, adventurousness, wanderlust, home attachment,
sociability, group loyalty, tavern/wilderness/city affinities and danger tolerance.
Activities and regional preferences are weighted. Low aggression reduces only
proactive attacks, never existing self-defence. Group support and the absolute
no-flee rule for grouped bots remain intact.
`movement.ground_mounts` contains weighted native creature display IDs for the
existing ground adapter; `flight_mounts` and `companions` contain weighted spell
IDs. Corresponding counts are emitted for inspection; parsed dictionary sizes
are authoritative. Ground mounts begin at 20, flight at 60; native geography,
spell/collision restrictions and flight licenses still apply. Companion choice
is rerolled at the beginning of a walk, not during an active scene. Generation
checks available native spell/model data. `inventory.keep_items` protects tools
and other explicitly permitted backpack items.
Older generated profiles are upgraded on their next load, preserving their
name/background, personality and professions. The baseline native level becomes
the fixed profile level during this one-time upgrade. Hand-written profiles are
not rewritten. Existing stored day plans are retained.
## Geography and daily plans
Apply `sql/custom/world/2026_10_29_00_adventure_population.sql`. Its editable
`vcore_adventure_zone` catalog describes outdoor leveling bands and capital
accessibility for Cataclysm. Reapplying preserves operator edits. Unknown outdoor
bands receive a low planning weight. Regions more than five levels above the bot
are excluded; lower-level home visits remain possible, while adventure favors
level-appropriate regions. Favorite adventure regions exclude capitals.
Racial home/capital preferences are strong during town activities. Curiosity
flattens preference weights and increases location/region variety. These remain
weights among legal targets, not exclusive destinations. Existing native travel
and MMAP validation still determines whether a planned destination is reachable;
existing failure recovery can select an alternative if a route fails.
The existing `vcore_adventure_day` world table persists exactly 24 hourly slots
per character/date. Every slot contains both intent and destination **before**
execution. An intent spans one or more complete civil hours; destination changes
inside it are also preplanned. Consecutive equal intent/destination slots share
the end of their complete block, including instance timing. Travel consumes this
budget. Group preparation still uses its leader's meeting location and subsequent
goal. Runtime group adoption does not overwrite members' personal plans.
Dates and hour boundaries use the server process's local timezone, including
native `mktime` DST handling. Same-day relog/restart restores all 24 slots. An
incomplete or outdated day is atomically replaced and old dates removed. The
controller refreshes the day even while combat/recovery temporarily owns visible
behavior. On the DST fall-back day a repeated civil hour uses the same slot; a
skipped spring hour has no extra synthetic activity.
## Loader configuration
Apply `sql/custom/characters/2026_10_29_00_adventure_population.sql` as well.
Defaults are deliberately disabled and have four zero online targets:
```ini
Clientless.Adventure.Loader.Enabled = 0
Clientless.Adventure.Loader.Alliance.Persistent = 0
Clientless.Adventure.Loader.Alliance.Temporary = 0
Clientless.Adventure.Loader.Horde.Persistent = 0
Clientless.Adventure.Loader.Horde.Temporary = 0
Clientless.Adventure.Loader.IntervalMs = 2000
Clientless.Adventure.Loader.MinLevel = 1
Clientless.Adventure.Loader.MaxLevel = 85
Clientless.Adventure.Loader.RotationPercent = 15
Clientless.Adventure.Loader.RotationMinAgeHours = 24
Clientless.Adventure.Manager.IntervalSeconds = 300
```
Each target is clamped to 0–5000. Zero suppresses new automated logins for that
pool; changing a target does not forcibly evict already connected bots. Persistent
and temporary Alliance/Horde inventories never substitute for one another.
Loading/Removing sessions reserve capacity. A failed saved bot is retried with
backoff instead of generating unbounded replacements.
Loading starts during normal world updates, with at least a ten-second initial
delay. It is not part of mandatory server initialization. One login/generation
operation is initiated per interval with round-robin pool selection. Snapshot,
account registration, creation preflight and atomic save use polled asynchronous
database callbacks; the loader never waits on a future. Native character setup,
training/equipping and native deletion still execute on the world thread, one
operation at a time. Existing native login/profile/catalog routines contain
synchronous reads; this is not a claim of a completely asynchronous game core.
Saved generated instance visitors also use these quotas. The former synchronous
WorldScript startup loop no longer starts them independently. Legacy manually
created visitors retain asynchronous, delayed and paced recovery; no generator
schema is required for that legacy path. This prevents saved visits from bypassing
a zero/disabled generated population.
Failures suspend/retry the loader and never request server shutdown. Large
populations need separate latency/memory/CPU load acceptance.
When saved inventory is insufficient, new normal accounts with random unknown
credentials are registered using native SRP6 and prepared account inserts.
`vcore_adventure_bot_account` records ownership and faction. Native account
expansion/security/session checks and per-realm character limits are enforced.
Accounts are retained when bots rotate. External/player accounts are not
converted or deleted.
## Temporary rotation and maintenance
Rotation is considered once per offline temporary candidate per process, before
login, with configurable chance/minimum age. It remains in the same faction and
temporary pool. First create and atomically save a replacement on an available
owned account (a new account can provide spare capacity). Then journal the pair
in `vcore_adventure_rotation` and mark the old bot `replacing`. Only a ready saved
replacement permits native permanent deletion. Persistent, connected and
non-owned characters cannot pass the deletion checks. Failed creation retains
the original. The journal resumes cleanup after a restart.
Cleanup verifies native character removal, then commits deletion of the old
world day plan before deleting profile, supply/instance state and the journal.
A failed world cleanup retains the journal for retry. Retired characters from
older manual `retire` operations are retained; retirement is not implicitly
converted to deletion.
The manager acts on versioned generated profiles only. At login and on staggered
periodic checks it restores fixed level/XP, native class abilities, profession
ranks, available talents, riding and profile equipment. Wrong class is reported,
not converted. Dead/combat/dungeon/transit/casting states defer mutation. A full
backpack is cleaned through native item destruction; equipped slots are untouched.
One stack of each currently appropriate supply, bags, fishing poles and explicit
profile tools/leash survive. Full inventories containing only permitted items
are retained. Missing/invalid gear is repaired through the native equip checks,
not rerolled unconditionally every interval. Ordinary player characters and
hand-written Arthoran profiles are outside this maintenance ownership boundary.
Console diagnostics:
```text
clientless population status
clientless population reload
clientless generate profile <guid>
clientless adventure plan <guid>
```
After editing the configuration, use the native `reload config` before
`clientless population reload` (or restart). Reload is refused while a population
operation is pending. Disabling the loader does not disable profile maintenance
for already online generated bots. No forced logout occurs on disable.
## Technical verification and remaining acceptance
Production-code tests cover daily persistence with the real TINYINT hour shape,
four pool quotas/retries, replacement ownership and cleanup failures, weighted
Lua dictionaries/companions, full-inventory protection, shared birth/maintenance
gear eligibility, native malformed item-spell cooldown safety and asynchronous
prepared-statement contracts. Existing clientless regressions are retained.
On the test server, both factions received persistent bots at levels 1 and 85.
All four pool targets were independently reached. A level-60 protection warrior
was generated with its tank profile, native protection tree and weapon/shield.
An administrative level1 -> level5 change was corrected to profile level1 on
relogin; the saved day hash did not change. An expired test day was replaced by
24 current-date slots, and same-day plans survived a complete server restart.
A dedicated temporary test character37 was replaced by38; native character,
profile, day and supply/instance state were removed, with no remaining journal.
Existing characters29–32 were retained. Inventory ownership hashes for human21
and Arthoran26 remained unchanged.
Two native defects exposed during these tests were fixed: the account insertion
statement needed asynchronous registration, and an item sentinel spell ID could
reach an asserting equip-cooldown lookup. The generator now rejects missing item
spell data and native equip cooldowns skip missing spells. Unowned characters
retain their prior native behavior for valid item data.
The test also encountered existing route failures involving unavailable map719
terrain and NPC/graph destinations. Those use existing fallback behavior; the
MMAP/obstacle/TravelGraph overhaul remains deferred. Neutral-tagged markers in
opposing faction capitals are now excluded explicitly from destination selection.
A large population (for example 1,000 concurrent bots), travel quality, class-role
combat behavior and visual gameplay have not been accepted by these technical
checks. The loader is restored to disabled/zero targets after testing.
@@ -0,0 +1,12 @@
-- Explicit ownership of automatically created ordinary bot accounts.
CREATE TABLE IF NOT EXISTS vcore_adventure_bot_account (
account_id INT UNSIGNED NOT NULL PRIMARY KEY,
faction TINYINT UNSIGNED NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Durable replacement journal. New native character and profile must exist
-- before the original temporary character can enter the deletion phase.
CREATE TABLE IF NOT EXISTS vcore_adventure_rotation (
old_guid INT UNSIGNED NOT NULL PRIMARY KEY,
new_guid INT UNSIGNED NOT NULL,
UNIQUE KEY replacement(new_guid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@@ -0,0 +1,22 @@
-- Cataclysm outdoor leveling bands. Editable; repeated application preserves edits.
CREATE TABLE IF NOT EXISTS vcore_adventure_zone (
zone_id INT UNSIGNED NOT NULL PRIMARY KEY,
min_level TINYINT UNSIGNED NOT NULL,
max_level TINYINT UNSIGNED NOT NULL,
enabled TINYINT UNSIGNED NOT NULL DEFAULT 1
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT IGNORE INTO vcore_adventure_zone (zone_id,min_level,max_level) VALUES
(1,1,10),(12,1,10),(14,1,10),(85,1,10),(141,1,10),(215,1,10),
(3430,1,10),(3524,1,10),(3433,10,20),(3525,10,20),(38,10,20),(40,10,15),
(130,10,20),(17,10,20),(148,10,20),(16,10,20),(44,15,20),(267,20,25),
(10,20,25),(331,20,25),(406,25,30),(11,20,25),(36,30,35),(45,25,30),
(33,25,30),(5287,30,35),(47,30,35),(210,77,80),(405,30,35),(357,35,40),(15,35,40),(4709,30,35),
(3,44,48),(51,47,51),(440,45,50),(400,40,45),(361,45,50),
(490,50,55),(46,49,52),(618,50,55),(28,35,40),(139,40,45),
(4,54,60),(1377,55,60),(3483,58,63),(3521,60,64),(3519,62,65),
(3518,64,67),(3522,65,68),(3523,67,70),(3520,67,70),
(3537,68,72),(495,68,72),(65,71,74),(394,73,75),(66,74,77),(3711,76,78),
(67,77,80),(2817,77,80),(4197,77,80),
(616,80,82),(4922,84,85),(5034,83,84),(5042,82,83),(5144,80,82),
(4815,80,82),(5145,80,82),(8,50,55),(1519,1,85),(1537,1,85),(1657,1,85),
(1637,1,85),(1638,1,85),(1497,1,85),(3487,1,85),(3557,1,85),(3703,60,85),(4395,68,85);
@@ -60,7 +60,7 @@ void LoginDatabaseConnection::DoPrepareStatements()
PrepareStatement(LOGIN_DEL_REALM_CHARACTERS, "DELETE FROM realmcharacters WHERE acctid = ?", CONNECTION_ASYNC);
PrepareStatement(LOGIN_REP_REALM_CHARACTERS, "REPLACE INTO realmcharacters (numchars, acctid, realmid) VALUES (?, ?, ?)", CONNECTION_ASYNC);
PrepareStatement(LOGIN_SEL_SUM_REALM_CHARACTERS, "SELECT SUM(numchars) FROM realmcharacters WHERE acctid = ?", CONNECTION_ASYNC);
PrepareStatement(LOGIN_INS_ACCOUNT, "INSERT INTO account(username, salt, verifier, reg_mail, email, joindate) VALUES(?, ?, ?, ?, ?, NOW())", CONNECTION_SYNCH);
PrepareStatement(LOGIN_INS_ACCOUNT, "INSERT INTO account(username, salt, verifier, reg_mail, email, joindate) VALUES(?, ?, ?, ?, ?, NOW())", CONNECTION_BOTH);
PrepareStatement(LOGIN_INS_REALM_CHARACTERS_INIT, "INSERT INTO realmcharacters (realmid, acctid, numchars) SELECT realmlist.id, account.id, 0 FROM realmlist, account LEFT JOIN realmcharacters ON acctid = account.id WHERE acctid IS NULL", CONNECTION_ASYNC);
PrepareStatement(LOGIN_UPD_EXPANSION, "UPDATE account SET expansion = ? WHERE id = ?", CONNECTION_ASYNC);
PrepareStatement(LOGIN_UPD_ACCOUNT_LOCK, "UPDATE account SET locked = ? WHERE id = ?", CONNECTION_ASYNC);
+3 -2
View File
@@ -23215,7 +23215,8 @@ void Player::ApplyEquipCooldown(Item* pItem)
for (ItemEffect const& effect : pItem->GetTemplate()->Effects)
{
// no spell
if (effect.SpellID <= 0)
SpellInfo const* itemSpell = sSpellMgr->GetSpellInfo(effect.SpellID);
if (!itemSpell)
continue;
// apply proc cooldown to equip auras if we have any
@@ -23235,7 +23236,7 @@ void Player::ApplyEquipCooldown(Item* pItem)
continue;
// Don't replace longer cooldowns by equip cooldown if we have any.
if (GetSpellHistory()->GetRemainingCooldown(sSpellMgr->AssertSpellInfo(effect.SpellID)) > 30 * IN_MILLISECONDS)
if (GetSpellHistory()->GetRemainingCooldown(itemSpell) > 30 * IN_MILLISECONDS)
continue;
GetSpellHistory()->AddCooldown(effect.SpellID, pItem->GetEntry(), std::chrono::seconds(30));
+44 -11
View File
@@ -186,17 +186,25 @@ void AdventureBehaviorMgr::Online(Player * player)
TC_LOG_ERROR("server.adventurebot", "ADVENTURE_DISABLED guid=%u missing catalogs", player->GetGUID().GetCounter());
return;
}
if (profile->CompanionSpell)
ClientlessCompanionMgr::Prepare(player,profile->CompanionSpell,profile->CompanionLeashItem);
for(auto const& pet:profile->Companions)ClientlessCompanionMgr::Prepare(player,pet.DisplayId,profile->CompanionLeashItem);
if (profile->CompanionSpell)ClientlessCompanionMgr::Prepare(player,profile->CompanionSpell,profile->CompanionLeashItem);
Record r;
r.Enabled = true;
r.Schedule = profile->DailySchedule;
r.Random.seed(player->GetGUID().GetCounter() ^ uint32(GameTime::GetGameTime()));
auto & record = _records.insert_or_assign(player->GetGUID().GetCounter(), std::move(r)).first->second;
if(record.Schedule)Generate(player,record,DateOf(Local(GameTime::GetGameTime())));
if (RestoreInstance(player,record)) return;
if (! record.Schedule && Known(profile->InitialIntent)) SetWanted(player, record, profile->InitialIntent, profile->InitialLocation, GameTime::GetGameTime() + profile->InitialDurationSeconds);
}
void AdventureBehaviorMgr::RefreshDay(Player* player)
{
auto it=_records.find(player->GetGUID().GetCounter());if(it==_records.end() || !it->second.Schedule)return;
auto date=DateOf(Local(GameTime::GetGameTime()));
if(it->second.Date!=date){Generate(player,it->second,date);it->second.Hour=-1;}
}
void AdventureBehaviorMgr::Offline(Player * player)
{
auto i = _records.find(player->GetGUID().GetCounter());
@@ -228,7 +236,10 @@ void AdventureBehaviorMgr::IntentChanged(Player * player, ClientlessIntent const
bool AdventureBehaviorMgr::Allowed(Player * player, AdventureLocation const & location, std::string const & intent) const
{
auto map = sMapStore.LookupEntry(location.Point.MapId);
return map && map->MapType == MAP_COMMON && sAdventureLocationMgr->Weight(location, intent) > 0 &&(! location.Faction || location.Faction ==(player->GetTeamId() == TEAM_ALLIANCE ? 1 : 2));
unsigned side=player->GetTeamId()==TEAM_ALLIANCE?1:2;
unsigned capital=AdventureGroupPolicy::CapitalFaction(location.ZoneId?location.ZoneId:location.AreaId);
return map && map->MapType==MAP_COMMON && (!capital || capital==side) &&
sAdventureLocationMgr->Weight(location,intent)>0 && (!location.Faction || location.Faction==side);
}
std::string AdventureBehaviorMgr::PickLocation(Player * player, Record & record, std::string const & intent)
@@ -243,10 +254,26 @@ std::string AdventureBehaviorMgr::PickLocation(Player * player, Record & record,
{
if (! Allowed(player, l, intent)) continue;
choices.push_back(& l);
weights.push_back(sAdventureLocationMgr->Weight(l, intent) * Affinity(profile->RegionAffinity, l.Region, 0.2f) * Affinity(profile->ActivityAffinity, l.Point.Category, 1.0f));
auto zone=profile->ZoneAffinity.find(l.ZoneId);
double region=zone==profile->ZoneAffinity.end()?Affinity(profile->RegionAffinity,l.Region,1.0f):zone->second;
float curiosity=Affinity(profile->Personality,"adventurousness",0.5f);
bool outing=intent=="adventure" || intent=="profession" || intent=="instance" || intent=="raid";
double home=(!outing && (l.ZoneId==profile->HomeZone || l.ZoneId==profile->HomeCapital || l.Region==profile->HomeRegion))?
1+12*Affinity(profile->Personality,"home_attachment",0.5f):1;
double setting=AdventureGroupPolicy::Capital(l.ZoneId)?Affinity(profile->Personality,"city_affinity",0.5f):
l.Point.Category=="tavern"?Affinity(profile->Personality,"tavern_affinity",0.5f):
l.IsArea?Affinity(profile->Personality,"wilderness_affinity",0.5f):0.5f;
weights.push_back(sAdventureLocationMgr->Weight(l,intent)*RegionWeight(region,curiosity)*home*(0.25+setting)*
LevelWeight(profile->Level?profile->Level:player->getLevel(),l.MinLevel,l.MaxLevel,outing)*
Affinity(profile->ActivityAffinity,l.Point.Category,1.0f)*
std::pow(Novelty(record.TargetHistory,l.Point.Id),0.5+curiosity)*
std::pow(Novelty(record.PlanRegions,l.Region),outing?0.25+curiosity:0.1));
}
auto n = Choose(weights, record.Random);
return n < choices.size() ? choices[n]->Point.Id : "";
if(n>=choices.size())return "";
Remember(record.TargetHistory,choices[n]->Point.Id);
Remember(record.PlanRegions,choices[n]->Region);
return choices[n]->Point.Id;
}
void AdventureBehaviorMgr::Generate(Player * player, Record & record, uint32 date)
@@ -257,7 +284,7 @@ void AdventureBehaviorMgr::Generate(Player * player, Record & record, uint32 dat
if (auto rows = WorldDatabase.PQuery("SELECT hour_slot,intent,location_key FROM vcore_adventure_day WHERE character_guid=%u AND local_date=%u ORDER BY hour_slot", guid, date)) do
{
auto f = rows->Fetch();
uint32 h = f[0].GetUInt32();
uint32 h = f[0].GetUInt8();
if (h < 24 && Known(f[1].GetString()))
{
record.Plan[h] =
@@ -268,7 +295,11 @@ void AdventureBehaviorMgr::Generate(Player * player, Record & record, uint32 dat
}
}
while (rows->NextRow());
if (loaded == 24) return;
if (loaded == 24)
{
WorldDatabase.PExecute("DELETE FROM vcore_adventure_day WHERE character_guid=%u AND local_date<>%u",guid,date);
return;
}
auto profile = sClientlessLuaEngine->GetAdventureProfile(guid);
if (! profile) return;
auto pick =[&]()
@@ -291,13 +322,14 @@ void AdventureBehaviorMgr::Generate(Player * player, Record & record, uint32 dat
AdventureGroupPolicy::Prepare(record.Plan,record.Random,Affinity(profile->Personality,"sociability",0.5f),
[&](){return PickLocation(player,record,"groupformation");});
auto tx = WorldDatabase.BeginTransaction();
tx->PAppend("DELETE FROM vcore_adventure_day WHERE character_guid=%u",guid);
for (unsigned h = 0; h < 24; ++ h)
{
auto key = record.Plan[h].Location;
WorldDatabase.EscapeString(key);
tx->PAppend("REPLACE INTO vcore_adventure_day (character_guid,local_date,hour_slot,intent,location_key) VALUES (%u,%u,%u,'%s','%s')", guid, date, h, record.Plan[h].Intent.c_str(), key.c_str());
}
WorldDatabase.DirectCommitTransaction(tx);
WorldDatabase.CommitTransaction(tx);
TC_LOG_INFO("server.adventurebot", "DAY_PLAN guid=%u date=%u slots=24", guid, date);
}
@@ -573,7 +605,7 @@ bool AdventureBehaviorMgr::LeisureAvailable(Player* p,Record const& r,AdventureM
}
// A seat/meal may leave the bot inside. Foot outings must still be able
// to navigate back outside; only mounted outings require an outdoor start.
if (a.Requirement=="pet_walk") return ClientlessCompanionMgr::Available(p,profile->CompanionSpell);
if (a.Requirement=="pet_walk") return (!profile->Companions.empty() || ClientlessCompanionMgr::Available(p,profile->CompanionSpell));
if (a.Requirement=="walk" || a.Requirement=="roam") return true;
if (!p->IsOutdoors()) return false;
if (a.Requirement=="ride") return profile->CanGroundMount && !profile->GroundMounts.empty();
@@ -721,7 +753,7 @@ bool AdventureBehaviorMgr::StartLeisure(Player* p,Record& r,AdventureMicroAction
s.Route.push_back(s.Origin);
}
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
if (s.Kind=="pet_walk" && !ClientlessCompanionMgr::Start(p,s.Companion,profile->CompanionSpell,profile->CompanionLeashItem))
if (s.Kind=="pet_walk" && !ClientlessCompanionMgr::Start(p,s.Companion,sClientlessLuaEngine->SelectCompanionSpell(p->GetGUID().GetCounter()),profile->CompanionLeashItem))
{s={};return false;}
s.Active=true;
r.Detail="leisure outing: "+s.Kind;
@@ -1262,6 +1294,7 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff)
end.tm_min = 0;
end.tm_sec = 0;
end.tm_hour = local.tm_hour + 1;
while(end.tm_hour<24 && r.Plan[end.tm_hour].Intent==slot.Intent && r.Plan[end.tm_hour].Location==slot.Location)++end.tm_hour;
end.tm_isdst = - 1;
time_t deadline = mktime(& end);
if (changed) SetWanted(player, r, slot.Intent, slot.Location, deadline);
@@ -1868,7 +1901,7 @@ void AdventureBehaviorMgr::ResumePersonalPlan(Player* player)
{
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;
std::tm end=local;end.tm_min=0;end.tm_sec=0;++end.tm_hour;while(end.tm_hour<24 && r.Plan[end.tm_hour].Intent==slot.Intent && r.Plan[end.tm_hour].Location==slot.Location)++end.tm_hour;end.tm_isdst=-1;
r.Hour=local.tm_hour;SetWanted(player,r,slot.Intent,slot.Location,mktime(&end));
}
else if(!r.PersonalDeadline || now<r.PersonalDeadline)
+2 -1
View File
@@ -29,6 +29,7 @@ public:
void Offline(Player* player);
void IntentChanged(Player* player, ClientlessIntent const* intent);
void Update(Player* player, uint32 diff);
void RefreshDay(Player* player);
bool Command(ChatHandler* handler, char const* args);
void InterruptForCombat(Player* player);
void InterruptForInteraction(Player* player);
@@ -62,7 +63,7 @@ private:
AdventureBehaviorPolicy::Day Plan;
std::mt19937 Random;
std::deque<std::string> ActionHistory, VariantHistory;
std::deque<std::string> TargetHistory;
std::deque<std::string> TargetHistory, PlanRegions;
AdventureSocialScene Social;
AdventureInstancePolicy::Visit Instance;
RecoveryScene Recovery;
@@ -49,6 +49,18 @@ inline std::size_t Choose(std::vector<double> const& weights, std::mt19937& rand
if (weights.empty() || std::none_of(weights.begin(), weights.end(), [](double w) { return w > 0 && std::isfinite(w); })) return weights.size();
return std::discrete_distribution<std::size_t>(weights.begin(), weights.end())(random);
}
inline double RegionWeight(double affinity,double adventurousness)
{
// Curiosity flattens preferences but never removes another legal region.
return std::pow(std::max(0.0,affinity),1.0-0.8*std::clamp(adventurousness,0.0,1.0));
}
inline double LevelWeight(unsigned level,unsigned minimum,unsigned maximum,bool outing)
{
if(!minimum || !maximum)return outing?0.15:1.0;
if(level<minimum)return minimum-level>5?0.0:0.1;
if(level<=maximum)return 1.0;
return outing?std::max(0.03,1.0/(1.0+(level-maximum)*0.4)):1.0;
}
struct Slot
{
std::string Intent, Location;
@@ -160,6 +160,8 @@ Unit* AdventureCombatMgr::PvPTarget(Player* p) const
{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"));
auto aggression=profile->Personality.find("aggression");
if(aggression!=profile->Personality.end())chance*=std::clamp(aggression->second,0.0f,1.0f);
if(chance<=0 || p->GetHealthPct()<AdventureCombatPolicy::MinimumHealth)return nullptr;
if(p->GetPowerType()==POWER_MANA && p->GetMaxPower(POWER_MANA) &&
100.0f*p->GetPower(POWER_MANA)/p->GetMaxPower(POWER_MANA)<AdventureCombatPolicy::MinimumMana)return nullptr;
@@ -194,6 +196,17 @@ Creature* AdventureCombatMgr::Proactive(Player* p) const
if(intent!="adventure" && intent!="profession")return nullptr;
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
if(intent=="profession" && (!profile || !profile->PrimaryProfessions.count("skinning")))return nullptr;
if(profile)
{
auto aggression=profile->Personality.find("aggression");
// Stable per-bot/per-target-window willingness, no repeated rolls each tick.
if(aggression!=profile->Personality.end())
{
auto window=uint32(GameTime::GetGameTime()/30);
auto value=(p->GetGUID().GetCounter()*2654435761u+window*2246822519u)%1000;
if(value>=std::clamp(aggression->second,0.0f,1.0f)*1000)return nullptr;
}
}
if(intent=="adventure" && p->movespline->Finalized())return nullptr;
Creature* best=nullptr;std::vector<Creature*> all;p->GetCreatureListWithEntryInGrid(all,0,AdventureCombatPolicy::SearchRadius);
for(auto c:all)
+216 -65
View File
@@ -1,9 +1,12 @@
#include "AdventureGeneratorMgr.h"
#include "AdventureCombatPreparationPolicy.h"
#include "AdventureBehaviorMgr.h"
#include "AdventureGroupPolicy.h"
#include "AdventureLocationMgr.h"
#include "AdventureProfessionPolicy.h"
#include "AdventureSupplyMgr.h"
#include "ClientlessCompanionMgr.h"
#include "Bag.h"
#include "CharacterCache.h"
#include "Chat.h"
#include "ClientlessCharacterFactory.h"
@@ -28,6 +31,7 @@
#include <filesystem>
#include <cctype>
#include <memory>
#include <limits>
#include <random>
#include <sstream>
#include <tuple>
@@ -37,7 +41,7 @@ namespace
{
using AdventureGeneratorPolicy::Archetype;
bool Schema()
{return bool(CharacterDatabase.Query("SHOW TABLES LIKE 'vcore_adventure_generated_bot'"));}
{static bool ready=false;if(!ready)ready=bool(CharacterDatabase.Query("SHOW TABLES LIKE 'vcore_adventure_generated_bot'"));return ready;}
uint32 LearnedSkill(uint32 id,uint32& step)
{
if(auto rank=sSpellMgr->GetSpellLearnSkill(id))
@@ -66,9 +70,12 @@ namespace
}
void ClassTraining(Player* p)
{
if(auto rows=WorldDatabase.Query("SELECT Id FROM trainer WHERE Type=0 ORDER BY Id"))do
static std::vector<uint32> trainers;
if(trainers.empty())if(auto rows=WorldDatabase.Query("SELECT Id FROM trainer WHERE Type=0 ORDER BY Id"))do
{trainers.push_back(rows->Fetch()[0].GetUInt32());}while(rows->NextRow());
for(auto trainerId:trainers)
{
auto trainer=sObjectMgr->GetTrainer(rows->Fetch()[0].GetUInt32());
auto trainer=sObjectMgr->GetTrainer(trainerId);
if(!trainer || !trainer->IsTrainerValidForPlayer(p))continue;
// Repeat for prerequisite chains without granting arbitrary class-
// family NPC spells or abilities from a different specialization.
@@ -78,7 +85,7 @@ namespace
for(auto id:Taught(spell.SpellId))
if(!p->HasSpell(id) && Learnable(p,id))p->LearnSpell(id,false);
}
}while(rows->NextRow());
}
for(auto ability:sSkillLineAbilityStore)
{
auto skill=sSkillLineStore.LookupEntry(ability->SkillLine);
@@ -97,7 +104,7 @@ namespace
{
if(p->getLevel()<10)return true;
p->InitTalentForLevel();
if(!p->LearnPrimaryTalentSpecialization(a.Spec))return false;
if(!p->GetPrimaryTalentTree(p->GetActiveSpec()) && !p->LearnPrimaryTalentSpecialization(a.Spec))return false;
auto tabs=sDBCManager.GetTalentTabPages(p->getClass());if(!tabs)return false;
if(a.Class==CLASS_WARRIOR && a.Spec==0)
AdventureCombatPreparationPolicy::SpendArms60([&](uint32 id,unsigned rank){return p->LearnTalent(id,rank);},[&](){return p->GetFreeTalentPoints();});
@@ -120,8 +127,10 @@ namespace
}
bool Profession(Player* p,unsigned wanted)
{
struct Rank {uint32 Id,Step,Level,Required;};std::vector<Rank> ranks;
if(auto rows=WorldDatabase.Query("SELECT DISTINCT SpellId,ReqLevel,ReqSkillRank FROM trainer_spell"))do
struct Rank {uint32 Id,Step,Level,Required;};
static std::unordered_map<unsigned,std::vector<Rank>> catalog;
auto& ranks=catalog[wanted];
if(ranks.empty())if(auto rows=WorldDatabase.Query("SELECT DISTINCT SpellId,ReqLevel,ReqSkillRank FROM trainer_spell"))do
{
auto f=rows->Fetch();
for(auto id:Taught(f[0].GetUInt32()))
@@ -145,7 +154,7 @@ namespace
bool UsefulSpell(uint32 id,Archetype const& a,unsigned depth=0)
{
if(depth>3)return true;
auto spell=sSpellMgr->GetSpellInfo(id);if(!spell)return true;
auto spell=sSpellMgr->GetSpellInfo(id);if(!spell)return false;
bool caster=a.Attribute==ITEM_MOD_INTELLECT;
for(auto const& effect:spell->Effects)
{
@@ -182,47 +191,99 @@ namespace
case EQUIPMENT_SLOT_BACK:return type==INVTYPE_CLOAK;
case EQUIPMENT_SLOT_MAINHAND:
if(a.Class==CLASS_ROGUE)return type==INVTYPE_WEAPON || type==INVTYPE_WEAPONMAINHAND;
if(std::string(AdventureGeneratorPolicy::Role(a))=="tank" && a.Class!=CLASS_DEATH_KNIGHT)return type==INVTYPE_WEAPON || type==INVTYPE_WEAPONMAINHAND;
if(a.Attribute==ITEM_MOD_INTELLECT)return type==INVTYPE_2HWEAPON && item->GetSubClass()==ITEM_SUBCLASS_WEAPON_STAFF;
return type==INVTYPE_2HWEAPON;
case EQUIPMENT_SLOT_OFFHAND:return a.Class==CLASS_ROGUE && (type==INVTYPE_WEAPON || type==INVTYPE_WEAPONOFFHAND);
case EQUIPMENT_SLOT_OFFHAND:
if(std::string(AdventureGeneratorPolicy::Role(a))=="tank" && a.Class!=CLASS_DEATH_KNIGHT)return type==INVTYPE_SHIELD;
return a.Class==CLASS_ROGUE && (type==INVTYPE_WEAPON || type==INVTYPE_WEAPONOFFHAND);
case EQUIPMENT_SLOT_RANGED:
return a.Class==CLASS_HUNTER?item->IsRangedWeapon():a.Armor==ITEM_SUBCLASS_ARMOR_CLOTH && item->GetClass()==ITEM_CLASS_WEAPON && item->GetSubClass()==ITEM_SUBCLASS_WEAPON_WAND;
default:return false;
}
}
bool Equip(Player* p,Archetype const& a,std::mt19937& random,std::string& error)
std::string ProfileMovement(unsigned race,unsigned level,std::mt19937& random)
{
std::vector<uint32> ground;
switch(race)
{
case 3:ground={6777,6898,6899};break;
case 4:ground={8394,10789,10793};break;
case 7:ground={10873,10969,17453};break;
case 11:ground={34406,35710,35711};break;
case 2:ground={580,6654,6653};break;
case 5:ground={17462,17463,17464};break;
case 6:ground={18989,18990,18991};break;
case 8:ground={10796,10799,8395};break;
case 10:ground={34795,35018,35020};break;
case 9:ground={87090};break;
case 22:ground={103195,103196};break;
default:ground={458,470,472};break;
}
std::ostringstream lua;lua<<"movement = { can_ground_mount = "<<(level>=20?"true":"false")
<<", ground_mount_speed_rate = "<<(level>=40?2.0:1.6)<<", ground_mounts = {";
unsigned count=0;
if(level>=20)for(auto id:ground)if(auto spell=sSpellMgr->GetSpellInfo(id))
for(auto const& effect:spell->Effects)if(effect.ApplyAuraName==SPELL_AURA_MOUNTED)
if(auto creature=sObjectMgr->GetCreatureTemplate(effect.MiscValue))
if(auto model=creature->GetFirstValidModel())
{lua<<"["<<model->CreatureDisplayID<<"]="<<(1+random()%10)<<",";++count;break;}
lua<<"}, ground_mount_count = "<<count<<", can_free_fly = "<<(level>=60?"true":"false")<<", flight_mounts = {";
unsigned flights=0;
if(level>=60)for(auto id:AdventureGeneratorPolicy::Faction(race)==1?std::vector<unsigned>{32235,32239,32240}:std::vector<unsigned>{32243,32244,32245})
if(sSpellMgr->GetSpellInfo(id)){lua<<"["<<id<<"]="<<(1+random()%10)<<",";++flights;}
lua<<"}, flight_mount_count = "<<flights<<", companions = {";
unsigned pets=0;
for(auto id:{10673u,10675u,10676u,10707u})
if(random()%2==0 && sSpellMgr->GetSpellInfo(id)){lua<<"["<<id<<"]="<<(1+random()%10)<<",";++pets;}
if(!pets && sSpellMgr->GetSpellInfo(10673)){lua<<"[10673]=1,";++pets;}
lua<<"}, companion_count = "<<pets<<", companion_leash_item = 37460 }";
return lua.str();
}
float EquipmentScore(Player* p,ItemTemplate const* item,unsigned slot,Archetype const& a)
{
float invalid=-std::numeric_limits<float>::infinity();
if(!Slot(item,slot,a) ||
!AdventureGeneratorPolicy::EligibleItem(p->getLevel(),std::max(0,item->GetRequiredLevel()),item->GetQuality(),item->GetBaseItemLevel(),
item->GetDuration() || (item->GetFlags()&ITEM_FLAG_CONJURED),item->GetRequiredReputationFaction() || item->GetArea() || (item->GetMap()!=0 && item->GetMap()!=uint32(-1)) || item->GetHolidayID()!=HOLIDAY_NONE || item->GetRandomProperty() || item->GetRandomSuffix()) ||
p->CanUseItem(item)!=EQUIP_ERR_OK)return invalid;
if(item->GetClass()==ITEM_CLASS_ARMOR && slot!=EQUIPMENT_SLOT_NECK && slot!=EQUIPMENT_SLOT_BACK &&
slot!=EQUIPMENT_SLOT_FINGER1 && slot!=EQUIPMENT_SLOT_FINGER2 && slot!=EQUIPMENT_SLOT_TRINKET1 && slot!=EQUIPMENT_SLOT_TRINKET2 && slot!=EQUIPMENT_SLOT_OFFHAND &&
item->GetSubClass()!=AdventureGeneratorPolicy::Armor(a,p->getLevel()))return invalid;
bool effects=true;
for(auto const& effect:item->Effects)if(effect.SpellID && !UsefulSpell(effect.SpellID,a))effects=false;
if(!effects)return invalid;
float stat=0,wrong=0;
for(unsigned index=0;index<MAX_ITEM_PROTO_STATS;++index)
{
auto type=item->GetItemStatType(index);float value=item->GetStatValue(index,p);
if(type==int(a.Attribute))stat+=value;
else if(type==ITEM_MOD_STRENGTH || type==ITEM_MOD_AGILITY || type==ITEM_MOD_INTELLECT ||
(a.Attribute==ITEM_MOD_INTELLECT && (type==ITEM_MOD_ATTACK_POWER || type==ITEM_MOD_RANGED_ATTACK_POWER)) ||
(a.Attribute!=ITEM_MOD_INTELLECT && (type==ITEM_MOD_SPIRIT || type==ITEM_MOD_MANA || type==ITEM_MOD_MANA_REGENERATION || type==ITEM_MOD_SPELL_POWER || type==ITEM_MOD_SPELL_PENETRATION)))wrong+=value;
}
if(wrong>0 && stat==0)return invalid;
return item->GetBaseItemLevel()+stat*2-wrong;
}
bool Equip(Player* p,Archetype const& a,std::mt19937& random,std::string& error,bool missingOnly=false)
{
for(unsigned slot=EQUIPMENT_SLOT_START;slot<EQUIPMENT_SLOT_END;++slot)
{
if(missingOnly)
if(auto current=p->GetItemByPos(INVENTORY_SLOT_BAG_0,slot))
if(std::isfinite(EquipmentScore(p,current->GetTemplate(),slot,a)))continue;
if(slot==EQUIPMENT_SLOT_BODY || slot==EQUIPMENT_SLOT_TABARD ||
(slot==EQUIPMENT_SLOT_OFFHAND && a.Class!=CLASS_ROGUE) ||
(slot==EQUIPMENT_SLOT_OFFHAND && a.Class!=CLASS_ROGUE && (std::string(AdventureGeneratorPolicy::Role(a))!="tank" || a.Class==CLASS_DEATH_KNIGHT)) ||
(slot==EQUIPMENT_SLOT_RANGED && a.Class!=CLASS_HUNTER && a.Armor!=ITEM_SUBCLASS_ARMOR_CLOTH))continue;
struct Candidate {uint32 Id;float Score;};std::vector<Candidate> candidates;
uint32 salt=random();
for(auto const& pair:*sObjectMgr->GetItemTemplateStore())
{
auto const* item=&pair.second;
if(!Slot(item,slot,a) || p->GetItemCount(pair.first)>0 ||
!AdventureGeneratorPolicy::EligibleItem(p->getLevel(),std::max(0,item->GetRequiredLevel()),item->GetQuality(),item->GetBaseItemLevel(),
item->GetDuration() || (item->GetFlags()&ITEM_FLAG_CONJURED),item->GetRequiredReputationFaction() || item->GetArea() || (item->GetMap()!=0 && item->GetMap()!=uint32(-1)) || item->GetHolidayID()!=HOLIDAY_NONE || item->GetRandomProperty() || item->GetRandomSuffix()) ||
p->CanUseItem(item)!=EQUIP_ERR_OK)continue;
if(item->GetClass()==ITEM_CLASS_ARMOR && slot!=EQUIPMENT_SLOT_NECK && slot!=EQUIPMENT_SLOT_BACK &&
slot!=EQUIPMENT_SLOT_FINGER1 && slot!=EQUIPMENT_SLOT_FINGER2 && slot!=EQUIPMENT_SLOT_TRINKET1 && slot!=EQUIPMENT_SLOT_TRINKET2 &&
item->GetSubClass()!=AdventureGeneratorPolicy::Armor(a,p->getLevel()))continue;
bool effects=true;
for(auto const& effect:item->Effects)if(effect.SpellID && !UsefulSpell(effect.SpellID,a))effects=false;
if(!effects)continue;
float stat=0,wrong=0;
for(unsigned index=0;index<MAX_ITEM_PROTO_STATS;++index)
{
auto type=item->GetItemStatType(index);float value=item->GetStatValue(index,p);
if(type==int(a.Attribute))stat+=value;
else if(type==ITEM_MOD_STRENGTH || type==ITEM_MOD_AGILITY || type==ITEM_MOD_INTELLECT ||
(a.Attribute==ITEM_MOD_INTELLECT && (type==ITEM_MOD_ATTACK_POWER || type==ITEM_MOD_RANGED_ATTACK_POWER)) ||
(a.Attribute!=ITEM_MOD_INTELLECT && (type==ITEM_MOD_SPIRIT || type==ITEM_MOD_MANA || type==ITEM_MOD_MANA_REGENERATION || type==ITEM_MOD_SPELL_POWER || type==ITEM_MOD_SPELL_PENETRATION)))wrong+=value;
}
if(wrong>0 && stat==0)continue;
candidates.push_back({pair.first,item->GetBaseItemLevel()+stat*2-wrong+float((pair.first*2654435761u+salt)%100)/100});
if(p->GetItemCount(pair.first)>0)continue;
auto score=EquipmentScore(p,item,slot,a);
if(!std::isfinite(score))continue;
candidates.push_back({pair.first,score+float((pair.first*2654435761u+salt)%100)/100});
}
std::sort(candidates.begin(),candidates.end(),[](Candidate const& x,Candidate const& y){return x.Score==y.Score?x.Id<y.Id:x.Score>y.Score;});
if(!candidates.empty())
@@ -246,9 +307,10 @@ namespace
if(!p->EquipItem(destination,item.get(),true)){error="Native equipment placement failed.";return false;}
item.release();equipped=true;break;
}
bool required=slot==EQUIPMENT_SLOT_MAINHAND || (a.Class==CLASS_HUNTER && slot==EQUIPMENT_SLOT_RANGED) ||
bool required=slot==EQUIPMENT_SLOT_MAINHAND || (slot==EQUIPMENT_SLOT_OFFHAND && std::string(AdventureGeneratorPolicy::Role(a))=="tank" && a.Class!=CLASS_DEATH_KNIGHT) || (a.Class==CLASS_HUNTER && slot==EQUIPMENT_SLOT_RANGED) ||
slot==EQUIPMENT_SLOT_CHEST || slot==EQUIPMENT_SLOT_LEGS || slot==EQUIPMENT_SLOT_FEET ||
(p->getLevel()>=30 && slot!=EQUIPMENT_SLOT_RANGED && slot!=EQUIPMENT_SLOT_OFFHAND);
if(p->getLevel()<10)required=slot==EQUIPMENT_SLOT_MAINHAND;
if(!equipped && required){error="No legal profile-matching equipment for slot "+std::to_string(slot);return false;}
}
return true;
@@ -258,21 +320,50 @@ AdventureGeneratorMgr* AdventureGeneratorMgr::instance(){static AdventureGenerat
bool AdventureGeneratorMgr::Profile(uint32 guid,std::string& lua)
{
if(!Schema())return false;
auto row=CharacterDatabase.PQuery("SELECT profile_lua FROM vcore_adventure_generated_bot WHERE character_guid=%u AND state='ready'",guid);
auto row=CharacterDatabase.PQuery("SELECT g.profile_lua,g.archetype,g.seed,c.race,c.level FROM vcore_adventure_generated_bot g JOIN characters c ON c.guid=g.character_guid WHERE g.character_guid=%u AND g.state='ready'",guid);
if(!row)return false;
lua=row->Fetch()[0].GetString();
auto f=row->Fetch();lua=f[0].GetString();
if(!lua.empty() && lua.find("profile_version")==std::string::npos && sAdventureLocationMgr->EnsureLoaded())
{
auto const& types=AdventureGeneratorPolicy::Archetypes();
auto found=std::find_if(types.begin(),types.end(),[&](Archetype const& a){return f[1].GetString()==a.Key;});
if(found!=types.end())
{
auto level=f[4].GetUInt8(),race=f[3].GetUInt8();std::mt19937 random(f[2].GetUInt32());
uint32 home=0;auto info=sObjectMgr->GetPlayerInfo(race==9?2:race==22?1:race,CLASS_WARRIOR);
if(info)if(auto area=sAreaTableStore.LookupEntry(info->areaId))home=area->ParentAreaID?area->ParentAreaID:area->ID;
std::vector<AdventureLocation const*> choices;
for(auto const& l:sAdventureLocationMgr->Locations())
if(l.IsArea && !AdventureGroupPolicy::Capital(l.ZoneId) && l.MinLevel && level>=l.MinLevel && level<=l.MaxLevel &&
(!l.Faction || l.Faction==AdventureGeneratorPolicy::Faction(race)) && sAdventureLocationMgr->Weight(l,"adventure")>0)choices.push_back(&l);
auto favorite=choices.empty()?nullptr:choices[random()%choices.size()];
std::ostringstream upgraded;
upgraded<<"local p=(function() "<<lua<<" end)(); p.identity.profile_version=2; p.identity.level="<<unsigned(level)
<<"; p.identity.class="<<found->Class<<"; p.identity.specialization="<<found->Spec
<<"; p.identity.role="<<AdventureGeneratorPolicy::LuaString(AdventureGeneratorPolicy::Role(*found))
<<"; p.identity.seed="<<f[2].GetUInt32()<<"; p.identity.home_capital="<<AdventureGeneratorPolicy::Capital(race)<<"; p.identity.home_zone="<<home<<"; p.identity.favorite_zone="<<(favorite?favorite->ZoneId:home)
<<"; p.identity.favorite_region="<<AdventureGeneratorPolicy::LuaString(favorite?favorite->Region:"")
<<"; p.personality.aggression=0.5; p.personality.home_attachment=0.65; p.behavior.daily_schedule=true; "
<<"p.inventory=p.inventory or {keep_items={37460,6256,5956,2901,7005,6217}}; p.zone_affinity={["<<home<<"]=4";
if(favorite && favorite->ZoneId!=home)upgraded<<",["<<favorite->ZoneId<<"]=5";
if(home!=AdventureGeneratorPolicy::Capital(race))upgraded<<",["<<AdventureGeneratorPolicy::Capital(race)<<"]=4";
upgraded<<"}; local m={"<<ProfileMovement(race,level,random)<<"}; p.movement=m.movement; return p";
lua=upgraded.str();std::string escaped=lua;CharacterDatabase.EscapeString(escaped);
CharacterDatabase.PExecute("UPDATE vcore_adventure_generated_bot SET profile_lua='%s' WHERE character_guid=%u AND state='ready'",escaped.c_str(),guid);
}
}
return !lua.empty();
}
bool AdventureGeneratorMgr::Prepare(Player* p,Archetype const& a,std::string& error)
{
ClassTraining(p);
if(!Talents(p,a)){error="Native talent plan could not be completed.";return false;}
for(auto skill:a.Skills)if(!Profession(p,skill)){error="No native profession rank could be learned for skill "+std::to_string(skill);return false;}
p->SetFreePrimaryProfessions(0); // the generated profile owns exactly two primaries
for(auto skill:a.Skills)if(!Profession(p,skill) && p->getLevel()>=5){error="No native profession rank could be learned for skill "+std::to_string(skill);return false;}
if(p->getLevel()>=5)p->SetFreePrimaryProfessions(0); // the generated profile owns exactly two primaries
if(!sAdventureSupplyMgr->PrepareCharacter(p)){error="Could not prepare secondary professions and bags.";return false;}
return true;
}
bool AdventureGeneratorMgr::Create(ChatHandler* handler,uint32 account,uint32 seed,uint8 level,unsigned faction,std::string const& key,bool persistent)
bool AdventureGeneratorMgr::Create(ChatHandler* handler,uint32 account,uint32 seed,uint8 level,unsigned faction,std::string const& key,bool persistent,Completion done)
{
std::mt19937 random(seed);
struct Choice {Archetype const* Type;uint8 Race;};std::vector<Choice> choices;
@@ -283,8 +374,8 @@ bool AdventureGeneratorMgr::Create(ChatHandler* handler,uint32 account,uint32 se
for(uint8 race:{1,2,3,4,5,6,7,8,9,10,11,22})
if((!faction || AdventureGeneratorPolicy::Faction(race)==faction) && sObjectMgr->GetPlayerInfo(race,a.Class))choices.push_back({&a,race});
}
if(choices.empty()){handler->SendSysMessage("No legal race/class archetype fits this request.");return false;}
auto choice=choices[random()%choices.size()];auto const& a=*choice.Type;
if(choices.empty()){if(handler)handler->SendSysMessage("No legal race/class archetype fits this request.");return false;}
auto choice=choices[random()%choices.size()];auto a=AdventureGeneratorPolicy::Personalize(*choice.Type,random);
uint8 gender=random()%2;std::string name;
for(unsigned attempt=0;attempt<1024;++attempt)
{
@@ -296,14 +387,14 @@ bool AdventureGeneratorMgr::Create(ChatHandler* handler,uint32 account,uint32 se
auto path=std::filesystem::path(sClientlessLuaEngine->GetScriptRoot())/"adventure"/"profiles"/(lower+".lua");
if(std::filesystem::exists(path))continue; // reserve hand-written identities too
std::string escaped=candidate;CharacterDatabase.EscapeString(escaped);
if(!CharacterDatabase.PQuery("SELECT 1 FROM characters WHERE name='%s' LIMIT 1",escaped.c_str())){name=candidate;break;}
if(done || !CharacterDatabase.PQuery("SELECT 1 FROM characters WHERE name='%s' LIMIT 1",escaped.c_str())){name=candidate;break;}
}
if(name.empty()){handler->SendSysMessage("Roleplay name pool exhausted; no numeric suffix was substituted.");return false;}
if(name.empty()){if(handler)handler->SendSysMessage("Roleplay name pool exhausted; no numeric suffix was substituted.");return false;}
// Mature goblin/worgen travelers have left their phased introduction.
uint8 homeRace=choice.Race==9?2:choice.Race==22?1:choice.Race;
auto info=sObjectMgr->GetPlayerInfo(homeRace,CLASS_WARRIOR);
if(!info){handler->SendSysMessage("Native racial home data unavailable.");return false;}
if(!sAdventureLocationMgr->EnsureLoaded()){handler->SendSysMessage("Adventure location catalog unavailable.");return false;}
if(!info){if(handler)handler->SendSysMessage("Native racial home data unavailable.");return false;}
if(!sAdventureLocationMgr->EnsureLoaded()){if(handler)handler->SendSysMessage("Adventure location catalog unavailable.");return false;}
AdventureLocation const* home=nullptr;float best=1e30f;
for(auto const& location:sAdventureLocationMgr->Locations())
{
@@ -315,36 +406,95 @@ bool AdventureGeneratorMgr::Create(ChatHandler* handler,uint32 account,uint32 se
float distance=dx*dx+dy*dy;
if(distance<best){best=distance;home=&location;}
}
if(!home){handler->SendSysMessage("No faction-compatible home near the native racial starting area.");return false;}
std::string profile=AdventureGeneratorPolicy::Profile(name,a,choice.Race,home->Point.Id,home->Region,random);
if(!home){if(handler)handler->SendSysMessage("No faction-compatible home near the native racial starting area.");return false;}
std::vector<AdventureLocation const*> favorites;
for(auto const& l:sAdventureLocationMgr->Locations())
if(l.IsArea && !AdventureGroupPolicy::Capital(l.ZoneId) && l.MinLevel && level>=l.MinLevel && level<=l.MaxLevel &&
(!l.Faction || l.Faction==AdventureGeneratorPolicy::Faction(choice.Race)) &&
sAdventureLocationMgr->Weight(l,"adventure")>0)favorites.push_back(&l);
auto favorite=favorites.empty()?home:favorites[random()%favorites.size()];
std::string movement=ProfileMovement(choice.Race,level,random);
std::string profile=AdventureGeneratorPolicy::Profile(name,a,choice.Race,home->Point.Id,home->Region,random,
level,home->ZoneId,favorite->ZoneId,favorite->Region,seed,movement);
std::string escapedProfile=profile;CharacterDatabase.EscapeString(escapedProfile);
ClientlessCharacterFactory::Definition definition{name,choice.Race,uint8(a.Class),gender,level,seed};
auto result=ClientlessCharacterFactory::Create(account,definition,
[&](Player* player,std::string& error)
auto homeCopy=*home;auto rng=std::make_shared<std::mt19937>(random);
auto prepare=[homeCopy,a,rng,movement](Player* player,std::string& error)
{
if(player->GetMapId()!=home->Point.MapId)
{
player->ResetMap();player->SetMap(sMapMgr->CreateMap(home->Point.MapId,player));
}
player->Relocate(home->Point.X,home->Point.Y,home->Point.Z,home->Point.O);
if(!Prepare(player,a,error) || !Equip(player,a,random,error))return false;
// Save its bind row in the SAME
// transaction instead of calling SetHomebind's immediate UPDATE.
if(player->GetMapId()!=homeCopy.Point.MapId)
{player->ResetMap();player->SetMap(sMapMgr->CreateMap(homeCopy.Point.MapId,player));}
player->Relocate(homeCopy.Point.X,homeCopy.Point.Y,homeCopy.Point.Z,homeCopy.Point.O);
if(!sAdventureGeneratorMgr->Prepare(player,a,error) || !Equip(player,a,*rng,error))return false;
if(player->getLevel()>=20)player->SetSkill(SKILL_RIDING,player->getLevel()>=60?3:player->getLevel()>=40?2:1,
player->getLevel()>=60?225:player->getLevel()>=40?150:75,player->getLevel()>=60?225:player->getLevel()>=40?150:75);
if(player->getLevel()>=60 && Learnable(player,90267))player->LearnSpell(90267,false);
if(player->getLevel()>=68 && Learnable(player,54197))player->LearnSpell(54197,false);
if(player->getLevel()>=60)for(auto id:AdventureGeneratorPolicy::Faction(player->getRace())==1?
std::vector<unsigned>{32235,32239,32240}:std::vector<unsigned>{32243,32244,32245})
if(Learnable(player,id))player->LearnSpell(id,false);
return true;
},
[&](CharacterDatabaseTransaction const& tx,Player* player)
};
auto append=[homeCopy,account,a,seed,persistent,escapedProfile](CharacterDatabaseTransaction const& tx,Player* player)
{
tx->PAppend("INSERT INTO vcore_adventure_generated_bot (character_guid,account_id,archetype,seed,persistent,state,profile_lua,created_at) VALUES (%u,%u,'%s',%u,%u,'ready','%s',%llu)",
player->GetGUID().GetCounter(),account,a.Key,seed,persistent?1:0,escapedProfile.c_str(),static_cast<unsigned long long>(GameTime::GetGameTime()));
tx->PAppend("INSERT INTO character_homebind (guid,mapId,zoneId,posX,posY,posZ) VALUES (%u,%u,%u,%f,%f,%f)",
player->GetGUID().GetCounter(),home->Point.MapId,home->ZoneId,home->Point.X,home->Point.Y,home->Point.Z);
});
if(!result.Guid){handler->PSendSysMessage("Generation failed name='%s': %s",name.c_str(),result.Error.c_str());return false;}
handler->PSendSysMessage("Generated %s guid=%u race=%u class=%u level=%u archetype=%s persistent=%u seed=%u home=%s (offline).",
player->GetGUID().GetCounter(),homeCopy.Point.MapId,homeCopy.ZoneId,homeCopy.Point.X,homeCopy.Point.Y,homeCopy.Point.Z);
};
if(done)
{
ClientlessCharacterFactory::CreateAsync(account,definition,prepare,append,[done](ClientlessCharacterFactory::Result result)
{done(result.Guid,result.Error);});
return true;
}
auto result=ClientlessCharacterFactory::Create(account,definition,prepare,append);
if(!result.Guid){if(handler)handler->PSendSysMessage("Generation failed name='%s': %s",name.c_str(),result.Error.c_str());return false;}
if(handler)handler->PSendSysMessage("Generated %s guid=%u race=%u class=%u level=%u archetype=%s persistent=%u seed=%u home=%s (offline).",
name.c_str(),result.Guid,choice.Race,a.Class,level,a.Key,persistent?1:0,seed,home->Point.Id.c_str());
TC_LOG_INFO("server.adventurebot","BOT_GENERATED guid=%u name='%s' race=%u class=%u level=%u archetype='%s' seed=%u persistent=%u",result.Guid,name.c_str(),choice.Race,a.Class,level,a.Key,seed,persistent?1:0);
return true;
}
void AdventureGeneratorMgr::GenerateAsync(uint32 account,uint32 seed,uint8 level,unsigned faction,bool persistent,Completion done)
{
if(!Create(nullptr,account,seed,level,faction,"any",persistent,done))done(0,"No valid name, archetype or home in loaded catalogs.");
}
bool AdventureGeneratorMgr::Reconcile(Player* player,std::string& error)
{
auto profile=sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter());
if(!profile || !profile->Level || profile->SourcePath.find("generated:")!=0)return true;
if(profile->Class!=player->getClass()){error="Profile class mismatch; no class conversion performed.";return false;}
auto found=std::find_if(AdventureGeneratorPolicy::Archetypes().begin(),AdventureGeneratorPolicy::Archetypes().end(),
[&](Archetype const& a){return a.Class==profile->Class;});
if(found==AdventureGeneratorPolicy::Archetypes().end())return false;
auto a=*found;a.Spec=profile->Specialization;
if(profile->Role!=AdventureGeneratorPolicy::Role(a)){error="Profile role and specialization disagree.";return false;}
bool changedLevel=player->getLevel()!=profile->Level;
if(changedLevel)
{
player->SetLevel(profile->Level);player->InitStatsForLevel(true);player->InitTalentForLevel();
player->InitTaxiNodesForLevel();player->InitGlyphsForLevel();
std::vector<uint32> remove;
for(auto const& pair:player->GetSpellMap())if(auto spell=sSpellMgr->GetSpellInfo(pair.first))
if(spell->SpellLevel>profile->Level || spell->BaseLevel>profile->Level)remove.push_back(pair.first);
for(auto id:remove)player->RemoveSpell(id,false,false);
}
auto tabs=sDBCManager.GetTalentTabPages(player->getClass());
if(changedLevel || (tabs && player->getLevel()>=10 && player->GetPrimaryTalentTree(player->GetActiveSpec())!=tabs[a.Spec]))player->ResetTalents(true);
if(!Prepare(player,a,error))return false;
std::mt19937 random(player->GetGUID().GetCounter());
if(!Equip(player,a,random,error,true))return false;
for(auto const& pet:profile->Companions)ClientlessCompanionMgr::Prepare(player,pet.DisplayId,profile->CompanionLeashItem);
if(player->getLevel()>=20 && profile->CanGroundMount)
{
uint32 rank=player->getLevel()>=60 && profile->CanFreeFly?3:player->getLevel()>=40?2:1;
player->SetSkill(SKILL_RIDING,rank,rank==3?225:rank==2?150:75,rank==3?225:rank==2?150:75);
}
if(player->getLevel()>=60 && Learnable(player,90267) && !player->HasSpell(90267))player->LearnSpell(90267,false);
if(player->getLevel()>=68 && Learnable(player,54197) && !player->HasSpell(54197))player->LearnSpell(54197,false);
for(auto const& mount:profile->FlightMounts)if(Learnable(player,mount.DisplayId) && !player->HasSpell(mount.DisplayId))player->LearnSpell(mount.DisplayId,false);
player->SetUInt32Value(PLAYER_XP,0);
return true;
}
bool AdventureGeneratorMgr::Command(ChatHandler* handler,char const* args)
{
if(!Schema()){handler->SendSysMessage("Apply the AdventureBot generator character migration first.");return false;}
@@ -352,13 +502,13 @@ bool AdventureGeneratorMgr::Command(ChatHandler* handler,char const* args)
if(op=="create")
{
uint32 account=0,count=0,level=60,seed=uint32(GameTime::GetGameTime());std::string side="any",key="any",mode="persistent",extra;
if(!(input>>account>>count)){handler->SendSysMessage("Usage: clientless generate create <bot-account> <count 1..10> [level 10..85] [any|alliance|horde] [archetype|any] [persistent|temporary] [seed]");return false;}
if(!(input>>account>>count)){handler->SendSysMessage("Usage: clientless generate create <bot-account> <count 1..10> [level 1..85] [any|alliance|horde] [archetype|any] [persistent|temporary] [seed]");return false;}
input>>std::ws;
if(!input.eof())
{
if(!(input>>level>>side>>key>>mode>>seed) || input>>extra){handler->SendSysMessage("Optional arguments must be supplied together: level faction archetype persistence seed.");return false;}
}
if(count<1 || count>10 || level<10 || level>85 || (side!="any" && side!="alliance" && side!="horde") || (mode!="persistent" && mode!="temporary"))return false;
if(count<1 || count>10 || level<1 || level>85 || (side!="any" && side!="alliance" && side!="horde") || (mode!="persistent" && mode!="temporary"))return false;
unsigned created=0;
for(uint32 i=0;i<count;++i)if(Create(handler,account,seed+i,uint8(level),side=="alliance"?1:side=="horde"?2:0,key,mode=="persistent"))++created;else break;
handler->PSendSysMessage("Generation batch complete: %u/%u characters, each atomically saved; no existing characters modified.",created,count);
@@ -386,9 +536,10 @@ bool AdventureGeneratorMgr::Command(ChatHandler* handler,char const* args)
if(!player || !player->IsInWorld() || !player->IsAlive() || player->IsInCombat() || player->IsBeingTeleported() ||
player->GetTransport() || player->IsInFlight() || player->HasUnitState(UNIT_STATE_CASTING))
{handler->SendSysMessage("Equipment refresh requires an idle online generator-owned bot outside combat/transit/casting.");return false;}
for(auto const& archetype:AdventureGeneratorPolicy::Archetypes())
for(auto archetype:AdventureGeneratorPolicy::Archetypes())
if(f[4].GetString()==archetype.Key && player->getClass()==archetype.Class)
{
if(auto profile=sClientlessLuaEngine->GetAdventureProfile(guid))archetype.Spec=profile->Specialization;
sAdventureBehaviorMgr->InterruptForInteraction(player);
std::mt19937 random(f[5].GetUInt32());std::string error;
if(!Equip(player,archetype,random,error)){handler->PSendSysMessage("Equipment refresh stopped: %s",error.c_str());return false;}
@@ -3,6 +3,7 @@
#include "Common.h"
#include "AdventureGeneratorPolicy.h"
#include <string>
#include <functional>
class Player;
class ChatHandler;
class TC_GAME_API AdventureGeneratorMgr
@@ -13,9 +14,12 @@ public:
// Lua profile stays in the character DB; hand-written named profiles retain
// precedence. A generated identity is never inferred from account/name.
static bool Profile(uint32 guid,std::string& lua);
using Completion=std::function<void(uint32,std::string)>;
void GenerateAsync(uint32 account,uint32 seed,uint8 level,unsigned faction,bool persistent,Completion done);
bool Reconcile(Player* player,std::string& error);
private:
bool Create(ChatHandler* handler,uint32 account,uint32 seed,uint8 level,unsigned faction,
std::string const& archetype,bool persistent);
std::string const& archetype,bool persistent,Completion done={});
bool Prepare(Player* player,AdventureGeneratorPolicy::Archetype const& archetype,std::string& error);
};
#define sAdventureGeneratorMgr AdventureGeneratorMgr::instance()
@@ -66,7 +66,10 @@ namespace AdventureGeneratorPolicy
for(unsigned i=0;i<choice;++i)start=list.find(' ',start)+1;
auto end=list.find(' ',start);return list.substr(start,end-start);
};
return pick(first)+pick(last);
auto prefix=pick(first),suffix=pick(last);
auto middle=random()%3==0?pick(race==2 || race==8?"a o u ak og uk ag ur":
race==3?"a i o ar or in un er":"a e i el an ar il en"):std::string();
return prefix+middle+suffix;
}
inline std::string LuaString(std::string const& input)
{
@@ -81,23 +84,55 @@ namespace AdventureGeneratorPolicy
return required<=level && quality>=1 && quality<=3 && !temporary && !restricted &&
(required+15>=level || level<20) && itemLevel<=level*5+20;
}
inline unsigned Capital(unsigned race)
{return race==3 || race==7?1537:race==4?1657:race==11?3557:race==5?1497:race==6?1638:race==10?3487:Faction(race)==1?1519:1637;}
inline char const* Role(Archetype const& a)
{
if ((a.Class==1 && a.Spec==2) || (a.Class==2 && a.Spec==1) || (a.Class==6 && a.Spec==0)) return "tank";
if ((a.Class==5 && a.Spec!=2) || (a.Class==7 && a.Spec==2) || (a.Class==11 && a.Spec==2)) return "healer";
return "damage";
}
inline Archetype Personalize(Archetype a, std::mt19937& random)
{
if (random()%3==0)
{
if(a.Class==1)a.Spec=2;
if(a.Class==2)a.Spec=1;
if(a.Class==6)a.Spec=0;
if(a.Class==5)a.Spec=random()%2;
if(a.Class==7 || a.Class==11)a.Spec=2;
}
return a;
}
inline std::string Profile(std::string const& name,Archetype const& a,unsigned race,
std::string const& location,std::string const& region,std::mt19937& random)
std::string const& location,std::string const& region,std::mt19937& random,
unsigned level=60,unsigned homeZone=0,unsigned favoriteZone=0,std::string const& favoriteRegion="",unsigned seed=0,
std::string const& movement="movement = { can_ground_mount = false, can_free_fly = false }")
{
auto quote=LuaString;
std::ostringstream lua;lua<<std::fixed<<std::setprecision(3);
auto personality=[&](){return 0.25f+float(random()%651)/1000;};
lua<<"return { identity = { name = "<<quote(name)<<", home_region = "<<quote(region)
<<", profile_version = 2, level = "<<level<<", class = "<<a.Class<<", specialization = "<<a.Spec
<<", role = "<<quote(Role(a))<<", seed = "<<seed<<", home_zone = "<<homeZone
<<", home_capital = "<<Capital(race)<<", favorite_zone = "<<favoriteZone<<", favorite_region = "<<quote(favoriteRegion)
<<", vocation = "<<quote(a.Vocation)<<", background = "
<<quote(std::string("Herkunft: ")+AdventureGeneratorPolicy::Origin(race)+". Sucht auf Reisen Arbeit, neue Wege und Begegnungen.")
<<" }, behavior = { enabled = true, daily_schedule = false, initial_intent = 'social', initial_location = "<<quote(location)
<<" }, behavior = { enabled = true, daily_schedule = true, initial_intent = 'social', initial_location = "<<quote(location)
<<", initial_duration_seconds = 3600, locale = 'deDE' }, professions = { primary = { "
<<quote(AdventureProfessionPolicy::PrimaryForSkill(a.Skills[0]))<<", "<<quote(AdventureProfessionPolicy::PrimaryForSkill(a.Skills[1]))
<<" } }, personality = { sociability = "<<personality()<<", adventurousness = "<<personality()
<<", wanderlust = "<<personality()<<", group_loyalty = "<<personality()<<", tavern_affinity = "<<personality()
<<", aggression = "<<personality()<<", home_attachment = "<<personality()<<", wanderlust = "<<personality()<<", group_loyalty = "<<personality()<<", tavern_affinity = "<<personality()
<<", wilderness_affinity = "<<personality()<<", city_affinity = "<<personality()<<", danger_tolerance = "<<personality()
<<" }, region_affinity = { ["<<quote(region)<<"] = 1.0 }, activity_affinity = { social = 0.8, adventure = 0.7, profession = 0.8, leisure = 0.7, economy = 0.5, rest = 0.7, instance = 0.3 }, "
<<"movement = { can_ground_mount = false, can_free_fly = false } }";
<<" }, region_affinity = { ["<<quote(region)<<"] = 2.5 }, zone_affinity = {";
if(homeZone)lua<<" ["<<homeZone<<"] = 4.0,";
if(favoriteZone && favoriteZone!=homeZone)lua<<" ["<<favoriteZone<<"] = 5.0,";
unsigned capital=Capital(race);
if(capital!=homeZone && capital!=favoriteZone)lua<<" ["<<capital<<"] = 4.0,";
lua<<" }, activity_affinity = { social = "<<personality()<<", adventure = "<<personality()
<<", profession = "<<personality()<<", leisure = "<<personality()<<", economy = "<<personality()
<<", rest = "<<personality()<<", instance = "<<(level>=15?personality():0.0f)
<<", raid = "<<(level>=60?personality()*0.5f:0.0f)<<" }, inventory = { keep_items = {37460,6256,5956,2901,7005,6217} }, "<<movement<<" }";
return lua.str();
}
@@ -6,6 +6,8 @@ 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 unsigned CapitalFaction(unsigned zone)
{switch(zone){case 1519:case 1537:case 1657:case 3557:return 1;case 1637:case 1638:case 1497:case 3487:return 2;default:return 0;}}
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)
@@ -54,6 +54,13 @@ bool AdventureLocationMgr::Reload()
}
while (rows->NextRow());
}
if(WorldDatabase.Query("SHOW TABLES LIKE 'vcore_adventure_zone'"))
if(auto rows=WorldDatabase.Query("SELECT zone_id,min_level,max_level FROM vcore_adventure_zone WHERE enabled=1"))do
{
auto f=rows->Fetch();
for(auto& l:_locations)if(l.ZoneId==f[0].GetUInt32() || (!l.ZoneId && l.AreaId==f[0].GetUInt32()))
{l.MinLevel=f[1].GetUInt8();l.MaxLevel=f[2].GetUInt8();}
}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
{
@@ -14,6 +14,7 @@ struct AdventureLocation
uint32 AreaId = 0, ZoneId = 0, InstanceMap = 0;
bool IsArea = false;
uint8 Faction = 0;
uint8 MinLevel = 0, MaxLevel = 0;
};
struct AdventureMicroAction
{
@@ -0,0 +1,282 @@
#include "AdventurePopulationMgr.h"
#include "AdventureGeneratorMgr.h"
#include "AdventureBehaviorMgr.h"
#include "AdventureLocationMgr.h"
#include "ClientlessCharacterFactory.h"
#include "ClientlessLuaEngine.h"
#include "ClientlessPlayerMgr.h"
#include "AdventureSupplyMgr.h"
#include "AdventureCombatMgr.h"
#include "AccountMgr.h"
#include "Bag.h"
#include "Chat.h"
#include "Config.h"
#include "CryptoRandom.h"
#include "GameTime.h"
#include "Item.h"
#include "ItemTemplate.h"
#include "Map.h"
#include "Log.h"
#include "ObjectAccessor.h"
#include "Player.h"
#include "SRP6.h"
#include "Util.h"
#include "World.h"
#include "WorldSession.h"
#include <sstream>
using namespace AdventurePopulationPolicy;
AdventurePopulationMgr* AdventurePopulationMgr::instance(){static AdventurePopulationMgr mgr;return &mgr;}
void AdventurePopulationMgr::Configure()
{
_configured=true;
_enabled=sConfigMgr->GetBoolDefault("Clientless.Adventure.Loader.Enabled",false);
char const* names[]={"Alliance.Persistent","Alliance.Temporary","Horde.Persistent","Horde.Temporary"};
for(unsigned i=0;i<4;++i)_targets[i]=std::clamp(sConfigMgr->GetIntDefault(std::string("Clientless.Adventure.Loader.")+names[i],0),0,5000);
_interval=std::clamp(sConfigMgr->GetIntDefault("Clientless.Adventure.Loader.IntervalMs",2000),250,60000);
_minLevel=std::clamp(sConfigMgr->GetIntDefault("Clientless.Adventure.Loader.MinLevel",1),1,85);
_maxLevel=std::clamp(sConfigMgr->GetIntDefault("Clientless.Adventure.Loader.MaxLevel",85),int(_minLevel),85);
_rotationAge=std::clamp(sConfigMgr->GetIntDefault("Clientless.Adventure.Loader.RotationMinAgeHours",24),0,8760)*3600;
_rotationChance=std::clamp(sConfigMgr->GetIntDefault("Clientless.Adventure.Loader.RotationPercent",15),0,100)/100.0;
_maintenance=std::clamp(sConfigMgr->GetIntDefault("Clientless.Adventure.Manager.IntervalSeconds",300),30,3600)*1000;
_timer=std::max(10000u,_interval); // first normal world updates, never startup initialization
if(_enabled)Snapshot();
}
void AdventurePopulationMgr::Failed(std::string const& detail)
{
_busy=false;_lastError=detail;_timer=30000;
TC_LOG_WARN("server.adventurebot","BOT_LOADER_RETRY detail='%s'",detail.c_str());
}
void AdventurePopulationMgr::Snapshot()
{
if(_busy)return;
_schema=false;_busy=true;
_queries.AddCallback(CharacterDatabase.AsyncQuery("SHOW TABLES LIKE 'vcore_adventure_rotation'").WithCallback([this](QueryResult schema)
{
if(!schema){Failed("Population character migration missing; loader suspended, world remains available.");return;}
_schema=true;
_queries.AddCallback(CharacterDatabase.AsyncQuery(
"SELECT g.character_guid,g.account_id,g.persistent,g.created_at,c.race FROM vcore_adventure_generated_bot g JOIN characters c ON c.guid=g.character_guid WHERE g.state='ready' AND c.deleteDate IS NULL ORDER BY g.character_guid").WithCallback([this](QueryResult rows)
{
std::vector<Bot> bots;
if(rows)do
{
auto f=rows->Fetch();Bot b{f[0].GetUInt32(),f[1].GetUInt32(),AdventurePopulationPolicy::Pool(AdventureGeneratorPolicy::Faction(f[4].GetUInt8()),f[2].GetUInt8()!=0),f[3].GetUInt64()};
auto old=std::find_if(_bots.begin(),_bots.end(),[&](Bot const& v){return v.Guid==b.Guid;});
if(old!=_bots.end()){b.Failures=old->Failures;b.Retry=old->Retry;}
bots.push_back(b);
}while(rows->NextRow());
_bots=std::move(bots);
_queries.AddCallback(CharacterDatabase.AsyncQuery(
"SELECT a.account_id,a.faction,COUNT(c.guid) FROM vcore_adventure_bot_account a LEFT JOIN characters c ON c.account=a.account_id AND c.deleteDate IS NULL GROUP BY a.account_id,a.faction ORDER BY a.account_id").WithCallback([this](QueryResult rows)
{
_accounts.clear();if(rows)do{auto f=rows->Fetch();_accounts.push_back({f[0].GetUInt32(),f[1].GetUInt8(),uint32(f[2].GetUInt64())});}while(rows->NextRow());
_queries.AddCallback(CharacterDatabase.AsyncQuery(
"SELECT r.old_guid,r.new_guid,g.account_id FROM vcore_adventure_rotation r JOIN vcore_adventure_generated_bot g ON g.character_guid=r.old_guid AND g.persistent=0 JOIN vcore_adventure_generated_bot n ON n.character_guid=r.new_guid AND n.state='ready' JOIN characters c ON c.guid=n.character_guid").WithCallback([this](QueryResult rows)
{
_rotations.clear();if(rows)do{auto f=rows->Fetch();_rotations.push_back({f[0].GetUInt32(),f[1].GetUInt32(),f[2].GetUInt32(),false});}while(rows->NextRow());
_busy=false;_refresh=60000;
}));
}));
}));
}));
}
void AdventurePopulationMgr::Update(uint32 diff)
{
ClientlessCharacterFactory::Update();
_queries.ProcessReadyCallbacks();_saves.ProcessReadyCallbacks();
if(!_configured){Configure();return;}
if(!_enabled || sWorld->IsStopped())return;
if(_refresh>diff)_refresh-=diff;else _refresh=0;
if(_timer>diff){_timer-=diff;return;}_timer=_interval;
if(_busy)return;
if(!_schema || !_refresh){Snapshot();return;}
if(!_rotations.empty()){DeleteRotated();return;}
Step();
}
void AdventurePopulationMgr::Step()
{
auto now=GameTime::GetGameTime();
for(unsigned count=0;count<4;++count)
{
unsigned pool=_cursor++%4,online=0,loading=0;
for(auto const& b:_bots)if(b.Pool==pool)
{
auto state=sWorld->GetClientlessPlayerState(b.Guid);
if(state==ClientlessPlayerState::Online)++online;
else if(state!=ClientlessPlayerState::Offline)++loading;
}
if(!Deficit(_targets[pool],online,loading))continue;
for(auto& b:_bots)if(b.Pool==pool && b.Retry<=now && sWorld->GetClientlessPlayerState(b.Guid)==ClientlessPlayerState::Offline && !ObjectAccessor::FindConnectedPlayer(ObjectGuid(HighGuid::Player,b.Guid)))
{
bool seen=_considered.count(b.Guid);_considered.insert(b.Guid);
if(Replace(Persistent(pool),true,seen,now-b.Created,_rotationAge,frand(0,1),_rotationChance))
{Generate(pool,b.Guid);return;}
if(sClientlessPlayerMgr->StartPlayer(b.Account,b.Guid,CreateClientlessAdventureLuaController()))
{b.Retry=now+120;TC_LOG_INFO("server.adventurebot","BOT_LOADER_LOGIN guid=%u pool=%u",b.Guid,pool);}
else {++b.Failures;b.Retry=now+std::min(1800u,30u*(1u<<std::min(b.Failures,5u)));}
return;
}
// Bots with failed logins count as inventory, so do not create replacements endlessly.
unsigned existing=std::count_if(_bots.begin(),_bots.end(),[&](Bot const& b){return b.Pool==pool;});
if(existing<_targets[pool]){Generate(pool);return;}
}
}
void AdventurePopulationMgr::Generate(unsigned pool,uint32 replace)
{
auto available=std::find_if(_accounts.begin(),_accounts.end(),[&](Account const& a)
{return a.Faction==Faction(pool) && a.Count<sWorld->getIntConfig(CONFIG_CHARACTERS_PER_REALM) && !sWorld->FindSession(a.Id);});
if(available==_accounts.end()){CreateAccount(pool,replace);return;}
_busy=true;auto account=available->Id;
sAdventureGeneratorMgr->GenerateAsync(account,urand(0,0xFFFFFFFF),urand(_minLevel,_maxLevel),Faction(pool),Persistent(pool),
[this,pool,replace,account](uint32 guid,std::string error)
{
if(!guid){Failed(error);return;}
_considered.insert(guid);
_bots.push_back({guid,account,pool,uint64(GameTime::GetGameTime())});
for(auto& a:_accounts)if(a.Id==account)++a.Count;
if(!replace){_busy=false;return;}
auto tx=CharacterDatabase.BeginTransaction();
tx->PAppend("INSERT INTO vcore_adventure_rotation(old_guid,new_guid) SELECT character_guid,%u FROM vcore_adventure_generated_bot WHERE character_guid=%u AND persistent=0 AND state='ready'",guid,replace);
tx->PAppend("UPDATE vcore_adventure_generated_bot g JOIN vcore_adventure_rotation r ON r.old_guid=g.character_guid SET g.state='replacing' WHERE g.character_guid=%u AND g.persistent=0",replace);
auto save=CharacterDatabase.AsyncCommitTransaction(tx);
save.AfterComplete([this,replace,guid,account](bool ok)
{
if(!ok){Failed("Could not journal rotation; original character retained.");return;}
_bots.erase(std::remove_if(_bots.begin(),_bots.end(),[&](Bot const& b){return b.Guid==replace;}),_bots.end());
_rotations.push_back({replace,guid,account,false});_busy=false;
});_saves.AddCallback(std::move(save));
});
}
void AdventurePopulationMgr::CreateAccount(unsigned pool,uint32 replace)
{
_busy=true;
// Same native SRP6 registration and prepared account insert, with an unrecorded
// random password. Ownership is explicit; existing similarly named accounts are never adopted.
auto username="VCB"+ByteArrayToHexStr(Trinity::Crypto::GetRandomBytes<6>());
auto password=ByteArrayToHexStr(Trinity::Crypto::GetRandomBytes<8>());
auto credentials=Trinity::Crypto::SRP6::MakeRegistrationData(username,password);
auto tx=LoginDatabase.BeginTransaction();auto stmt=LoginDatabase.GetPreparedStatement(LOGIN_INS_ACCOUNT);
stmt->setString(0,username);stmt->setBinary(1,credentials.first);stmt->setBinary(2,credentials.second);stmt->setString(3,"");stmt->setString(4,"");tx->Append(stmt);
tx->PAppend("UPDATE account SET expansion=3 WHERE username='%s'",username.c_str());
auto save=LoginDatabase.AsyncCommitTransaction(tx);save.AfterComplete([this,pool,replace,username](bool ok)
{
if(!ok){Failed("Dedicated bot account creation failed.");return;}
_queries.AddCallback(LoginDatabase.AsyncQuery(Trinity::StringFormat("SELECT id FROM account WHERE username='%s'",username.c_str()).c_str()).WithCallback([this,pool,replace](QueryResult rows)
{
if(!rows){Failed("New bot account could not be resolved.");return;}
auto account=rows->Fetch()[0].GetUInt32();
auto tx=CharacterDatabase.BeginTransaction();tx->PAppend("INSERT INTO vcore_adventure_bot_account(account_id,faction) VALUES(%u,%u)",account,Faction(pool));
auto saved=CharacterDatabase.AsyncCommitTransaction(tx);saved.AfterComplete([this,pool,replace,account](bool ok)
{
if(!ok){Failed("Could not register ownership of new bot account.");return;}
_accounts.push_back({account,Faction(pool),0});_busy=false;Generate(pool,replace);
});_saves.AddCallback(std::move(saved));
}));
});_saves.AddCallback(std::move(save));
}
void AdventurePopulationMgr::DeleteRotated()
{
auto& r=_rotations.front();auto guid=r.Old;
if(sWorld->GetClientlessPlayerState(guid)!=ClientlessPlayerState::Offline || ObjectAccessor::FindConnectedPlayer(ObjectGuid(HighGuid::Player,guid)))return;
_busy=true;
// Recheck ownership and native offline state immediately before permanent native deletion.
_queries.AddCallback(CharacterDatabase.AsyncQuery(Trinity::StringFormat(
"SELECT g.account_id FROM vcore_adventure_generated_bot g JOIN vcore_adventure_rotation r ON r.old_guid=g.character_guid JOIN characters n ON n.guid=r.new_guid LEFT JOIN characters c ON c.guid=g.character_guid WHERE g.character_guid=%u AND g.persistent=0 AND g.state='replacing' AND COALESCE(c.online,0)=0",guid).c_str()).WithCallback([this,guid](QueryResult rows)
{
if(!rows){Failed("Rotation ownership/offline check failed; original retained.");return;}
if(sWorld->GetClientlessPlayerState(guid)!=ClientlessPlayerState::Offline || ObjectAccessor::FindConnectedPlayer(ObjectGuid(HighGuid::Player,guid))){_busy=false;return;}
auto account=rows->Fetch()[0].GetUInt32();
Player::DeleteFromDB(ObjectGuid(HighGuid::Player,guid),account,true,true);
// Verify the native asynchronous deletion before deleting profile and plans.
auto barrier=CharacterDatabase.BeginTransaction();barrier->PAppend("DO 0");
auto saved=CharacterDatabase.AsyncCommitTransaction(barrier);saved.AfterComplete([this,guid](bool ok)
{
if(!ok){Failed("Native deletion barrier failed; cleanup deferred.");return;}
_queries.AddCallback(CharacterDatabase.AsyncQuery(Trinity::StringFormat("SELECT guid FROM characters WHERE guid=%u",guid).c_str()).WithCallback([this,guid](QueryResult rows)
{
if(rows){Failed("Native deletion not complete; cleanup deferred.");return;}
auto world=WorldDatabase.BeginTransaction();
world->PAppend("DELETE FROM vcore_adventure_day WHERE character_guid=%u",guid);
auto worldSaved=WorldDatabase.AsyncCommitTransaction(world);
worldSaved.AfterComplete([this,guid](bool ok)
{
if(!ok){Failed("Rotated day-plan cleanup failed; journal retained.");return;}
auto tx=CharacterDatabase.BeginTransaction();
tx->PAppend("DELETE FROM vcore_adventure_supply_state WHERE character_guid=%u",guid);
tx->PAppend("DELETE FROM vcore_adventure_instance_visit WHERE character_guid=%u",guid);
tx->PAppend("DELETE FROM vcore_adventure_generated_bot WHERE character_guid=%u AND persistent=0 AND state='replacing'",guid);
tx->PAppend("DELETE FROM vcore_adventure_rotation WHERE old_guid=%u",guid);
auto saved=CharacterDatabase.AsyncCommitTransaction(tx);saved.AfterComplete([this,guid](bool ok)
{
if(!ok){Failed("Rotation metadata cleanup deferred.");return;}
_rotations.erase(_rotations.begin());_busy=false;_refresh=0;
TC_LOG_INFO("server.adventurebot","BOT_ROTATED_REMOVED guid=%u",guid);
});_saves.AddCallback(std::move(saved));
});_saves.AddCallback(std::move(worldSaved));
}));
});_saves.AddCallback(std::move(saved));
}));
}
void AdventurePopulationMgr::Online(Player* p)
{
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
if(profile && profile->ProfileVersion>=2 && profile->SourcePath.find("generated:")==0)_checks[p->GetGUID().GetCounter()]=0;
}
void AdventurePopulationMgr::Offline(Player* p){_checks.erase(p->GetGUID().GetCounter());}
void AdventurePopulationMgr::CleanInventory(Player* p)
{
if(p->GetFreeInventorySpace()>0)return;
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
auto allowance=sAdventureSupplyMgr->InventoryAllowance(p);
auto clean=[&](uint8 bag,uint8 slot)
{
auto item=p->GetItemByPos(bag,slot);if(!item)return;
auto proto=item->GetTemplate();
bool keep=proto->GetClass()==ITEM_CLASS_CONTAINER ||
(proto->GetClass()==ITEM_CLASS_WEAPON && proto->GetSubClass()==ITEM_SUBCLASS_WEAPON_FISHING_POLE) ||
(profile && (profile->KeepItems.count(item->GetEntry()) || item->GetEntry()==profile->CompanionLeashItem));
if(auto it=allowance.find(item->GetEntry());it!=allowance.end())
{
auto retain=std::min(it->second,item->GetCount());it->second-=retain;
if(!retain)p->DestroyItem(bag,slot,true);
else if(retain<item->GetCount())
{auto remove=item->GetCount()-retain;p->DestroyItemCount(item,remove,true);}
}
else if(!keep)p->DestroyItem(bag,slot,true);
};
for(uint8 slot=INVENTORY_SLOT_ITEM_START;slot<INVENTORY_SLOT_ITEM_END;++slot)clean(INVENTORY_SLOT_BAG_0,slot);
for(uint8 bag=INVENTORY_SLOT_BAG_START;bag<INVENTORY_SLOT_BAG_END;++bag)
if(auto item=p->GetBagByPos(bag))for(uint8 slot=0;slot<item->GetBagSize();++slot)clean(bag,slot);
}
void AdventurePopulationMgr::Maintain(Player* p,uint32 diff)
{
sAdventureBehaviorMgr->RefreshDay(p);
auto it=_checks.find(p->GetGUID().GetCounter());if(it==_checks.end())return;
if(it->second>diff){it->second-=diff;return;}
if(!p->IsInWorld() || !p->IsAlive() || p->IsInCombat() || p->GetMap()->IsDungeon() || p->GetTransport() || p->IsInFlight() || p->IsFlying() || p->IsBeingTeleported() || p->HasUnitState(UNIT_STATE_CASTING))return;
it->second=_maintenance+urand(0,30000);CleanInventory(p);
std::string error;
if(!sAdventureGeneratorMgr->Reconcile(p,error))TC_LOG_WARN("server.adventurebot","BOT_MANAGER_CHECK guid=%u detail='%s'",p->GetGUID().GetCounter(),error.c_str());
auto tx=CharacterDatabase.BeginTransaction();p->SaveToDB(tx);CharacterDatabase.CommitTransaction(tx);
}
bool AdventurePopulationMgr::Command(ChatHandler* handler,char const* args)
{
std::string op=args?args:"";
if(op=="reload"){if(_busy){handler->SendSysMessage("Loader has a pending operation; retry reload shortly.");return false;}_configured=false;Configure();}
else if(!op.empty() && op!="status")return false;
handler->PSendSysMessage("Adventure loader enabled=%u schema=%u busy=%u pools=%u/%u/%u/%u known=%u rotations=%u manager=%u error='%s'",_enabled,_schema,_busy,_targets[0],_targets[1],_targets[2],_targets[3],uint32(_bots.size()),uint32(_rotations.size()),uint32(_checks.size()),_lastError.c_str());
for(unsigned pool=0;pool<4;++pool)
{
unsigned online=0,loading=0,saved=0;
for(auto const& b:_bots)if(b.Pool==pool)
{
++saved;auto state=sWorld->GetClientlessPlayerState(b.Guid);
if(state==ClientlessPlayerState::Online)++online;
else if(state!=ClientlessPlayerState::Offline)++loading;
}
handler->PSendSysMessage("pool=%u faction=%s persistent=%u target=%u online=%u pending=%u saved=%u",pool,
Faction(pool)==1?"alliance":"horde",Persistent(pool),_targets[pool],online,loading,saved);
}
return true;
}
@@ -0,0 +1,49 @@
#ifndef VCORE_ADVENTURE_POPULATION_MGR_H
#define VCORE_ADVENTURE_POPULATION_MGR_H
#include "Common.h"
#include "AsyncCallbackProcessor.h"
#include "DatabaseEnv.h"
#include "AdventurePopulationPolicy.h"
#include <array>
#include <unordered_map>
#include <unordered_set>
#include <vector>
class Player;
class ChatHandler;
class TC_GAME_API AdventurePopulationMgr
{
public:
static AdventurePopulationMgr* instance();
void Update(uint32 diff);
void Online(Player* player);
void Offline(Player* player);
void Maintain(Player* player,uint32 diff);
bool Command(ChatHandler* handler,char const* args);
private:
struct Bot {uint32 Guid=0,Account=0,Pool=0;uint64 Created=0;uint32 Failures=0;time_t Retry=0;};
struct Account {uint32 Id=0,Faction=0,Count=0;};
struct Rotation {uint32 Old=0,New=0,Account=0;bool Submitted=false;};
void Configure();
void Snapshot();
void Step();
void Generate(unsigned pool,uint32 replace=0);
void CreateAccount(unsigned pool,uint32 replace);
void DeleteRotated();
void Failed(std::string const& detail);
void CleanInventory(Player* player);
bool _configured=false,_enabled=false,_busy=false,_schema=false;
uint32 _timer=0,_interval=2000,_refresh=0,_cursor=0,_minLevel=1,_maxLevel=85;
uint32 _maintenance=300000,_rotationAge=86400;
double _rotationChance=0.15;
std::array<unsigned,4> _targets{};
std::vector<Bot> _bots;
std::vector<Account> _accounts;
std::vector<Rotation> _rotations;
std::unordered_set<uint32> _considered;
std::unordered_map<uint32,uint32> _checks;
QueryCallbackProcessor _queries;
AsyncCallbackProcessor<TransactionCallback> _saves;
std::string _lastError;
};
#define sAdventurePopulationMgr AdventurePopulationMgr::instance()
#endif
@@ -0,0 +1,17 @@
#ifndef VCORE_ADVENTURE_POPULATION_POLICY_H
#define VCORE_ADVENTURE_POPULATION_POLICY_H
#include <array>
#include <algorithm>
#include <cstdint>
namespace AdventurePopulationPolicy
{
constexpr unsigned Pool(unsigned faction,bool persistent){return (faction==2?2:0)+(persistent?0:1);}
constexpr unsigned Faction(unsigned pool){return pool<2?1:2;}
constexpr bool Persistent(unsigned pool){return pool%2==0;}
inline bool Replace(bool persistent,bool offline,bool considered,std::int64_t age,std::int64_t minimumAge,
double sample,double chance)
{return !persistent && offline && !considered && age>=minimumAge && sample<std::clamp(chance,0.0,1.0);}
inline unsigned Deficit(unsigned target,unsigned online,unsigned loading)
{return target>online+loading?target-online-loading:0;}
}
#endif
@@ -313,3 +313,11 @@ bool AdventureSupplyMgr::PrepareCharacter(Player* player)
{
LoadCatalog();LearnSecondaries(player);return Bags(player);
}
std::unordered_map<uint32,uint32> AdventureSupplyMgr::InventoryAllowance(Player* player) const
{
std::unordered_map<uint32,uint32> result;
for(auto role:{"bandage","food","drink","healing_potion","mana_potion"})
if(auto item=Select(player,role))result[item->GetId()]=item->GetMaxStackSize();
return result;
}
@@ -19,6 +19,7 @@ public:
// True only while an out-of-combat recovery owns movement/intent execution.
bool Update(Player* player,uint32 diff,bool combatReaction);
void Report(ChatHandler* handler,Player* player);
std::unordered_map<uint32,uint32> InventoryAllowance(Player* player) const;
private:
using Use=AdventureSupplyPolicy::Use;
struct Supply {std::string Role;uint32 Item=0,MinLevel=0;};
@@ -1,5 +1,6 @@
#include "ClientlessCharacterFactory.h"
#include "AccountMgr.h"
#include "AsyncCallbackProcessor.h"
#include "CharacterCache.h"
#include "DBCStores.h"
#include "GameTime.h"
@@ -98,3 +99,76 @@ bool ClientlessCharacterFactory::PrepareSkillRank(Player* player,uint32 skill,ui
player->SetSkill(skill,step,maximum,maximum);
return player->GetPureSkillValue(skill)==maximum && player->GetPureMaxSkillValue(skill)==maximum;
}
namespace
{
auto FactoryQueries=std::make_unique<QueryCallbackProcessor>();
auto FactorySaves=std::make_unique<AsyncCallbackProcessor<TransactionCallback>>();
}
void ClientlessCharacterFactory::Update()
{
if(FactoryQueries)FactoryQueries->ProcessReadyCallbacks();
if(FactorySaves)FactorySaves->ProcessReadyCallbacks();
}
void ClientlessCharacterFactory::Shutdown()
{
// Drop retained offline Players while map/DBC/session infrastructure is alive.
// Queued atomic DB transactions can finish; next startup rebuilds the cache.
FactoryQueries.reset();FactorySaves.reset();
}
void ClientlessCharacterFactory::CreateAsync(uint32 account,Definition d,Prepare prepare,Append append,Completion done)
{
if(!FactoryQueries || !FactorySaves || !account || sWorld->FindSession(account) || !sObjectMgr->GetPlayerInfo(d.Race,d.Class) || d.Gender>GENDER_FEMALE ||
d.Level<1 || d.Level>sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL) || (d.Class==CLASS_DEATH_KNIGHT && d.Level<55))
{done({0,"Invalid account/race/class/gender/level."});return;}
std::string name=d.Name;
if(!normalizePlayerName(name) || name!=d.Name || ObjectMgr::CheckPlayerName(name,LOCALE_enUS,true)!=CHAR_NAME_SUCCESS ||
sObjectMgr->IsReservedName(name) || sCharacterCache->GetCharacterCacheByName(name))
{done({0,"Invalid, reserved or occupied name."});return;}
FactoryQueries->AddCallback(LoginDatabase.AsyncQuery(Trinity::StringFormat(
"SELECT a.username,a.expansion,a.locale,CAST(COALESCE(MAX(x.SecurityLevel),0) AS UNSIGNED) FROM account a LEFT JOIN account_access x ON x.AccountID=a.id AND x.RealmID IN (-1,%u) WHERE a.id=%u GROUP BY a.id",realm.Id.Realm,account).c_str()).WithCallback(
[account,d,prepare,append,done](QueryResult acc)
{
if(!acc || acc->Fetch()[3].GetUInt64()!=SEC_PLAYER || sWorld->FindSession(account))
{done({0,"Account missing, privileged or in use."});return;}
auto f=acc->Fetch();auto race=sChrRacesStore.LookupEntry(d.Race);auto cls=sChrClassesStore.LookupEntry(d.Class);
if(!race || !cls || race->Race_related>f[1].GetUInt8() || cls->Required_expansion>f[1].GetUInt8())
{done({0,"Account expansion mismatch."});return;}
std::string name=d.Name;CharacterDatabase.EscapeString(name);
FactoryQueries->AddCallback(CharacterDatabase.AsyncQuery(Trinity::StringFormat(
"SELECT (SELECT COUNT(*) FROM characters WHERE account=%u AND deleteDate IS NULL), (SELECT COUNT(*) FROM characters WHERE name='%s')",account,name.c_str()).c_str()).WithCallback(
[account,d,prepare,append,done,acc](QueryResult rows)
{
if(!rows || rows->Fetch()[0].GetUInt64()>=sWorld->getIntConfig(CONFIG_CHARACTERS_PER_REALM) || rows->Fetch()[1].GetUInt64() ||
sWorld->FindSession(account) || sCharacterCache->GetCharacterCacheByName(d.Name))
{done({0,"Character capacity/name changed or database unavailable."});return;}
auto f=acc->Fetch();
auto session=std::make_shared<WorldSession>(account,f[0].GetString(),nullptr,SEC_PLAYER,f[1].GetUInt8(),0,
LocaleConstant(std::min<uint8>(f[2].GetUInt8(),TOTAL_LOCALES-1)),0,false,WorldSession::WorldSessionMode::Clientless);
auto player=std::shared_ptr<Player>(new Player(session.get()),[session](Player* p){p->CleanupsBeforeDelete();delete p;});
player->GetMotionMaster()->Initialize();CreateInfo info(d);
if(!player->Create(sObjectMgr->GetGenerator<HighGuid::Player>().Generate(),&info))
{done({0,"Native creation failed."});return;}
player->setCinematic(1);
if(player->getLevel()!=d.Level)
{player->SetLevel(d.Level);player->InitStatsForLevel(true);player->InitTalentForLevel();player->InitTaxiNodesForLevel();player->InitGlyphsForLevel();}
player->SetCanModifyStats(true);player->SetMoney(0);player->LearnDefaultSkills();player->UpdateSkillsForLevel();
std::string error;
if(prepare && !prepare(player.get(),error)){done({0,error});return;}
player->UpdateMaxHealth();player->UpdateMaxPower(POWER_MANA);player->SetFullHealth();player->SetFullPower(POWER_MANA);
auto tx=CharacterDatabase.BeginTransaction();player->SaveToDB(tx,true);if(append)append(tx,player.get());
auto save=CharacterDatabase.AsyncCommitTransaction(tx);
save.AfterComplete([account,d,done,player,session,rows](bool ok)
{
if(!ok){done({0,"Atomic character save failed."});return;}
auto guid=player->GetGUID().GetCounter();
sCharacterCache->AddCharacterCacheEntry(player->GetGUID(),account,d.Name,d.Gender,d.Race,d.Class,d.Level);
sScriptMgr->OnPlayerCreate(player.get());
auto count=LoginDatabase.GetPreparedStatement(LOGIN_REP_REALM_CHARACTERS);
count->setUInt32(0,rows->Fetch()[0].GetUInt64()+1);count->setUInt32(1,account);count->setUInt32(2,realm.Id.Realm);LoginDatabase.Execute(count);
done({guid,{}});
});
FactorySaves->AddCallback(std::move(save));
}));
}));
}
@@ -22,6 +22,12 @@ public:
// Administrative preparation before world insertion; callers still gate
// trainer/rank/level eligibility. Uses the native SkillRaceClass/SkillTiers cap.
static bool PrepareSkillRank(Player* player,uint32 skill,uint32 step);
using Completion=std::function<void(Result)>;
// World-thread native preparation, asynchronous preflight and atomic save.
// Poll once per world update; never wait on a database future in the loader.
static void CreateAsync(uint32 account,Definition definition,Prepare prepare,Append append,Completion completion);
static void Update();
static void Shutdown();
static Result Create(uint32 account,Definition const& definition,Prepare prepare,Append append);
};
#endif
+58 -2
View File
@@ -8,6 +8,7 @@
#include "AdventureInteractionMgr.h"
#include "AdventureGroupMgr.h"
#include "AdventureGeneratorMgr.h"
#include "AdventurePopulationMgr.h"
#include "AdventureSupplyMgr.h"
#include "AdventureSocialActions.h"
#include "Config.h"
@@ -16,6 +17,7 @@
#include "Random.h"
#include <lua.hpp>
#include <algorithm>
#include <cmath>
#include <cctype>
#include <cstdlib>
#include <filesystem>
@@ -201,7 +203,7 @@ std::vector<ClientlessWeightedMount> ReadMountDictionary(lua_State* L, int movem
{
lua_Integer id = lua_tointeger(L, -2);
lua_Number weight = lua_tonumber(L, -1);
if (id > 0 && id <= 0xFFFFFFFFll && weight > 0.0)
if (id > 0 && id <= 0xFFFFFFFFll && std::isfinite(weight) && weight > 0.0 && weight <= 1000000.0)
mounts.push_back({ uint32(id), uint32(std::max<lua_Number>(1.0, weight)) });
}
lua_pop(L, 1);
@@ -220,11 +222,13 @@ public:
sClientlessLuaEngine->ExecuteAdventureScript(player);
sAdventureBehaviorMgr->Online(player);
sAdventureSupplyMgr->Online(player);
sAdventurePopulationMgr->Online(player);
}
void OnIntentChanged(Player* player, ClientlessIntent const* intent) override
{ sAdventureBehaviorMgr->IntentChanged(player, intent); }
void Update(Player* player, uint32 diff) override
{
sAdventurePopulationMgr->Maintain(player,diff);
if(sAdventureSupplyMgr->Update(player,diff,sAdventureCombatMgr->Busy(player)))return;
if(!sAdventureCombatMgr->Update(player,diff) && !sAdventureInteractionMgr->Update(player,diff))
sAdventureBehaviorMgr->Update(player,diff);
@@ -238,7 +242,7 @@ public:
void OnDamageTaken(Player* player, Unit* attacker, uint32 damage) override
{ sAdventureCombatMgr->DamageTaken(player, attacker, damage); }
void OnPlayerLogout(Player* player) override
{ sAdventureInteractionMgr->Offline(player); sAdventureSupplyMgr->Offline(player); sAdventureCombatMgr->Offline(player); sAdventureBehaviorMgr->Offline(player); }
{ sAdventurePopulationMgr->Offline(player); sAdventureInteractionMgr->Offline(player); sAdventureSupplyMgr->Offline(player); sAdventureCombatMgr->Offline(player); sAdventureBehaviorMgr->Offline(player); }
};
}
@@ -394,6 +398,19 @@ bool ClientlessLuaEngine::LoadAdventureProfile(Player* player)
profile.HomeRegion = ReadStringField(_state, -1, "home_region");
profile.Background = ReadStringField(_state, -1, "background");
profile.Vocation = ReadStringField(_state, -1, "vocation");
profile.ProfileVersion = uint32(std::clamp(ReadFloatField(_state,-1,"profile_version",0),0.0f,100.0f));
profile.Level = uint32(std::clamp(ReadFloatField(_state,-1,"level",0),0.0f,85.0f));
profile.Class = uint32(std::clamp(ReadFloatField(_state,-1,"class",0),0.0f,11.0f));
profile.Specialization = uint32(std::clamp(ReadFloatField(_state,-1,"specialization",0),0.0f,2.0f));
lua_getfield(_state,-1,"seed");
if(lua_isinteger(_state,-1)){auto value=lua_tointeger(_state,-1);if(value>=0 && value<=0xFFFFFFFFll)profile.Seed=uint32(value);}
lua_pop(_state,1);
profile.Role = ReadStringField(_state,-1,"role","damage");
if(profile.Role!="tank" && profile.Role!="healer" && profile.Role!="damage")profile.Role="damage";
profile.HomeZone = uint32(std::max(0.0f,ReadFloatField(_state,-1,"home_zone",0)));
profile.HomeCapital = uint32(std::max(0.0f,ReadFloatField(_state,-1,"home_capital",0)));
profile.FavoriteZone = uint32(std::max(0.0f,ReadFloatField(_state,-1,"favorite_zone",0)));
profile.FavoriteRegion = ReadStringField(_state,-1,"favorite_region");
}
lua_pop(_state, 1);
@@ -408,11 +425,38 @@ bool ClientlessLuaEngine::LoadAdventureProfile(Player* player)
profile.GroundMounts = ReadMountDictionary(_state, -1);
profile.CanFreeFly = ReadBoolField(_state, -1, "can_free_fly", false);
profile.FlightMounts = ReadMountDictionary(_state, -1, "flight_mounts");
profile.Companions = ReadMountDictionary(_state,-1,"companions");
profile.CompanionSpell = uint32(std::max(0.0f, ReadFloatField(_state, -1, "companion_spell", 0)));
profile.CompanionLeashItem = uint32(std::max(0.0f, ReadFloatField(_state, -1, "companion_leash_item", 0)));
}
lua_pop(_state, 1);
lua_getfield(_state,rootTable,"inventory");
if(lua_istable(_state,-1))
{
lua_getfield(_state,-1,"keep_items");
if(lua_istable(_state,-1))for(size_t i=1;i<=lua_rawlen(_state,-1);++i)
{
lua_rawgeti(_state,-1,i);auto id=lua_tointeger(_state,-1);
if(id>0 && id<=0xFFFFFFFFll)profile.KeepItems.insert(uint32(id));
lua_pop(_state,1);
}
lua_pop(_state,1);
}
lua_pop(_state,1);
lua_getfield(_state,rootTable,"zone_affinity");
if(lua_istable(_state,-1))
{
lua_pushnil(_state);
while(lua_next(_state,-2))
{
auto id=lua_tointeger(_state,-2);auto weight=lua_tonumber(_state,-1);
if(lua_isinteger(_state,-2) && lua_isnumber(_state,-1) && id>0 && id<=0xFFFFFFFFll && std::isfinite(weight))
profile.ZoneAffinity[uint32(id)]=float(std::clamp(weight,0.0,100.0));
lua_pop(_state,1);
}
}
lua_pop(_state,1);
profile.Personality = ReadFloatDictionary(_state, rootTable, "personality");
profile.RegionAffinity = ReadFloatDictionary(_state, rootTable, "region_affinity");
profile.ActivityAffinity = ReadFloatDictionary(_state, rootTable, "activity_affinity");
@@ -501,6 +545,18 @@ uint32 ClientlessLuaEngine::SelectGroundMountDisplayId(uint32 characterGuid) con
return profile->GroundMounts.back().DisplayId;
}
uint32 ClientlessLuaEngine::SelectCompanionSpell(uint32 guid) const
{
auto profile=GetAdventureProfile(guid);
if(!profile)return 0;
uint32 total=0;
for(auto const& pet:profile->Companions)total+=pet.Weight;
if(!total)return profile->CompanionSpell;
auto roll=urand(1,total);
for(auto const& pet:profile->Companions){if(roll<=pet.Weight)return pet.DisplayId;roll-=pet.Weight;}
return 0;
}
bool ClientlessLuaEngine::PrepareGroundMount(Player* player, float travelDistance, char const* context)
{
if (!player || !player->IsInWorld())
@@ -28,6 +28,12 @@ struct TC_GAME_API ClientlessAdventureProfile
std::string CharacterName;
std::string HomeRegion;
std::string Background, Vocation;
uint32 ProfileVersion = 0, Level = 0, Class = 0, Specialization = 0, Seed = 0;
std::string Role = "damage", FavoriteRegion;
uint32 HomeZone = 0, HomeCapital = 0, FavoriteZone = 0;
std::unordered_map<uint32, float> ZoneAffinity;
std::vector<ClientlessWeightedMount> Companions;
std::unordered_set<uint32> KeepItems;
std::string SourcePath;
bool CanGroundMount = false;
@@ -63,6 +69,7 @@ public:
bool LoadAdventureProfile(Player* player);
ClientlessAdventureProfile const* GetAdventureProfile(uint32 characterGuid) const;
uint32 SelectGroundMountDisplayId(uint32 characterGuid) const;
uint32 SelectCompanionSpell(uint32 characterGuid) const;
bool PrepareGroundMount(Player* player, float travelDistance, char const* context);
void DismountGroundMount(Player* player, char const* reason);
+2
View File
@@ -21,6 +21,7 @@
#include "World.h"
#include "ClientlessPlayerMgr.h"
#include "AdventurePopulationMgr.h"
#include "ClientlessRouteRecorderMgr.h"
#include "AccountMgr.h"
#include "AchievementMgr.h"
@@ -3393,6 +3394,7 @@ void World::UpdateSessions(uint32 diff)
// High-level clientless controllers observe state before the low-level
// lifecycle loop may destroy a Removing session.
sAdventurePopulationMgr->Update(diff);
sClientlessPlayerMgr->Update(diff);
sClientlessRouteRecorderMgr->Update(diff);
@@ -22,11 +22,14 @@
#include "AdventureCombatMgr.h"
#include "AdventureInteractionMgr.h"
#include "AdventureGeneratorMgr.h"
#include "AdventurePopulationMgr.h"
#include "AdventureSupplyMgr.h"
#include "ClientlessTravelGraphMgr.h"
#include "ClientlessRouteRecorderMgr.h"
#include "ClientlessLuaEngine.h"
#include "DatabaseEnv.h"
#include "AsyncCallbackProcessor.h"
#include <deque>
#include "DBCStores.h"
#include "Creature.h"
#include "Group.h"
@@ -64,6 +67,8 @@
namespace
{
bool Population(ChatHandler* handler,char const* args)
{return sAdventurePopulationMgr->Command(handler,args);}
bool GenerateBot(ChatHandler* handler,char const* args)
{ return sAdventureGeneratorMgr->Command(handler,args); }
bool InteractionStatus(ChatHandler* handler,char const* args)
@@ -2525,6 +2530,7 @@ static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*arg
{ "combat", rbac::RBAC_PERM_COMMAND_GM, true, &ClientlessCombatPreparation::Command, "" },
{ "interaction", rbac::RBAC_PERM_COMMAND_GM, true, &InteractionStatus, "" },
{ "generate", rbac::RBAC_PERM_COMMAND_GM, true, &GenerateBot, "" },
{ "population", rbac::RBAC_PERM_COMMAND_GM, true, &Population, "" },
{ "luareload", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessLuaReload, "" },
{ "transport", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessTransport, "" },
{ "login", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessLogin, "" },
@@ -2595,16 +2601,47 @@ class ClientlessInstanceVisitResume : public WorldScript
{
public:
ClientlessInstanceVisitResume() : WorldScript("ClientlessInstanceVisitResume") { }
void OnUpdate(uint32 diff) override {ClientlessCombatPreparation::Update(diff);}
void OnStartup() override
{
if (!CharacterDatabase.Query("SHOW TABLES LIKE 'vcore_adventure_instance_visit'")) return;
if (auto rows=CharacterDatabase.Query("SELECT c.account,c.guid FROM vcore_adventure_instance_visit v JOIN characters c ON c.guid=v.character_guid ORDER BY c.guid")) do
{
auto f=rows->Fetch();
sClientlessPlayerMgr->StartPlayer(f[0].GetUInt32(),f[1].GetUInt32(),CreateClientlessAdventureLuaController());
} while(rows->NextRow());
// Native instance continuation must not bypass generated population
// quotas or perform synchronous login/database loops during startup.
_requested=false;_delay=10000;_resume.clear();
}
void OnUpdate(uint32 diff) override
{
ClientlessCombatPreparation::Update(diff);
_queries.ProcessReadyCallbacks();
if(_delay>diff){_delay-=diff;return;}
_delay=2000;
if(!_requested)
{
_requested=true;
_queries.AddCallback(CharacterDatabase.AsyncQuery("SHOW TABLES LIKE 'vcore_adventure_instance_visit'").WithCallback([this](QueryResult visits)
{
if(!visits)return;
_queries.AddCallback(CharacterDatabase.AsyncQuery("SHOW TABLES LIKE 'vcore_adventure_generated_bot'").WithCallback([this](QueryResult generated)
{
std::string sql="SELECT c.account,c.guid FROM vcore_adventure_instance_visit v JOIN characters c ON c.guid=v.character_guid";
if(generated)sql+=" LEFT JOIN vcore_adventure_generated_bot g ON g.character_guid=c.guid WHERE g.character_guid IS NULL";
sql+=" ORDER BY c.guid";
_queries.AddCallback(CharacterDatabase.AsyncQuery(sql.c_str()).WithCallback([this](QueryResult rows)
{
if(rows)do{auto f=rows->Fetch();_resume.emplace_back(f[0].GetUInt32(),f[1].GetUInt32());}while(rows->NextRow());
}));
}));
}));
return;
}
if(_resume.empty())return;
auto next=_resume.front();_resume.pop_front();
if(sWorld->GetClientlessPlayerState(next.second)==ClientlessPlayerState::Offline)
sClientlessPlayerMgr->StartPlayer(next.first,next.second,CreateClientlessAdventureLuaController());
}
private:
uint32 _delay=10000;
bool _requested=false;
QueryCallbackProcessor _queries;
std::deque<std::pair<uint32,uint32>> _resume;
};
void AddSC_viano_bot_commands()
+2
View File
@@ -1,3 +1,4 @@
#include "ClientlessCharacterFactory.h"
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*
@@ -306,6 +307,7 @@ extern int main(int argc, char** argv)
std::shared_ptr<void> mapManagementHandle(nullptr, [](void*)
{
ClientlessCharacterFactory::Shutdown();
// unload battleground templates before different singletons destroyed
sBattlegroundMgr->DeleteAllBattlegrounds();
@@ -4340,3 +4340,19 @@ Clientless.Flight.Clearance = 60
# the final node/virtual-exit -> location leg (Icecrown, Wintergrasp).
Clientless.Flight.MinDistance = 600
Clientless.Flight.PreferredZones = "210 4197"
# AdventureBot population. Four independent online targets; 0 loads none.
# Initial loading happens gradually during normal world updates after startup.
# Existing manually created characters are never adopted or rotated.
Clientless.Adventure.Loader.Enabled = 0
Clientless.Adventure.Loader.Alliance.Persistent = 0
Clientless.Adventure.Loader.Alliance.Temporary = 0
Clientless.Adventure.Loader.Horde.Persistent = 0
Clientless.Adventure.Loader.Horde.Temporary = 0
Clientless.Adventure.Loader.IntervalMs = 2000
Clientless.Adventure.Loader.MinLevel = 1
Clientless.Adventure.Loader.MaxLevel = 85
# Chance considered once per offline temporary bot per process, before login.
Clientless.Adventure.Loader.RotationPercent = 15
Clientless.Adventure.Loader.RotationMinAgeHours = 24
Clientless.Adventure.Manager.IntervalSeconds = 300
@@ -0,0 +1,19 @@
"""Validate database contracts used by the asynchronous native creation path."""
from pathlib import Path
import re
root=Path(__file__).resolve().parents[2]
registration=(root/'src/server/database/Database/Implementation/LoginDatabase.cpp').read_text()
for name in ('LOGIN_INS_ACCOUNT','LOGIN_REP_REALM_CHARACTERS'):
match=re.search(r'PrepareStatement\('+name+r',\s*"([^"]+)",\s*(\w+)\)',registration)
assert match and match[2] in ('CONNECTION_ASYNC','CONNECTION_BOTH'),name+' unavailable on async connection'
factory=(root/'src/server/game/World/ClientlessCharacterFactory.cpp').read_text().split('void ClientlessCharacterFactory::CreateAsync(')[1]
assert 'x.AccountID=a.id' in factory and 'x.SecurityLevel' in factory
assert 'x.gmlevel' not in factory and 'x.id=a.id' not in factory
assert '.m_future.get(' not in factory and '.wait(' not in factory
assert 'FactorySaves->AddCallback' in factory and 'if(!ok)' in factory
assert factory.index('if(!ok)')<factory.index('AddCharacterCacheEntry')<factory.index('done({guid')
loader=(root/'src/server/game/World/AdventurePopulationMgr.cpp').read_text()
assert '.m_future.get(' not in loader and '.wait(' not in loader
world=(root/'src/server/game/World/World.cpp').read_text()
assert 'sAdventurePopulationMgr->Update(diff)' in world
print('Async creation DB contracts: prepared connection masks, native security column names, no future waits, successful-save cache publication and world-update-only loader hook passed')
@@ -0,0 +1,64 @@
"""Execute actual day generation/restoration with the real TINYINT hour schema."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text()
def fn(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]
code=r'''
#include "AdventureBehaviorPolicy.h"
#include "AdventureGroupPolicy.h"
#include <cassert>
#include <memory>
#include <unordered_map>
using uint32=unsigned;using namespace AdventureBehaviorPolicy;
#define TC_LOG_INFO(...) ((void)0)
struct Guid{unsigned GetCounter(){return 99;}};
struct Player{Guid GetGUID(){return {};}};
struct Field{unsigned n=0;std::string s;unsigned GetUInt8(){return n;}unsigned GetUInt32(){return 0;}std::string GetString(){return s;}};
struct Rows{std::vector<std::array<Field,3>> data;unsigned index=0;Field*Fetch(){return data[index].data();}bool NextRow(){return ++index<data.size();}};
struct Tx{unsigned slots=0,deletes=0;void PAppend(char const*,unsigned){++deletes;}void PAppend(char const*,unsigned,unsigned,unsigned,char const*,char const*){++slots;}};
struct DB{void EscapeString(std::string&){}
std::shared_ptr<Rows> rows;std::shared_ptr<Tx> saved;unsigned oldCleanup=0;
auto PQuery(char const*,unsigned,unsigned){if(rows)rows->index=0;return rows;}
void PExecute(char const*,unsigned,unsigned){++oldCleanup;}
auto BeginTransaction(){return std::make_shared<Tx>();}void CommitTransaction(std::shared_ptr<Tx> t){saved=t;}
}WorldDatabase;
struct Profile{std::unordered_map<std::string,float>Personality,ActivityAffinity;}profile;
struct Lua{Profile*GetAdventureProfile(unsigned){return &profile;}}lua;auto sClientlessLuaEngine=&lua;
struct AdventureBehaviorMgr{
struct Record{uint32 Date=0;Day Plan;std::mt19937 Random{9};};
int picks=0;float Affinity(std::unordered_map<std::string,float>const&,std::string const&,float);
bool Known(std::string const& s){return s=="social" || s=="adventure" || s=="profession" || s=="rest" || s=="leisure" || s=="economy" || s=="instance" || s=="raid" || s=="groupformation";}
std::string PickLocation(Player*,Record&,std::string const& intent){++picks;return intent+"_"+std::to_string(picks);}
void Generate(Player*,Record&,uint32);
};
'''+fn('float AdventureBehaviorMgr::Affinity(')+fn('void AdventureBehaviorMgr::Generate(')+r'''
int main(){
assert(AdventureGroupPolicy::CapitalFaction(1537)==1 && AdventureGroupPolicy::CapitalFaction(1637)==2 && AdventureGroupPolicy::CapitalFaction(4395)==0);
Player p;AdventureBehaviorMgr mgr;AdventureBehaviorMgr::Record r;
auto rows=std::make_shared<Rows>();
for(unsigned h=0;h<24;++h)rows->data.push_back({Field{h,""},Field{0,h%2?"social":"adventure"},Field{0,"saved_"+std::to_string(h)}});
WorldDatabase.rows=rows;mgr.Generate(&p,r,20261029);
assert(r.Date==20261029 && mgr.picks==0 && !WorldDatabase.saved && WorldDatabase.oldCleanup==1);
for(unsigned h=0;h<24;++h)assert(r.Plan[h].Location=="saved_"+std::to_string(h));
// New login uses the same stored slots instead of rerolling the day.
AdventureBehaviorMgr::Record relog;mgr.Generate(&p,relog,20261029);assert(relog.Plan[23].Location==r.Plan[23].Location && mgr.picks==0);
// Missing/incomplete current day is completely replaced, deleting older dates.
rows->data.resize(4);mgr.Generate(&p,r,20261030);
assert(r.Date==20261030 && mgr.picks>0 && WorldDatabase.saved->slots==24 && WorldDatabase.saved->deletes==1);
for(auto const& slot:r.Plan)assert(!slot.Intent.empty() && !slot.Location.empty());
// Geography remains weighted: curiosity reduces home bias without excluding it.
assert(RegionWeight(5,0)>RegionWeight(5,1));assert(RegionWeight(1,1)==1);
assert(LevelWeight(60,58,63,true)==1 && LevelWeight(60,80,82,true)==0);
assert(LevelWeight(60,1,10,true)<LevelWeight(60,58,63,true));
assert(LevelWeight(60,1,85,false)==1);
}
'''
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 day planner: all 24 TINYINT slots restored across logins, incomplete/old-day replacement, full transaction and curiosity/level weighting passed')
@@ -0,0 +1,49 @@
"""Execute the shared production birth/maintenance equipment eligibility."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/AdventureGeneratorMgr.cpp').read_text()
a=s.index(' float EquipmentScore(');b=s.index('{',a);e=b+1;n=1
while n:n+=(s[e]=='{')-(s[e]=='}');e+=1
code=r'''
#include "AdventureGeneratorPolicy.h"
#include <cassert>
#include <cmath>
#include <limits>
using uint32=unsigned;using Archetype=AdventureGeneratorPolicy::Archetype;
enum{EQUIP_ERR_OK=0,ITEM_FLAG_CONJURED=1,HOLIDAY_NONE=0,ITEM_CLASS_ARMOR=4,
EQUIPMENT_SLOT_NECK=1,EQUIPMENT_SLOT_BACK=14,EQUIPMENT_SLOT_FINGER1=10,EQUIPMENT_SLOT_FINGER2=11,EQUIPMENT_SLOT_TRINKET1=12,EQUIPMENT_SLOT_TRINKET2=13,EQUIPMENT_SLOT_OFFHAND=16,
ITEM_MOD_STRENGTH=4,ITEM_MOD_AGILITY=3,ITEM_MOD_INTELLECT=5,ITEM_MOD_ATTACK_POWER=38,ITEM_MOD_RANGED_ATTACK_POWER=39,ITEM_MOD_SPIRIT=6,ITEM_MOD_MANA=0,ITEM_MOD_MANA_REGENERATION=43,ITEM_MOD_SPELL_POWER=45,ITEM_MOD_SPELL_PENETRATION=47,MAX_ITEM_PROTO_STATS=1};
struct Player{unsigned level=60;bool usable=true;unsigned getLevel(){return level;}template<class T>unsigned CanUseItem(T*){return usable?0:1;}};
struct Effect{unsigned SpellID;};
struct ItemTemplate{
bool fits=true;unsigned quality=3,ilevel=80,cls=4,sub=4,flags=0,holiday=0,map=0,duration=0,rep=0,area=0,prop=0,suffix=0;int req=60,stat=4;float value=10;std::vector<Effect>Effects;
int GetRequiredLevel()const{return req;}unsigned GetQuality()const{return quality;}unsigned GetBaseItemLevel()const{return ilevel;}
unsigned GetDuration()const{return duration;}unsigned GetFlags()const{return flags;}unsigned GetRequiredReputationFaction()const{return rep;}unsigned GetArea()const{return area;}unsigned GetMap()const{return map;}unsigned GetHolidayID()const{return holiday;}unsigned GetRandomProperty()const{return prop;}unsigned GetRandomSuffix()const{return suffix;}
unsigned GetClass()const{return cls;}unsigned GetSubClass()const{return sub;}int GetItemStatType(unsigned)const{return stat;}float GetStatValue(unsigned,Player*)const{return value;}
};
bool Slot(ItemTemplate const*i,unsigned,Archetype const&){return i->fits;}
bool UsefulSpell(unsigned id,Archetype const&){return id==42;}
'''+s[a:e]+r'''
int main(){Player p;ItemTemplate item;auto a=AdventureGeneratorPolicy::Archetypes()[0];
auto check=[&]{return EquipmentScore(&p,&item,4,a);};assert(check()==100);
item.sub=1;assert(!std::isfinite(check()));item.sub=4; // wearable cloth is wrong for this warrior
item.stat=ITEM_MOD_INTELLECT;assert(!std::isfinite(check()));item.stat=4;
item.Effects={{0xFFFFFFFFu}};assert(!std::isfinite(check()));item.Effects={{42}};assert(std::isfinite(check()));
item.req=61;assert(!std::isfinite(check()));item.req=60;
item.flags=ITEM_FLAG_CONJURED;assert(!std::isfinite(check()));item.flags=0;
item.rep=1;assert(!std::isfinite(check()));item.rep=0;
item.map=719;assert(!std::isfinite(check()));item.map=0;
item.holiday=1;assert(!std::isfinite(check()));item.holiday=0;
p.usable=false;assert(!std::isfinite(check()));p.usable=true;
item.fits=false;assert(!std::isfinite(check()));
}
'''
# Both birth candidate ranking and retention of already equipped items consume this exact rule.
assert 'EquipmentScore(p,current->GetTemplate(),slot,a)' in s
assert 'auto score=EquipmentScore(p,item,slot,a)' in s
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 gear maintenance: same birth/retention gate rejects wrong armor/stats, malformed effects, level, temporary, reputation, map, holiday, native-use and slot mismatches passed')
@@ -29,7 +29,7 @@ int main(){
spells.data[7].Effects={{SPELL_AURA_MOD_SPELL_HIT_CHANCE,0,0,0}};assert(!UsefulSpell(7,warrior));assert(UsefulSpell(7,mage));
spells.data[8].Effects={{SPELL_AURA_MOD_DAMAGE_DONE,2,0,0}};assert(!UsefulSpell(8,warrior));assert(UsefulSpell(8,mage));
spells.data[9].Effects={{SPELL_AURA_MOD_DAMAGE_DONE,1,0,0}};assert(UsefulSpell(9,warrior));assert(!UsefulSpell(9,mage));
assert(UsefulSpell(999,warrior)); // missing metadata never grants a spell
assert(!UsefulSpell(999,warrior));assert(!UsefulSpell(0xFFFFFFFFu,warrior)); // malformed proc metadata cannot reach native equip assertions
}
'''
with tempfile.TemporaryDirectory() as d:
@@ -33,6 +33,7 @@ struct Behavior{void InterruptForInteraction(Player*){}}behavior;auto sAdventure
#define TC_LOG_INFO(...) ((void)0)
struct World{Session session;bool available=false;Session*FindClientlessSession(unsigned){return available?&session:nullptr;}ClientlessPlayerState state=ClientlessPlayerState::Offline;auto GetClientlessPlayerState(unsigned){return state;}}world;auto sWorld=&world;
struct Core{int starts=0,stops=0;bool StartPlayer(unsigned,unsigned,int){++starts;return true;}bool StopPlayer(unsigned){++stops;return true;}}core;auto sClientlessPlayerMgr=&core;int CreateClientlessAdventureLuaController(){return 0;}
struct Profile{unsigned Specialization=0;}profile;struct Lua{Profile*GetAdventureProfile(unsigned){return &profile;}}lua;auto sClientlessLuaEngine=&lua;
struct AdventureGeneratorMgr{
int creates=0;bool Command(ChatHandler*,char const*);
bool Create(ChatHandler*,uint32,uint32,uint8,unsigned,std::string const&,bool){++creates;return true;}
@@ -39,6 +39,18 @@ int main(){
assert(EligibleItem(60,58,3,85,false,false));assert(!EligibleItem(60,61,3,85,false,false));
assert(!EligibleItem(60,58,5,85,false,false));assert(!EligibleItem(60,58,3,85,true,false));assert(!EligibleItem(60,58,3,85,false,true));
assert(!EligibleItem(60,1,3,85,false,false));
for(unsigned level:{1,5,60,85})for(unsigned seed=0;seed<50;++seed)for(auto a:Archetypes()){
std::mt19937 random(seed);a=Personalize(a,random);
auto text=Profile("Testname",a,3,"inn","Dun Morogh",random,level,1,3483,"Hellfire",seed);
assert(luaL_dostring(L,text.c_str())==LUA_OK);
lua_getfield(L,-1,"identity");lua_getfield(L,-1,"level");assert(lua_tointeger(L,-1)==level);lua_pop(L,1);
lua_getfield(L,-1,"class");assert(lua_tointeger(L,-1)==a.Class);lua_pop(L,1);
lua_getfield(L,-1,"specialization");assert(lua_tointeger(L,-1)==a.Spec);lua_pop(L,1);
lua_getfield(L,-1,"role");assert(std::string(lua_tostring(L,-1))==Role(a));lua_pop(L,1);
lua_getfield(L,-1,"home_capital");assert(lua_tointeger(L,-1)==1537);lua_pop(L,2);
lua_getfield(L,-1,"zone_affinity");for(unsigned zone:{1,1537,3483}){lua_rawgeti(L,-1,zone);assert(lua_tonumber(L,-1)>1);lua_pop(L,1);}lua_pop(L,1);
lua_getfield(L,-1,"behavior");lua_getfield(L,-1,"daily_schedule");assert(lua_toboolean(L,-1));lua_settop(L,0);
}
lua_close(L);
}
'''
@@ -1,4 +1,4 @@
"""Run the actual optional WorldScript startup recovery with stored visits."""
"""Run the actual optional WorldScript deferred recovery with stored visits."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
@@ -9,27 +9,38 @@ code=r'''
#include <cstdint>
#include <memory>
#include <vector>
#include <deque>
#include <functional>
#include <string>
using uint32=uint32_t;
struct WorldScript{WorldScript(char const*){}virtual void OnStartup(){}virtual void OnUpdate(uint32){}};
namespace ClientlessCombatPreparation{int ticks=0;void Update(uint32){++ticks;}}
struct Field{uint32 value;uint32 GetUInt32(){return value;}};
struct Rows{std::vector<std::vector<Field>> data;unsigned index=0;Field* Fetch(){return data[index].data();}bool NextRow(){return ++index<data.size();}};
struct Database{bool schema=true;std::shared_ptr<Rows> rows=std::make_shared<Rows>();
std::shared_ptr<Rows> Query(char const* sql){if(sql[0]=='S'&&sql[1]=='H')return schema?std::make_shared<Rows>():nullptr;rows->index=0;return rows->data.empty()?nullptr:rows;}
using QueryResult=std::shared_ptr<Rows>;
struct Callback{std::function<void()> run;};
struct Query{QueryResult row;template<class F>Callback WithCallback(F f){return {[f,row=row]{f(row);}};}};
struct QueryCallbackProcessor{std::vector<Callback> pending;void AddCallback(Callback c){pending.push_back(c);}void ProcessReadyCallbacks(){auto work=std::move(pending);pending.clear();for(auto&c:work)c.run();}};
struct Database{bool schema=true;std::shared_ptr<Rows> rows=std::make_shared<Rows>();int queries=0;
Query AsyncQuery(char const* sql){++queries;std::string text=sql;if(text.find("SHOW TABLES")!=std::string::npos)return {text.find("generated_bot")==std::string::npos && schema?std::make_shared<Rows>():nullptr};rows->index=0;return {rows->data.empty()?nullptr:rows};}
}CharacterDatabase;
enum class ClientlessPlayerState{Offline,Online};
struct World{ClientlessPlayerState GetClientlessPlayerState(unsigned){return ClientlessPlayerState::Offline;}}world;auto sWorld=&world;
struct Manager{std::vector<std::pair<uint32,uint32>> started;void StartPlayer(uint32 account,uint32 guid,std::unique_ptr<int> controller){assert(controller&&*controller==42);started.emplace_back(account,guid);}}manager;
auto sClientlessPlayerMgr=&manager;
std::unique_ptr<int> CreateClientlessAdventureLuaController(){return std::make_unique<int>(42);}
'''+s+r'''
int main(){ClientlessInstanceVisitResume script;
CharacterDatabase.schema=false;script.OnStartup();assert(manager.started.empty());
CharacterDatabase.schema=true;script.OnStartup();assert(manager.started.empty());
CharacterDatabase.rows->data={{{6},{26}},{{7},{27}}};script.OnStartup();
int main(){
auto advance=[](ClientlessInstanceVisitResume& script){script.OnUpdate(10000);for(int i=0;i<5;++i)script.OnUpdate(2000);};
CharacterDatabase.schema=false;ClientlessInstanceVisitResume absent;absent.OnStartup();assert(!CharacterDatabase.queries);advance(absent);assert(manager.started.empty());
CharacterDatabase.schema=true;ClientlessInstanceVisitResume empty;empty.OnStartup();advance(empty);assert(manager.started.empty());
CharacterDatabase.rows->data={{{6},{26}},{{7},{27}}};ClientlessInstanceVisitResume saved;saved.OnStartup();assert(manager.started.empty());advance(saved);
assert((manager.started==std::vector<std::pair<uint32,uint32>>{{6,26},{7,27}}));
assert(ClientlessCombatPreparation::ticks>0);
}
'''
with tempfile.TemporaryDirectory() as tmp:
src=Path(tmp)/'startup.cpp';exe=Path(tmp)/'startup';src.write_text(code)
subprocess.run(['g++','-std=c++17',str(src),'-o',str(exe)],check=True)
subprocess.run([str(exe)],check=True)
print('Instance startup script: no schema/no visits stays idle; saved visitors resume with the Adventure controller and native accounts.')
print('Deferred instance script: missing schema/visits stays idle; no login work on startup; saved visitors resume with Adventure controllers and native accounts.')
@@ -0,0 +1,56 @@
"""Execute production legacy instance recovery without bypassing bot quotas."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/scripts/Custom/viano_bot_commands.cpp').read_text()
a=s.index('class ClientlessInstanceVisitResume : public WorldScript');b=s.index('\nvoid AddSC_viano_bot_commands()',a)
code=r'''
#include <cassert>
#include <deque>
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
using uint32=unsigned;
struct WorldScript{WorldScript(char const*){}virtual~WorldScript()=default;virtual void OnStartup(){}virtual void OnUpdate(unsigned){}};
struct Field{unsigned v=0;unsigned GetUInt32(){return v;}};
struct Rows{std::vector<std::pair<unsigned,unsigned>>data;unsigned index=0;Field f[2];Field*Fetch(){f[0].v=data[index].first;f[1].v=data[index].second;return f;}bool NextRow(){return ++index<data.size();}};
using QueryResult=std::shared_ptr<Rows>;
struct Callback{std::function<void()>run;};
struct Query{QueryResult rows;template<class F>Callback WithCallback(F f){return {[f,rows=rows]{f(rows);}};}};
struct QueryCallbackProcessor{std::vector<Callback>work;void AddCallback(Callback c){work.push_back(c);}void ProcessReadyCallbacks(){auto batch=std::move(work);work.clear();for(auto&c:batch)c.run();}};
struct Database{
bool visits=true,generated=true;std::vector<std::string>queries;
Query AsyncQuery(char const*text){std::string s=text;queries.push_back(s);if(s.find("SHOW TABLES")!=std::string::npos){bool exists=s.find("generated_bot")!=std::string::npos?generated:visits;return {exists?std::make_shared<Rows>():nullptr};}
auto r=std::make_shared<Rows>();r->data={{6,26},{6,40}};if(s.find("g.character_guid IS NULL")==std::string::npos)r->data.push_back({8,36});return {r};}
}CharacterDatabase;
enum class ClientlessPlayerState{Offline,Online};
struct World{std::unordered_map<unsigned,ClientlessPlayerState>states;auto GetClientlessPlayerState(unsigned id){return states[id];}}world;auto sWorld=&world;
struct Core{std::vector<unsigned>starts;void StartPlayer(unsigned,unsigned guid,int){starts.push_back(guid);world.states[guid]=ClientlessPlayerState::Online;}}core;auto sClientlessPlayerMgr=&core;
int CreateClientlessAdventureLuaController(){return 0;}
namespace ClientlessCombatPreparation{unsigned updates=0;void Update(unsigned){++updates;}}
'''+s[a:b]+r'''
int main(){
ClientlessInstanceVisitResume script;script.OnStartup();assert(CharacterDatabase.queries.empty());
script.OnUpdate(9999);assert(CharacterDatabase.queries.empty());script.OnUpdate(1);assert(CharacterDatabase.queries.size()==1 && core.starts.empty());
script.OnUpdate(1);script.OnUpdate(1);script.OnUpdate(1);assert(CharacterDatabase.queries.size()==3 && core.starts.empty());
script.OnUpdate(1997);assert(core.starts==std::vector<unsigned>{26});script.OnUpdate(1999);assert(core.starts.size()==1);script.OnUpdate(1);assert((core.starts==std::vector<unsigned>{26,40}));
script.OnUpdate(2000);assert(core.starts.size()==2); // generated36 belongs exclusively to the population loader
assert(CharacterDatabase.queries.back().find("g.character_guid IS NULL")!=std::string::npos);
assert(ClientlessCombatPreparation::updates>0);
// Backward compatibility: a server without generator schema still resumes manual visits.
CharacterDatabase={};CharacterDatabase.generated=false;world={};core={};
ClientlessInstanceVisitResume legacy;legacy.OnStartup();legacy.OnUpdate(10000);for(int i=0;i<5;++i)legacy.OnUpdate(2000);
assert((core.starts==std::vector<unsigned>{26,40,36}));
// Missing optional visit schema never blocks the world or starts characters.
CharacterDatabase={};CharacterDatabase.visits=false;world={};core={};
ClientlessInstanceVisitResume absent;absent.OnStartup();absent.OnUpdate(10000);for(int i=0;i<5;++i)absent.OnUpdate(2000);assert(core.starts.empty());
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['g++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Production instance recovery: no startup DB/login work, asynchronous delayed/paced manual recovery, generated-bot quota ownership, absent-schema compatibility and existing preparation ticks passed')
+2 -2
View File
@@ -72,8 +72,8 @@ struct ClientlessCompanionMgr{
static Creature* Resolve(Player*,ClientlessCompanionScene const&){return valid?&cat:nullptr;}
static void Reset(Player*,ClientlessCompanionScene& s){if(s.Active)++resets;s={};}
};
struct Profile{bool CanGroundMount=true;float GroundMountMinDistance=45;std::vector<int> GroundMounts{1};uint32 CompanionSpell=10673,CompanionLeashItem=37460;}profile;
struct Lua{int mounts=0,dismounts=0;Profile* GetAdventureProfile(uint32){return &profile;}bool PrepareGroundMount(Player*,float,char const*){++mounts;return true;}void DismountGroundMount(Player*,char const*){++dismounts;}}lua;auto sClientlessLuaEngine=&lua;
struct Profile{bool CanGroundMount=true;float GroundMountMinDistance=45;std::vector<int> GroundMounts{1},Companions;uint32 CompanionSpell=10673,CompanionLeashItem=37460;}profile;
struct Lua{uint32 SelectCompanionSpell(uint32){return profile.CompanionSpell;}int mounts=0,dismounts=0;Profile* GetAdventureProfile(uint32){return &profile;}bool PrepareGroundMount(Player*,float,char const*){++mounts;return true;}void DismountGroundMount(Player*,char const*){++dismounts;}}lua;auto sClientlessLuaEngine=&lua;
struct CombatMgr{bool available=true;int starts=0;bool TrainingAvailable(Player*){return available;}bool Train(Player*,uint32 duration,time_t){assert(duration>0);++starts;return available;}}combatMgr;auto sAdventureCombatMgr=&combatMgr;
struct AdventureMicroAction{std::string Requirement;};
struct Location{struct{std::string Category="leisure";float O=0;}Point;bool IsArea=false;uint32 AreaId=0,ZoneId=1519;}location;
@@ -0,0 +1,50 @@
"""Execute production full-inventory cleanup and all protection decisions."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/AdventurePopulationMgr.cpp').read_text()
a=s.index('void AdventurePopulationMgr::CleanInventory(');b=s.index('{',a);e=b+1;depth=1
while depth:depth+=(s[e]=='{')-(s[e]=='}');e+=1
code=r'''
#include <cassert>
#include <map>
#include <unordered_map>
#include <unordered_set>
#include <algorithm>
using uint8=unsigned char;using uint32=unsigned;
constexpr unsigned ITEM_CLASS_CONTAINER=1,ITEM_CLASS_WEAPON=2,ITEM_SUBCLASS_WEAPON_FISHING_POLE=3;
constexpr unsigned INVENTORY_SLOT_ITEM_START=0,INVENTORY_SLOT_ITEM_END=4,INVENTORY_SLOT_BAG_START=4,INVENTORY_SLOT_BAG_END=6,INVENTORY_SLOT_BAG_0=255;
struct Proto{unsigned cls=0,sub=0;unsigned GetClass(){return cls;}unsigned GetSubClass(){return sub;}};
struct Item{unsigned entry,count=1;Proto proto;Proto*GetTemplate(){return &proto;}unsigned GetEntry(){return entry;}unsigned GetCount(){return count;}};
struct Bag{unsigned GetBagSize(){return 8;}};
struct Guid{unsigned GetCounter(){return 50;}};
struct Player{
unsigned free=0,destroys=0;std::map<std::pair<unsigned,unsigned>,Item>items;Bag bag;
unsigned GetFreeInventorySpace(){return free;}Guid GetGUID(){return {};}
Item*GetItemByPos(unsigned b,unsigned s){auto it=items.find({b,s});return it==items.end()?nullptr:&it->second;}
Bag*GetBagByPos(unsigned b){return b==4?&bag:nullptr;}
void DestroyItem(unsigned b,unsigned s,bool){items.erase({b,s});++destroys;}
void DestroyItemCount(Item*item,unsigned& count,bool){assert(count<item->count);item->count-=count;++destroys;count=0;}
};
struct Profile{std::unordered_set<unsigned>KeepItems{2901};unsigned CompanionLeashItem=37460;}profile;
struct Lua{Profile*GetAdventureProfile(unsigned){return &profile;}}lua;auto sClientlessLuaEngine=&lua;
struct Supply{std::unordered_map<unsigned,unsigned>InventoryAllowance(Player*){return {{100,20},{101,20}};}}supply;auto sAdventureSupplyMgr=&supply;
struct AdventurePopulationMgr{void CleanInventory(Player*);};
'''+s[a:e]+r'''
int main(){
Player p;AdventurePopulationMgr mgr;
p.items={{ {255,0},Item{100,15}},{{255,1},Item{999,1}},{{255,2},Item{2901,1}},{{255,3},Item{37460,1}},
{{4,0},Item{100,10}},{{4,1},Item{100,20}},{{4,2},Item{101,20}},{{4,3},Item{6256,1,{ITEM_CLASS_WEAPON,ITEM_SUBCLASS_WEAPON_FISHING_POLE}}},
{{4,4},Item{54443,1,{ITEM_CLASS_CONTAINER,0}}},{{4,5},Item{777,1,{ITEM_CLASS_WEAPON,0}}},{{0,10},Item{888,1}}};
p.free=1;mgr.CleanInventory(&p);assert(!p.destroys && p.items.size()==11);
p.free=0;mgr.CleanInventory(&p);
assert(!p.GetItemByPos(255,1));assert(!p.GetItemByPos(4,1));assert(!p.GetItemByPos(4,5));
assert(p.GetItemByPos(255,0)->count==15 && p.GetItemByPos(4,0)->count==5);
for(auto pos:{std::pair{255,2},std::pair{255,3},std::pair{4,2},std::pair{4,3},std::pair{4,4},std::pair{0,10}})assert(p.GetItemByPos(pos.first,pos.second));
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['g++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Production inventory maintenance: only full backpacks, native stack trimming, level-appropriate supplies, tools, leash, fishing pole, containers and untouched equipped slots passed')
@@ -0,0 +1,70 @@
"""Execute the production loader selection, quotas, retry and rotation gates."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/AdventurePopulationMgr.cpp').read_text()
def function(sig):
a=s.index(sig);b=s.index('{',a);e=b+1;depth=1
while depth:depth+=(s[e]=='{')-(s[e]=='}');e+=1
return s[a:e]
code=r'''
#include "AdventurePopulationPolicy.h"
#include <cassert>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <string>
using uint32=unsigned;using uint64=unsigned long long;
using namespace AdventurePopulationPolicy;
namespace GameTime{time_t now=100000;time_t GetGameTime(){return now;}}
struct ObjectGuid{enum{Player};unsigned id;ObjectGuid(int,unsigned i):id(i){}};
namespace HighGuid{constexpr int Player=0;}
namespace ObjectAccessor{std::unordered_set<unsigned>connected;bool FindConnectedPlayer(ObjectGuid g){return connected.count(g.id);}}
enum class ClientlessPlayerState{Offline,Loading,Online,Removing};
struct World{std::unordered_map<unsigned,ClientlessPlayerState>states;auto GetClientlessPlayerState(unsigned id){return states[id];}}world;auto sWorld=&world;
struct Core{bool succeed=true;std::vector<unsigned>started;bool StartPlayer(unsigned,unsigned guid,int){started.push_back(guid);if(succeed)world.states[guid]=ClientlessPlayerState::Loading;return succeed;}}core;auto sClientlessPlayerMgr=&core;
int CreateClientlessAdventureLuaController(){return 0;}
float sample=.5;float frand(int,int){return sample;}
#define TC_LOG_INFO(...) ((void)0)
struct AdventurePopulationMgr{
struct Bot{uint32 Guid,Account,Pool;uint64 Created;uint32 Failures=0;time_t Retry=0;};
unsigned _cursor=0,_rotationAge=86400;double _rotationChance=.15;
std::array<unsigned,4>_targets{};std::vector<Bot>_bots;std::unordered_set<unsigned>_considered;
std::vector<std::pair<unsigned,unsigned>>generated;
void Step();void Generate(unsigned pool,unsigned replace=0){generated.push_back({pool,replace});}
};
'''+function('void AdventurePopulationMgr::Step()')+r'''
int main(){
assert(Pool(1,true)==0 && Pool(1,false)==1 && Pool(2,true)==2 && Pool(2,false)==3);
assert(!Replace(true,true,false,100,0,0,1));assert(!Replace(false,false,false,100,0,0,1));
assert(!Replace(false,true,true,100,0,0,1));assert(!Replace(false,true,false,99,100,0,1));
assert(Replace(false,true,false,100,100,.1,.2));assert(!Replace(false,true,false,100,0,.3,.2));
assert(!Deficit(5,3,2) && Deficit(5,2,1)==2 && !Deficit(0,2,0));
AdventurePopulationMgr mgr;
mgr.Step();assert(mgr.generated.empty() && core.started.empty());
mgr._targets={1,2,3,4};
mgr.Step();assert(mgr.generated.back().first==0);mgr.Step();assert(mgr.generated.back().first==1);
mgr.Step();assert(mgr.generated.back().first==2);mgr.Step();assert(mgr.generated.back().first==3);
mgr={};mgr._targets={1,0,0,0};mgr._bots={{10,6,0,0},{11,6,0,0},{12,6,2,0}};
world.states[10]=ClientlessPlayerState::Online;mgr.Step();assert(core.started.empty());
world.states[10]=ClientlessPlayerState::Loading;mgr.Step();assert(core.started.empty());
world.states[10]=ClientlessPlayerState::Offline;mgr.Step();assert(core.started.back()==10);assert(mgr.generated.empty());
world.states[10]=ClientlessPlayerState::Offline;core.succeed=false;core.started.clear();
mgr._bots[0].Retry=0;mgr.Step();assert(core.started.size()==1 && mgr._bots[0].Failures==1);
mgr.Step();assert(core.started.back()==11 && mgr.generated.empty()); // retry other saved candidate
mgr.Step();assert(core.started.size()==2 && mgr.generated.empty()); // no unbounded regeneration
mgr={};world.states.clear();core.started.clear();core.succeed=true;
mgr._targets={0,1,0,0};mgr._bots={{20,6,1,1}};sample=0;mgr.Step();
assert(mgr.generated.size()==1 && mgr.generated[0].second==20 && core.started.empty());
mgr.Step();assert(core.started.back()==20); // considered once per boot, no replacement loop
mgr={};core.started.clear();world.states.clear();mgr._targets={1,0,0,0};mgr._bots={{30,6,0,1}};
mgr._rotationChance=1;mgr.Step();assert(mgr.generated.empty() && core.started.back()==30); // persistence absolute
mgr={};core.started.clear();world.states.clear();mgr._targets={0,0,0,1};mgr._bots={{40,6,3,1}};
ObjectAccessor::connected.insert(40);mgr.Step();assert(core.started.empty() && mgr.generated.empty()); // real connected player protected
}
'''
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 population: four independent faction/persistence targets, fair selection, pending reservations, retries, existing inventory, once-per-boot rotation and connected/persistent protection passed')
@@ -0,0 +1,39 @@
"""Execute actual Lua weighted dictionaries and companion selection."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/ClientlessLuaEngine.cpp').read_text()
def fn(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]
code=r'''
#include <lua.hpp>
#include <algorithm>
#include <cassert>
#include <cmath>
#include <vector>
#include <unordered_map>
using uint32=unsigned;
struct ClientlessWeightedMount{uint32 DisplayId,Weight;};
struct Profile{std::vector<ClientlessWeightedMount>Companions;unsigned CompanionSpell=0;}profile;
unsigned roll=1;unsigned urand(unsigned low,unsigned high){assert(roll>=low && roll<=high);return roll;}
struct ClientlessLuaEngine{bool available=true;Profile const*GetAdventureProfile(unsigned)const{return available?&profile:nullptr;}unsigned SelectCompanionSpell(unsigned)const;};
'''+fn('std::vector<ClientlessWeightedMount> ReadMountDictionary(')+fn('uint32 ClientlessLuaEngine::SelectCompanionSpell(')+r'''
int main(){
lua_State*L=luaL_newstate();luaL_openlibs(L);
assert(luaL_dostring(L,"return { companions = { [10673]=9,[10675]=1,[10676]=0,[10707]=-1,[123]=math.huge,[124]=0/0,['bad']=8,[125]=1e30 } }")==LUA_OK);
profile.Companions=ReadMountDictionary(L,-1,"companions");assert(profile.Companions.size()==2);
ClientlessLuaEngine engine;std::unordered_map<unsigned,unsigned>counts;
for(roll=1;roll<=10;++roll)++counts[engine.SelectCompanionSpell(50)];
assert(counts[10673]==9 && counts[10675]==1 && counts.size()==2);
profile.Companions.clear();profile.CompanionSpell=10707;assert(engine.SelectCompanionSpell(50)==10707);
engine.available=false;assert(!engine.SelectCompanionSpell(50));lua_close(L);
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'test.cpp').write_text(code)
flags=subprocess.check_output(['pkg-config','--cflags','--libs','lua5.4'],text=True).split()
subprocess.run(['g++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test'),*flags],check=True)
subprocess.run([str(p/'test')],check=True)
print('Production profile selection: real Lua ID dictionaries, exact 9:1 companion weighting, zero/negative/infinite/NaN/oversized weights rejected and legacy companion fallback passed')
@@ -0,0 +1,62 @@
"""Execute production rotation cleanup with failed ownership/save/delete phases."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/AdventurePopulationMgr.cpp').read_text()
a=s.index('void AdventurePopulationMgr::DeleteRotated()');b=s.index('{',a);e=b+1;n=1
while n:n+=(s[e]=='{')-(s[e]=='}');e+=1
code=r'''
#include <cassert>
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include <algorithm>
#include <cstdio>
using uint32=unsigned;
#define TC_LOG_INFO(...) ((void)0)
namespace Trinity{template<class...A>std::string StringFormat(char const* f,A...args){char b[4096];std::snprintf(b,sizeof(b),f,args...);return b;}}
struct ObjectGuid{unsigned id;ObjectGuid(int,unsigned v):id(v){}};
namespace HighGuid{constexpr int Player=0;}
namespace ObjectAccessor{bool connected=false;bool FindConnectedPlayer(ObjectGuid){return connected;}}
enum class ClientlessPlayerState{Offline,Online};
struct World{ClientlessPlayerState state=ClientlessPlayerState::Offline;auto GetClientlessPlayerState(unsigned){return state;}}world;auto sWorld=&world;
struct Field{unsigned GetUInt32(){return 6;}};
struct Row{Field f;Field*Fetch(){return &f;}};using QueryResult=std::shared_ptr<Row>;
struct Callback{std::function<void()> invoke;};
struct Processor{std::vector<Callback>pending;void AddCallback(Callback c){pending.push_back(std::move(c));}void run(){auto batch=std::move(pending);pending.clear();for(auto&c:batch)c.invoke();}};
struct Query{QueryResult row;template<class F>Callback WithCallback(F f){return {[row=row,f]{f(row);}};}};
struct Tx{std::vector<std::string>sql;template<class...A>void PAppend(char const*f,A...a){sql.push_back(Trinity::StringFormat(f,a...));}};
struct Save{bool ok;std::function<void(bool)>done;template<class F>void AfterComplete(F f){done=f;}operator Callback(){return {[ok=ok,done=done]{done(ok);}};}};
struct DB{
bool world=false,owns=true,exists=true,ok=true;int queries=0;std::vector<std::string>committed;
Query AsyncQuery(char const*sql){++queries;bool present=std::string(sql).find("SELECT guid FROM characters")!=std::string::npos?exists:owns;return {present?std::make_shared<Row>():nullptr};}
auto BeginTransaction(){return std::make_shared<Tx>();}
Save AsyncCommitTransaction(std::shared_ptr<Tx>tx){if(ok)for(auto const&s:tx->sql)committed.push_back(s);return {ok,{}};}
}CharacterDatabase,WorldDatabase;
struct Player{static int deletes;static bool fail;static void DeleteFromDB(ObjectGuid,unsigned,bool,bool){++deletes;if(!fail)CharacterDatabase.exists=false;}};int Player::deletes=0;bool Player::fail=false;
struct AdventurePopulationMgr{
struct Rotation{unsigned Old=10,New=11,Account=6;bool Submitted=false;};std::vector<Rotation>_rotations{{}};
bool _busy=false;unsigned _refresh=99;Processor _queries,_saves;std::string error;
void Failed(std::string s){_busy=false;error=s;}void DeleteRotated();
void drain(){for(int i=0;i<15;++i){_queries.run();_saves.run();}}
};
'''+s[a:e]+r'''
int main(){
AdventurePopulationMgr mgr;world.state=ClientlessPlayerState::Online;mgr.DeleteRotated();assert(!mgr._busy && !CharacterDatabase.queries && !Player::deletes);
world.state=ClientlessPlayerState::Offline;ObjectAccessor::connected=true;mgr.DeleteRotated();assert(!CharacterDatabase.queries);ObjectAccessor::connected=false;
CharacterDatabase.owns=false;mgr.DeleteRotated();mgr.drain();assert(!mgr.error.empty() && !Player::deletes && mgr._rotations.size()==1);
mgr={};CharacterDatabase.owns=true;Player::fail=true;mgr.DeleteRotated();mgr.drain();assert(Player::deletes==1 && !mgr.error.empty() && mgr._rotations.size()==1 && WorldDatabase.committed.empty());
mgr={};Player::fail=false;WorldDatabase.ok=false;mgr.DeleteRotated();mgr.drain();assert(!CharacterDatabase.exists && mgr._rotations.size()==1 && !mgr.error.empty());
assert(CharacterDatabase.committed.size()==2); // native deletion barriers only; profile/journal retained
mgr={};WorldDatabase.ok=true;mgr.DeleteRotated();mgr.drain();assert(mgr._rotations.empty() && !mgr._busy && !mgr._refresh);
auto contains=[](auto const& list,char const*text){return std::any_of(list.begin(),list.end(),[&](auto const&s){return s.find(text)!=std::string::npos;});};
assert(contains(WorldDatabase.committed,"vcore_adventure_day"));
for(auto name:{"vcore_adventure_generated_bot","vcore_adventure_supply_state","vcore_adventure_instance_visit","vcore_adventure_rotation"})assert(contains(CharacterDatabase.committed,name));
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['g++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Production rotation: connected/native-online protection, ownership refusal, native delete failure, world cleanup failure and journal-retained retry before successful profile/plan/state removal passed')
@@ -19,7 +19,7 @@ struct Player{
void SetSkill(unsigned,unsigned s,unsigned v,unsigned c){++writes;step=s;value=v;cap=c;}
};
struct ClientlessCharacterFactory{static bool PrepareSkillRank(Player*,unsigned,unsigned);};
'''+s[a:]+r'''
'''+s[a:s.index('\nnamespace',a)]+r'''
int main(){
Player p;using F=ClientlessCharacterFactory;
assert(F::PrepareSkillRank(&p,186,1));assert(p.step==1 && p.value==75 && p.cap==75);
@@ -0,0 +1,33 @@
"""Execute native equip cooldowns with invalid/sentinel spell metadata."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/Entities/Player/Player.cpp').read_text()
a=s.index('void Player::ApplyEquipCooldown(');b=s.index('{',a);e=b+1;n=1
while n:n+=(s[e]=='{')-(s[e]=='}');e+=1
code=r'''
#include <cassert>
#include <chrono>
#include <vector>
using uint32=unsigned;using uint64=unsigned long long;
constexpr unsigned ITEM_FLAG_NO_EQUIP_COOLDOWN=1,ITEM_SPELLTRIGGER_ON_EQUIP=1,ITEM_SPELLTRIGGER_ON_USE=2,IN_MILLISECONDS=1000,SMSG_ITEM_COOLDOWN=1;
struct SpellInfo{unsigned id;};struct SpellProcEntry{};
struct Spells{SpellInfo spell{42};SpellInfo const*GetSpellInfo(unsigned id){return id==42?&spell:nullptr;}SpellProcEntry const*GetSpellProcEntry(unsigned){return nullptr;}}spells;auto sSpellMgr=&spells;
struct ItemEffect{unsigned SpellID,Trigger;};struct Proto{unsigned flags=0;std::vector<ItemEffect>Effects;unsigned GetFlags(){return flags;}};
struct Item{Proto proto;Proto*GetTemplate(){return &proto;}unsigned GetEntry(){return 5;}unsigned GetGUID(){return 9;}};
struct Aura{void AddProcCooldown(SpellProcEntry const*,std::chrono::steady_clock::time_point){}};
struct WorldPacket{WorldPacket(unsigned,unsigned){}template<class T>WorldPacket&operator<<(T){return *this;}};
struct History{unsigned adds=0,remaining=0;unsigned GetRemainingCooldown(SpellInfo const*s){assert(s);return remaining;}void AddCooldown(unsigned id,unsigned,std::chrono::seconds){assert(id==42);++adds;}};
struct Player{History history;unsigned packets=0;unsigned GetGUID(){return 1;}Aura*GetAura(unsigned,unsigned,unsigned){return nullptr;}History*GetSpellHistory(){return &history;}void SendDirectMessage(WorldPacket*){++packets;}void ApplyEquipCooldown(Item*);};
'''+s[a:e]+r'''
int main(){Player p;Item item;item.proto.Effects={{0,2},{0xFFFFFFFFu,2},{999999,2},{42,0},{42,2}};
p.ApplyEquipCooldown(&item);assert(p.history.adds==1 && p.packets==1);
p.history.remaining=31000;p.ApplyEquipCooldown(&item);assert(p.history.adds==1);
item.proto.flags=ITEM_FLAG_NO_EQUIP_COOLDOWN;p.ApplyEquipCooldown(&item);assert(p.history.adds==1);
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['g++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Native item equip cooldown: zero/sentinel/missing spells safely skipped, valid use cooldown preserved and longer/disabled cooldowns untouched passed')