Implement AdventureBot profiles, daily planning and population management
This commit is contained in:
1 parent
d6fea6ee68
commit
d3273c677e
61 files changed
+2280
-116
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
Reference in new issue
Block a user