Compare commits

...
3 Commits
Author SHA1 Message Date
Nico 04aa87fcfc Add profile-driven AdventureBot character generation
clang PCH / build (push) Waiting to run
GCC PCH / build (push) Canceled after 0s
2026-10-28 11:30:47 +01:00
Nico e333f2f9fc Add AdventureBot social interactions and player-led groups 2026-10-28 01:11:54 +01:00
Nico 39a515dbd1 Add AdventureBot self-care, supply restocking and secondary professions 2026-10-27 23:56:43 +01:00
69 changed files with 6355 additions and 32 deletions

No files matched your search

+183
View File
@@ -1,3 +1,186 @@
# Aktuell: Automatische AdventureBot-Erzeugung abgeschlossen
Stand28.10.2026 auf feature/clientless-player-core. Nutzer beauftragte einen
Generator für neue Bots mit RP-Namen/Profilen, passenden Fähigkeiten, Berufen
und Ausrüstung; Kennzeichnung für spätere temporäre Rotation. Nutzer hat den
lokalen Commit beauftragt; kein Push. SQL-Migration2026_10_28_00_adventure_generator.sql
wegen SQL-Ignore bereits gezielt vorgemerkt; Test-Charakter-DB zweimal angewendet.
ClientlessCharacterFactory ist generische native Infrastruktur ohne Adventure-
Abhängigkeit: Player::Create, validierte zufällige Optik, Geburt auf Zielstufe
via native Initialisierung statt GiveLevel-Belohnungen, Vorbereitungscallback,
gemeinsamer Save+Profil+Homebind+Ownership-Commit; Cache/Hook erst nach Erfolg.
Neue AdventureGeneratorMgr/Policy: zehn native legale Klassen-Archetypen,
zwölf Völker, kulturelle Namensgrammatiken, Herkunft/Beruf/Personalityprofile.
Profile liegen in Character-DB und werden als generated:<guid> im vorhandenen
Lua-Sandboxparser geladen. Handgeschriebene Namensprofile haben Vorrang und ihre
Namen werden bei Erzeugung reserviert. Keine Laufzeitdateien im Profilquellbaum.
Wichtige Live-Fixes: Berufs-Lernzauber mit Effekt44 wirken vor Welteintritt
nicht zuverlässig. Native SkillRaceClass/SkillTiers + Player::SetSkill bereiten
legale volle Ränge vor; Hilfsfunktion verweigert Online-Spieler. Online-
Selbstversorgung unverändert. Rollen-Ausrüstung prüft native Equipregeln,
Werte, temporäre/reputations-/gebietsgebundene Items, Mana-/Zauber-/physische
Effekte und keine doppelten ausgerüsteten Einträge. Ersetzte Items bleiben in
Taschen. Native Trainer::CanTeachSpell lediglich read-only öffentlich gemacht.
Arms nutzt vorhandenen Talentplan, andere Klassen gültigen Tier-Basisplan.
Befehle: clientless generate create <account> <count> [level faction archetype
persistent|temporary seed]; optionale fünf Parameter nur gemeinsam. Kurzform
level60, any/any, persistent. Count1..10, level10..85, DK>=55, Account normal
und ohne Netzwerksitzung, normale Realm-Charaktergrenze. Beispiel für Tests:
clientless generate create 6 1 60 alliance warrior temporary 42.
list/profile/login/logout/equip/retire <guid>. Erzeugung offline; login explizit.
equip nur eigener online/lebender/ruhiger Bot, native Checks, alte Items behalten.
retire nur eigener offline-temporärer Bot, markiert nur Metadaten, keine Löschung.
Automatische Rotation, Löschung, Accountanlage und Populationssteuerung später.
Vollständige Doku: doc/clientless/generator.md.
Debug-j10 erfolgreich installiert,62/62 Tests bestanden. Testserver88947 läuft,
frische Ready + Port8085 verifiziert. Vier neue Stufe60-Bots online und mit
individuellen DB-Profilen nach Neustart/Relog wieder geladen:
-29 Aelion, Nachtelf-Krieger, Teldrassil/poi_guard_111.
-30 Zanzan, Troll-Krieger, Durotar/poi_tele_823.
-31 Lenara, Worgen-Magierin, Elwynn/poi_npc_280710.
-32 Aldren, Untoter-Todesritter, Silberwald/poi_npc_321922.
Native Talentbäume746/746/851/400, Talentzeilen13/13/14/12,
Zauberanzahl80/80/87/64. Alle vier Nebenberufe und vier22er-Glutseidenstofftaschen,
alle fünf Vorratsstacks20. Primär: Krieger/DK Mining450+Smith375; Magierin
Tailoring375+Enchant375. Werte stammen aus gegenwärtigen nativen Trainerdaten.
Ausrüstungsauffrischung29..32 nativ erfolgreich gespeichert, alte Items behalten.
NPC-Szenen und lokale soziale Intents laufen; keine visuelle Abnahme behauptet.
Eigene frühere Testcharaktere27 Dagri und28 Jarana sind retired/offline, mit
vollständig erhaltenen Daten. Retire26 verweigert (kein generierter Besitz),
Login27 verweigert (retired). Inventarzuordnungen/Entries für21 und26 gegenüber
vorher bytegleich (Hash im Checkpoint). Arthoran wurde für Wartung sauber
abgemeldet und bleibt OFFLINE; keine Profiloverrides, keine Teleportierung.
Bestehende Sourceprofile unverändert. Account6 enthält nun7 Charaktere, normale
Realmgrenze beachten; archivierte Charaktere belegen weiterhin Plätze.
Ein erster Start traf bekannten temporären Auth-DB-Ausfall10.0.0.1. Nicht
reagierender Fehlstart76094 perSIGTERM beendet, zweiter Versuch erfolgreich;
keine VPS-/Produktionskonfiguration geändert. Spätere Starts erfolgreich.
Für alle neuen Klassen gilt weiter: bekannte Fähigkeiten sind keine fertige
Klassen-Kampfrotation; außer Krieger bisher grundlegender Autoangriff. Rollen,
Bot-Gruppenbildung, PvP-Erstangriffe und Rotation bleiben spätere Aufgaben.
Belege: doc/clientless/checkpoints/2026-10-28-generator/. Aktuelle Logs
/tmp/vcore-generator-{build-refit,install-refit}.log; DB-Helfer
/tmp/vcore-generator-db.py audit/details/compare (keine Zugangsdaten ausgeben).
Nutzer hat alle CMake- und tmux-Befehle sowie git add/commit auf diesem Branch
ausdrücklich dauerhaft autorisiert. Keine erneuten inhaltlichen Rückfragen;
technische Sandboxregeln bleiben maßgeblich. Kein Push ohne ausdrücklichen Auftrag.
---
# Aktuell: Interaktionen und spielergeführte Gruppen umgesetzt
Stand 28.10.2026. Nutzer beauftragte Umsetzung nach gemeinsamer Planung.
Branch feature/clientless-player-core; vorher sauber. Nutzer hat den lokalen
Commit dieses Stands beauftragt; kein Push. Neuer AdventureInteractionMgr/Policy,
generische GUID-Queues für Textemotes und tatsächliche Kampfhilfe, native
Einladungsantworten, intent-/persönlichkeitsabhängige Annahme, MMAP-Folgen und
TravelGraph-Handoff, grundlegende Gruppen-Kampfunterstützung. Spieler/Pet-
Selbstverteidigung im bestehenden CombatMgr, keine eigenständigen PvP-Angriffe.
Details und Grenzen in doc/clientless/interactions.md.
Austritt nur außerhalb Kampf/Tod/Instanz/Transport/Flug. Intent-/Locationwechsel
oder manuelle Blockfrist erlaubt Entscheidung; group_loyalty beeinflusst Bleiben
(Arthoran0.35, Standard0.25). Native AFK oder beobachtete Inaktivität des
Anführers volle600s; Führungswechsel beginnt neues Zeitfenster. Chat/Emotes,
Zauber/Itemanfragen, Bewegung/Blick/Ziel, Kampf/Casts zählen als Aktivität.
Native Gruppenmitgliedschaft bleibt über Neustart; Beobachtungsfristen starten
neu. Rollenprofile, autonome Bot-Gruppen, freie Chatgespräche und eigenständige
PvP-Erstangriffe bleiben ausdrücklich spätere Arbeit.
Debug-Build-j10 erfolgreich installiert. 57 Testskripte bestanden: vollständiger
56er-Lauf plus neuer Social-Queue-Concurrencytest; der anschließend erweiterte
Combat-Test für echten Spielerfokus zusätzlich erneut erfolgreich. Native
Testserver PID59086, frische Ready-Meldung und Port8085 bestätigt. Arthoran26
online, Persönlichkeit8Werte, neue Diagnose clientless interaction26 erfolgreich:
sociability0.65/loyalty0.35, keine Gruppe. Echte Kreaturen-Selbstverteidigung
nach Neustart weiterhin beobachtet, kein Beleg für Live-PvP/Gruppenannahme.
Arthorans unveränderte initiale Profilvorgabe startete bei Login die normale
Goldhain-Reise. clientless adventure stop26 wurde angefordert, letzte Diagnose
noch Switching/waiting for safe player/transport state. Keine Teleportierung,
kein temporärer Profiloverride; normale Herkunftsdateien bleiben erhalten.
Für Live-Abnahme Position und Reise zuerst erneut prüfen. Gruppeneinladungen
werden in Flug-/Transport-/Kampfzuständen bis zur sicheren Lage zurückgestellt.
Beim sauberen Shutdown der ALTEN, vor Installation laufenden Binärdatei trat
Object.h:448 GetMap ASSERTION FAILED m_currMap auf. AlterProzess49765 endete;
keine fremden Prozesse beendet. Das ist ein beobachteter Shutdownfehler des
Ausgangsstands, Ursache hier nicht untersucht/behoben. Neuer Start erfolgreich.
Offene Gameplay-Abnahmen: echte Spielereinladungen, zwei gleichzeitige Bots,
PvP/Pet-Verteidigung, native Gruppenportale/kartenübergreifendes Folgen,
zehn echte Minuten AFK. Klassenrotationen außer Krieger und Supportrollen
weiterhin begrenzt. Belege: checkpoints/2026-10-28-interactions/.
Logs /tmp/vcore-interactions-{build-final,install}.log und zugehörige Tests.
---
# Aktuell: Selbstversorgung und gemeinsame Nebenberufe abgeschlossen
Stand 27.10.2026. Branch feature/clientless-player-core, vorheriger Commit
c0c799c4b6. Nutzer hat den lokalen Commit der Selbstversorgung beauftragt; kein Push.
Debug-Build -j10 erfolgreich installiert, alle 55 Testskripte bestanden.
Testserver PID 49765 läuft; frisches Ready und Port 8085 bestätigt.
AdventureSupplyMgr/Policy versorgt AdventureBots zu Tagesbeginn und nach
Kampfende bis zu jeweils einem nativen Stack: Verbände, Heil-/Manatränke,
Essen und Trinken. Vier Glutseidenstofftaschen54443 mit je22 Plätzen ergänzen
Arthoran. Alte Gegenstände blieben erhalten, Geld weiterhin0.
Migration world10 und characters01 angewendet und trotz SQL-Ignore gezielt
vorgemerkt. Persistierter Tagesstempel verhindert Nachschub bei jedem Login.
Nutzerregeln umgesetzt: Selbstheilung beginnt erst bei mindestens10% fehlenden
HP. Außerhalb des Kampfes zuerst ein nutzbarer Verband; bei Sperre direkt Essen,
nach Verband Essen nur bei weiter mindestens10% Mangel. Mananutzer trinken bei
passendem Mangel. Im Kampf nur native Tränke. Echte Item-Anfragen, Verbrauch,
Abklingzeiten und Verbandssperre; keine direkte HP/Mana-Zuweisung außer dem
expliziten Diagnosebefehl testrecovery. Intents behalten ihre ursprüngliche Frist.
Auf ausdrücklichen Nutzerwunsch erhalten alle AdventureBots die stufengerecht
verfügbaren Nebenberufe Erste Hilfe, Kochen, Angeln und Archäologie, innerhalb
nativer Traineranforderungen und Fertigkeitsgrenzen. Keine Hauptberufe geändert.
Arthoran gespeichert: ErsteHilfe375/375, Kochen525/525, Angeln525/525,
Archäologie375/375. Er verwendet schwere Netherstoffverbände21991.
Native Trainer erlauben Kochen/Angeln525 bei diesem Charakter.
Notwendige native Fehlerkorrektur: Player::GetPureMaxSkillValue hatte eine
invertierte Prüfung und lieferte für gelernte Fertigkeiten null. Fehlendes !
korrigiert, eigener nativer Regressionstest vorhanden. Nebenberufsausbildung
berücksichtigt sowohl Skill-Effekt118 als auch den nativen Lerneffekt44.
Live bestätigt:
- Vier22er-Taschen und alle fünf Stacks20, vorhandene Item-GUIDs erhalten.
- Markierte8%-Testverletzung: kein Verbrauch.
- Markierte80%-Testverletzung: echter Verbandverbrauch, danach4961/5249HP;
weniger als10% fehlend, daher kein Essen.
- Erneute Testverletzung bei nativer Verbandssperre: echtes Essen, volleHP.
- Gleicher Tagesstempel beim erneuten Login: Verband19/Essen19 bleiben bestehen,
kein weiterer Tagesnachschub.
- Echter Bärenkampf gegen Spawn300425 mit markiertem anfänglichem30%-Defizit:
Heiltrank22829 im Kampf verbraucht. Danach exakt je ein Verband, Heiltrank
und Essen ergänzt; alle fünf Bestände20. GespeichertHP5249, Geld0.
- Manaspezifisches Verhalten produktiv mit Adaptern geprüft, nicht mit echtem
Manacharakter. Administrative Testpositionierung ist kein Fernreisebeleg.
Arthoran bleibt angemeldet in den Verwüsteten Landen am Bären-Testplatz,
reagiert auf Gegner und versorgt sich selbst. Normales Lua-Profil bytegleich
restauriert; kein temporärer Override. Dokumentation: doc/clientless/supplies.md.
Belege: doc/clientless/checkpoints/2026-10-27-supplies/.
Logs: /tmp/vcore-supplies-build-final.log und /tmp/vcore-supplies-tests-final.log.
DB-Helfer /tmp/vcore-instance-db.py supplies_audit. Diagnose:
clientless combat supplies26, testrecovery26<fehlende-Prozent1..80>.
Die bisherigen Server-/Branchregeln und vertagte Pfadfindungsarbeit gelten weiter.
---
# Aktuell: Training, echte Gegner und Todesverhalten abgeschlossen
Stand 27.10.2026. Auf feature/clientless-player-core bleiben alle bisherigen
@@ -0,0 +1,26 @@
TrinityCore rev. c0c799c4b686+ 2026-10-27 23:17:05 +0100 (feature/clientless-player-core branch) (Unix, Debug, Static) (worldserver-daemon) ready...
SUPPLY_SECONDARY guid=26 skill=356 value=525 max=525
SUPPLY_SECONDARY guid=26 skill=794 value=375 max=375
SUPPLY_SECONDARY guid=26 skill=129 value=375 max=375
SUPPLY_SECONDARY guid=26 skill=185 value=525 max=525
SUPPLY_TEST_INJURY guid=26 missingPercent=8
SUPPLY_TEST_INJURY guid=26 missingPercent=80
SUPPLY_USE_REQUEST guid=26 role='bandage' item=21991 spell=27031 combat=0
SUPPLY_USED guid=26 role='bandage' item=21991
SUPPLY_RECOVERY_END guid=26 health=4961
SUPPLY_TEST_INJURY guid=26 missingPercent=30
SUPPLY_USE_REQUEST guid=26 role='bandage' item=21991 spell=27031 combat=0
SUPPLY_USE_REQUEST guid=26 role='food' item=27854 spell=27094 combat=0
SUPPLY_USED guid=26 role='food' item=27854
SUPPLY_RECOVERY_END guid=26 health=5249
SUPPLY_TEST_INJURY guid=26 missingPercent=30
COMBAT_START guid=26 target='GUID Full: 0xf130a56000000b1c Type: Creature Entry: 42336 Low: 2844' training=0 defending=0 until=0
COMBAT_END guid=26 target='GUID Full: 0xf130a56000000b1c Type: Creature Entry: 42336 Low: 2844' training=0
COMBAT_START guid=26 target='GUID Full: 0xf130a56000000b1c Type: Creature Entry: 42336 Low: 2844' training=0 defending=1 until=0
SUPPLY_USE_REQUEST guid=26 role='healing_potion' item=22829 spell=28495 combat=1
SUPPLY_USED guid=26 role='healing_potion' item=22829
COMBAT_END guid=26 target='GUID Full: 0xf130a56000000b1c Type: Creature Entry: 42336 Low: 2844' training=0
SUPPLY_TOPUP guid=26 role='bandage' item=21991 added=1 stack=20
SUPPLY_TOPUP guid=26 role='healing_potion' item=22829 added=1 stack=20
SUPPLY_TOPUP guid=26 role='food' item=27854 added=1 stack=20
SUPPLY_RESTOCK guid=26 day=20261027 daily=0 afterCombat=1
@@ -0,0 +1,11 @@
Debug build -j10 installed;55 tests/clientless scripts passed.
Native skill getter corrected and regression-tested.
Arthoran native skill values: FirstAid375/375, Cooking525/525, Fishing525/525, Archaeology375/375. Trainer requirements permit these ranks at level60.
Four new EmbersilkBags54443,22 slots each. Existing equipment/items preserved.
Each supply role initially20; bandage21991, healing22829, mana22832, food27854, drink28399. Money0 unchanged.
Marked8% injury: no supply use. Marked80% injury: real heavy netherweave bandage consumed; remainingHP4961/5249, no food because deficit<10%.
Marked30% injury with native bandage lockout: food consumed, HP5249/5249.
Same-day relog: bandage19/food19 remained; no daily topup.
Real bear fight with marked initial30% deficit: healing potion consumed in combat. After native combat exit, exactly1 bandage,1 healing potion,1 food replenished to20 each; no duplicate stacks.
Mana-specific behavior tested with production adapters, not a live mana-using character.
Isolated test positions set administratively offline; no long-distance travel claim.
@@ -0,0 +1,4 @@
[1/4] cd /home/wow_admin/wow_cata_server/source/TrinityCore && /usr/bin/cmake -DBUILDDIR="/home/wow_admin/wow_cata_server/build/debug" -P /home/wow_admin/wow_cata_server/source/TrinityCore/cmake/genrev.cmake /home/wow_admin/wow_cata_server/build/debug
[2/4] Building CXX object src/server/game/CMakeFiles/game.dir/World/AdventureGeneratorMgr.cpp.o
[3/4] Linking CXX static library src/server/game/libgame.a
[4/4] Linking CXX executable src/server/worldserver/worldserver
@@ -0,0 +1 @@
Production equipment affinity: mana-only/direct/triggered procs, caster/physical stat effects and bounded cycles passed
@@ -0,0 +1 @@
Production native factory: character/profile atomicity, failed preparation/commit, cache publication order, account/security/name/cap gates passed
@@ -0,0 +1,30 @@
from pathlib import Path
import os,re,subprocess,sys
root=Path('/home/wow_admin/wow_cata_server/source/TrinityCore')
conf=Path('/home/wow_admin/wow_cata_server/server/debug/etc/worldserver.conf').read_text()
def query(kind,sql):
h,port,u,p,db=re.search(r'^'+kind+r'DatabaseInfo\s*=\s*"([^"]+)"',conf,re.M).group(1).split(';')
env=os.environ.copy();env['MYSQL_PWD']=p
r=subprocess.run(['mysql','--protocol=TCP','--host='+h,'--port='+port,'--user='+u,'--database='+db,'--batch','--connect-timeout=10'],input=sql,text=True,capture_output=True,env=env)
if r.returncode:raise SystemExit(r.stderr.replace(p,'[redacted]'))
return r.stdout
mode=sys.argv[1]
if mode=='inspect':
print(query('World',"SHOW COLUMNS FROM trainer; SHOW COLUMNS FROM trainer_spell; SHOW COLUMNS FROM vcore_clientless_world_point; SELECT l.point_key,l.display_name,l.faction,w.map,w.position_x,w.position_y,w.position_z,w.category FROM vcore_adventure_location l JOIN vcore_clientless_world_point w ON w.point_key=l.point_key WHERE w.enabled=1 AND w.category='city' ORDER BY l.point_key LIMIT 25;"))
print(query('Character','SELECT account,COUNT(*) AS characters FROM characters WHERE account=6 GROUP BY account;'))
elif mode=='migrate':
print(query('Character',(root/'sql/custom/characters/2026_10_28_00_adventure_generator.sql').read_text()))
elif mode=='baseline':
import hashlib
text=query('Character',"SELECT ci.guid,ci.bag,ci.slot,ci.item,i.itemEntry FROM character_inventory ci JOIN item_instance i ON i.guid=ci.item WHERE ci.guid IN (21,26) ORDER BY ci.guid,ci.bag,ci.slot,ci.item;")
Path('/tmp/vcore-generator-existing-items.tsv').write_text(text)
print('Existing item ownership baseline',hashlib.sha256(text.encode()).hexdigest())
elif mode=='compare':
import hashlib
text=query('Character',"SELECT ci.guid,ci.bag,ci.slot,ci.item,i.itemEntry FROM character_inventory ci JOIN item_instance i ON i.guid=ci.item WHERE ci.guid IN (21,26) ORDER BY ci.guid,ci.bag,ci.slot,ci.item;")
print('Existing item ownership unchanged:',text==Path('/tmp/vcore-generator-existing-items.tsv').read_text(),hashlib.sha256(text.encode()).hexdigest())
elif mode=='details':
print(query('Character',"SELECT c.guid,c.name,c.level,c.talentTree,c.map,c.instance_id,c.position_x,c.position_y,c.position_z,c.health,c.power1,g.state,g.persistent FROM characters c JOIN vcore_adventure_generated_bot g ON g.character_guid=c.guid ORDER BY c.guid; SELECT ci.guid,ci.slot,i.itemEntry FROM character_inventory ci JOIN item_instance i ON i.guid=ci.item JOIN vcore_adventure_generated_bot g ON g.character_guid=ci.guid WHERE g.state='ready' AND ci.bag=0 AND ci.slot IN (12,13) ORDER BY ci.guid,ci.slot; SELECT ci.guid,i.itemEntry,SUM(i.count) AS count FROM character_inventory ci JOIN item_instance i ON i.guid=ci.item JOIN vcore_adventure_generated_bot g ON g.character_guid=ci.guid WHERE g.state='ready' AND i.itemEntry IN (21991,22829,22832,27854,28399) GROUP BY ci.guid,i.itemEntry ORDER BY ci.guid,i.itemEntry; SELECT g.character_guid,LENGTH(g.profile_lua) AS profile_bytes,c.name,CONCAT('identity name matches: ',g.profile_lua LIKE CONCAT('%name = ',CHAR(34),c.name,CHAR(34),'%')) AS identity_check FROM vcore_adventure_generated_bot g JOIN characters c ON c.guid=g.character_guid WHERE g.state='ready' ORDER BY g.character_guid;"))
elif mode=='audit':
print(query('Character',"SELECT g.character_guid,c.name,c.race,c.class,c.gender,c.level,c.online,g.archetype,g.persistent,g.state,g.seed,c.health FROM vcore_adventure_generated_bot g JOIN characters c ON c.guid=g.character_guid ORDER BY g.character_guid; SELECT ci.guid,ci.slot,i.itemEntry,i.durability FROM character_inventory ci JOIN item_instance i ON i.guid=ci.item JOIN vcore_adventure_generated_bot g ON g.character_guid=ci.guid WHERE ci.bag=0 AND ci.slot<23 ORDER BY ci.guid,ci.slot; SELECT s.guid,s.skill,s.value,s.max FROM character_skills s JOIN vcore_adventure_generated_bot g ON g.character_guid=s.guid WHERE s.skill IN (129,185,356,794,164,186,182,171,197,333,165,393) ORDER BY s.guid,s.skill; SELECT t.guid,COUNT(*) AS talent_spells FROM character_talent t JOIN vcore_adventure_generated_bot g ON g.character_guid=t.guid GROUP BY t.guid; SELECT s.guid,COUNT(*) AS spells FROM character_spell s JOIN vcore_adventure_generated_bot g ON g.character_guid=s.guid GROUP BY s.guid;"))
else:raise SystemExit('Unknown mode')
@@ -0,0 +1,27 @@
TrinityCore rev. e333f2f9fc8b+ 2026-10-28 01:11:54 +0100 (feature/clientless-pla
yer-core branch) (Unix, Debug, Static) (worldserver-daemon)
<Ctrl-C> to stop.
______ __
/\__ _\ __ __/\ \__
\/_/\ \/ _ __ /\_\ ___ /\_\ \, _\ __ __
\ \ \/\`'__\/\ \ /' _ `\/\ \ \ \/ /\ \/\ \
\ \ \ \ \/ \ \ \/\ \/\ \ \ \ \ \_\ \ \_\ \
\ \_\ \_\ \ \_\ \_\ \_\ \_\ \__\\/`____ \
\/_/\/_/ \/_/\/_/\/_/\/_/\/__/ `/___/> \
C O R E /\___/
http://TrinityCore.org \/__/
Using configuration file /home/wow_admin/wow_cata_server/server/debug/etc/worlds
erver.conf.
Using SSL version: OpenSSL 3.5.5 27 Jan 2026 (library: OpenSSL 3.5.5 27 Jan 2026
)
Using Boost version: 1.90.0
Automatic database updates are disabled for all databases!
Could not connect to MySQL database at 10.0.0.1: Can't connect to server on '10.
0.0.1' (115)
DatabasePool Login NOT opened. There were errors opening the MySQL connections.
Check your SQLDriverLogFile for specific errors. Read wiki at http://www.trinity
core.info/display/tc/TrinityCore+Home
server shutdown 0
@@ -0,0 +1,176 @@
character_guid name race class gender level online archetype persistent state seed health
27 Dagri 3 1 1 60 0 warrior 0 retired 20261028 5049
28 Jarana 8 1 1 60 0 warrior 0 retired 20261029 5309
29 Aelion 4 1 0 60 1 warrior 0 ready 20261101 5189
30 Zanzan 8 1 0 60 1 warrior 0 ready 20261102 5409
31 Lenara 22 8 1 60 1 mage 0 ready 20261103 3600
32 Aldren 5 6 0 60 1 death_knight 0 ready 20261104 5259
guid slot itemEntry durability
27 0 23314 80
27 1 24073 0
27 2 21805 80
27 4 23300 135
27 5 22197 45
27 6 13498 100
27 7 31125 65
27 8 18754 45
27 9 22714 45
27 10 19873 0
27 11 24151 0
27 12 19930 0
27 13 19930 0
27 14 34659 0
27 15 31134 100
27 19 54443 0
27 20 54443 0
27 21 54443 0
27 22 54443 0
28 0 23244 80
28 1 24073 0
28 2 77868 80
28 4 22872 135
28 5 22197 45
28 6 21495 100
28 7 31125 65
28 8 22205 45
28 9 22868 45
28 10 27832 0
28 11 24151 0
28 12 19930 0
28 13 19930 0
28 14 22337 0
28 15 24044 100
28 17 25861 25
28 19 54443 0
28 20 54443 0
28 21 54443 0
28 22 54443 0
29 0 77867 80
29 1 24073 0
29 2 23315 80
29 4 77858 135
29 5 21503 45
29 6 13498 100
29 7 24064 65
29 8 12936 45
29 9 22714 45
29 10 27834 0
29 11 21477 0
29 12 13213 0
29 13 22321 0
29 14 34659 0
29 15 31134 100
29 19 54443 0
29 20 54443 0
29 21 54443 0
29 22 54443 0
30 0 23244 80
30 1 24073 0
30 2 23243 80
30 4 34650 115
30 5 19051 45
30 6 21475 100
30 7 24064 65
30 8 77861 45
30 9 22868 45
30 10 12058 0
30 11 24151 0
30 12 20072 0
30 13 11810 0
30 14 22337 0
30 15 31134 100
30 17 25861 25
30 19 54443 0
30 20 54443 0
30 21 54443 0
30 22 54443 0
31 0 22720 80
31 1 23125 0
31 2 21468 80
31 4 16700 135
31 5 16696 45
31 6 19683 100
31 7 21489 65
31 8 13107 45
31 9 16391 45
31 10 24045 0
31 11 19905 0
31 12 11819 0
31 13 21473 0
31 14 21470 0
31 15 20654 100
31 17 21801 65
31 19 54443 0
31 20 54443 0
31 21 54443 0
31 22 54443 0
32 0 22223 80
32 1 24073 0
32 2 21805 80
32 4 10845 135
32 5 21503 45
32 6 21475 100
32 7 24064 65
32 8 12936 45
32 9 22714 45
32 10 11934 0
32 11 19510 0
32 12 13164 0
32 13 1973 0
32 14 22337 0
32 15 31134 100
32 19 54443 0
32 20 54443 0
32 21 54443 0
32 22 54443 0
guid skill value max
27 129 375 375
27 164 375 375
27 185 525 525
27 186 450 450
27 356 525 525
27 794 375 375
28 129 375 375
28 164 375 375
28 185 525 525
28 186 450 450
28 356 525 525
28 794 375 375
29 129 375 375
29 164 375 375
29 185 525 525
29 186 450 450
29 356 525 525
29 794 375 375
30 129 375 375
30 164 375 375
30 185 525 525
30 186 450 450
30 356 525 525
30 794 375 375
31 129 375 375
31 185 525 525
31 197 375 375
31 333 375 375
31 356 525 525
31 794 375 375
32 129 375 375
32 164 375 375
32 185 525 525
32 186 450 450
32 356 525 525
32 794 375 375
guid talent_spells
27 13
28 13
29 13
30 13
31 14
32 12
guid spells
27 80
28 80
29 80
30 80
31 87
32 64
@@ -0,0 +1,42 @@
guid name level talentTree map instance_id position_x position_y position_z health power1 state persistent
27 Dagri 60 746 0 0 0 -5385.04 310.278 394.151 5049 0 retired 0
28 Jarana 60 746 0 1 0 -813.097 -4880.08 18.995 5309 0 retired 0
29 Aelion 60 746 0 1 0 9808.68 969.19 1309.75 5189 0 ready 0
30 Zanzan 60 746 0 1 0 -826.364 -4879.64 19.5037 5409 0 ready 0
31 Lenara 60 851 0 0 0 -9470.25 25.9936 56.3394 3600 6813 ready 0
32 Aldren 60 400 0 0 0 1095.38 1587.06 28.5303 5259 0 ready 0
guid slot itemEntry
29 12 13213
29 13 22321
30 12 20072
30 13 11810
31 12 11819
31 13 21473
32 12 13164
32 13 1973
guid itemEntry count
29 21991 20
29 22829 20
29 22832 20
29 27854 20
29 28399 20
30 21991 20
30 22829 20
30 22832 20
30 27854 20
30 28399 20
31 21991 20
31 22829 20
31 22832 20
31 27854 20
31 28399 20
32 21991 20
32 22829 20
32 22832 20
32 27854 20
32 28399 20
character_guid profile_bytes name identity_check
29 873 Aelion identity name matches: 1
30 870 Zanzan identity name matches: 1
31 858 Lenara identity name matches: 1
32 886 Aldren identity name matches: 1
@@ -0,0 +1 @@
Production generator commands: batch bounds, dedicated ownership, persistent/online/removing retirement protection and retired login refusal passed
@@ -0,0 +1 @@
Production generator: 6144 cultural names and 61440 real Lua profiles, profession coherence, personality ranges, escaping and equipment/armor gates passed
@@ -0,0 +1 @@
Production offline skill preparation: native tier caps, no downgrading, rank/level/race/tier gates and no modification of online players passed
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,63 @@
test_adventure_area_node.py: PASS
test_adventure_behavior_policy.py: PASS
test_adventure_combat_exercise.py: PASS
test_adventure_combat_preparation.py: PASS
test_adventure_combat_reactions.py: PASS
test_adventure_economy.py: PASS
test_adventure_economy_catalog.py: PASS
test_adventure_exploration.py: PASS
test_adventure_exploration_catalog.py: PASS
test_adventure_generator_equipment.py: PASS
test_adventure_generator_ownership.py: PASS
test_adventure_generator_policy.py: PASS
test_adventure_graph_handover.py: PASS
test_adventure_instance.py: PASS
test_adventure_instance_autoresume.py: PASS
test_adventure_instance_begin.py: PASS
test_adventure_instance_catalog.py: PASS
test_adventure_instance_persistence.py: PASS
test_adventure_intent_state.py: PASS
test_adventure_interactions.py: PASS
test_adventure_leisure.py: PASS
test_adventure_leisure_catalog.py: PASS
test_adventure_location_import.py: PASS
test_adventure_micro_reset.py: PASS
test_adventure_profession_catalog.py: PASS
test_adventure_profession_profile.py: PASS
test_adventure_profession_sightline.py: PASS
test_adventure_profession_targets.py: PASS
test_adventure_repair_catalog.py: PASS
test_adventure_rest.py: PASS
test_adventure_rest_fire.py: PASS
test_adventure_rest_space.py: PASS
test_adventure_rest_targets.py: PASS
test_adventure_social.py: PASS
test_adventure_social_fire.py: PASS
test_adventure_supplies.py: PASS
test_adventure_supply_catalog.py: PASS
test_adventure_supply_items.py: PASS
test_adventure_training_catalog.py: PASS
test_adventure_walk.py: PASS
test_clientless_character_factory.py: PASS
test_clientless_companion.py: PASS
test_clientless_damage_notifications.py: PASS
test_clientless_near_teleport_observer.py: PASS
test_clientless_skill_preparation.py: PASS
test_clientless_social_notifications.py: PASS
test_default_phase.py: PASS
test_flight_area_query.py: PASS
test_flight_handoff.py: PASS
test_flight_policy.py: PASS
test_follow_spline_velocity.py: PASS
test_free_flight.py: PASS
test_free_flight_planner.py: PASS
test_ground_exit_continuation.py: PASS
test_local_spline_segments.py: PASS
test_lua_reload.py: PASS
test_player_pure_max_skill.py: PASS
test_portal_transfer.py: PASS
test_portal_use.py: PASS
test_taxi_import_disabled.py: PASS
test_transport.py: PASS
test_transport_grid_loading.py: PASS
62/62 passed; failures: []
@@ -0,0 +1,17 @@
Generator implemented on feature/clientless-player-core, local commit requested by user; no push.
Final Debug-j10 build and installation succeeded. Native test worldserver88947 freshly ready, port8085 verified.
62/62 test scripts passed.
Character migration applied twice successfully (additive/idempotent).
Initial offline profession preparation failed without saving a partial character; fixed with validated native DBC skill tier initialization before world insertion.
Generated four active temporary level60 characters:29 Aelion night elf warrior;30 Zanzan troll warrior;31 Lenara worgen mage;32 Aldren forsaken death knight.
Their profiles load as generated:29..32 after native logout/restart/login. All four online and running local social actions.
Native talent trees:746/746/851/400. Stored talent row counts13/13/14/12; spell counts80/80/87/64.
All four have FirstAid375, Cooking525, Fishing525, Archaeology375 using current native data. Warriors/DK:Mining450+Blacksmithing375. Mage:Tailoring375+Enchanting375.
All four have four22-slot Embersilk Bags and one full20-stack of each of the five supply roles.
Equipment affinity checks include stats, native legality, mana/physical/spell-only procs and duplicate avoidance. Native online equipment refresh29..32 saved; all replaced items retained.
Own earlier fixture characters27/28 were archived, not deleted. Their inventories/profiles retained.
Live generator retirement of Arthoran26 refused (not owned); generator login27 refused (retired).
Existing item ownership/entry/slot snapshot for21 and26 unchanged:sha256 dd5869ffb3f10aa7d25dfc93b8de61d0b982715ab27b28e7d6311f562a6d066c.
First server start in this task hit transient remote Login DB error10.0.0.1; failed process76094 did not respond to console and was terminated with SIGTERM. Second attempt succeeded. No remote DB configuration modified.
Arthoran was logged out for maintenance and remains offline. No temporary profile/source overrides were used. His source profile and Allitester's equipment were not changed.
Rotation/deletion/population targets/restart autologin are not enabled. Non-warrior combat/support AI limitations remain. Final visible gameplay acceptance is user-owned.
@@ -0,0 +1,22 @@
[1/10] cd /home/wow_admin/wow_cata_server/source/TrinityCore && /usr/bin/cmake -DBUILDDIR="/home/wow_admin/wow_cata_server/build/debug" -P /home/wow_admin/wow_cata_server/source/TrinityCore/cmake/genrev.cmake /home/wow_admin/wow_cata_server/build/debug
[2/10] Building CXX object src/server/game/CMakeFiles/game.dir/World/AdventureCombatMgr.cpp.o
[3/10] Building CXX object src/server/game/CMakeFiles/game.dir/World/ClientlessLuaEngine.cpp.o
[4/10] Building CXX object src/server/game/CMakeFiles/game.dir/World/AdventureInteractionMgr.cpp.o
[5/10] Building CXX object src/server/game/CMakeFiles/game.dir/World/AdventureSupplyMgr.cpp.o
[6/10] Building CXX object src/server/scripts/CMakeFiles/scripts.dir/Custom/viano_bot_commands.cpp.o
/home/wow_admin/wow_cata_server/source/TrinityCore/src/server/scripts/Custom/viano_bot_commands.cpp: In function ‘bool VianoBotCommands::HandleClientlessLocationAdd(ChatHandler*, const char*)’:
/home/wow_admin/wow_cata_server/source/TrinityCore/src/server/scripts/Custom/viano_bot_commands.cpp:2341:20: warning: unused variable ‘loaded’ [-Wunused-variable]
2341 | bool const loaded = sClientlessWorldPointMgr->Reload();
| ^~~~~~
/home/wow_admin/wow_cata_server/source/TrinityCore/src/server/scripts/Custom/viano_bot_commands.cpp: In function ‘bool VianoBotCommands::HandleClientlessLocationMove(ChatHandler*, const char*)’:
/home/wow_admin/wow_cata_server/source/TrinityCore/src/server/scripts/Custom/viano_bot_commands.cpp:2378:20: warning: unused variable ‘loaded’ [-Wunused-variable]
2378 | bool const loaded = sClientlessWorldPointMgr->Reload();
| ^~~~~~
/home/wow_admin/wow_cata_server/source/TrinityCore/src/server/scripts/Custom/viano_bot_commands.cpp: In function ‘bool VianoBotCommands::HandleClientlessLocationRemove(ChatHandler*, const char*)’:
/home/wow_admin/wow_cata_server/source/TrinityCore/src/server/scripts/Custom/viano_bot_commands.cpp:2402:20: warning: unused variable ‘loaded’ [-Wunused-variable]
2402 | bool const loaded = sClientlessWorldPointMgr->Reload();
| ^~~~~~
[7/10] Building CXX object src/server/game/CMakeFiles/game.dir/World/AdventureBehaviorMgr.cpp.o
[8/10] Linking CXX static library src/server/game/libgame.a
[9/10] Linking CXX static library src/server/scripts/libscripts.a
[10/10] Linking CXX executable src/server/worldserver/worldserver
@@ -0,0 +1 @@
Production combat focus, damage ordering, risk filters, corpse radius/delay, alternate paths and malus-free persisted fallback passed.
@@ -0,0 +1,5 @@
/tmp/tmp2j7tsqhj/test.cpp: In member function ‘void AdventureInteractionMgr::Leave(Player*, Record&, const char*)’:
/tmp/tmp2j7tsqhj/test.cpp:140:69: warning: unused parameter ‘reason’ [-Wunused-parameter]
140 | void AdventureInteractionMgr::Leave(Player* p,Record& r,char const* reason)
| ~~~~~~~~~~~~^~~~~~
production interaction manager: invitations, factions, reservations, follow, AFK and deferred departure passed
@@ -0,0 +1 @@
Production social/helper queues: concurrent GUID-only delivery, bounded queues, human/self/zero-damage gates, pet attribution and lethal-cleanup ordering passed
@@ -0,0 +1,114 @@
Validating spell scripts...
Spell `85123` Effect `Index: EFFECT_0 Target: 7` of script `spell_siege_cannon`
did not match dbc effect data - handler bound to hook `OnObjectAreaTargetSelect`
of SpellScript won't be executed
>> Validated 3470 scripts in 12 ms
Loading SmartAI scripts...
>> Loaded SmartAIMgr Helpers in 16 ms
>> Loaded 13506 SmartAI scripts in 167 ms
Loading Calendar data...
>> Loaded 0 calendar events in 1 ms
>> Loaded 0 calendar invites in 0 ms
Loading Petitions...
>> Loaded 0 petitions.
Loading Signatures...
>> Loaded 0 Petition signs!
Loading Summon Properties parameter data...
>> Loaded 78 summon properties parameters in 1 ms
Loading Item loot...
>> Loaded 0 stored item loots
>> Loaded 0 stored item money
Initialize query data...
>> Initialized query cache data in 1019 ms
Initialize commands...
Initialize game time and timers
Mail timer set to: 1260, mail return is called every 1440 minutes
Starting Map System
Starting Game Event system...
Initialize AuctionHouseBot...
Initializing Opcodes...
Starting Arena Season...
Starting Battleground System
>> Loaded 15 battlegrounds in 0 ms
Starting Outdoor PvP System
>> Loaded 5 outdoor PvP definitions in 1 ms
Starting Battlefield System
>> Loaded 2 battlefields in 0 ms
Loading Warden Checks...
Loading Warden Action Overrides...
Deleting expired bans...
Initializing quest reset times...
Calculate random battleground reset time...
Calculate guild limitation(s) reset time...
Calculate next currency reset time...
World initialized in 0 minutes 13 seconds
TC>TC>TrinityCore rev. 39a515dbd13e+ 2026-10-27 23:56:43 +0100 (feature/clientle
ss-player-core branch) (Unix, Debug, Static) (worldserver-daemon) ready...
clientless login 26
TC>Registered clientless session for character GUID 26 (account 6), state=Loadin
g.
Created clientless player session for character GUID 26 (account 6).
ClientlessPlayerMgr started character GUID 26 (account 6).
Clientless login started for 'Arthoran' (GUID 26, account 6).
TC> Loading saved respawn time of 1790175804 for spawnid (0,323981) - spawn does
not exist, ignoring
Loading saved respawn time of 1790090644 for spawnid (0,396680) - spawn does not
exist, ignoring
Loading saved respawn time of 1790019808 for spawnid (0,396681) - spawn does not
exist, ignoring
Loading saved respawn time of 1790019853 for spawnid (0,396682) - spawn does not
exist, ignoring
Initialized transport GameObject grids: map=0 instance=0 grids=4 candidates=15
Clientless player character GUID 26 (account 6) reached state=Online.
ClientlessLua initialized, script root '/home/wow_admin/wow_cata_server/source/T
rinityCore/src/server/scripts/Custom/clientless'.
[AdventureBot][Profile] loaded guid=26 name='Arthoran' source='/home/wow_admin/w
ow_cata_server/source/TrinityCore/src/server/scripts/Custom/clientless/adventure
/profiles/arthoran.lua' home='elwynn' canGroundMount=1 mounts=1 mountMinDistance
=45.0 mountSpeedRate=1.60 personality=8 regionAffinity=4 activityAffinity=7
PROFILE_PROFESSIONS guid=26 primary=2
ClientlessLua[26]: Arthoran ready; no automatic test journey queued
ClientlessLua[26]: executed adventure override script '/home/wow_admin/wow_cata_
server/source/TrinityCore/src/server/scripts/Custom/clientless/adventure/overrid
es/arthoran.lua'.
ADVENTURE_CATALOG locations=2973 actions=84 lines=76
INTENT_TRAVEL guid=26 intent='social' location='goldshire_tavern' area=0
[AdventureBot] loaded 3966 clientless world points.
[AdventureBot] loaded travel graph: 993 node(s), 1765 directed edge(s).
clientless interaction 26
TC>Interactions guid=26 grouped=0 leader=0 partner=0 intent='social' schedule=''
leaderIdleSeconds=0 afkSeconds=0 sociability=0.65 loyalty=0.35
TC> SUPPLY_RESTOCK guid=26 day=20261028 daily=0 afterCombat=1
COMBAT_START guid=26 target='GUID Full: 0xf130a1fe000006ce Type: Creature Entry:
41470 Low: 1742' training=0 defending=1 until=0
COMBAT_CAST guid=26 target='GUID Full: 0x000000000000001a Type: Player Low: 26'
spell=6673
COMBAT_CAST guid=26 target='GUID Full: 0xf130a1fe000006ce Type: Creature Entry:
41470 Low: 1742' spell=78
COMBAT_CAST guid=26 target='GUID Full: 0xf130a1fe000006ce Type: Creature Entry:
41470 Low: 1742' spell=12294
COMBAT_CAST guid=26 target='GUID Full: 0xf130a1fe000006ce Type: Creature Entry:
41470 Low: 1742' spell=772
COMBAT_END guid=26 target='GUID Full: 0xf130a1fe000006ce Type: Creature Entry: 4
1470 Low: 1742' training=0
INTENT_TRAVEL guid=26 intent='social' location='goldshire_tavern' area=0
SUPPLY_RESTOCK guid=26 day=20261028 daily=0 afterCombat=1
COMBAT_START guid=26 target='GUID Full: 0xf130a1ff000006d0 Type: Creature Entry:
41471 Low: 1744' training=0 defending=1 until=0
COMBAT_CAST guid=26 target='GUID Full: 0xf130a1ff000006d0 Type: Creature Entry:
41471 Low: 1744' spell=12294
COMBAT_END guid=26 target='GUID Full: 0xf130a1ff000006d0 Type: Creature Entry: 4
1471 Low: 1744' training=0
SUPPLY_RESTOCK guid=26 day=20261028 daily=0 afterCombat=1
INTENT_TRAVEL guid=26 intent='social' location='goldshire_tavern' area=0
clientless adventure stop 26
TC>Behavior stop requested at a safe point.
TC> clientless adventure status 26
TC>clientless interaction 26
TC>Adventure GUID 26: enabled=1 schedule=0 phase=3 intent='social' location='gol
dshire_tavern' micro='' deadline=0 detail='waiting for safe player/transport sta
te'
TC> Interactions guid=26 grouped=0 leader=0 partner=0 intent='social' schedule='
'
leaderIdleSeconds=0 afkSeconds=0 sociability=0.65 loyalty=0.35
TC>
@@ -0,0 +1,968 @@
TrinityCore rev. 39a515dbd13e+ 2026-10-27 23:56:43 +0100 (feature/clientless-pla
yer-core branch) (Unix, Debug, Static) (worldserver-daemon)
<Ctrl-C> to stop.
______ __
/\__ _\ __ __/\ \__
\/_/\ \/ _ __ /\_\ ___ /\_\ \, _\ __ __
\ \ \/\`'__\/\ \ /' _ `\/\ \ \ \/ /\ \/\ \
\ \ \ \ \/ \ \ \/\ \/\ \ \ \ \ \_\ \ \_\ \
\ \_\ \_\ \ \_\ \_\ \_\ \_\ \__\\/`____ \
\/_/\/_/ \/_/\/_/\/_/\/_/\/__/ `/___/> \
C O R E /\___/
http://TrinityCore.org \/__/
Using configuration file /home/wow_admin/wow_cata_server/server/debug/etc/worlds
erver.conf.
Using SSL version: OpenSSL 3.5.5 27 Jan 2026 (library: OpenSSL 3.5.5 27 Jan 2026
)
Using Boost version: 1.90.0
Automatic database updates are disabled for all databases!
Realm running as realm ID 2 region 2 battlegroup 1
Using World DB: TDB 434.22011
Using enUS DBC Locale
Will clear `logs` table of entries older than 1209600 seconds every 10 minutes.
Client cache version set to: 22011
Using DataDir /home/wow_admin/wow_cata_server/data/
WORLD: MMap data directory is: /home/wow_admin/wow_cata_server/data/mmaps
VMap support included. LineOfSight: 1, getHeight: 1, indoorCheck: 1
VMap data directory is: /home/wow_admin/wow_cata_server/data/vmaps
Missing name DungeonFinder.LowPopulation.Enable in config file /home/wow_admin/w
ow_cata_server/server/debug/etc/worldserver.conf, add "DungeonFinder.LowPopulati
on.Enable = 0" to this file
Missing name DungeonFinder.LowPopulation.FallbackDelay in config file /home/wow_
admin/wow_cata_server/server/debug/etc/worldserver.conf, add "DungeonFinder.LowP
opulation.FallbackDelay = 60" to this file
Missing name DungeonFinder.LowPopulation.MinPlayers in config file /home/wow_adm
in/wow_cata_server/server/debug/etc/worldserver.conf, add "DungeonFinder.LowPopu
lation.MinPlayers = 1" to this file
Missing name DungeonFinder.LowPopulation.SoloGroupOutsideTimeout in config file
/home/wow_admin/wow_cata_server/server/debug/etc/worldserver.conf, add "DungeonF
inder.LowPopulation.SoloGroupOutsideTimeout = 300" to this file
Loading Trinity strings...
>> Loaded 991 trinity strings in 7 ms
Initialize data stores...
>> Initialized 167 DBC data stores in 857 ms
>> Initialized 5 DB2 data stores in 200 ms
>> Loading Cinematic Camera files
>> Loaded 0 cinematic waypoint sets in 0 ms
Loading IP Location Database...
Initializing PlayerDump tables...
>> Initialized tables for PlayerDump in 14 ms.
Loading SpellInfo store...
>> Loaded SpellInfo store in 110 ms
Loading SpellInfo corrections...
>> Loaded SpellInfo corrections in 22 ms
Loading SkillLineAbilityMultiMap Data...
>> Loaded 8384 SkillLineAbility MultiMap Data in 3 ms
Loading SpellInfo custom attributes...
>> Loaded 195 spell custom attributes from DB in 2 ms
>> Loaded SpellInfo custom attributes in 76 ms
Loading SpellInfo diminishing infos...
>> Loaded SpellInfo diminishing infos in 5 ms
Loading SpellInfo immunity infos...
>> Loaded SpellInfo immunity infos in 16 ms
Loading GameObject models...
>> Loaded 3255 GameObject models in 3 ms
Loading Script Names...
>> Loaded 4057 ScriptNames in 1376 ms
Loading Instance Template...
>> Loaded 91 instance templates in 0 ms
Loading instances...
>> Loaded instances in 6 ms
Loading character cache store...
Loaded character infos for 13 characters in 0 ms
Loading Broadcast texts...
>> Loaded 73039 broadcast texts in 360 ms
>> Loaded 73039 broadcast text locales in 1637 ms
Loading Localization strings...
>> Loaded 29790 creature locale strings in 405 ms
>> Loaded 19670 gameobject_template_locale strings in 226 ms
>> Loaded 14986 Quest locale strings in 999 ms
>> Loaded 1946 PageText locale strings in 109 ms
>> Loaded 453 points_of_interest locale strings in 4 ms
>> Loaded 0 quest_greeting locales. DB table `quest_greeting_locale` is empty.
>> Localization strings loaded in 1758 ms
Loading Account Roles and Permissions...
>> Loaded 666 permission definitions, 650 linked permissions and 4 default permi
ssions in 343 ms
Loading Page Texts...
>> Loaded 1962 page texts in 13 ms
Loading Game Object Templates...
>> Loaded 29501 game object templates in 156 ms
Loading Game Object template addons...
>> Loaded 29495 game object template addons in 41 ms
Loading Transport templates...
>> Loaded 43 transport templates in 9 ms
Loading Transport animations and rotations...
Loading Transport spawns...
>> Spawned 0 continent transports in 0 ms
Loading Spell Rank Data...
>> Loaded 174 spell rank records in 1 ms
Loading Spell Required Data...
>> Loaded 22 spell required records in 1 ms
Loading Spell Group types...
>> Loaded 550 spell group definitions in 2 ms
Loading Spell Learn Skills...
>> Loaded 137 Spell Learn Skills from DBC in 5 ms
Loading SpellInfo SpellSpecific and AuraState...
>> Loaded SpellInfo SpellSpecific and AuraState in 10 ms
Loading Spell Learn Spells...
The spell 17002 listed in `spell_learn_spell` attempts learning talent spell 248
67, skipped.
The spell 24866 listed in `spell_learn_spell` attempts learning talent spell 248
64, skipped.
The spell 33872 listed in `spell_learn_spell` attempts learning talent spell 471
79, skipped.
The spell 33873 listed in `spell_learn_spell` attempts learning talent spell 471
80, skipped.
>> Loaded 24 spell learn spells, 512 found in Spell.dbc and 31 from TalentTab.db
c in 7 ms
Loading Spell Proc conditions and data...
>> Loaded 1556 spell proc conditions and data in 5 ms
Generating spell proc data from SpellMap...
Spell Id 36488 has SPELL_ATTR3_CAN_PROC_FROM_PROCS attribute and no restriction
on what spells can cause it to proc and no cooldown. This spell can cause infini
te proc loops. Proc data for this spell was not generated, data in `spell_proc`
table is required for it to function!
Spell Id 39440 has SPELL_ATTR3_CAN_PROC_FROM_PROCS attribute and no restriction
on what spells can cause it to proc and no cooldown. This spell can cause infini
te proc loops. Proc data for this spell was not generated, data in `spell_proc`
table is required for it to function!
Spell Id 47261 has SPELL_ATTR3_CAN_PROC_FROM_PROCS attribute and no restriction
on what spells can cause it to proc and no cooldown. This spell can cause infini
te proc loops. Proc data for this spell was not generated, data in `spell_proc`
table is required for it to function!
Spell Id 47262 has SPELL_ATTR3_CAN_PROC_FROM_PROCS attribute and no restriction
on what spells can cause it to proc and no cooldown. This spell can cause infini
te proc loops. Proc data for this spell was not generated, data in `spell_proc`
table is required for it to function!
Spell Id 67726 has SPELL_ATTR3_CAN_PROC_FROM_PROCS attribute and no restriction
on what spells can cause it to proc and no cooldown. This spell can cause infini
te proc loops. Proc data for this spell was not generated, data in `spell_proc`
table is required for it to function!
Spell Id 67736 has SPELL_ATTR3_CAN_PROC_FROM_PROCS attribute and no restriction
on what spells can cause it to proc and no cooldown. This spell can cause infini
te proc loops. Proc data for this spell was not generated, data in `spell_proc`
table is required for it to function!
Spell Id 67740 has SPELL_ATTR3_CAN_PROC_FROM_PROCS attribute and no restriction
on what spells can cause it to proc and no cooldown. This spell can cause infini
te proc loops. Proc data for this spell was not generated, data in `spell_proc`
table is required for it to function!
Spell Id 67744 has SPELL_ATTR3_CAN_PROC_FROM_PROCS attribute and no restriction
on what spells can cause it to proc and no cooldown. This spell can cause infini
te proc loops. Proc data for this spell was not generated, data in `spell_proc`
table is required for it to function!
Spell Id 85556 has SPELL_ATTR3_CAN_PROC_FROM_PROCS attribute and no restriction
on what spells can cause it to proc and no cooldown. This spell can cause infini
te proc loops. Proc data for this spell was not generated, data in `spell_proc`
table is required for it to function!
>> Generated spell proc data for 1620 spells in 8 ms
Loading Aggro Spells Definitions...
>> Loaded 44 SpellThreatEntries in 2 ms
Loading Spell Group Stack Rules...
>> Loaded 41 spell group stack rules in 2 ms
>> Parsing SPELL_GROUP_STACK_RULE_EXCLUSIVE_SAME_EFFECT stack rules...
>> Parsed 23 SPELL_GROUP_STACK_RULE_EXCLUSIVE_SAME_EFFECT stack rules in 0 ms
Loading NPC Texts...
>> Loaded 8406 npc texts in 92 ms
Loading Enchant Spells Proc datas...
>> Loaded 45 enchant proc data definitions in 1 ms
Loading Item Random Enchantments Table...
>> Loaded 29217 Item Enchantment definitions in 25 ms
Loading Disables
>> Loaded 5211 disables in 10 ms
Loading Items...
>> Loaded 54296 item templates in 39 ms
Loading Item set names...
>> Loaded 622 item addon templates in 2 ms
>> Loaded 21 item script names in 0 ms
Loading Creature Model Based Info Data...
>> Loaded 31775 creature model based info in 33 ms
Loading Creature templates...
>> Loaded 64891 creature template models in 77 ms
>> Loaded 46633 creature definitions in 580 ms
Loading Equipment templates...
>> Loaded 16167 equipment templates in 23 ms
Loading Creature template addons...
Creature (Entry: 47203) has SPELL_AURA_CONTROL_VEHICLE aura 87978 defined in `au
ras` field in `creature_template_addon`.
>> Loaded 46613 creature template addons in 73 ms
Loading Reputation Reward Rates...
>> Loaded 16 reputation_reward_rate in 0 ms
Loading Creature Reward OnKill Data...
>> Loaded 2343 creature award reputation definitions in 7 ms
Loading Reputation Spillover Data...
>> Loaded 28 reputation_spillover_template in 2 ms
Loading Points Of Interest Data...
>> Loaded 603 Points of Interest definitions in 2 ms
Loading Database Game Tables...
>> Loaded 0 npc total health game table entries in 5 ms
>> Loaded 0 npc damage by class game table entries in 4 ms
Loading Creature Base Stats...
>> Loaded 400 creature base stats in 8 ms
Loading Spawn Group Templates...
>> Loaded 98 spawn group templates in 1 ms
Loading Creature Data...
Table `creature` has creature (GUID: 396679 Entry: 32491) with disallowed `unit_
flags` 32768, removing incorrect flag.
>> Loaded 221565 creatures in 1182 ms
Loading Temporary Summon Data...
>> Loaded 869 temp summons in 4 ms
Loading pet levelup spells...
>> Loaded 263 pet levelup and default spells for 50 families in 8 ms
Loading pet default spells additional to levelup spells...
>> Loaded addition spells for 0 pet spell data entries in 2 ms
Loading summonable creature templates...
>> Loaded 427 summonable creature templates in 10 ms
Loading Creature Addon Data...
>> Loaded 52002 creature addons in 93 ms
Loading Creature Movement Overrides...
>> Loaded 0 creature movement overrides. DB table `creature_movement_override` i
s empty!
Loading Creature Movement Info...
>> Loaded 270 creature movement info override entries in 1 ms
Loading Gameobject Data...
>> Loaded 78938 gameobjects in 392 ms
Loading Spawn Group Data...
>> Loaded 94612 spawn group members in 58 ms
Loading instance spawn groups...
>> Loaded 27 instance spawn groups in 2 ms
Loading GameObject Addon Data...
>> Loaded 24493 gameobject addons in 38 ms
Loading GameObject Quest Items...
>> Loaded 1352 gameobject quest items in 3 ms
Loading Creature Quest Items...
>> Loaded 5361 creature quest items in 8 ms
Loading Creature Sparring Data...
>> Loaded 0 creature sparring templates in 1 ms
Loading Creature Linked Respawn...
>> Loaded 5239 linked respawns in 7 ms
Loading Weather Data...
>> Loaded 36 weather definitions in 0 ms
Loading Quests...
Quest 29507 has `RequiredCurrencyId1` = 393 but `RequiredCurrencyCount1` = 0, qu
est can't be done.
Quest 29283 has `RequiredCurrencyId1` = 416 but `RequiredCurrencyCount1` = 0, qu
est can't be done.
Quest 29281 has `RequiredCurrencyId1` = 416 but `RequiredCurrencyCount1` = 0, qu
est can't be done.
Quest 29279 has `RequiredCurrencyId1` = 416 but `RequiredCurrencyCount1` = 0, qu
est can't be done.
Quest 29214 has `RequiredCurrencyId1` = 416 but `RequiredCurrencyCount1` = 0, qu
est can't be done.
Quest 29199 has `RequiredCurrencyId1` = 416 but `RequiredCurrencyCount1` = 0, qu
est can't be done.
Quest 29181 has `RequiredCurrencyId1` = 416 but `RequiredCurrencyCount1` = 0, qu
est can't be done.
Quest 13478 has `RequiredCurrencyId1` = 392 but `RequiredCurrencyCount1` = 0, qu
est can't be done.
Quest 13476 has `RequiredCurrencyId1` = 392 but `RequiredCurrencyCount1` = 0, qu
est can't be done.
Quest 8371 has `RequiredCurrencyId1` = 392 but `RequiredCurrencyCount1` = 0, que
st can't be done.
Quest 8368 has `RequiredCurrencyId1` = 392 but `RequiredCurrencyCount1` = 0, que
st can't be done.
Quest 8367 has `RequiredCurrencyId1` = 392 but `RequiredCurrencyCount1` = 0, que
st can't be done.
>> Loaded 14991 quests definitions in 347 ms
Checking Quest Disables
>> Checked 5124 quest disables in 0 ms
Loading Quest POI
>> Loaded 13941 quest POI definitions in 189 ms
Loading Quest Money Rewards
>> Loaded 100 quest money reward entries in 1 ms
Loading Quests Starters and Enders...
Loading GO Start Quest Data...
>> Loaded 618 quest relations from gameobject_queststarter in 2 ms
Loading GO End Quest Data...
>> Loaded 517 quest relations from gameobject_questender in 2 ms
Loading Creature Start Quest Data...
>> Loaded 8725 quest relations from creature_queststarter in 10 ms
Loading Creature End Quest Data...
>> Loaded 9136 quest relations from creature_questender in 12 ms
Loading Quests Greetings...
>> Loaded 0 quest greetings. DB table `quest_greeting` is empty.
Loading Objects Pooling Data...
>> Loaded 1721 objects pools in 3 ms
Loading Creatures Pooling Data...
>> Loaded 835 creatures in pools in 2 ms
Loading Gameobject Pooling Data...
>> Loaded 7404 gameobject in pools in 10 ms
Loading Mother Pooling Data...
>> Loaded 1218 pools in mother pools in 3 ms
Starting objects pooling system...
Loading Quest Pooling Data...
>> Loaded 56 daily, 5 weekly and 0 monthly quest pools in 2 ms
Loading Game Event Data...
>> Loaded 156 holiday dates in 1 ms
>> Loaded 78 game events in 1 ms.
Loading Game Event Saves Data...
>> Loaded 0 game event saves in game events. DB table `game_event_save` is empty
.
Loading Game Event Prerequisite Data...
>> Loaded 0 game event prerequisites in game events. DB table `game_event_prereq
uisite` is empty.
Loading Game Event Creature Data...
>> Loaded 2790 creatures in game events in 2 ms.
Loading Game Event GO Data...
>> Loaded 21863 gameobjects in game events in 10 ms.
Loading Game Event Model/Equipment Change Data...
>> Loaded 22 model/equipment changes in game events in 2 ms.
Loading Game Event Quest Data...
>> Loaded 327 quests additions in game events in 1 ms.
Loading Game Event GO Quest Data...
>> Loaded 189 quests additions in game events in 1 ms.
Loading Game Event Quest Condition Data...
>> Loaded 0 quest event conditions in game events. DB table `game_event_quest_co
ndition` is empty.
Loading Game Event Condition Data...
>> Loaded 0 conditions in game events. DB table `game_event_condition` is empty.
Loading Game Event Condition Save Data...
>> Loaded 0 condition saves in game events. DB table `game_event_condition_save`
is empty.
Loading Game Event NPCflag Data...
>> Loaded 0 npcflags in game events. DB table `game_event_npcflag` is empty.
Loading Game Event Seasonal Quest Relations...
>> Loaded 690 quests additions in game events in 1 ms.
Loading Game Event Vendor Additions Data...
>> Loaded 2 vendor additions in game events in 1 ms.
Loading Game Event Battleground Holiday Data...
>> Loaded 8 battleground holidays in game events in 1 ms.
Loading Game Event Pool Data...
>> Loaded 2 pools for game events in 0 ms.
Loading UNIT_NPC_FLAG_SPELLCLICK Data...
>> Loaded 702 spellclick definitions in 3 ms
Loading Vehicle Template Accessories...
>> Loaded 431 Vehicle Template Accessories in 2 ms
Loading Vehicle Accessories...
>> Loaded 0 Vehicle Accessories in 0 ms
Loading Vehicle Seat Addon Data...
>> Loaded 15 Vehicle Seat Addon entries in 1 ms
Loading SpellArea Data...
>> Loaded 872 spell area requirements in 4 ms
Loading AreaTrigger definitions...
>> Loaded 316 area trigger teleport definitions in 2 ms
Loading Access Requirements...
>> Loaded 152 access requirement definitions in 1 ms
Loading Quest Area Triggers...
>> Loaded 49 quest trigger points in 1 ms
Loading Tavern Area Triggers...
>> Loaded 175 tavern triggers in 1 ms
Loading AreaTrigger script names...
>> Loaded 229 areatrigger scripts in 0 ms
Loading LFG entrance positions...
>> Loaded 68 lfg dungeon templates in 2 ms
Loading Dungeon boss data...
>> Loaded 617 instance encounters in 2 ms
Loading LFG rewards...
>> Loaded 20 lfg dungeon rewards in 1 ms
Loading Graveyard-zone links...
>> Loaded 855 graveyard-zone links in 2 ms
Loading Graveyard Orientations...
>> Loaded 1016 graveyard orientations in 2 ms
Loading spell pet auras...
>> Loaded 34 spell pet auras in 1 ms
Loading Spell target coordinates...
>> Loaded 1438 spell teleport coordinates in 4 ms
Loading linked spells...
>> Loaded 413 linked spells in 1 ms
Loading Player Create Data...
>> Loaded 91 player create definitions in 1 ms
Loading Player Create Items Data...
>> Loaded 1 custom player create items in 1 ms
Loading Player Create Skill Data...
>> Loaded player create skills in 1 ms
Loading Player Create Custom Spell Data...
>> Loaded 4507 custom player create spells in 1 ms
Loading Player Create Cast Spell Data...
>> Loaded 63 player create cast spells in 1 ms
Loading Player Create Action Data...
>> Loaded 352 player create actions in 1 ms
Loading Player Create Level Stats Data...
>> Loaded 850 level stats definitions in 5 ms
Loading Player Create XP Data...
>> Loaded 84 xp for level definitions in 1 ms
Loading Exploration BaseXP Data...
>> Loaded 80 BaseXP definitions in 1 ms
Loading Pet Name Parts...
>> Loaded 313 pet name parts in 1 ms
Loading the max pet number...
>> Loaded the max pet number: 28 in 0 ms
Loading pet level stats...
>> Loaded 2720 level pet stats definitions in 5 ms
Loading Player level dependent mail rewards...
>> Loaded 24 level dependent mail rewards in 1 ms
Loading creature loot templates...
>> Loaded 528193 creature loot templates in 634 ms
Loading fishing loot templates...
>> Loaded 266 fishing loot templates in 2 ms
Loading gameobject loot templates...
>> Loaded 19834 gameobject loot templates in 32 ms
Loading item loot templates...
>> Loaded 5031 item loot templates in 16 ms
Loading mail loot templates...
>> Loaded 90 mail loot templates in 0 ms
Loading milling loot templates...
>> Loaded 51 milling loot templates in 4 ms
Loading pickpocketing loot templates...
>> Loaded 10981 pickpocketing loot templates in 20 ms
Loading skinning loot templates...
>> Loaded 3390 skinning loot templates in 10 ms
Loading disenchanting loot templates...
>> Loaded 180 disenchanting loot templates in 1 ms
Loading prospecting loot templates...
>> Loaded 43 prospecting loot templates in 4 ms
Loading spell loot templates...
>> Loaded 292 spell loot templates in 3 ms
Loading reference loot templates...
>> Loaded refence loot templates in 48 ms
Loading Skill Discovery Table...
>> Loaded 347 skill discovery definitions in 3 ms.
Loading Skill Extra Item Table...
>> Loaded 225 spell specialization definitions in 1 ms
Loading Skill Perfection Data Table...
>> Loaded 72 spell perfection definitions in 0 ms.
Loading Skill Fishing base level requirements...
>> Loaded 95 areas for fishing base skill level in 0 ms
Loading Archaeology store...
>> Loaded 180 archaeology sites in 1 ms
>> Loaded 180 archaeology site polygons in 157 ms
>> Loaded 9 archaeology branches in 0 ms
>> Loaded 139 archaeology projects in 1 ms
>> Calculated 9 archaeology branch chances in 0 ms
Loading Achievements...
>> Loaded 47 achievement references in 0 ms.
Loading Achievement Criteria Lists...
>> Loaded 9803 achievement criteria and 952 guild achievement crieteria in 5 ms.
Loading Achievement Criteria Data...
>> Loaded 1291 additional achievement criteria data in 2 ms
Loading Achievement Rewards...
>> Loaded 120 achievement rewards in 1 ms.
Loading Achievement Reward Locales...
>> Loaded 0 achievement reward locale strings. DB table `achievement_reward_loc
ale` is empty.
Loading Completed Achievements...
>> Loaded 77 realm first completed achievements in 1 ms.
Loading Item Auctions...
>> Loaded 0 auction items. DB table `auctionhouse` or `item_instance` is empty!
Loading Auctions...
>> Loaded 0 auctions. DB table `auctionhouse` is empty.
Loading Guild XP for level...
>> Loaded 24 xp for guild level definitions in 1 ms
Loading Guild rewards...
>> Loaded 42 guild reward definitions in 0 ms
Initializing Guild Profession Data Store...
Loading Guild Challenges...
>> Loaded 4 guild challenge definitions in 1 ms
Loading Guilds...
Loading guilds definitions...
>> Loaded 1 guild definitions in 2 ms
Loading guild ranks...
>> Loaded 5 guild ranks in 0 ms
Loading guild members...
>> Loaded 4 guild members in 5 ms
Loading bank tab rights...
>> Loaded 30 bank tab rights in 1 ms
Loading guild event logs...
>> Loaded 8 guild event logs in 2 ms
Loading guild bank event logs...
>> Loaded 22 guild bank event logs in 0 ms
Loading Guild News...
>> Loaded 0 guild event logs. DB table `guild_newslog` is empty.
Loading guild bank tabs...
>> Loaded 6 guild bank tabs in 0 ms
>> Loaded 22 guild bank tab items in 1 ms
>> Validated data of loaded guilds in 0 ms
Loading guild finder guild-related settings...
>> Loaded 0 guild finder guild-related settings. Table `guild_finder_guild_setti
ngs` is empty.
Loading guild finder membership requests...
>> Loaded 0 guild finder membership requests. Table `guild_finder_applicant` is
empty.
Loading ArenaTeams...
>> Loaded 0 arena teams. DB table `arena_team` is empty!
Loading Groups...
>> Loaded 0 group definitions. DB table `groups` is empty!
Loading ReservedNames...
>> Loaded 0 reserved player names. DB table `reserved_name` is empty!
Loading GameObjects for quests...
>> Loaded 2750 GameObjects for quests in 5 ms
Loading BattleMasters...
>> Loaded 171 battlemaster entries in 3 ms
Loading GameTeleports...
>> Loaded 1531 GameTeleports in 5 ms
Loading Trainers...
>> Loaded 88 Trainers in 10 ms
Loading Gossip menu...
>> Loaded 8225 gossip_menu IDs in 10 ms
Loading Gossip menu options...
>> Loaded 5837 gossip_menu_option entries in 18 ms
Loading Creature Template Gossip...
creature_template_gossip has gossip definitions for menu id 55002 but this menu
doesn't exist
creature_template_gossip has gossip definitions for menu id 55002 but this menu
doesn't exist
creature_template_gossip has gossip definitions for menu id 6268 but this menu d
oesn't exist
creature_template_gossip has gossip definitions for menu id 9856 but this menu d
oesn't exist
creature_template_gossip has gossip definitions for menu id 10844 but this menu
doesn't exist
creature_template_gossip has gossip definitions for menu id 10852 but this menu
doesn't exist
creature_template_gossip has gossip definitions for menu id 2522 but this menu d
oesn't exist
creature_template_gossip has gossip definitions for menu id 12720 but this menu
doesn't exist
creature_template_gossip has gossip definitions for menu id 12718 but this menu
doesn't exist
creature_template_gossip has gossip definitions for menu id 13160 but this menu
doesn't exist
>> Loaded 5435 creature template gossip menus in 8 ms
Loading Creature trainers...
>> Loaded 1187 default trainers in 4 ms
Loading Vendors...
Table `(game_event_)npc_vendor` for Vendor (Entry: 44972) have in item list non-
existed item (60822, type 1), ignore
>> Loaded 48278 Vendors in 71 ms
Loading Waypoints...
>> Loaded 110626 waypoints in 177 ms
Loading Waypoint Addons...
>> Loaded 639 waypoint addon data in 2 ms
Loading SmartAI Waypoints...
>> Loaded 586 SmartAI waypoint paths (total 7064 waypoints) in 10 ms
Loading Creature Formations...
>> Loaded 2517 creatures in formations in 5 ms
Loading World State templates...
Table `world_state` contains a world state 3490 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3490 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3491 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3491 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3600 with invalid MapID (1134), map i
gnored
Table `world_state` contains a world state 3600 with invalid MapID (1504), map i
gnored
Table `world_state` contains a world state 3600 with invalid MapID (1505), map i
gnored
Table `world_state` contains a world state 3600 with invalid MapID (1552), map i
gnored
Table `world_state` contains a world state 3600 with invalid MapID (1672), map i
gnored
Table `world_state` contains a world state 3600 with invalid MapID (1825), map i
gnored
Table `world_state` contains a world state 3600 with invalid MapID (1911), map i
gnored
Table `world_state` contains a world state 3600 with invalid MapID (2167), map i
gnored
Table `world_state` contains a world state 3600 with invalid MapID (2373), map i
gnored
Table `world_state` contains a world state 3600 with invalid MapID (2509), map i
gnored
Table `world_state` contains a world state 3600 with invalid MapID (2511), map i
gnored
Table `world_state` contains a world state 3600 with invalid MapID (2547), map i
gnored
Table `world_state` contains a world state 3601 with invalid MapID (1134), map i
gnored
Table `world_state` contains a world state 3601 with invalid MapID (1504), map i
gnored
Table `world_state` contains a world state 3601 with invalid MapID (1505), map i
gnored
Table `world_state` contains a world state 3601 with invalid MapID (1552), map i
gnored
Table `world_state` contains a world state 3601 with invalid MapID (1672), map i
gnored
Table `world_state` contains a world state 3601 with invalid MapID (1825), map i
gnored
Table `world_state` contains a world state 3601 with invalid MapID (1911), map i
gnored
Table `world_state` contains a world state 3601 with invalid MapID (2167), map i
gnored
Table `world_state` contains a world state 3601 with invalid MapID (2373), map i
gnored
Table `world_state` contains a world state 3601 with invalid MapID (2509), map i
gnored
Table `world_state` contains a world state 3601 with invalid MapID (2511), map i
gnored
Table `world_state` contains a world state 3601 with invalid MapID (2547), map i
gnored
Table `world_state` contains a world state 3610 with invalid MapID (1134), map i
gnored
Table `world_state` contains a world state 3610 with invalid MapID (1504), map i
gnored
Table `world_state` contains a world state 3610 with invalid MapID (1505), map i
gnored
Table `world_state` contains a world state 3610 with invalid MapID (1552), map i
gnored
Table `world_state` contains a world state 3610 with invalid MapID (1672), map i
gnored
Table `world_state` contains a world state 3610 with invalid MapID (1825), map i
gnored
Table `world_state` contains a world state 3610 with invalid MapID (1911), map i
gnored
Table `world_state` contains a world state 3610 with invalid MapID (2167), map i
gnored
Table `world_state` contains a world state 3610 with invalid MapID (2373), map i
gnored
Table `world_state` contains a world state 3610 with invalid MapID (2509), map i
gnored
Table `world_state` contains a world state 3610 with invalid MapID (2511), map i
gnored
Table `world_state` contains a world state 3610 with invalid MapID (2547), map i
gnored
Table `world_state` contains a world state 3680 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3680 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3681 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3681 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3698 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3698 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3699 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3699 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3700 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3700 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3701 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3701 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3702 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3702 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3703 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3703 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3704 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3704 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3705 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3705 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3706 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3706 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3710 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3710 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3711 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3711 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3712 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3712 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3713 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3713 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3714 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3714 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3749 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3749 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3750 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3750 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3751 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3751 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3752 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3752 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3753 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3753 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3754 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3754 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3755 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3755 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3756 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3756 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3757 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3757 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3758 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3758 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3759 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3759 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3760 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3760 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3761 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3761 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3762 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3762 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3763 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3763 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3764 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3764 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3765 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3765 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3766 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3766 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3767 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3767 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3768 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3768 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3769 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3769 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3770 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3770 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3771 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3771 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3772 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3772 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3773 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3773 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3781 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3781 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3801 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3801 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3802 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3802 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3803 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 3803 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 3815 with invalid MapID (1544), map i
gnored
Table `world_state` contains a world state 3816 with invalid MapID (1544), map i
gnored
Table `world_state` contains a world state 4022 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 4022 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 4023 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 4023 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 4024 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 4024 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 4025 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 4025 with invalid AreaID (10176), are
a ignored
Table `world_state` contains a world state 4354 with invalid MapID (2118), map i
gnored
Table `world_state` contains a world state 4354 with invalid AreaID (10176), are
a ignored
>> Loaded 526 world state templates 4 ms
>> Loaded 41 saved world state values 24 ms
Loading Persistend World Variables...
>> Loaded 9 world variables in 4 ms
Loading Terrain World Map definitions...
>> Loaded 4 terrain world maps in 0 ms.
Loading Terrain Swap Default definitions...
>> Loaded 4 terrain swap defaults in 1 ms.
Loading Phase Area definitions...
>> Loaded 25 phase areas in 1 ms.
Loading Conditions...
>> Loaded 15912 conditions in 45 ms
Loading faction change achievement pairs...
>> Loaded 124 faction change achievement pairs in 2 ms
Loading faction change spell pairs...
>> Loaded 113 faction change spell pairs in 1 ms
Loading faction change quest pairs...
>> Loaded 250 faction change quest pairs in 1 ms
Loading faction change item pairs...
>> Loaded 752 faction change item pairs in 2 ms
Loading faction change reputation pairs...
>> Loaded 17 faction change reputation pairs in 1 ms
Loading faction change title pairs...
>> Loaded 22 faction change title pairs in 1 ms
Loading GM tickets...
>> Loaded 2 GM tickets in 0 ms
Loading GM surveys...
>> Loaded GM Survey count from database in 1 ms
Loading client addons...
>> Loaded 35 known addons in 0 ms
Returning old mails...
>> No expired mails found.
Loading Autobroadcasts...
>> Loaded 0 autobroadcasts definitions. DB table `autobroadcast` is empty for th
is realm!
Loading spell_scripts...
>> Loaded 143 script definitions in 1 ms
Loading event_scripts...
>> Loaded 461 script definitions in 2 ms
Loading waypoint_scripts...
>> Loaded 422 script definitions in 2 ms
Loading spell script names...
Scriptname: `spell_vcore_dungeon_horn` spell (Id: 120000) does not exist.
Scriptname: `spell_vcore_dungeon_horn` spell (Id: 120001) does not exist.
Scriptname: `spell_vcore_dungeon_horn` spell (Id: 120002) does not exist.
Scriptname: `spell_vcore_dungeon_horn` spell (Id: 120003) does not exist.
>> Loaded 3282 spell script names in 7 ms
Loading Creature Texts...
>> Loaded 16797 creature texts for 3997 creatures in 53 ms
Loading Creature Text Locales...
Loading creature StaticFlags overrides...
>> Loaded 133 creature static flag overrides in 1 ms
Loading Taxi node level definitions...
>> Loaded 274 taxi node level definitions in 1 ms
Initializing Scripts...
Loading Script Waypoints for 54 creature(s)...
>> Loaded 1377 Script Waypoint nodes in 4 ms
>> Loaded spline chain data for 85 chains, consisting of 272 splines with 1414 w
aypoints in 4 ms
Loading C++ scripts
Script named 'world_map_set_faction_worldstates_609' does not have a script name
assigned in database.
Script named 'item_gor_dreks_ointment' does not have a script name assigned in d
atabase.
>> Loaded 4106 C++ scripts in 146 ms
Validating spell scripts...
Spell `85123` Effect `Index: EFFECT_0 Target: 7` of script `spell_siege_cannon`
did not match dbc effect data - handler bound to hook `OnObjectAreaTargetSelect`
of SpellScript won't be executed
>> Validated 3470 scripts in 12 ms
Loading SmartAI scripts...
>> Loaded SmartAIMgr Helpers in 16 ms
>> Loaded 13506 SmartAI scripts in 167 ms
Loading Calendar data...
>> Loaded 0 calendar events in 1 ms
>> Loaded 0 calendar invites in 0 ms
Loading Petitions...
>> Loaded 0 petitions.
Loading Signatures...
>> Loaded 0 Petition signs!
Loading Summon Properties parameter data...
>> Loaded 78 summon properties parameters in 1 ms
Loading Item loot...
>> Loaded 0 stored item loots
>> Loaded 0 stored item money
Initialize query data...
>> Initialized query cache data in 1019 ms
Initialize commands...
Initialize game time and timers
Mail timer set to: 1260, mail return is called every 1440 minutes
Starting Map System
Starting Game Event system...
Initialize AuctionHouseBot...
Initializing Opcodes...
Starting Arena Season...
Starting Battleground System
>> Loaded 15 battlegrounds in 0 ms
Starting Outdoor PvP System
>> Loaded 5 outdoor PvP definitions in 1 ms
Starting Battlefield System
>> Loaded 2 battlefields in 0 ms
Loading Warden Checks...
Loading Warden Action Overrides...
Deleting expired bans...
Initializing quest reset times...
Calculate random battleground reset time...
Calculate guild limitation(s) reset time...
Calculate next currency reset time...
World initialized in 0 minutes 13 seconds
TC>TC>TrinityCore rev. 39a515dbd13e+ 2026-10-27 23:56:43 +0100 (feature/clientle
ss-player-core branch) (Unix, Debug, Static) (worldserver-daemon) ready...
clientless login 26
TC>Registered clientless session for character GUID 26 (account 6), state=Loadin
g.
Created clientless player session for character GUID 26 (account 6).
ClientlessPlayerMgr started character GUID 26 (account 6).
Clientless login started for 'Arthoran' (GUID 26, account 6).
TC> Loading saved respawn time of 1790175804 for spawnid (0,323981) - spawn does
not exist, ignoring
Loading saved respawn time of 1790090644 for spawnid (0,396680) - spawn does not
exist, ignoring
Loading saved respawn time of 1790019808 for spawnid (0,396681) - spawn does not
exist, ignoring
Loading saved respawn time of 1790019853 for spawnid (0,396682) - spawn does not
exist, ignoring
Initialized transport GameObject grids: map=0 instance=0 grids=4 candidates=15
Clientless player character GUID 26 (account 6) reached state=Online.
ClientlessLua initialized, script root '/home/wow_admin/wow_cata_server/source/T
rinityCore/src/server/scripts/Custom/clientless'.
[AdventureBot][Profile] loaded guid=26 name='Arthoran' source='/home/wow_admin/w
ow_cata_server/source/TrinityCore/src/server/scripts/Custom/clientless/adventure
/profiles/arthoran.lua' home='elwynn' canGroundMount=1 mounts=1 mountMinDistance
=45.0 mountSpeedRate=1.60 personality=8 regionAffinity=4 activityAffinity=7
PROFILE_PROFESSIONS guid=26 primary=2
ClientlessLua[26]: Arthoran ready; no automatic test journey queued
ClientlessLua[26]: executed adventure override script '/home/wow_admin/wow_cata_
server/source/TrinityCore/src/server/scripts/Custom/clientless/adventure/overrid
es/arthoran.lua'.
ADVENTURE_CATALOG locations=2973 actions=84 lines=76
INTENT_TRAVEL guid=26 intent='social' location='goldshire_tavern' area=0
[AdventureBot] loaded 3966 clientless world points.
[AdventureBot] loaded travel graph: 993 node(s), 1765 directed edge(s).
clientless interaction 26
TC>Interactions guid=26 grouped=0 leader=0 partner=0 intent='social' schedule=''
leaderIdleSeconds=0 afkSeconds=0 sociability=0.65 loyalty=0.35
TC> SUPPLY_RESTOCK guid=26 day=20261028 daily=0 afterCombat=1
@@ -0,0 +1,57 @@
test_adventure_area_node.py: PASS
test_adventure_behavior_policy.py: PASS
test_adventure_combat_exercise.py: PASS
test_adventure_combat_preparation.py: PASS
test_adventure_combat_reactions.py: PASS
test_adventure_economy.py: PASS
test_adventure_economy_catalog.py: PASS
test_adventure_exploration.py: PASS
test_adventure_exploration_catalog.py: PASS
test_adventure_graph_handover.py: PASS
test_adventure_instance.py: PASS
test_adventure_instance_autoresume.py: PASS
test_adventure_instance_begin.py: PASS
test_adventure_instance_catalog.py: PASS
test_adventure_instance_persistence.py: PASS
test_adventure_intent_state.py: PASS
test_adventure_interactions.py: PASS
test_adventure_leisure.py: PASS
test_adventure_leisure_catalog.py: PASS
test_adventure_location_import.py: PASS
test_adventure_micro_reset.py: PASS
test_adventure_profession_catalog.py: PASS
test_adventure_profession_profile.py: PASS
test_adventure_profession_sightline.py: PASS
test_adventure_profession_targets.py: PASS
test_adventure_repair_catalog.py: PASS
test_adventure_rest.py: PASS
test_adventure_rest_fire.py: PASS
test_adventure_rest_space.py: PASS
test_adventure_rest_targets.py: PASS
test_adventure_social.py: PASS
test_adventure_social_fire.py: PASS
test_adventure_supplies.py: PASS
test_adventure_supply_catalog.py: PASS
test_adventure_supply_items.py: PASS
test_adventure_training_catalog.py: PASS
test_adventure_walk.py: PASS
test_clientless_companion.py: PASS
test_clientless_damage_notifications.py: PASS
test_clientless_near_teleport_observer.py: PASS
test_default_phase.py: PASS
test_flight_area_query.py: PASS
test_flight_handoff.py: PASS
test_flight_policy.py: PASS
test_follow_spline_velocity.py: PASS
test_free_flight.py: PASS
test_free_flight_planner.py: PASS
test_ground_exit_continuation.py: PASS
test_local_spline_segments.py: PASS
test_lua_reload.py: PASS
test_player_pure_max_skill.py: PASS
test_portal_transfer.py: PASS
test_portal_use.py: PASS
test_taxi_import_disabled.py: PASS
test_transport.py: PASS
test_transport_grid_loading.py: PASS
56/56 passed; failures: []
@@ -0,0 +1,8 @@
Debug build -j10 and installation succeeded. Test worldserver59086 freshly ready, port8085 listening.
56/56 full suite passed; additional new social notification concurrency test passed (57 scripts total).
Final expanded combat test with player focus also passed.
Native login26 and interaction diagnostic succeeded; sociability0.65 loyalty0.35.
Real creature defense after restart observed. No live player invitation/PvP/pet/portal/ten-minute AFK claim.
Normal initial Goldshire trip started on login; behavior stop requested at safe point, last observed switching/waiting for safe state.
OLD executable shutdown observed Object.h:448 GetMap ASSERTION FAILED m_currMap; process49765 exited before installation. Cause not investigated; new startup succeeded.
Local commit requested by user; no push. No DB migrations. No temporary profile overrides.
+151
View File
@@ -0,0 +1,151 @@
# AdventureBot generation
The GM/console command `clientless generate` creates complete new native player
characters on an existing normal bot account. No source player is cloned and no
authentication account/password is created or changed. The native realm-character
counter is updated for that account after a successful character commit.
## Commands
```
clientless generate create <account> <count>
clientless generate create <account> <count> <level> <faction> <archetype> <persistence> <seed>
clientless generate list
clientless generate profile <guid>
clientless generate login <guid>
clientless generate logout <guid>
clientless generate equip <guid>
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
`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.
Available archetypes: `warrior`, `paladin`, `hunter`, `rogue`, `priest`,
`death_knight` (minimum55), `shaman`, `mage`, `warlock`, `druid`, or `any`.
Native race/class data and account expansion restrictions determine eligible
combinations. Both factions can use the administrative bot-account pool; this
is not a normal client's character-creation request.
`equip` refreshes the equipment of an idle online generator-owned bot using its
stored archetype and seed. Native checks still apply, casting/combat/transit are
rejected, and replaced items remain in bags. It requires sufficient bag space;
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.
## Identity and profile
Names use curated cultural syllable combinations for the supported races and
sexes. They contain no numbers, bot markers or numeric collision suffixes.
Native name validation, reserved-name checks, character cache/database checks,
and reservation of hand-written named Lua profiles apply. An exhausted pool
fails cleanly instead of manufacturing a non-roleplay name.
Profiles include a racial origin, vocation, background, two coherent primary
professions, personality values and regional/activity affinities. The stored
seed makes choices reproducible against unchanged catalogs and name availability.
Existing name collisions legitimately advance the random stream. Equipment
varies within a set of similar high-scoring legal options.
Generated Lua is stored in the character database, avoiding runtime writes to
source-controlled profile files. The ordinary sandbox/parser loads it with
source `generated:<guid>`. Existing hand-written named Lua profiles retain
precedence; ordinary non-generated characters keep the generic fallback.
`identity.background` and `identity.vocation` are also available in the parsed
profile. Generated strings are Lua-escaped, then escaped separately for SQL.
Home points come from enabled, faction-compatible social tavern/settlement
locations nearest the native racial starting region. Mature goblins and worgen
use mainland orc/human home regions, rather than phased introductory maps.
Death knights also choose racial mainland homes. Map relocation happens before
native talent calculation, so introductory map609 does not reduce their talent
budget. This is administrative creation, not a claim that these bots played or
completed introductory quests.
## Native preparation
`ClientlessCharacterFactory` is generic infrastructure with preparation and
transaction-extension callbacks. It has no AdventureBot policy dependency.
It uses native `Player::Create`, validated random appearance, level/skill setup,
new player/item GUID allocators and native `SaveToDB`. Requested levels are
initialized as birth state through native stat/talent/taxi/glyph initialization,
avoiding earned-level reward mails/hooks before the character transaction. Network-active accounts,
privileged accounts, unavailable names and the configured per-realm character
limit are rejected.
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
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.
- Coherent primary profession pairs, trained to their legal native rank/cap;
all four available secondary professions through existing supply preparation.
Before world insertion, effect44 learning casts cannot be relied on to apply
their target effects. The factory therefore initializes validated skill ranks
with native SkillRaceClass/SkillTiers caps and `Player::SetSkill`. This helper
refuses online players; ordinary live supply learning keeps its native casts.
This does not teach every profession recipe or synthesize quest rewards.
- Four native Embersilk Bags and class/stat/level-matching equipment. Candidate
items are normal/uncommon/rare; native use/equip checks remain authoritative.
Armor transitions at40 are respected. Temporary, holiday, reputation-bound,
restricted-map/area and random-affix equipment is excluded. Caster/physical
stat affinity and mana-only/triggered utility effects are checked, so a
warrior does not receive a mana-restoration trinket. Identical equipped
entries are not duplicated, in addition to native unique-equipment checks. Initial items
are retained in bags rather than destroyed.
- 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
having class-specific automated combat/support AI.
## Persistence and future rotation
Apply `sql/custom/characters/2026_10_28_00_adventure_generator.sql` before using
commands. The additive migration is idempotent. Native character state, inventory,
professions, Lua profile, homebind and generated ownership are committed in one
character-database transaction. Cache publication and create hooks happen only
after confirmed success. Failure before/at commit does not publish a ready bot.
The registry explicitly records ownership, seed, archetype, creation time,
persistence and state. Existing characters such as Arthoran are not adopted.
Only offline temporary generator-owned characters may be retired. Retirement
marks metadata only; the native character, items and profile remain available
for audit. Generator login refuses retired entries. Persistent characters are
protected. There is no automatic deletion, replacement, new-account allocation
or restart autologin. A later rotating population controller must define those
policies and account/name capacity management explicitly.
## Verification
Production-code adapter tests cover factory transaction failure/publication,
account/name/security/character-cap gates, retirement ownership protections,
command bounds, cultural naming, real Lua profile execution, personality/skill
coherence, string escaping and equipment/armor eligibility.
Compilation and adapters do not establish visible in-game behavior. Live-created
character identities, gear, profession/talent/spell rows and login/profile
results are recorded in the task checkpoint after native server checks.
Live checkpoint (2026-10-28): four level60 characters are online: Aelion29
(night elf warrior), Zanzan30 (troll warrior), Lenara31 (worgen mage), Aldren32
(forsaken death knight). Their generated profiles were reloaded after a server
restart, and native equipment refresh was saved for all four. Earlier temporary
fixtures27/28 remain archived and offline. Existing inventory ownership for
Allitester21 and Arthoran26 matched the pre-generation snapshot. All62 clientless
test scripts passed. See checkpoints/2026-10-28-generator/ for native data/logs.
+103
View File
@@ -0,0 +1,103 @@
# AdventureBot interactions and player-led groups
Implemented 2026-10-28 on feature/clientless-player-core. No autonomous bot-to-bot
party formation, role assignment, healer/tank AI or proactive PvP aggression.
PlayerAI.cpp supplies spell/attack foundations and charmed-player follow logic;
it does not implement invitation decisions. AdventureBots keep independent
controllers and use the native party invitation response handler.
## Encounters
Directed native text emotes greet/hello/wave/bow/salute/smile/thank and
rude/threaten/roar generate GUID-only notices in ClientlessPlayerMgr. Map workers
never call AdventureBot policy directly. Queues are bounded and delivered on the
world thread. Participants must be alive, mutually visible, in the same map,
instance and phase, within 25 yards and in line of sight.
Replies wait 1–3 seconds, briefly interrupt the current activity, face the other
player and use native animation emotes. Friendly encounters wave/bow; hostility
or hostile emotes produce a negative gesture. There is no automatic attack just
because another player is hostile. Original intent deadlines are retained.
The global reply cooldown is 15 seconds, the same partner has 120 seconds.
Direct chat/free conversation parsing is not implemented.
Bots in social/leisure/rest can occasionally initiate a five-second encounter
with another available AdventureBot within 12 yards. The lower character GUID
initiates and reserves both participants. The peer must also have a compatible
intent. Sociability weights initiation. Pair cooldown is five minutes; native
animation replies never feed back into incoming text-emote events. Occasional
localized 'Safe travels' accompanies greetings. This is a short greeting, not a
multi-line conversation or synchronized shared activity.
Real positive damage by another player or their pet against the bot's active
combat target can queue a thank-you. Damage attribution is captured after damage
rejection/duel clamping and before lethal cleanup; no threat-only inference.
The thank-you is deferred until combat and the existing self-care finish, and
requires the helper still to be nearby and friendly. Dummy training is excluded.
## Invitations and group behavior
An actual native invitation is answered after 2–4 seconds in a safe state.
Acceptance probability clamps sociability plus an intent adjustment to [0,1]:
rest −0.50, profession/economy −0.25, adventure/instance +0.20, other +0.10.
Inviter must be alive and friendly. Bots do not accept invitations from other
clientless players yet; autonomous bot grouping is explicitly deferred.
Invitations are declined inside dungeons. Normal faction/group restrictions run
before the invitation reaches the bot; none are bypassed.
Membership suspends autonomous intent execution, clears its queued actions and
retains its deadline. Native membership survives server restart. Bots follow the
current leader with checked local MMAP paths, aiming about three yards behind
and stopping within five yards. Longer/cross-map routes use existing
travel_to_node actions toward a nearby TravelGraph node at the leader's location;
no new teleport shortcut is introduced. Coverage of the graph and native portal
rules determines whether these routes work. An active graph trip is allowed to
finish before local follow resumes. Transport/flight states are not interrupted.
Unreachable destinations are retried conservatively; an active leader does not
cause a timeout merely because the route is missing.
Nearby (40 yards) group members' actual combat targets or attackers can be
supported with the existing damage executor. Self-defense retains priority and
the first actual damager focus. Assist requires native attack permission, valid
visibility/phase, a verified path, at least 30% HP, and excludes world bosses and
creatures more than two levels above the bot. Other classes currently retain
basic autoattacks; class-specific support roles remain future work.
## Leaving
Never voluntarily leave while dead, fighting, in a dungeon/raid, on a transport,
or in flight. Pending conditions are reevaluated when safe:
- A changed scheduled intent/location permits leaving. Personality
`group_loyalty` is the chance of staying for that new block (default 0.25,
Arthoran 0.35). The choice is made once per observed change, not each tick.
A new date generates/loads the native daily plan even while grouped.
For manual intents, expiry of the original deadline also changes this token.
- The leader being explicitly AFK for ten minutes, or ten minutes of observed
inactivity, permits leaving. Position/orientation, selection, combat,
casting, native chat, text emotes, spell and item requests count as activity.
Sampling cannot detect every conceivable stationary action. Native AFK takes
precedence while set. A new leader starts a fresh observation window; returning
from AFK ends the explicit AFK window. Observation clocks restart after a
server restart, so a restart does not immediately evict a bot.
Before voluntarily leaving, a localized farewell is sent to the native party
chat. Native RemoveMember performs removal. Removal/disbanding by other players
also resumes autonomous scheduling at the current time, retaining deadlines.
## Diagnostics and verification
`clientless interaction 26` reports membership, leader, partner, intent,
schedule observation, idle/AFK seconds, sociability and loyalty. No state changes.
`tests/clientless/test_adventure_interactions.py` compiles and executes production
manager code with deterministic native-world adapters: invitation acceptance and
rejection, group following/path rejection, leader changes, 599/600-second
boundaries, activity, deferred exits, directed group emotes, hostile replies,
pair reservation, cooldowns, graph handoff and deferred thanks.
`test_adventure_combat_reactions.py` also covers player target permission and
player damage notices while preserving corpse recovery regression checks.
These tests do not replace live client acceptance. Multi-client player invites,
real PvP/pet defense, cross-map/instance group following, ten real minutes of AFK,
and two simultaneously online AdventureBots require separate gameplay checks.
+81
View File
@@ -0,0 +1,81 @@
# Selbstversorgung und Nebenberufe
AdventureBots erhalten bei Tagesbeginn und nach einem tatsächlich beendeten
Kampf passende Vorräte. Es werden vorhandene Bestände bis zu einem nativen
vollen Stack ergänzt, keine zusätzlichen Stacks angesammelt. Fünf Rollen:
Verbände, Heiltränke, Manatränke, Essen und Trinken. Die erlaubte kostenfreie
Beschaffung verwendet die native Gegenstandserzeugung und Inventarverwaltung.
Ein persistenter Tagesstempel verhindert erneute Tagesausgaben bei jedem Login.
Bei sehr niedrigen Stufen gibt es noch keinen nutzbaren Manatrank; andere
Vorräte werden trotzdem aufgefüllt. Bei Platzmangel wird später erneut versucht,
ohne vorhandene Gegenstände zu verwerfen.
Vier Glutseidenstofftaschen (Item 54443, je 22 Plätze) ergänzen leere Taschenslots.
Kleinere Taschen werden über den nativen Taschentausch mit Inhaltserhaltung
ersetzt. Bereits mindestens gleich große Taschen bleiben erhalten. Alte Taschen
und sonstige Gegenstände werden nicht gelöscht.
Alle AdventureBots erhalten die stufengerecht verfügbaren Ränge von Erste Hilfe,
Kochen, Angeln und Archäologie. Lehrzauber und Mindeststufen stammen aus den
nativen Trainerdaten, Skillstufen und Maxima aus der nativen Lernlogik. Fertigkeiten
werden innerhalb dieser Grenzen aufgefüllt. Das vergibt keine Hauptberufe und
ändert deren bestehende Profilfreigaben nicht. Zusätzliche Rezepte werden nur
insoweit gelernt, wie es die native automatische Fertigkeitsbelohnung vorsieht.
Die Benutzung bleibt echt: native Item-Zauberanfragen mit tatsächlicher Tasche,
Slot und Item-GUID, Selbstziel, Benutzungsvoraussetzungen, Abklingzeiten,
Verbandssperre und Gegenstandsverbrauch. Keine direkten HP-/Mana-Zuweisungen
im Versorgungsablauf, kein Zurücksetzen von Cooldowns. Ein gesondert markierter
Diagnosebefehl kann für Tests einen HP-Mangel herstellen.
## Heilungsreihenfolge
- Unter 10 Prozent fehlenden HP beginnt keine Selbstheilung.
- Außerhalb des Kampfes zuerst einen tatsächlich nutzbaren Verband versuchen.
Eine laufende andere Zauberaktion oder globale Abklingzeit zählt nicht bereits
als fehlgeschlagener Verbandversuch.
- Nach dem Verband nur bei weiterhin mindestens 10 Prozent fehlenden HP essen.
Ist kein Verband erlaubt, direkt Essen versuchen.
- Eine bereits begonnene Mahlzeit darf bis zur vollständigen Auffrischung laufen.
- Bei Mananutzern und mindestens 10 Prozent fehlendem Mana trinken; Krieger
benutzen weder Wasser noch Manatränke.
- Im Kampf nur native Heil-/Manatränke verwenden, bei mindestens 10 Prozent
fehlender passender Ressource. HP hat bei gleichzeitigem Mangel Vorrang.
Gemeinsame native Trank-Cooldowns und Kampfbegrenzungen bleiben wirksam.
- Eingehender Kampf unterbricht eine eigene Erholung. Selbstverteidigung, Tod
und Transport erhalten Vorrang. Der ursprüngliche Intent wird danach mit
unveränderter Endzeit fortgesetzt.
## Daten und Diagnose
Migrationen:
`world/2026_10_27_10_adventure_supplies.sql` und
`characters/2026_10_27_01_adventure_supplies.sql` unter `sql/custom/`.
Der editierbare Katalog enthält ausdrückliche Rollen und Stufenschwellen;
native Item-Voraussetzungen bleiben maßgeblich. Bei Verbänden wird der beste
bereits nutzbare Rang bevorzugt. Wiederholte Migrationen erhalten Änderungen.
`clientless combat supplies <guid>` zeigt Fertigkeiten, ausgewählte Vorräte,
Bestände und Taschen. `clientless combat testrecovery <guid> <fehlende-Prozent>`
erzeugt ausschließlich bei einem lebenden, kampffreien verwalteten Bot eine
markierte Testverletzung (1–80 Prozent). Das ersetzt keinen echten Schadensfall.
Automatisierte Prüfung: 55 Testskripte erfolgreich, einschließlich produktiver
Erholungssteuerung, exakter Zehn-Prozent-Grenze, nutzbarer Verbandränge, nativer
Item-Anfragen, Tagesstempel und stufengerechter Nebenberufsgrenzen.
Live erfolgreich geprüft: vier Glutseidenstofftaschen, Erste Hilfe375 und
schwere Netherstoffverbände, alle vier Nebenberufe, erhaltene Altgegenstände und
Geld0. Bei8%Testdefizit kein Verbrauch; bei80% zuerst ein tatsächlich verbrauchter
Verband. Anschließend weniger als10%HP fehlend, daher kein Essen. Bei erneuter
Testverletzung mit nativer Verbandssperre echtes Essen bis5249/5249HP.
Gleicher Tagesstempel beim erneuten Login: keine neue Ausgabe. Echter Bärenkampf
mit markiertem Anfangsdefizit: Heiltrank im Kampf verbraucht, danach fehlende
Vorräte exakt bis20 ergänzt. Die Anfangsverletzungen waren Diagnosefixtures;
Fernanreise und Manaverbrauch wurden damit nicht live geprüft. Belege unter
`checkpoints/2026-10-27-supplies/`.
Die native Abfrage `Player::GetPureMaxSkillValue` hatte eine invertierte Prüfung
auf vorhandene Fertigkeitsränge und lieferte bei gelernten Fertigkeiten null.
Die Prüfung ist korrigiert; ein eigener nativer Regressionstest sichert reguläre
Werte sowie fehlende, gelöschte und leere Fertigkeiten ab. Nebenberufsränge mit
EffectLearnSkill44 und normalen SpellMgr-Skillknoten werden beide berücksichtigt.
@@ -0,0 +1,6 @@
-- Last completed daily top-up survives logout and server restart.
CREATE TABLE IF NOT EXISTS vcore_adventure_supply_state (
character_guid INT UNSIGNED NOT NULL,
last_day INT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY(character_guid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -0,0 +1,16 @@
-- Generated ownership/profile metadata is committed with the native character.
-- Never infer ownership from account/name/level when retiring a bot.
CREATE TABLE IF NOT EXISTS vcore_adventure_generated_bot (
character_guid INT UNSIGNED NOT NULL,
account_id INT UNSIGNED NOT NULL,
archetype VARCHAR(32) NOT NULL,
seed INT UNSIGNED NOT NULL,
persistent TINYINT UNSIGNED NOT NULL DEFAULT 1,
state VARCHAR(16) NOT NULL DEFAULT 'ready',
profile_lua MEDIUMTEXT NOT NULL,
created_at BIGINT UNSIGNED NOT NULL,
retired_at BIGINT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (character_guid),
KEY account_state (account_id,state),
KEY rotation_pool (persistent,state)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -0,0 +1,26 @@
-- Explicit roles and level tiers; actual native requirements, stack limits and
-- item-use checks remain authoritative. Secondary ranks follow native trainers.
CREATE TABLE IF NOT EXISTS vcore_adventure_supply (
role VARCHAR(24) NOT NULL,
item_id INT UNSIGNED NOT NULL,
min_level TINYINT UNSIGNED NOT NULL DEFAULT 1,
enabled TINYINT UNSIGNED NOT NULL DEFAULT 1,
PRIMARY KEY(role,item_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT IGNORE INTO vcore_adventure_supply(role,item_id,min_level) VALUES
('bandage',1251,1),('bandage',2581,5),('bandage',3530,10),('bandage',3531,15),
('bandage',6450,20),('bandage',6451,25),('bandage',8544,30),('bandage',8545,35),
('bandage',14529,40),('bandage',14530,45),('bandage',21990,55),('bandage',21991,60),
('bandage',34721,70),('bandage',34722,75),('bandage',53049,80),('bandage',53051,85),
('healing_potion',118,1),('healing_potion',858,3),('healing_potion',929,12),
('healing_potion',1710,21),('healing_potion',3928,35),('healing_potion',13446,45),
('healing_potion',22829,55),('healing_potion',33447,70),('healing_potion',57191,80),
('mana_potion',2455,5),('mana_potion',3385,14),('mana_potion',3827,22),
('mana_potion',6149,31),('mana_potion',13443,41),('mana_potion',13444,49),
('mana_potion',22832,55),('mana_potion',33448,70),('mana_potion',57192,80),
('food',117,1),('food',2287,5),('food',3770,15),('food',3771,25),
('food',4599,35),('food',8952,45),('food',27854,55),('food',33454,65),
('food',35953,75),('food',58260,80),
('drink',159,1),('drink',1179,5),('drink',1205,15),('drink',1708,25),
('drink',1645,35),('drink',8766,45),('drink',28399,60),('drink',33444,65),
('drink',33445,75),('drink',58256,80);
+2 -1
View File
@@ -75,10 +75,11 @@ namespace Trainer
Type GetTrainerType() const { return _type; }
uint32 GetTrainerRequirement() const { return _requirement; }
bool IsTrainerValidForPlayer(Player const* player) const;
// Read-only eligibility, shared by native teaching and character provisioning.
bool CanTeachSpell(Player const* player, Spell const* trainerSpell) const;
private:
Spell const* GetSpell(uint32 spellId) const;
bool CanTeachSpell(Player const* player, Spell const* trainerSpell) const;
SpellState GetSpellState(Player const* player, Spell const* trainerSpell) const;
void SendTeachFailure(Creature const* npc, Player const* player, uint32 spellId, FailReason reason) const;
void SendTeachSucceeded(Creature const* npc, Player const* player, uint32 spellId) const;
+1 -1
View File
@@ -5700,7 +5700,7 @@ uint16 Player::GetPureMaxSkillValue(uint32 skill) const
return 0;
SkillStatusMap::const_iterator itr = mSkillStatus.find(skill);
if (itr == mSkillStatus.end() || itr->second.uState == SKILL_DELETED || GetSkillRank(itr->second.pos))
if (itr == mSkillStatus.end() || itr->second.uState == SKILL_DELETED || !GetSkillRank(itr->second.pos))
return 0;
uint16 field = itr->second.pos / 2;
+3
View File
@@ -890,6 +890,9 @@ bool Unit::HasBreakableByDamageCrowdControlAura(Unit* excludeCasterChannel) cons
victim->ToCreature()->LowerPlayerDamageReq(health < damage ? health : damage);
}
if (damage)
sClientlessPlayerMgr->SharedTargetDamage(victim, attacker, std::min(health, damage));
if (health <= damage)
{
if (victim->GetTypeId() == TYPEID_PLAYER && victim != attacker)
+7
View File
@@ -16,6 +16,7 @@
*/
#include "WorldSession.h"
#include "ClientlessPlayerMgr.h"
#include <cstdlib>
#include "AccountMgr.h"
@@ -104,6 +105,7 @@ void WorldSession::HandleMessagechatOpcode(WorldPacket& recvData)
}
Player* sender = GetPlayer();
sClientlessPlayerMgr->MarkActivity(sender);
//TC_LOG_DEBUG("misc", "CHAT: packet received. type %u, lang %u", type, lang);
@@ -569,6 +571,7 @@ void WorldSession::HandleMessagechatOpcode(WorldPacket& recvData)
void WorldSession::HandleAddonMessagechatOpcode(WorldPacket& recvData)
{
Player* sender = GetPlayer();
sClientlessPlayerMgr->MarkActivity(sender);
ChatMsg type;
switch (recvData.GetOpcode())
@@ -808,6 +811,7 @@ void WorldSession::HandleSendTextEmoteOpcode(WorldPackets::Chat::SendTextEmote&
return;
}
sClientlessPlayerMgr->MarkActivity(GetPlayer());
sScriptMgr->OnPlayerTextEmote(GetPlayer(), packet.SoundIndex, packet.EmoteID, packet.Target);
EmotesTextEntry const* em = sEmotesTextStore.LookupEntry(packet.EmoteID);
@@ -848,6 +852,9 @@ void WorldSession::HandleSendTextEmoteOpcode(WorldPackets::Chat::SendTextEmote&
GetPlayer()->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_DO_EMOTE, packet.EmoteID, 0, 0, unit);
if (unit && unit->ToPlayer())
sClientlessPlayerMgr->TextEmote(_player, unit->ToPlayer(), packet.EmoteID);
//Send scripted event call
if (unit && unit->GetTypeId() == TYPEID_UNIT && ((Creature*)unit)->AI())
((Creature*)unit)->AI()->ReceiveEmote(GetPlayer(), packet.EmoteID);
@@ -16,6 +16,7 @@
*/
#include "WorldSession.h"
#include "ClientlessPlayerMgr.h"
#include "Archaeology.h"
#include "Common.h"
#include "Config.h"
@@ -93,6 +94,7 @@ void WorldSession::HandleClientCastFlags(WorldPacket& recvPacket, uint8 castFlag
void WorldSession::HandleUseItemOpcode(WorldPackets::Spells::UseItem& packet)
{
sClientlessPlayerMgr->MarkActivity(GetPlayer());
Player* user = _player;
// ignore for remote control state
@@ -265,6 +267,7 @@ void WorldSession::HandleGameobjectReportUse(WorldPacket& recvPacket)
void WorldSession::HandleCastSpellOpcode(WorldPackets::Spells::CastSpell& cast)
{
sClientlessPlayerMgr->MarkActivity(GetPlayer());
// ignore for remote control state (for player case)
Unit* mover = GetGameClient()->GetActivelyMovedUnit();
if (!mover || (mover != _player && mover->IsPlayer()))
@@ -1763,3 +1763,27 @@ std::string AdventureBehaviorMgr::CombatIntent(Player* player) const
(it->second.State!=Phase::Travel && it->second.State!=Phase::OnSite)) return {};
return it->second.Intent;
}
std::string AdventureBehaviorMgr::InteractionIntent(Player* player) const
{
auto it=_records.find(player->GetGUID().GetCounter());
return it==_records.end()?std::string():it->second.Intent;
}
std::string AdventureBehaviorMgr::InteractionSchedule(Player* player)
{
auto it=_records.find(player->GetGUID().GetCounter());
if(it==_records.end())return {};
auto& r=it->second;
auto local=Local(GameTime::GetGameTime());
if(r.Schedule && r.Date!=DateOf(local)){Generate(player,r,DateOf(local));r.Hour=-1;}
if(!r.Schedule)return (r.Deadline && GameTime::GetGameTime()>=r.Deadline?"expired:":"")+r.Intent+":"+r.Location;
auto const& slot=r.Plan[Local(GameTime::GetGameTime()).tm_hour];
return slot.Intent+":"+slot.Location;
}
void AdventureBehaviorMgr::InterruptForInteraction(Player* player)
{
InterruptForCombat(player);
auto it=_records.find(player->GetGUID().GetCounter());
if(it!=_records.end())it->second.Detail="social/group interaction; original deadline retained";
}
@@ -30,7 +30,10 @@ public:
void Update(Player* player, uint32 diff);
bool Command(ChatHandler* handler, char const* args);
void InterruptForCombat(Player* player);
void InterruptForInteraction(Player* player);
std::string CombatIntent(Player* player) const;
std::string InteractionIntent(Player* player) const;
std::string InteractionSchedule(Player* player);
private:
enum class Phase {Idle, Travel, OnSite, Switching, Handover};
struct RecoveryScene
+40 -13
View File
@@ -1,4 +1,5 @@
#include "AdventureCombatMgr.h"
#include "AdventureInteractionMgr.h"
#include "AdventureCombatPolicy.h"
#include "AdventureBehaviorMgr.h"
#include "ClientlessLuaEngine.h"
@@ -30,7 +31,7 @@ class AdventureCombatMgr::Fighter : public PlayerAI
public:
explicit Fighter(Player* player):PlayerAI(player) { }
void UpdateAI(uint32) override { }
void Tick(Creature* target,uint32 diff)
void Tick(Unit* target,uint32 diff)
{
_rend=_rend>diff?_rend-diff:0;_filler=_filler>diff?_filler-diff:0;
me->SetFacingToObject(target);
@@ -59,13 +60,16 @@ private:
uint32 _rend=0,_filler=0;
};
AdventureCombatMgr* AdventureCombatMgr::instance(){static AdventureCombatMgr mgr;return &mgr;}
bool AdventureCombatMgr::Valid(Player* p,Creature* c,bool training) const
bool AdventureCombatMgr::Valid(Player* p,Unit* c,bool training) const
{
return c && c->IsAlive() && c->IsInWorld() && p->IsInPhase(c) && p->CanSeeOrDetect(c) &&
p->IsValidAttackTarget(c) && !c->GetControllingPlayer() && !c->IsEvadingAttacks() &&
(training?(c->GetScriptName()=="npc_training_dummy" && c->getLevel()<=p->getLevel()+2 && !c->isWorldBoss()):c->GetScriptName()!="npc_training_dummy");
if(!c || !c->IsAlive() || !c->IsInWorld() || !p->IsInPhase(c) ||
!p->CanSeeOrDetect(c) || !p->IsValidAttackTarget(c))return false;
if(auto creature=c->ToCreature())
return !creature->IsEvadingAttacks() && (!training || !creature->GetControllingPlayer()) &&
(training?(creature->GetScriptName()=="npc_training_dummy" && creature->getLevel()<=p->getLevel()+2 && !creature->isWorldBoss()):creature->GetScriptName()!="npc_training_dummy");
return !training && c->ToPlayer(); // Native PvP/sanctuary/duel checks above.
}
bool AdventureCombatMgr::Reachable(Player* p,Creature* c) const
bool AdventureCombatMgr::Reachable(Player* p,Unit* c) const
{
if(!p->IsWithinLOSInMap(c))return false;
if(p->IsWithinMeleeRange(c))return true;
@@ -75,7 +79,7 @@ bool AdventureCombatMgr::Reachable(Player* p,Creature* c) const
}
bool AdventureCombatMgr::Safe(Player* p,Creature* c) const
{
if(!Valid(p,c,false) || p->GetExactDist(c)>40 || (c->GetVictim() && c->GetVictim()!=p))return false;
if(!Valid(p,c,false) || c->GetControllingPlayer() || p->GetExactDist(c)>40 || (c->GetVictim() && c->GetVictim()!=p))return false;
std::vector<Creature*> others;p->GetCreatureListWithEntryInGrid(others,0,35);
unsigned hostiles=0;
for(auto other:others)if(other->IsAlive() && p->IsInPhase(other) && p->IsHostileTo(other) &&
@@ -95,7 +99,7 @@ Creature* AdventureCombatMgr::Dummy(Player* p) const
return best;
}
bool AdventureCombatMgr::TrainingAvailable(Player* p) const{return Dummy(p)!=nullptr;}
bool AdventureCombatMgr::Start(Player* p,Record& r,Creature* c,bool training,bool defending,time_t until)
bool AdventureCombatMgr::Start(Player* p,Record& r,Unit* c,bool training,bool defending,time_t until)
{
if(!Valid(p,c,training) || !p->IsAlive() || p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsCharmed())return false;
sAdventureBehaviorMgr->InterruptForCombat(p);
@@ -122,8 +126,8 @@ bool AdventureCombatMgr::Attack(Player* p,uint32 spawn)
}
void AdventureCombatMgr::DamageTaken(Player* p,Unit* attacker,uint32 damage)
{
auto c=attacker?attacker->ToCreature():nullptr;
if(!damage || !p->IsAlive() || !c || c->GetControllingPlayer())return;
auto c=attacker;
if(!damage || !p->IsAlive() || !c)return;
auto& r=_records[p->GetGUID().GetCounter()];
if(std::find(r.Damagers.begin(),r.Damagers.end(),c->GetGUID())==r.Damagers.end())r.Damagers.push_back(c->GetGUID());
}
@@ -131,6 +135,8 @@ void AdventureCombatMgr::Finish(Player* p,Record& r)
{
TC_LOG_INFO("server.adventurebot","COMBAT_END guid=%u target='%s' training=%u",p->GetGUID().GetCounter(),r.Target.ToString().c_str(),r.Training);
bool training=r.Training;
if(!training && r.Helper)sAdventureInteractionMgr->CombatHelp(p,r.Helper);
r.Helper.Clear();
p->AttackStop();p->CastStop();p->StopMoving();p->GetMotionMaster()->Clear();p->SetSelection(ObjectGuid::Empty);
sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());
if(training && std::all_of(p->getAttackers().begin(),p->getAttackers().end(),[](Unit* u)
@@ -253,15 +259,15 @@ bool AdventureCombatMgr::Update(Player* p,uint32 diff)
// target. Keep all subsequent damagers ordered for after that focus dies.
if(!r.Defending)while(!r.Damagers.empty())
{
auto id=r.Damagers.front();r.Damagers.pop_front();auto c=ObjectAccessor::GetCreature(*p,id);
auto id=r.Damagers.front();r.Damagers.pop_front();auto c=ObjectAccessor::GetUnit(*p,id);
if(!Valid(p,c,false))continue;
if(r.AI)Finish(p,r);
Start(p,r,c,false,true,0);break;
}
if(!r.AI && !p->IsInCombat() && !r.Cooldown)
if(!r.AI && !p->GetGroup() && !p->IsInCombat() && !r.Cooldown)
if(auto c=Proactive(p))Start(p,r,c,false,false,0);
if(!r.AI)return false;
auto c=ObjectAccessor::GetCreature(*p,r.Target);
auto c=ObjectAccessor::GetUnit(*p,r.Target);
if(!Valid(p,c,r.Training) || (r.Until && GameTime::GetGameTime()>=r.Until) || p->GetExactDist(c)>80)
{Finish(p,r);return true;}
if(!Reachable(p,c))
@@ -285,3 +291,24 @@ bool AdventureCombatMgr::Update(Player* p,uint32 diff)
p->StopMoving();r.AI->Tick(c,500);
return true;
}
bool AdventureCombatMgr::Busy(Player* player) const
{
auto it=_records.find(player->GetGUID().GetCounter());
return it!=_records.end() && (it->second.AI || it->second.Dead || !it->second.Damagers.empty());
}
bool AdventureCombatMgr::Assist(Player* p,Unit* target)
{
if(!p->GetGroup() || Busy(p) || !Valid(p,target,false) || p->GetExactDist(target)>40 ||
!Reachable(p,target) || p->GetHealthPct()<30)return false;
if(auto c=target->ToCreature())
if(c->isWorldBoss() || c->getLevel()>p->getLevel()+2)return false;
return Start(p,_records[p->GetGUID().GetCounter()],target,false,false,0);
}
void AdventureCombatMgr::SharedTargetDamage(Player* p,ObjectGuid helper,ObjectGuid target)
{
auto it=_records.find(p->GetGUID().GetCounter());
if(it!=_records.end() && it->second.Target==target && !it->second.Training)it->second.Helper=helper;
}
+7 -4
View File
@@ -22,22 +22,25 @@ public:
bool Train(Player* player,uint32 durationMs,time_t deadline=0);
bool Attack(Player* player,uint32 creatureSpawnId);
void Stop(Player* player);
bool Assist(Player* player,Unit* target);
void SharedTargetDamage(Player* player,ObjectGuid helper,ObjectGuid target);
bool Busy(Player* player) const;
private:
class Fighter;
struct Record
{
std::shared_ptr<Fighter> AI;
ObjectGuid Target;
ObjectGuid Target,Helper;
std::deque<ObjectGuid> Damagers;
bool Training=false,Defending=false,Suspended=false,Dead=false,CemeteryPending=false;
time_t Until=0,NextCorpseSearch=0;
std::vector<Position> FailedCorpsePoints;
uint32 Timer=0,UnreachableMs=0,Cooldown=0;
};
bool Start(Player* player,Record& record,Creature* target,bool training,bool defending,time_t until);
bool Valid(Player* player,Creature* target,bool training) const;
bool Start(Player* player,Record& record,Unit* target,bool training,bool defending,time_t until);
bool Valid(Player* player,Unit* target,bool training) const;
bool Safe(Player* player,Creature* target) const;
bool Reachable(Player* player,Creature* target) const;
bool Reachable(Player* player,Unit* target) const;
Creature* Dummy(Player* player) const;
Creature* Proactive(Player* player) const;
bool Death(Player* player,Record& record);
@@ -0,0 +1,416 @@
#include "AdventureGeneratorMgr.h"
#include "AdventureCombatPreparationPolicy.h"
#include "AdventureBehaviorMgr.h"
#include "AdventureLocationMgr.h"
#include "AdventureProfessionPolicy.h"
#include "AdventureSupplyMgr.h"
#include "CharacterCache.h"
#include "Chat.h"
#include "ClientlessCharacterFactory.h"
#include "ClientlessLuaEngine.h"
#include "ClientlessPlayerMgr.h"
#include "DBCStores.h"
#include "GameTime.h"
#include "Item.h"
#include "ItemTemplate.h"
#include "Log.h"
#include "MapManager.h"
#include "ObjectMgr.h"
#include "Player.h"
#include "SpellMgr.h"
#include "Stats.h"
#include "Trainer.h"
#include "World.h"
#include "WorldSession.h"
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <filesystem>
#include <cctype>
#include <memory>
#include <random>
#include <sstream>
#include <tuple>
#include <vector>
namespace
{
using AdventureGeneratorPolicy::Archetype;
bool Schema()
{return bool(CharacterDatabase.Query("SHOW TABLES LIKE 'vcore_adventure_generated_bot'"));}
uint32 LearnedSkill(uint32 id,uint32& step)
{
if(auto rank=sSpellMgr->GetSpellLearnSkill(id))
if(rank->step){step=rank->step;return rank->skill;}
if(auto info=sSpellMgr->GetSpellInfo(id))for(auto const& effect:info->Effects)
if(effect.Effect==SPELL_EFFECT_SKILL_STEP && effect.CalcValue()>0)
{step=effect.CalcValue();return effect.MiscValue;}
return 0;
}
bool Learnable(Player* p,uint32 id)
{
auto info=sSpellMgr->GetSpellInfo(id);
return info && info->SpellLevel<=p->getLevel() && info->BaseLevel<=p->getLevel() &&
p->IsSpellFitByClassAndRace(id) && SpellMgr::IsSpellValid(info,p,false);
}
std::vector<uint32> Taught(uint32 id)
{
std::vector<uint32> result;
if(auto info=sSpellMgr->GetSpellInfo(id))
{
for(auto const& effect:info->Effects)
if(effect.Effect==SPELL_EFFECT_LEARN_SPELL && effect.TriggerSpell)result.push_back(effect.TriggerSpell);
if(result.empty())result.push_back(id);
}
return result;
}
void ClassTraining(Player* p)
{
if(auto rows=WorldDatabase.Query("SELECT Id FROM trainer WHERE Type=0 ORDER BY Id"))do
{
auto trainer=sObjectMgr->GetTrainer(rows->Fetch()[0].GetUInt32());
if(!trainer || !trainer->IsTrainerValidForPlayer(p))continue;
// Repeat for prerequisite chains without granting arbitrary class-
// family NPC spells or abilities from a different specialization.
for(unsigned pass=0;pass<8;++pass)for(auto const& spell:trainer->GetSpells())
{
if(!trainer->CanTeachSpell(p,&spell))continue;
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);
auto rc=sDBCManager.GetSkillRaceClassInfo(ability->SkillLine,p->getRace(),p->getClass());
if(!skill || !rc || rc->MinLevel>p->getLevel() ||
(skill->CategoryID!=SKILL_CATEGORY_ARMOR && skill->CategoryID!=SKILL_CATEGORY_WEAPON))continue;
if(!p->HasSpell(ability->Spell) && Learnable(p,ability->Spell))p->LearnSpell(ability->Spell,false);
}
p->UpdateSkillsForLevel();
for(auto skill:sSkillLineStore)
if(skill->CategoryID==SKILL_CATEGORY_WEAPON && p->HasSkill(skill->ID))
if(auto maximum=p->GetPureMaxSkillValue(skill->ID))
p->SetSkill(skill->ID,p->GetSkillStep(skill->ID),maximum,maximum);
}
bool Talents(Player* p,Archetype const& a)
{
if(p->getLevel()<10)return true;
p->InitTalentForLevel();
if(!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();});
// Native LearnTalent enforces tier/dependency/rank and 31-point gates.
// The named solo spec is preferred; secondary trees only use leftovers.
for(unsigned tree=0;tree<MAX_TALENT_TABS && p->GetFreeTalentPoints();++tree)
{
uint32 tab=tabs[(a.Spec+tree)%MAX_TALENT_TABS];std::vector<TalentEntry const*> talents;
for(auto entry:sTalentStore)if(entry->TabID==tab)talents.push_back(entry);
std::sort(talents.begin(),talents.end(),[](TalentEntry const* x,TalentEntry const* y){return std::tie(x->TierID,x->ColumnIndex,x->ID)<std::tie(y->TierID,y->ColumnIndex,y->ID);});
for(unsigned pass=0;pass<50 && p->GetFreeTalentPoints();++pass)
{
bool progress=false;
for(auto talent:talents)for(unsigned rank=0;rank<MAX_TALENT_RANK && p->GetFreeTalentPoints();++rank)
if(talent->SpellRank[rank] && p->LearnTalent(talent->ID,rank))progress=true;
if(!progress)break;
}
}
return !p->GetFreeTalentPoints();
}
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
{
auto f=rows->Fetch();
for(auto id:Taught(f[0].GetUInt32()))
{
uint32 step=0;if(LearnedSkill(id,step)!=wanted || !step)continue;
auto info=sSpellMgr->GetSpellInfo(id);if(!info)continue;
ranks.push_back({id,step,std::max<uint32>({f[1].GetUInt8(),info->SpellLevel,info->BaseLevel}),f[2].GetUInt32()});
}
}while(rows->NextRow());
std::sort(ranks.begin(),ranks.end(),[](Rank const& a,Rank const& b){return std::tie(a.Step,a.Level,a.Id)<std::tie(b.Step,b.Level,b.Id);});
for(auto const& rank:ranks)
{
if(rank.Level>p->getLevel() || rank.Required>p->GetPureSkillValue(wanted) || !Learnable(p,rank.Id))continue;
if(!p->HasSpell(rank.Id))p->LearnSpell(rank.Id,false);
if(!p->IsInWorld() && !ClientlessCharacterFactory::PrepareSkillRank(p,wanted,rank.Step))continue;
auto maximum=p->GetPureMaxSkillValue(wanted);
if(maximum)p->SetSkill(wanted,p->GetSkillStep(wanted),maximum,maximum);
}
return p->HasSkill(wanted) && p->GetPureSkillValue(wanted)>0;
}
bool UsefulSpell(uint32 id,Archetype const& a,unsigned depth=0)
{
if(depth>3)return true;
auto spell=sSpellMgr->GetSpellInfo(id);if(!spell)return true;
bool caster=a.Attribute==ITEM_MOD_INTELLECT;
for(auto const& effect:spell->Effects)
{
bool manaAura=effect.ApplyAuraName==SPELL_AURA_PERIODIC_ENERGIZE || effect.ApplyAuraName==SPELL_AURA_MOD_POWER_REGEN ||
effect.ApplyAuraName==SPELL_AURA_MOD_POWER_REGEN_PERCENT || effect.ApplyAuraName==SPELL_AURA_MOD_INCREASE_ENERGY || effect.ApplyAuraName==SPELL_AURA_MOD_INCREASE_ENERGY_PERCENT;
if(!caster && effect.MiscValue==POWER_MANA && (manaAura || effect.Effect==SPELL_EFFECT_ENERGIZE || effect.Effect==SPELL_EFFECT_ENERGIZE_PCT))return false;
if(!caster && (effect.ApplyAuraName==SPELL_AURA_MOD_SPELL_HIT_CHANCE || effect.ApplyAuraName==SPELL_AURA_MOD_SPELL_CRIT_CHANCE ||
effect.ApplyAuraName==SPELL_AURA_MOD_SPELL_CRIT_CHANCE_SCHOOL || effect.ApplyAuraName==SPELL_AURA_MOD_HEALING_DONE ||
(effect.ApplyAuraName==SPELL_AURA_MOD_DAMAGE_DONE && !(effect.MiscValue&1))))return false;
if(caster && (effect.ApplyAuraName==SPELL_AURA_MOD_ATTACK_POWER || effect.ApplyAuraName==SPELL_AURA_MOD_RANGED_ATTACK_POWER ||
(effect.ApplyAuraName==SPELL_AURA_MOD_DAMAGE_DONE && !(effect.MiscValue&126))))return false;
if(effect.ApplyAuraName==SPELL_AURA_MOD_STAT &&
((!caster && effect.MiscValue==int(StatType::Intellect)) || (caster && (effect.MiscValue==int(StatType::Strength) || effect.MiscValue==int(StatType::Agility)))))return false;
if(effect.TriggerSpell && !UsefulSpell(effect.TriggerSpell,a,depth+1))return false;
}
return true;
}
bool Slot(ItemTemplate const* item,unsigned slot,Archetype const& a)
{
unsigned type=item->GetInventoryType();
switch(slot)
{
case EQUIPMENT_SLOT_HEAD:return type==INVTYPE_HEAD;
case EQUIPMENT_SLOT_NECK:return type==INVTYPE_NECK;
case EQUIPMENT_SLOT_SHOULDERS:return type==INVTYPE_SHOULDERS;
case EQUIPMENT_SLOT_CHEST:return type==INVTYPE_CHEST || type==INVTYPE_ROBE;
case EQUIPMENT_SLOT_WAIST:return type==INVTYPE_WAIST;
case EQUIPMENT_SLOT_LEGS:return type==INVTYPE_LEGS;
case EQUIPMENT_SLOT_FEET:return type==INVTYPE_FEET;
case EQUIPMENT_SLOT_WRISTS:return type==INVTYPE_WRISTS;
case EQUIPMENT_SLOT_HANDS:return type==INVTYPE_HANDS;
case EQUIPMENT_SLOT_FINGER1:case EQUIPMENT_SLOT_FINGER2:return type==INVTYPE_FINGER;
case EQUIPMENT_SLOT_TRINKET1:case EQUIPMENT_SLOT_TRINKET2:return type==INVTYPE_TRINKET;
case EQUIPMENT_SLOT_BACK:return type==INVTYPE_CLOAK;
case EQUIPMENT_SLOT_MAINHAND:
if(a.Class==CLASS_ROGUE)return type==INVTYPE_WEAPON || type==INVTYPE_WEAPONMAINHAND;
if(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_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)
{
for(unsigned slot=EQUIPMENT_SLOT_START;slot<EQUIPMENT_SLOT_END;++slot)
{
if(slot==EQUIPMENT_SLOT_BODY || slot==EQUIPMENT_SLOT_TABARD ||
(slot==EQUIPMENT_SLOT_OFFHAND && a.Class!=CLASS_ROGUE) ||
(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});
}
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())
{
auto end=candidates.begin();
while(end!=candidates.end() && end-candidates.begin()<8 && end->Score>=candidates.front().Score*0.9f)++end;
std::shuffle(candidates.begin(),end,random);
}
bool equipped=false;
for(auto const& candidate:candidates)
{
auto dispose=[&](Item* item){if(item){RemoveItemFromUpdateQueueOf(item,p);delete item;}};
std::unique_ptr<Item,decltype(dispose)> item(Item::CreateItem(candidate.Id,1,p),dispose);uint16 destination=0;
if(!item || p->CanEquipItem(slot,destination,item.get(),true)!=EQUIP_ERR_OK)continue;
if(auto previous=p->GetItemByPos(INVENTORY_SLOT_BAG_0,slot))
{
ItemPosCountVec storage;
if(p->CanStoreItem(NULL_BAG,NULL_SLOT,storage,previous,false)!=EQUIP_ERR_OK){error="No space to retain initial equipment.";return false;}
p->RemoveItem(INVENTORY_SLOT_BAG_0,slot,true);p->StoreItem(storage,previous,true);
}
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) ||
slot==EQUIPMENT_SLOT_CHEST || slot==EQUIPMENT_SLOT_LEGS || slot==EQUIPMENT_SLOT_FEET ||
(p->getLevel()>=30 && slot!=EQUIPMENT_SLOT_RANGED && slot!=EQUIPMENT_SLOT_OFFHAND);
if(!equipped && required){error="No legal profile-matching equipment for slot "+std::to_string(slot);return false;}
}
return true;
}
}
AdventureGeneratorMgr* AdventureGeneratorMgr::instance(){static AdventureGeneratorMgr mgr;return &mgr;}
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);
if(!row)return false;
lua=row->Fetch()[0].GetString();
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
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)
{
std::mt19937 random(seed);
struct Choice {Archetype const* Type;uint8 Race;};std::vector<Choice> choices;
for(auto const& a:AdventureGeneratorPolicy::Archetypes())
{
if(key!="any" && key!=a.Key)continue;
if(a.Class==CLASS_DEATH_KNIGHT && level<55)continue;
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;
uint8 gender=random()%2;std::string name;
for(unsigned attempt=0;attempt<1024;++attempt)
{
auto candidate=AdventureGeneratorPolicy::Name(choice.Race,gender,random);
if(ObjectMgr::CheckPlayerName(candidate,LOCALE_enUS,true)!=CHAR_NAME_SUCCESS || sObjectMgr->IsReservedName(candidate) ||
sCharacterCache->GetCharacterCacheByName(candidate))continue;
std::string lower=candidate;
std::transform(lower.begin(),lower.end(),lower.begin(),[](unsigned char ch){return char(std::tolower(ch));});
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(name.empty()){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;}
AdventureLocation const* home=nullptr;float best=1e30f;
for(auto const& location:sAdventureLocationMgr->Locations())
{
if(location.IsArea || location.Point.MapId!=info->mapId ||
(location.Faction && location.Faction!=AdventureGeneratorPolicy::Faction(choice.Race)) ||
(location.Point.Category!="tavern" && location.Point.Category!="settlement") ||
sAdventureLocationMgr->Weight(location,"social")<=0)continue;
float dx=location.Point.X-info->positionX,dy=location.Point.Y-info->positionY;
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);
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)
{
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.
return true;
},
[&](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).",
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;
}
bool AdventureGeneratorMgr::Command(ChatHandler* handler,char const* args)
{
if(!Schema()){handler->SendSysMessage("Apply the AdventureBot generator character migration first.");return false;}
std::istringstream input(args?args:"");std::string op;input>>op;
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;}
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;
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);
return created==count;
}
if(op=="list")
{
if(auto rows=CharacterDatabase.Query("SELECT g.character_guid,c.name,g.archetype,g.persistent,g.state,c.online,g.seed FROM vcore_adventure_generated_bot g JOIN characters c ON c.guid=g.character_guid ORDER BY g.character_guid LIMIT 100"))do
{
auto f=rows->Fetch();handler->PSendSysMessage("%u %s archetype=%s persistent=%u state=%s online=%u seed=%u",f[0].GetUInt32(),f[1].GetString().c_str(),f[2].GetString().c_str(),f[3].GetUInt8(),f[4].GetString().c_str(),f[5].GetUInt8(),f[6].GetUInt32());
}while(rows->NextRow());
return true;
}
uint32 guid=0;input>>guid;
auto row=CharacterDatabase.PQuery("SELECT g.account_id,g.state,g.persistent,c.online,g.archetype,g.seed FROM vcore_adventure_generated_bot g JOIN characters c ON c.guid=g.character_guid WHERE g.character_guid=%u",guid);
if(!row){handler->SendSysMessage("Not a generator-owned character; no action taken.");return false;}
auto f=row->Fetch();
if(op=="profile")
{
std::string lua;if(!Profile(guid,lua))return false;handler->SendSysMessage(lua.c_str());return true;
}
if(op=="equip" && f[1].GetString()=="ready")
{
auto session=sWorld->FindClientlessSession(guid);auto player=session?session->GetPlayer():nullptr;
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())
if(f[4].GetString()==archetype.Key && player->getClass()==archetype.Class)
{
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;}
player->UpdateMaxHealth();player->SetFullHealth();player->SetFullPower(POWER_MANA);
auto tx=CharacterDatabase.BeginTransaction();player->SaveToDB(tx);
auto saved=CharacterDatabase.AsyncCommitTransaction(tx);if(!saved.m_future.get())return false;
handler->SendSysMessage("Profile equipment refreshed through native checks; previous items retained in bags.");
TC_LOG_INFO("server.adventurebot","BOT_EQUIPMENT_REFRESH guid=%u archetype='%s'",guid,archetype.Key);
return true;
}
return false;
}
if(op=="login" && f[1].GetString()=="ready")return sClientlessPlayerMgr->StartPlayer(f[0].GetUInt32(),guid,CreateClientlessAdventureLuaController());
if(op=="logout")return sClientlessPlayerMgr->StopPlayer(guid);
if(op=="retire")
{
if(f[2].GetUInt8() || f[3].GetUInt8() || sWorld->GetClientlessPlayerState(guid)!=ClientlessPlayerState::Offline)
{handler->SendSysMessage("Retirement requires an offline temporary generator-owned character. Persistent characters are protected.");return false;}
auto tx=CharacterDatabase.BeginTransaction();tx->PAppend("UPDATE vcore_adventure_generated_bot SET state='retired',retired_at=%llu WHERE character_guid=%u AND persistent=0 AND state='ready'",static_cast<unsigned long long>(GameTime::GetGameTime()),guid);
auto result=CharacterDatabase.AsyncCommitTransaction(tx);
if(!result.m_future.get())return false;
handler->SendSysMessage("Temporary bot retired. Native character, items and profile retained; no deletion or automatic replacement.");return true;
}
handler->SendSysMessage("Usage: clientless generate create|list|profile <guid>|login <guid>|logout <guid>|equip <guid>|retire <guid>");return false;
}
@@ -0,0 +1,22 @@
#ifndef VCORE_ADVENTURE_GENERATOR_MGR_H
#define VCORE_ADVENTURE_GENERATOR_MGR_H
#include "Common.h"
#include "AdventureGeneratorPolicy.h"
#include <string>
class Player;
class ChatHandler;
class TC_GAME_API AdventureGeneratorMgr
{
public:
static AdventureGeneratorMgr* instance();
bool Command(ChatHandler* handler,char const* args);
// 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);
private:
bool Create(ChatHandler* handler,uint32 account,uint32 seed,uint8 level,unsigned faction,
std::string const& archetype,bool persistent);
bool Prepare(Player* player,AdventureGeneratorPolicy::Archetype const& archetype,std::string& error);
};
#define sAdventureGeneratorMgr AdventureGeneratorMgr::instance()
#endif
@@ -0,0 +1,105 @@
#ifndef VCORE_ADVENTURE_GENERATOR_POLICY_H
#define VCORE_ADVENTURE_GENERATOR_POLICY_H
#include <array>
#include <cstdint>
#include <random>
#include <string>
#include <iomanip>
#include <sstream>
#include "AdventureProfessionPolicy.h"
namespace AdventureGeneratorPolicy
{
struct Archetype
{
char const* Key;unsigned Class,Spec,Attribute,Armor;
std::array<unsigned,2> Skills;
char const* Vocation;
};
inline std::array<Archetype,10> const& Archetypes()
{
static std::array<Archetype,10> const values={{{"warrior",1,0,4,4,{186,164},"Wandernder Waffenschmied"},
{"paladin",2,2,4,4,{186,755},"Pilger und Edelsteinschleifer"},
{"hunter",3,1,3,3,{393,165},"Faehrtensucher und Lederhandwerker"},
{"rogue",4,1,3,2,{186,202},"Kundschafter und Tueftler"},
{"priest",5,2,5,1,{182,773},"Reisender Chronist"},
{"death_knight",6,2,4,4,{186,164},"Schweigsamer Wanderschmied"},
{"shaman",7,0,5,3,{182,171},"Pfadfinder und Kraeuterkundiger"},
{"mage",8,1,5,1,{197,333},"Reisender Gelehrter"},
{"warlock",9,0,5,1,{197,333},"Sucher vergessener Schriften"},
{"druid",11,0,5,2,{182,171},"Hueter der wilden Pfade"}}};
return values;
}
inline unsigned Faction(unsigned race)
{return race==1 || race==3 || race==4 || race==7 || race==11 || race==22?1:2;}
inline char const* Origin(unsigned race)
{
switch(race)
{
case 1:return "Elwynn";case 2:return "Durotar";case 3:return "Dun Morogh";
case 4:return "Teldrassil";case 5:return "Tirisfal";case 6:return "Mulgore";
case 7:return "Dun Morogh";case 8:return "den Echoinseln";case 9:return "Kezan";
case 10:return "dem Immersangwald";case 11:return "der Azurmythosinsel";case 22:return "Gilneas";
default:return "fernen Landen";
}
}
inline std::string Name(unsigned race,unsigned gender,std::mt19937& random)
{
// Small cultural grammars, not fragments of arbitrary player/NPC names.
// ASCII names keep native client naming rules and SQL/Lua paths simple.
char const* first=nullptr;char const* last=nullptr;
switch(race)
{
case 2:first=gender?"Gra Sha Mog Drak Ruk Gar Kor Ur":"Gor Thok Drak Rog Mok Gar Kor Ur";last=gender?"sha ra ga dra ka na gra tha":"gar thar mok nak gor rak dan zug";break;
case 3:first=gender?"Hild Bryn Dag Ragn Tor Hel Marn Brin":"Brol Durn Thar Karg Brom Dorn Thol Grun";last=gender?"ra dis na hild ri dra li da":"gar grim din rik gan rum dan rin";break;
case 4:first=gender?"Lun Shae Ely Nara Thala Syl Mira Aela":"Ther Dala Shan Fael Reth Lor Ael Thal";last=gender?"ria na riel dra ra niel sia nor":"ion dris nor rion dor thas riel ron";break;
case 6:first=gender?"Mahu Hara Tahu Nara Asha Oru Runa Yana":"Hamu Tahu Ranu Maku Oru Koru Toru Yaru";last=gender?"na ra la mi sha ni ma wa":"ran mak tar rok nan tam hon kan";break;
case 7:first=gender?"Tilli Bixi Nimi Pelli Fizzi Wini Lilli Miri":"Bim Nib Pib Gim Fizz Tink Wib Nim";last=gender?"na ra li wi mi ni la si":"wick bin win nix mik rik wip lin";break;
case 8:first=gender?"Zan Sha Vola Jara Tali Zula Yala Mala":"Zan Vol Jar Zul Rok Tal Sen Jin";last=gender?"ra jin sha li na ri la ni":"jin rok zan zul tal kan rin mar";break;
case 9:first=gender?"Kiz Vex Nix Rix Gix Zix Bix Tix":"Gaz Rix Niz Vex Gix Zik Bix Kaz";last=gender?"zi ra ni za li ki na si":"nik wick zik rat zle nix bin zek";break;
case 10:first=gender?"Ela Lora Syla Vela Nara Fala Mira Sera":"Vael Aeth Lor Ther Bel Sel Dar Fael";last=gender?"ria riel na ra lin thia sia niel":"rin thas rion dor nor dris ran riel";break;
case 11:first=gender?"Yra Sora Nara Vela Isha Tali Asha Ela":"Vara Mora Aru Tura Iru Dara Kora Baru";last=gender?"na ra li sha la ni sa ri":"dor nun ran vos mar das nol ven";break;
default:first=gender?"Ela Mira Mara Lena Nora Sera Talia Rena":"Ald Gar Ed Bran Cor Dar Ren Hal";last=gender?"na ra lin ria la nda sia nora":"ren ric dan win lan den ron ven";break;
}
auto pick=[&](char const* words)
{
std::string list(words);unsigned choice=random()%8;std::size_t start=0;
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);
}
inline std::string LuaString(std::string const& input)
{
std::string out="\"";
for(char ch:input){if(ch=='\\' || ch=='\"')out+='\\';if(ch=='\n')out+="\\n";else if(ch=='\r')out+="\\r";else out+=ch;}
return out+'\"';
}
inline unsigned Armor(Archetype const& a,unsigned level)
{return level<40 && a.Armor>=3?a.Armor-1:a.Armor;}
inline bool EligibleItem(unsigned level,unsigned required,unsigned quality,unsigned itemLevel,bool temporary,bool restricted)
{
return required<=level && quality>=1 && quality<=3 && !temporary && !restricted &&
(required+15>=level || level<20) && itemLevel<=level*5+20;
}
inline std::string Profile(std::string const& name,Archetype const& a,unsigned race,
std::string const& location,std::string const& region,std::mt19937& random)
{
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)
<<", 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)
<<", 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()
<<", 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 } }";
return lua.str();
}
}
#endif
@@ -0,0 +1,254 @@
#include "AdventureInteractionMgr.h"
#include "AdventureInteractionPolicy.h"
#include "AdventureBehaviorMgr.h"
#include "AdventureCombatMgr.h"
#include "ClientlessLuaEngine.h"
#include "ClientlessPlayerMgr.h"
#include "ClientlessTravelMgr.h"
#include "ClientlessActionExecutorMgr.h"
#include "ClientlessTravelGraphMgr.h"
#include "Chat.h"
#include "Creature.h"
#include "GameTime.h"
#include "Group.h"
#include "Map.h"
#include "MotionMaster.h"
#include "ObjectAccessor.h"
#include "Opcodes.h"
#include "PartyPackets.h"
#include "Player.h"
#include "Random.h"
#include "WorldPacket.h"
#include "WorldSession.h"
#include "Log.h"
#include <vector>
namespace
{
float Sociability(Player* p)
{
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
if(profile){auto it=profile->Personality.find("sociability");if(it!=profile->Personality.end())return it->second;}
return 0.5f;
}
float Loyalty(Player* p)
{
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
if(profile){auto it=profile->Personality.find("group_loyalty");if(it!=profile->Personality.end())return std::max(0.0f,std::min(1.0f,it->second));}
return 0.25f;
}
bool German(Player* p)
{
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
return profile && profile->Locale=="deDE";
}
bool HostileEmote(uint32 emote)
{return emote==TEXT_EMOTE_RUDE || emote==TEXT_EMOTE_THREATEN || emote==TEXT_EMOTE_ROAR;}
bool FriendlyEmote(uint32 emote)
{
switch(emote)
{
case TEXT_EMOTE_GREET:case TEXT_EMOTE_HELLO:case TEXT_EMOTE_WAVE:case TEXT_EMOTE_BOW:
case TEXT_EMOTE_SALUTE:case TEXT_EMOTE_SMILE:case TEXT_EMOTE_THANK:return true;
default:return false;
}
}
}
AdventureInteractionMgr* AdventureInteractionMgr::instance(){static AdventureInteractionMgr mgr;return &mgr;}
bool AdventureInteractionMgr::Visible(Player* p,Player* other,float distance) const
{
return other && other!=p && other->IsAlive() && other->IsInWorld() && p->IsInMap(other) &&
p->IsInPhase(other) && p->CanSeeOrDetect(other) && other->CanSeeOrDetect(p) && p->GetExactDist(other)<=distance && p->IsWithinLOSInMap(other);
}
void AdventureInteractionMgr::PartySay(Player* p,char const* german,char const* english)
{
auto group=p->GetGroup();if(!group)return;
WorldPacket packet;
ChatHandler::BuildChatPacket(packet,CHAT_MSG_PARTY,p->GetTeam()==ALLIANCE?LANG_COMMON:LANG_ORCISH,p,nullptr,German(p)?german:english);
group->BroadcastPacket(&packet,false);
}
void AdventureInteractionMgr::Leave(Player* p,Record& r,char const* reason)
{
PartySay(p,"Ich muss weiter. Gute Reise!","I must be going. Safe travels!");
TC_LOG_INFO("server.adventurebot","INTERACTION_GROUP_LEAVE guid=%u reason='%s'",p->GetGUID().GetCounter(),reason);
p->GetGroup()->RemoveMember(p->GetGUID(),GROUP_REMOVEMETHOD_LEAVE);
sAdventureBehaviorMgr->InterruptForInteraction(p);
r.Grouped=false;r.Leader.Clear();r.Invite.Clear();r.Partner.Clear();r.SceneUntil=0;
}
void AdventureInteractionMgr::Emote(Player* p,Player* source,uint32 emote)
{
if(!Visible(p,source,25) || p->IsInCombat() || !p->IsAlive() || p->GetMap()->IsDungeon() ||
p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsCharmed())return;
auto now=GameTime::GetGameTime();auto& r=_records[p->GetGUID().GetCounter()];
if(r.Partner || now<r.NextSocial || (!FriendlyEmote(emote) && !HostileEmote(emote)))return;
auto it=r.Encounters.find(source->GetGUID());if(it!=r.Encounters.end() && now<it->second)return;
r.Partner=source->GetGUID();r.Reply=emote;r.ReplyAt=now+urand(1,3);r.SceneUntil=r.ReplyAt+2;
r.NextSocial=now+15;r.Encounters[source->GetGUID()]=now+120;
}
void AdventureInteractionMgr::Reply(Player* p,Player* other,uint32 emote)
{
p->SetStandState(UNIT_STAND_STATE_STAND);p->SetFacingToObject(other);
// Hostility alone never grants permission to attack. Native attack checks
// remain in combat; these are visual reactions without combat side effects.
bool friendly=p->IsFriendlyTo(other) && !HostileEmote(emote);
p->HandleEmoteCommand(friendly?(emote==TEXT_EMOTE_BOW?EMOTE_ONESHOT_BOW:EMOTE_ONESHOT_WAVE):EMOTE_ONESHOT_NO);
if(emote==TEXT_EMOTE_GREET && friendly && urand(0,2)==0)
p->Say(German(p)?"Gute Reise!":"Safe travels!",p->GetTeam()==ALLIANCE?LANG_COMMON:LANG_ORCISH,other);
TC_LOG_INFO("server.adventurebot","INTERACTION_EMOTE guid=%u other=%u friendly=%u",p->GetGUID().GetCounter(),other->GetGUID().GetCounter(),friendly);
}
void AdventureInteractionMgr::CombatHelp(Player* p,ObjectGuid helper)
{ _records[p->GetGUID().GetCounter()].Helper=helper; }
void AdventureInteractionMgr::Offline(Player* p){_records.erase(p->GetGUID().GetCounter());}
bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
{
auto& r=_records[p->GetGUID().GetCounter()];auto now=GameTime::GetGameTime();
if(r.Timer>diff){r.Timer-=diff;return r.Grouped || r.SceneUntil>now;}
r.Timer=1000;
bool unsafe=!p->IsAlive() || p->IsInCombat() || sAdventureCombatMgr->Busy(p) ||
p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsCharmed();
if(unsafe){r.Partner.Clear();r.SceneUntil=0;return p->GetGroup()!=nullptr;}
for(auto it=r.Encounters.begin();it!=r.Encounters.end();)if(now>=it->second)it=r.Encounters.erase(it);else ++it;
if(r.Helper)
{
auto helper=ObjectAccessor::FindPlayer(r.Helper);r.Helper.Clear();
if(Visible(p,helper,25) && p->IsFriendlyTo(helper))
p->Say(German(p)?"Danke für die Hilfe!":"Thank you for the help!",p->GetTeam()==ALLIANCE?LANG_COMMON:LANG_ORCISH,helper);
}
if(auto invite=p->GetGroupInvite())
{
auto leader=invite->GetLeaderGUID();
if(r.Invite!=leader){r.Invite=leader;r.InviteAt=now+urand(2,4);}
if(now>=r.InviteAt)
{
auto inviter=ObjectAccessor::FindPlayer(leader);
bool accept=inviter && inviter->IsAlive() && p->IsFriendlyTo(inviter) && !p->GetMap()->IsDungeon() &&
!inviter->GetSession()->IsClientless() && frand(0,1)<AdventureInteractionPolicy::Willingness(sAdventureBehaviorMgr->InteractionIntent(p),Sociability(p));
WorldPackets::Party::PartyInviteResponse response{WorldPacket(CMSG_PARTY_INVITE_RESPONSE)};response.Accept=accept;
p->GetSession()->HandlePartyInviteResponseOpcode(response);r.Invite.Clear();
TC_LOG_INFO("server.adventurebot","INTERACTION_INVITE guid=%u leader=%u accepted=%u",p->GetGUID().GetCounter(),leader.GetCounter(),accept);
}
}
else r.Invite.Clear();
if(r.Partner)
{
auto other=ObjectAccessor::FindPlayer(r.Partner);
if(!Visible(p,other,25) || now>=r.SceneUntil){r.Partner.Clear();r.SceneUntil=0;}
else if(now>=r.ReplyAt)
{
sAdventureBehaviorMgr->InterruptForInteraction(p);Reply(p,other,r.Reply);r.ReplyAt=r.SceneUntil+1;
}
if(r.Partner)return true;
}
if(auto group=p->GetGroup())
{
auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID());
if(!r.Grouped)
{
r.Grouped=true;r.Schedule=sAdventureBehaviorMgr->InteractionSchedule(p);
r.Partner.Clear();r.SceneUntil=0;
sAdventureBehaviorMgr->InterruptForInteraction(p);
PartySay(p,"Ich begleite euch eine Weile.","I'll accompany you for a while.");
}
if(r.Leader!=group->GetLeaderGUID())
{
r.Leader=group->GetLeaderGUID();r.LastActivity=now;r.AfkSince=0;
if(leader){r.LeaderPosition.Relocate(leader);r.LeaderMap=leader->GetMapId();r.Selection=leader->GetTarget();}
}
if(leader)
{
r.LastActivity=std::max(r.LastActivity,sClientlessPlayerMgr->LastActivity(leader->GetGUID()));
if(r.LeaderMap!=leader->GetMapId() || r.LeaderPosition.GetExactDist(leader)>0.1f ||
r.LeaderPosition.GetOrientation()!=leader->GetOrientation() || r.Selection!=leader->GetTarget() ||
leader->IsInCombat() || leader->HasUnitState(UNIT_STATE_CASTING))r.LastActivity=now;
r.LeaderPosition.Relocate(leader);r.LeaderMap=leader->GetMapId();r.Selection=leader->GetTarget();
if(leader->isAFK()){if(!r.AfkSince)r.AfkSince=now;}else r.AfkSince=0;
}
if(AdventureInteractionPolicy::CanLeave(p->IsAlive(),p->IsInCombat(),p->GetMap()->IsDungeon(),p->GetTransport() || p->IsInFlight()))
{
if(r.Schedule!=sAdventureBehaviorMgr->InteractionSchedule(p))
{
r.Schedule=sAdventureBehaviorMgr->InteractionSchedule(p);
if(frand(0,1)>=Loyalty(p)){Leave(p,r,"intent-change");return false;}
}
if(AdventureInteractionPolicy::LeaderExpired(now,r.AfkSince?r.AfkSince:r.LastActivity)){Leave(p,r,"leader-afk");return false;}
}
// Existing warrior executor supplies damage assistance. Roles/healing
// deliberately remain a later profile-driven extension.
for(auto member=group->GetFirstMember();member;member=member->next())
{
auto ally=member->GetSource();if(!ally || !ally->IsAlive() || !p->IsInMap(ally) || p->GetExactDist(ally)>40)continue;
auto target=ally->IsInCombat()?ally->GetVictim():nullptr;
if(!target)for(auto attacker:ally->getAttackers())if(attacker->GetVictim()==ally){target=attacker;break;}
if(target && sAdventureCombatMgr->Assist(p,target))return true;
}
if(!leader || leader==p || !leader->IsAlive())return true;
auto execution=sClientlessActionExecutorMgr->GetStatus(p->GetGUID().GetCounter());
if(execution && (execution->State==ClientlessActionExecutionState::Moving || execution->State==ClientlessActionExecutionState::SelectingTarget))return true;
if(!p->IsInMap(leader) || p->GetExactDist(leader)>=100)
{
if(now>=r.NextRoute)
{
r.NextRoute=now+15;
std::string node;float distance=0;
sClientlessTravelGraphMgr->EnsureLoaded();
if(sClientlessTravelGraphMgr->FindNearestNodeToPosition(leader->GetMapId(),leader->GetPositionX(),leader->GetPositionY(),leader->GetPositionZ(),node,distance) && distance<1000)
{
ClientlessAction action;action.Name="travel_to_node";action.Parameters["node"]=node;
sClientlessPlayerMgr->QueueAction(p->GetGUID().GetCounter(),std::move(action));
}
}
return true;
}
if(!p->IsInPhase(leader) || !p->CanSeeOrDetect(leader))return true;
if(p->GetExactDist(leader)>5 && p->GetExactDist(leader)<100)
{
auto nav=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter());
if(!nav || nav->State!=ClientlessTravelState::Moving || nav->Target.GetExactDist(leader)>5)
{
Position end;leader->GetNearPoint(p,end.m_positionX,end.m_positionY,end.m_positionZ,3,leader->GetOrientation()+3.14159265f);
ClientlessPathDiagnostics path;
if(sClientlessTravelMgr->AnalyzePath(p,end,path) && path.Complete && !path.Suspicious && path.PathLength<150)
sClientlessTravelMgr->StartTravel(p,end);
}
}
else if(p->GetExactDist(leader)<=5)sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());
return true;
}
if(r.Grouped){r.Grouped=false;r.Leader.Clear();sAdventureBehaviorMgr->InterruptForInteraction(p);}
auto intent=sAdventureBehaviorMgr->InteractionIntent(p);
if(now>=r.NextEncounter && now>=r.NextSocial && !p->GetMap()->IsDungeon() && (intent=="social" || intent=="leisure" || intent=="rest"))
{
r.NextEncounter=now+urand(30,60);
std::vector<Player*> people;p->GetPlayerListInGrid(people,12);
for(auto other:people)
{
if(!Visible(p,other,12) || !sClientlessLuaEngine->GetAdventureProfile(other->GetGUID().GetCounter()) ||
p->GetGUID().GetCounter()>=other->GetGUID().GetCounter() || other->IsInCombat() || other->GetGroup())continue;
auto& peer=_records[other->GetGUID().GetCounter()];
auto peerIntent=sAdventureBehaviorMgr->InteractionIntent(other);
if((peerIntent!="social" && peerIntent!="leisure" && peerIntent!="rest") || other->HasUnitState(UNIT_STATE_CASTING))continue;
if(peer.Partner || peer.NextSocial>now || r.Encounters.count(other->GetGUID()) || frand(0,1)>Sociability(p))continue;
// One initiator reserves both participants; animation replies do
// not generate incoming text-emote events, preventing loops.
r.Partner=other->GetGUID();r.Reply=p->IsFriendlyTo(other)?TEXT_EMOTE_GREET:TEXT_EMOTE_THREATEN;
r.ReplyAt=now+1;r.SceneUntil=now+5;r.Encounters[other->GetGUID()]=now+300;
peer.Partner=p->GetGUID();peer.Reply=r.Reply;peer.ReplyAt=now+3;peer.SceneUntil=now+5;
peer.NextSocial=now+60;peer.Encounters[p->GetGUID()]=now+300;
return true;
}
}
return false;
}
void AdventureInteractionMgr::Report(ChatHandler* handler,Player* p) const
{
auto it=_records.find(p->GetGUID().GetCounter());
if(it==_records.end()){handler->SendSysMessage("No interaction state yet.");return;}
auto const& r=it->second;auto now=GameTime::GetGameTime();
handler->PSendSysMessage("Interactions guid=%u grouped=%u leader=%u partner=%u intent='%s' schedule='%s'",
p->GetGUID().GetCounter(),p->GetGroup()?1:0,r.Leader.GetCounter(),r.Partner.GetCounter(),
sAdventureBehaviorMgr->InteractionIntent(p).c_str(),r.Schedule.c_str());
handler->PSendSysMessage(" leaderIdleSeconds=%llu afkSeconds=%llu sociability=%.2f loyalty=%.2f",
static_cast<unsigned long long>(r.LastActivity && now>=r.LastActivity?now-r.LastActivity:0),
static_cast<unsigned long long>(r.AfkSince && now>=r.AfkSince?now-r.AfkSince:0),Sociability(p),Loyalty(p));
}
@@ -0,0 +1,41 @@
#ifndef VCORE_ADVENTURE_INTERACTION_MGR_H
#define VCORE_ADVENTURE_INTERACTION_MGR_H
#include "Common.h"
#include "ObjectGuid.h"
#include "Position.h"
#include <ctime>
#include <string>
#include <unordered_map>
class Player;
class ChatHandler;
class TC_GAME_API AdventureInteractionMgr
{
public:
static AdventureInteractionMgr* instance();
bool Update(Player* player,uint32 diff);
void Emote(Player* player,Player* source,uint32 emote);
void Offline(Player* player);
void CombatHelp(Player* player,ObjectGuid helper);
void Report(ChatHandler* handler,Player* player) const;
private:
struct Record
{
uint32 Timer=0;
ObjectGuid Leader,Invite,Partner,Selection,Helper;
time_t NextRoute=0,NextEncounter=0;
Position LeaderPosition;
uint32 LeaderMap=0;
time_t InviteAt=0,LastActivity=0,AfkSince=0,ReplyAt=0,SceneUntil=0,NextSocial=0;
uint32 Reply=0;
bool Grouped=false;
std::string Schedule;
std::unordered_map<ObjectGuid,time_t> Encounters;
};
bool Visible(Player* player,Player* other,float distance) const;
void Reply(Player* player,Player* other,uint32 emote);
void Leave(Player* player,Record& record,char const* reason);
void PartySay(Player* player,char const* german,char const* english);
std::unordered_map<uint32,Record> _records;
};
#define sAdventureInteractionMgr AdventureInteractionMgr::instance()
#endif
@@ -0,0 +1,20 @@
#ifndef VCORE_ADVENTURE_INTERACTION_POLICY_H
#define VCORE_ADVENTURE_INTERACTION_POLICY_H
#include <algorithm>
#include <cstdint>
#include <string>
namespace AdventureInteractionPolicy
{
constexpr std::int64_t LeaderTimeout=600;
inline float Willingness(std::string const& intent,float sociability)
{
float bias=intent=="rest"?-0.5f:intent=="profession"||intent=="economy"?-0.25f:
intent=="adventure"||intent=="instance"?0.2f:0.1f;
return std::max(0.0f,std::min(1.0f,sociability+bias));
}
inline bool CanLeave(bool alive,bool combat,bool dungeon,bool transport)
{return alive && !combat && !dungeon && !transport;}
inline bool LeaderExpired(std::int64_t now,std::int64_t lastActivity)
{return lastActivity>0 && now>=lastActivity && now-lastActivity>=LeaderTimeout;}
}
#endif
@@ -0,0 +1,315 @@
#include "AdventureSupplyMgr.h"
#include "ClientlessCharacterFactory.h"
#include "AdventureBehaviorMgr.h"
#include "ClientlessLuaEngine.h"
#include "DatabaseEnv.h"
#include "DBCStores.h"
#include "Bag.h"
#include "Item.h"
#include "ItemTemplate.h"
#include "ObjectMgr.h"
#include "Player.h"
#include "Spell.h"
#include "SpellCastRequest.h"
#include "SpellHistory.h"
#include "SpellMgr.h"
#include "GameTime.h"
#include "Chat.h"
#include "Log.h"
#include <algorithm>
#include <memory>
namespace
{
constexpr uint32 EmbersilkBag=54443;
uint32 Day(time_t now)
{
std::tm local{};localtime_r(&now,&local);
return (local.tm_year+1900)*10000+(local.tm_mon+1)*100+local.tm_mday;
}
bool RoleMatches(ItemTemplate const* item,std::string const& role)
{
if(!item || item->GetClass()!=ITEM_CLASS_CONSUMABLE)return false;
if(role=="bandage")return item->GetSubClass()==ITEM_SUBCLASS_BANDAGE;
if(role=="food" || role=="drink")return item->GetSubClass()==ITEM_SUBCLASS_FOOD;
return (role=="healing_potion" || role=="mana_potion") && item->IsPotion();
}
}
AdventureSupplyMgr* AdventureSupplyMgr::instance(){static AdventureSupplyMgr mgr;return &mgr;}
bool AdventureSupplyMgr::LoadCatalog()
{
if(!_catalog.empty())return true;
auto rows=WorldDatabase.Query("SELECT role,item_id,min_level FROM vcore_adventure_supply WHERE enabled=1 ORDER BY min_level DESC,item_id DESC");
if(!rows)return false;
do {auto f=rows->Fetch();_catalog.push_back({f[0].GetString(),f[1].GetUInt32(),f[2].GetUInt8()});}while(rows->NextRow());
return true;
}
void AdventureSupplyMgr::Online(Player* p)
{
auto& r=_records[p->GetGUID().GetCounter()];r={};
LoadCatalog();LearnSecondaries(p);
if(auto row=CharacterDatabase.PQuery("SELECT last_day FROM vcore_adventure_supply_state WHERE character_guid=%u",p->GetGUID().GetCounter()))r.LastDay=row->Fetch()[0].GetUInt32();
r.Loaded=true;r.WasInCombat=p->IsInCombat();
}
void AdventureSupplyMgr::LearnSecondaries(Player* p)
{
if(_secondaryRanks.empty())
if(auto rows=WorldDatabase.Query("SELECT DISTINCT SpellId,ReqLevel,ReqSkillRank FROM trainer_spell"))do
{
auto f=rows->Fetch();auto trainerSpell=sSpellMgr->GetSpellInfo(f[0].GetUInt32());
if(!trainerSpell)continue;
std::vector<uint32> taught{trainerSpell->Id};
for(auto const& effect:trainerSpell->Effects)
if(effect.Effect==SPELL_EFFECT_LEARN_SPELL && effect.TriggerSpell)taught.push_back(effect.TriggerSpell);
for(auto id:taught)
{
auto rank=sSpellMgr->GetSpellLearnSkill(id);auto spell=sSpellMgr->GetSpellInfo(id);
if(!spell)continue;
uint32 skill=rank?rank->skill:0,step=rank?rank->step:0;
// Profession ranks also use EffectLearnSkill (44), handled by
// Player::AddSpell, rather than SpellMgr's effect118 map.
if(!step)for(auto const& effect:spell->Effects)
if(effect.Effect==SPELL_EFFECT_SKILL_STEP && effect.CalcValue()>0)
{skill=effect.MiscValue;step=effect.CalcValue();break;}
if(!step || (skill!=SKILL_FIRST_AID && skill!=SKILL_COOKING && skill!=SKILL_FISHING && skill!=SKILL_ARCHAEOLOGY))continue;
uint32 minimum=std::max<uint32>({f[1].GetUInt8(),spell->SpellLevel,spell->BaseLevel});
auto existing=std::find_if(_secondaryRanks.begin(),_secondaryRanks.end(),[&](SecondaryRank const& value){return value.Spell==id;});
if(existing==_secondaryRanks.end())_secondaryRanks.push_back({skill,id,step,minimum,f[2].GetUInt32()});
else {existing->MinLevel=std::max(existing->MinLevel,minimum);existing->Required=std::max<uint32>(existing->Required,f[2].GetUInt32());}
}
}while(rows->NextRow());
std::sort(_secondaryRanks.begin(),_secondaryRanks.end(),[](SecondaryRank const& a,SecondaryRank const& b){return a.Step<b.Step;});
for(auto const& rank:_secondaryRanks)
{
if(rank.MinLevel>p->getLevel() || rank.Required>p->GetPureSkillValue(rank.Skill) ||
!p->IsSpellFitByClassAndRace(rank.Spell))continue;
auto rc=sDBCManager.GetSkillRaceClassInfo(rank.Skill,p->getRace(),p->getClass());
auto info=sSpellMgr->GetSpellInfo(rank.Spell);
if(!rc || uint32(rc->MinLevel)>p->getLevel() || !info || !SpellMgr::IsSpellValid(info,p,false))continue;
if(!p->HasSpell(rank.Spell))p->LearnSpell(rank.Spell,false);
if(!p->IsInWorld() && !ClientlessCharacterFactory::PrepareSkillRank(p,rank.Skill,rank.Step))continue;
auto maximum=p->GetPureMaxSkillValue(rank.Skill);
if(maximum && p->GetPureSkillValue(rank.Skill)<maximum)
{
p->SetSkill(rank.Skill,p->GetSkillStep(rank.Skill),maximum,maximum);
TC_LOG_INFO("server.adventurebot","SUPPLY_SECONDARY guid=%u skill=%u value=%u max=%u",p->GetGUID().GetCounter(),rank.Skill,maximum,maximum);
}
}
}
ItemTemplate const* AdventureSupplyMgr::Select(Player* p,char const* role) const
{
ItemTemplate const* fallback=nullptr;
for(auto const& row:_catalog)
{
if(row.Role!=role || row.MinLevel>p->getLevel())continue;
auto item=sObjectMgr->GetItemTemplate(row.Item);
if(!RoleMatches(item,row.Role) || item->GetRequiredLevel()>p->getLevel())continue;
if(p->CanUseItem(item)==EQUIP_ERR_OK)return item;
// Owning a level-appropriate bandage does not grant First Aid. Prefer
// the best usable tier, otherwise retain one appropriate future stack.
if(row.Role=="bandage" && !fallback)fallback=item;
}
return fallback;
}
bool AdventureSupplyMgr::Bags(Player* p)
{
auto bagTemplate=sObjectMgr->GetItemTemplate(EmbersilkBag);
if(!bagTemplate || bagTemplate->GetContainerSlots()!=22 || p->CanUseItem(bagTemplate)!=EQUIP_ERR_OK)return false;
for(uint8 slot=INVENTORY_SLOT_BAG_START;slot<INVENTORY_SLOT_BAG_END;++slot)
{
auto current=p->GetBagByPos(slot);
if(current && current->GetBagSize()>=22)continue;
if(current)
{
// Native bag swapping transfers all contents and retains the old
// bag. Reuse a previous staged bag if a swap could not complete.
Item* replacement=nullptr;
for(uint8 pos=INVENTORY_SLOT_ITEM_START;pos<INVENTORY_SLOT_ITEM_END;++pos)
if(auto item=p->GetItemByPos(INVENTORY_SLOT_BAG_0,pos))
if(item->GetEntry()==EmbersilkBag){replacement=item;break;}
if(!replacement)
{
ItemPosCountVec positions;
if(p->CanStoreNewItem(INVENTORY_SLOT_BAG_0,NULL_SLOT,positions,EmbersilkBag,1)!=EQUIP_ERR_OK)return false;
replacement=p->StoreNewItem(positions,EmbersilkBag,true);
}
if(!replacement)return false;
p->SwapItem(replacement->GetPos(),uint16(INVENTORY_SLOT_BAG_0)<<8|slot);
current=p->GetBagByPos(slot);
if(!current || current->GetBagSize()<22)return false;
}
else
{
auto dispose=[&](Item* item){if(item){RemoveItemFromUpdateQueueOf(item,p);delete item;}};
std::unique_ptr<Item,decltype(dispose)> item(Item::CreateItem(EmbersilkBag,1,p),dispose);
uint16 destination=0;
if(!item || p->CanEquipItem(slot,destination,item.get(),true)!=EQUIP_ERR_OK || !p->EquipItem(destination,item.get(),true))return false;
item.release();
}
TC_LOG_INFO("server.adventurebot","SUPPLY_BAG guid=%u slot=%u item=%u slots=22",p->GetGUID().GetCounter(),slot,EmbersilkBag);
}
return true;
}
bool AdventureSupplyMgr::Stock(Player* p,Record& r,uint32 today)
{
if(!LoadCatalog())return false;
LearnSecondaries(p);
if(!Bags(p))return false;
for(auto role:{"bandage","healing_potion","mana_potion","food","drink"})
{
auto item=Select(p,role);if(!item)continue; // No legal mana potion exists at the lowest levels.
auto count=AdventureSupplyPolicy::TopUp(p->GetItemCount(item->GetId()),item->GetMaxStackSize());
if(!count)continue;
ItemPosCountVec positions;
if(p->CanStoreNewItem(NULL_BAG,NULL_SLOT,positions,item->GetId(),count)!=EQUIP_ERR_OK || !p->StoreNewItem(positions,item->GetId(),true))return false;
TC_LOG_INFO("server.adventurebot","SUPPLY_TOPUP guid=%u role='%s' item=%u added=%u stack=%u",p->GetGUID().GetCounter(),role,item->GetId(),count,item->GetMaxStackSize());
}
auto transaction=CharacterDatabase.BeginTransaction();p->SaveToDB(transaction);
transaction->PAppend("REPLACE INTO vcore_adventure_supply_state(character_guid,last_day) VALUES(%u,%u)",p->GetGUID().GetCounter(),today);
auto commit=CharacterDatabase.AsyncCommitTransaction(transaction);
if(!commit.m_future.get())return false;
TC_LOG_INFO("server.adventurebot","SUPPLY_RESTOCK guid=%u day=%u daily=%u afterCombat=%u",p->GetGUID().GetCounter(),today,r.LastDay!=today,r.AfterCombat);
r.LastDay=today;r.AfterCombat=false;return true;
}
AdventureSupplyMgr::UseResult AdventureSupplyMgr::TryUse(Player* p,Record& r,Use use)
{
auto proto=Select(p,AdventureSupplyPolicy::Role(use));
auto item=proto?p->GetItemByEntry(proto->GetId()):nullptr;
if(!item || p->CanUseItem(item)!=EQUIP_ERR_OK)return UseResult::Unavailable;
if(p->IsNonMeleeSpellCast(false) || p->HasUnitState(UNIT_STATE_CASTING))return UseResult::Busy;
for(auto const& effect:proto->Effects)
{
if(effect.Trigger!=ITEM_SPELLTRIGGER_ON_USE || effect.SpellID<=0)continue;
auto info=sSpellMgr->GetSpellInfo(effect.SpellID);
if(!info || (p->IsInCombat() && !info->CanBeUsedInCombat()))continue;
if(p->GetSpellHistory()->HasGlobalCooldown(info))return UseResult::Busy;
if(!p->GetSpellHistory()->IsReady(info,proto->GetId()) || !p->CanRequestSpellCast(info))continue;
auto probe=std::make_unique<Spell>(p,info,TRIGGERED_NONE);probe->m_CastItem=item;
if(!probe->CanAutoCast(p))continue;
probe.reset();
uint32 entry=proto->GetId(),spell=info->Id;
r.PendingItem=entry;r.PendingCount=p->GetItemCount(entry);r.PendingRole=use;r.Requested=GameTime::GetGameTime();
WorldPackets::Spells::SpellCastRequest request;request.SpellID=spell;
request.Target.Flags=TARGET_FLAG_UNIT;request.Target.Unit=p->GetGUID();
p->RequestSpellCast(std::make_unique<PendingSpellCastRequest>(std::move(request),SpellCastRequestItemData(item->GetBagSlot(),item->GetSlot(),item->GetGUID())),info);
if(!p->IsInCombat()){r.Spell=spell;r.Active=use;}
TC_LOG_INFO("server.adventurebot","SUPPLY_USE_REQUEST guid=%u role='%s' item=%u spell=%u combat=%u",p->GetGUID().GetCounter(),AdventureSupplyPolicy::Role(use),entry,spell,p->IsInCombat());
return UseResult::Started;
}
return UseResult::Unavailable;
}
void AdventureSupplyMgr::Cancel(Player* p,Record& r)
{
if(r.Spell)
{
p->InterruptNonMeleeSpells(false,r.Spell);
p->RemoveAurasDueToSpell(r.Spell,p->GetGUID());
}
if(r.Recovering && p->IsAlive())p->SetStandState(UNIT_STAND_STATE_STAND);
r.Spell=0;r.Active=Use::None;r.Recovering=false;r.BandageTried=false;
}
void AdventureSupplyMgr::Offline(Player* p)
{
auto it=_records.find(p->GetGUID().GetCounter());if(it==_records.end())return;
Cancel(p,it->second);_records.erase(it);
}
bool AdventureSupplyMgr::Update(Player* p,uint32 diff,bool combatReaction)
{
auto& r=_records[p->GetGUID().GetCounter()];if(!r.Loaded)Online(p);
bool combat=p->IsInCombat();
if(r.WasInCombat && !combat)r.AfterCombat=true;
r.WasInCombat=combat;
if(r.Timer>diff){r.Timer-=diff;return r.Recovering && !combat && !combatReaction && p->IsAlive();}
r.Timer=500;auto now=GameTime::GetGameTime();
if(r.PendingItem)
{
if(p->GetItemCount(r.PendingItem)<r.PendingCount)
{
TC_LOG_INFO("server.adventurebot","SUPPLY_USED guid=%u role='%s' item=%u",p->GetGUID().GetCounter(),AdventureSupplyPolicy::Role(r.PendingRole),r.PendingItem);
r.PendingItem=0;
}
else if(now-r.Requested>5)r.PendingItem=0;
}
if(!p->IsAlive() || p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsFlying() || p->IsCharmed())
{if(r.Recovering)Cancel(p,r);return false;}
if(combat)
{
if(r.Recovering)Cancel(p,r);
bool health=AdventureSupplyPolicy::MissingTenPercent(p->GetHealth(),p->GetMaxHealth());
bool mana=p->GetPowerType()==POWER_MANA && AdventureSupplyPolicy::MissingTenPercent(p->GetPower(POWER_MANA),p->GetMaxPower(POWER_MANA));
if(!r.PendingItem)
{
auto use=AdventureSupplyPolicy::Choose(true,health,mana,false,false);
if(use!=Use::None)TryUse(p,r,use);
}
return false; // Native combat and attacks keep running.
}
if(combatReaction)return false;
if((r.LastDay!=Day(now) || r.AfterCombat) && now>=r.RetryStock)
{
if(!Stock(p,r,Day(now)))
{
TC_LOG_WARN("server.adventurebot","SUPPLY_RESTOCK_DEFERRED guid=%u",p->GetGUID().GetCounter());
r.RetryStock=now+30;
}
else r.RetryStock=0;
}
if(p->IsFalling())return false;
if(r.Recovering && now-r.Started>=90)
{Cancel(p,r);r.RetryRecovery=now+5;return false;}
if(r.Spell)
{
auto cast=p->GetCurrentSpell(CURRENT_CHANNELED_SPELL);
if(!cast)cast=p->GetCurrentSpell(CURRENT_GENERIC_SPELL);
bool running=(cast && cast->GetSpellInfo()->Id==r.Spell) || p->HasAura(r.Spell,p->GetGUID());
bool full=(r.Active==Use::Food && p->GetHealth()>=p->GetMaxHealth()) ||
(r.Active==Use::Drink && p->GetPower(POWER_MANA)>=p->GetMaxPower(POWER_MANA));
if(running && !full)return true;
if(!running && now-r.Requested<2)return true;
p->InterruptNonMeleeSpells(false,r.Spell);p->RemoveAurasDueToSpell(r.Spell,p->GetGUID());
r.Spell=0;r.Active=Use::None;
}
bool health=AdventureSupplyPolicy::MissingTenPercent(p->GetHealth(),p->GetMaxHealth());
bool mana=p->GetPowerType()==POWER_MANA && AdventureSupplyPolicy::MissingTenPercent(p->GetPower(POWER_MANA),p->GetMaxPower(POWER_MANA));
if(!health && !mana)
{
if(r.Recovering){TC_LOG_INFO("server.adventurebot","SUPPLY_RECOVERY_END guid=%u health=%u",p->GetGUID().GetCounter(),p->GetHealth());Cancel(p,r);}
return false;
}
if(now<r.RetryRecovery)return false;
if(!r.Recovering)
{
sAdventureBehaviorMgr->InterruptForCombat(p);sClientlessLuaEngine->DismountGroundMount(p,"supply-recovery");
p->StopMoving();r.Recovering=true;r.BandageTried=false;r.Started=now;
}
auto use=AdventureSupplyPolicy::Choose(false,health,mana,r.BandageTried,true);
p->SetStandState(use==Use::Bandage?UNIT_STAND_STATE_STAND:UNIT_STAND_STATE_SIT);
auto result=TryUse(p,r,use);
if(use==Use::Bandage && result!=UseResult::Busy)
{
r.BandageTried=true;
if(result==UseResult::Unavailable)
{
p->SetStandState(UNIT_STAND_STATE_SIT);
result=TryUse(p,r,Use::Food);
}
}
if(result==UseResult::Unavailable){Cancel(p,r);r.RetryRecovery=now+5;return false;}
return true;
}
void AdventureSupplyMgr::Report(ChatHandler* handler,Player* p)
{
auto& r=_records[p->GetGUID().GetCounter()];
handler->PSendSysMessage("Supplies: lastDay=%u recovering=%u afterCombat=%u health=%u/%u",r.LastDay,r.Recovering,r.AfterCombat,p->GetHealth(),p->GetMaxHealth());
for(uint32 skill:{uint32(SKILL_FIRST_AID),uint32(SKILL_COOKING),uint32(SKILL_FISHING),uint32(SKILL_ARCHAEOLOGY)})
handler->PSendSysMessage("secondary=%u value=%u max=%u",skill,p->GetPureSkillValue(skill),p->GetPureMaxSkillValue(skill));
for(auto role:{"bandage","healing_potion","mana_potion","food","drink"})
if(auto item=Select(p,role))handler->PSendSysMessage("%s: item=%u count=%u stack=%u requiredLevel=%u requiredSkill=%u/%u usable=%u",role,item->GetId(),p->GetItemCount(item->GetId()),item->GetMaxStackSize(),item->GetRequiredLevel(),item->GetRequiredSkill(),item->GetRequiredSkillRank(),p->CanUseItem(item)==EQUIP_ERR_OK);
for(uint8 slot=INVENTORY_SLOT_BAG_START;slot<INVENTORY_SLOT_BAG_END;++slot)
if(auto bag=p->GetBagByPos(slot))handler->PSendSysMessage("bagSlot=%u item=%u capacity=%u",slot,bag->GetEntry(),bag->GetBagSize());
}
bool AdventureSupplyMgr::PrepareCharacter(Player* player)
{
LoadCatalog();LearnSecondaries(player);return Bags(player);
}
@@ -0,0 +1,47 @@
#ifndef VCORE_ADVENTURE_SUPPLY_MGR_H
#define VCORE_ADVENTURE_SUPPLY_MGR_H
#include "Common.h"
#include "AdventureSupplyPolicy.h"
#include <ctime>
#include <string>
#include <unordered_map>
#include <vector>
class Player;
class ChatHandler;
struct ItemTemplate;
class TC_GAME_API AdventureSupplyMgr
{
public:
static AdventureSupplyMgr* instance();
void Online(Player* player);
bool PrepareCharacter(Player* player);
void Offline(Player* player);
// 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);
private:
using Use=AdventureSupplyPolicy::Use;
struct Supply {std::string Role;uint32 Item=0,MinLevel=0;};
struct Record
{
bool Loaded=false,WasInCombat=false,AfterCombat=false,Recovering=false,BandageTried=false;
uint32 LastDay=0,Timer=0,Spell=0;
Use Active=Use::None;
time_t RetryStock=0,RetryRecovery=0,Requested=0,Started=0;
uint32 PendingItem=0,PendingCount=0;Use PendingRole=Use::None;
};
enum class UseResult {Unavailable,Busy,Started};
bool LoadCatalog();
void LearnSecondaries(Player* player);
ItemTemplate const* Select(Player* player,char const* role) const;
bool Bags(Player* player);
bool Stock(Player* player,Record& record,uint32 today);
UseResult TryUse(Player* player,Record& record,Use use);
void Cancel(Player* player,Record& record);
struct SecondaryRank {uint32 Skill=0,Spell=0,Step=0,MinLevel=0,Required=0;};
std::vector<SecondaryRank> _secondaryRanks;
std::vector<Supply> _catalog;
std::unordered_map<uint32,Record> _records;
};
#define sAdventureSupplyMgr AdventureSupplyMgr::instance()
#endif
@@ -0,0 +1,30 @@
#ifndef VCORE_ADVENTURE_SUPPLY_POLICY_H
#define VCORE_ADVENTURE_SUPPLY_POLICY_H
#include <cstdint>
namespace AdventureSupplyPolicy
{
enum class Use { None, Bandage, Food, Drink, HealingPotion, ManaPotion };
inline bool MissingTenPercent(std::uint32_t current,std::uint32_t maximum)
{ return maximum && std::uint64_t(current)*100 <= std::uint64_t(maximum)*90; }
inline std::uint32_t TopUp(std::uint32_t count,std::uint32_t stack)
{ return count<stack?stack-count:0; }
inline Use Choose(bool combat,bool health,bool mana,bool bandageTried,bool bandageAllowed)
{
if(combat)return health?Use::HealingPotion:mana?Use::ManaPotion:Use::None;
if(health)return !bandageTried && bandageAllowed?Use::Bandage:Use::Food;
return mana?Use::Drink:Use::None;
}
inline char const* Role(Use use)
{
switch(use)
{
case Use::Bandage:return "bandage";
case Use::Food:return "food";
case Use::Drink:return "drink";
case Use::HealingPotion:return "healing_potion";
case Use::ManaPotion:return "mana_potion";
default:return "";
}
}
}
#endif
@@ -0,0 +1,100 @@
#include "ClientlessCharacterFactory.h"
#include "AccountMgr.h"
#include "CharacterCache.h"
#include "DBCStores.h"
#include "GameTime.h"
#include "MotionMaster.h"
#include "ObjectMgr.h"
#include "Player.h"
#include "Realm.h"
#include "ScriptMgr.h"
#include "World.h"
#include "WorldSession.h"
#include <algorithm>
#include <memory>
#include <random>
namespace
{
struct CreateInfo final : CharacterCreateInfo
{
explicit CreateInfo(ClientlessCharacterFactory::Definition const& d)
{
Name=d.Name;Race=d.Race;Class=d.Class;Gender=d.Gender;
std::mt19937 random(d.Seed);
// Native appearance checks are authoritative, including hero-class
// and race-specific restrictions. No unvalidated display overrides.
for(unsigned attempt=0;attempt<20000;++attempt)
{
Skin=random()%16;Face=random()%10;HairStyle=random()%20;HairColor=random()%16;FacialHair=random()%9;
if(Player::ValidateAppearance(Race,Class,Gender,HairStyle,HairColor,Face,FacialHair,Skin,true))return;
}
Skin=Face=HairStyle=HairColor=FacialHair=0; // native Create still validates this fallback
}
};
}
ClientlessCharacterFactory::Result ClientlessCharacterFactory::Create(uint32 account,Definition const& d,Prepare prepare,Append append)
{
auto fail=[](char const* error){return Result{0,error};};
if(!account || sWorld->FindSession(account))return fail("Account is missing or has an active network session.");
if(AccountMgr::GetSecurity(account,realm.Id.Realm)!=SEC_PLAYER)return fail("Use a normal dedicated bot account, never a privileged account.");
if(!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))
return fail("Invalid race/class/gender/level combination.");
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))return fail("Name is invalid, reserved or already used.");
CharacterDatabase.EscapeString(name);
if(CharacterDatabase.PQuery("SELECT 1 FROM characters WHERE name='%s' LIMIT 1",name.c_str()))return fail("Name already exists in the character database.");
auto acc=LoginDatabase.PQuery("SELECT username,expansion,locale FROM account WHERE id=%u",account);
if(!acc)return fail("Account does not exist.");
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())return fail("Account expansion does not permit this race/class.");
auto count=CharacterDatabase.PQuery("SELECT COUNT(*) FROM characters WHERE account=%u AND deleteDate IS NULL",account);
if(!count || count->Fetch()[0].GetUInt32()>=sWorld->getIntConfig(CONFIG_CHARACTERS_PER_REALM))return fail("Native per-realm character limit reached.");
WorldSession session(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 cleanup=[](Player* player){player->CleanupsBeforeDelete();delete player;};
std::unique_ptr<Player,decltype(cleanup)> player(new Player(&session),cleanup);
player->GetMotionMaster()->Initialize();CreateInfo info(d);
if(!player->Create(sObjectMgr->GetGenerator<HighGuid::Player>().Generate(),&info))return fail("Native character creation failed.");
player->setCinematic(1);
if(player->getLevel()!=d.Level)
{
// This character is born at its requested level, rather than earning
// levels. Avoid GiveLevel's independent reward-mail transactions/hooks.
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))return {0,error};
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 commit=CharacterDatabase.AsyncCommitTransaction(tx);
if(!commit.m_future.get())return fail("Atomic character save failed; no ready character was published.");
uint32 guid=player->GetGUID().GetCounter();
sCharacterCache->AddCharacterCacheEntry(player->GetGUID(),account,d.Name,d.Gender,d.Race,d.Class,d.Level);
sScriptMgr->OnPlayerCreate(player.get());
auto realmCount=LoginDatabase.GetPreparedStatement(LOGIN_REP_REALM_CHARACTERS);
realmCount->setUInt32(0,count->Fetch()[0].GetUInt32()+1);realmCount->setUInt32(1,account);realmCount->setUInt32(2,realm.Id.Realm);
LoginDatabase.Execute(realmCount);
return {guid,{}};
}
bool ClientlessCharacterFactory::PrepareSkillRank(Player* player,uint32 skill,uint32 step)
{
if(!player || player->IsInWorld() || !step || step>MAX_SKILL_STEP)return false;
auto rc=sDBCManager.GetSkillRaceClassInfo(skill,player->getRace(),player->getClass());
if(!rc || rc->MinLevel>player->getLevel())return false;
auto tier=sSkillTiersStore.LookupEntry(rc->SkillTierID);
if(!tier || !tier->Value[step-1])return false;
if(player->GetSkillStep(skill)>step)return true;
auto maximum=tier->Value[step-1];
player->SetSkill(skill,step,maximum,maximum);
return player->GetPureSkillValue(skill)==maximum && player->GetPureMaxSkillValue(skill)==maximum;
}
@@ -0,0 +1,27 @@
#ifndef VCORE_CLIENTLESS_CHARACTER_FACTORY_H
#define VCORE_CLIENTLESS_CHARACTER_FACTORY_H
#include "Common.h"
#include "DatabaseEnv.h"
#include <functional>
#include <string>
class Player;
// Native character creation for clientless consumers. No AdventureBot policy,
// source-character cloning, authentication credentials or deletion behavior.
class TC_GAME_API ClientlessCharacterFactory
{
public:
struct Definition
{
std::string Name;
uint8 Race=0,Class=0,Gender=0,Level=60;
uint32 Seed=0;
};
struct Result { uint32 Guid=0;std::string Error; };
using Prepare=std::function<bool(Player*,std::string&)>;
using Append=std::function<void(CharacterDatabaseTransaction const&,Player*)>;
// 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);
static Result Create(uint32 account,Definition const& definition,Prepare prepare,Append append);
};
#endif
+21 -3
View File
@@ -5,6 +5,9 @@
#include "ClientlessTravelMgr.h"
#include "AdventureBehaviorMgr.h"
#include "AdventureCombatMgr.h"
#include "AdventureInteractionMgr.h"
#include "AdventureGeneratorMgr.h"
#include "AdventureSupplyMgr.h"
#include "AdventureSocialActions.h"
#include "Config.h"
#include "Log.h"
@@ -215,15 +218,24 @@ public:
AdventureSocialActions::Prepare(player);
sClientlessLuaEngine->ExecuteAdventureScript(player);
sAdventureBehaviorMgr->Online(player);
sAdventureSupplyMgr->Online(player);
}
void OnIntentChanged(Player* player, ClientlessIntent const* intent) override
{ sAdventureBehaviorMgr->IntentChanged(player, intent); }
void Update(Player* player, uint32 diff) override
{ if (!sAdventureCombatMgr->Update(player, diff)) sAdventureBehaviorMgr->Update(player, diff); }
{
if(sAdventureSupplyMgr->Update(player,diff,sAdventureCombatMgr->Busy(player)))return;
if(!sAdventureCombatMgr->Update(player,diff) && !sAdventureInteractionMgr->Update(player,diff))
sAdventureBehaviorMgr->Update(player,diff);
}
void OnSharedTargetDamage(Player* player,ObjectGuid helper,ObjectGuid target) override
{ sAdventureCombatMgr->SharedTargetDamage(player,helper,target); }
void OnTextEmote(Player* player, Player* source, uint32 emote) override
{ sAdventureInteractionMgr->Emote(player,source,emote); }
void OnDamageTaken(Player* player, Unit* attacker, uint32 damage) override
{ sAdventureCombatMgr->DamageTaken(player, attacker, damage); }
void OnPlayerLogout(Player* player) override
{ sAdventureCombatMgr->Offline(player); sAdventureBehaviorMgr->Offline(player); }
{ sAdventureInteractionMgr->Offline(player); sAdventureSupplyMgr->Offline(player); sAdventureCombatMgr->Offline(player); sAdventureBehaviorMgr->Offline(player); }
};
}
@@ -324,8 +336,12 @@ bool ClientlessLuaEngine::LoadAdventureProfile(Player* player)
fs::path generic = root / "adventure" / "profiles" / "generic.lua";
fs::path chosen = fs::exists(specific) ? specific : generic;
std::string generated;
bool databaseProfile=!fs::exists(specific) && AdventureGeneratorMgr::Profile(player->GetGUID().GetCounter(),generated);
if(databaseProfile)chosen=fs::path("generated:"+std::to_string(player->GetGUID().GetCounter()));
int base = lua_gettop(_state);
if (luaL_loadfile(_state, chosen.string().c_str()) != LUA_OK)
int loaded=databaseProfile?luaL_loadbuffer(_state,generated.data(),generated.size(),chosen.string().c_str()):luaL_loadfile(_state,chosen.string().c_str());
if (loaded != LUA_OK)
{
TC_LOG_ERROR("server.adventurebot",
"[AdventureBot][Profile] guid=%u name='%s' load failed path='%s': %s",
@@ -373,6 +389,8 @@ bool ClientlessLuaEngine::LoadAdventureProfile(Player* player)
{
profile.CharacterName = ReadStringField(_state, -1, "name", profile.CharacterName);
profile.HomeRegion = ReadStringField(_state, -1, "home_region");
profile.Background = ReadStringField(_state, -1, "background");
profile.Vocation = ReadStringField(_state, -1, "vocation");
}
lua_pop(_state, 1);
@@ -27,6 +27,7 @@ struct TC_GAME_API ClientlessAdventureProfile
std::unordered_set<std::string> PrimaryProfessions;
std::string CharacterName;
std::string HomeRegion;
std::string Background, Vocation;
std::string SourcePath;
bool CanGroundMount = false;
+50 -1
View File
@@ -1,4 +1,5 @@
#include "ClientlessPlayerMgr.h"
#include "GameTime.h"
#include "ClientlessTravelMgr.h"
#include "ClientlessTransportMgr.h"
#include "ClientlessPerceptionMgr.h"
@@ -154,7 +155,9 @@ void ClientlessPlayerMgr::Update(uint32 diff)
// Damage can arrive from different map workers. Hand GUID-only notices to
// the world-thread controller, preserving order within each character.
decltype(_damageNotices) damageNotices;
{ std::lock_guard<std::mutex> lock(_damageMutex); damageNotices.swap(_damageNotices); }
decltype(_emoteNotices) emoteNotices;
decltype(_helpNotices) helpNotices;
{ std::lock_guard<std::mutex> lock(_damageMutex); damageNotices.swap(_damageNotices); emoteNotices.swap(_emoteNotices); helpNotices.swap(_helpNotices); }
// CLIENTLESS_PLAYER_CORE_C8_2_TRAVEL_ROBUSTNESS
sClientlessTravelMgr->Update(diff);
@@ -199,6 +202,12 @@ void ClientlessPlayerMgr::Update(uint32 diff)
auto notices=damageNotices.find(characterGuid);
if(notices!=damageNotices.end())for(auto const& notice:notices->second)
record.Controller->OnDamageTaken(player,ObjectAccessor::GetUnit(*player,notice.Attacker),notice.Damage);
auto emotes=emoteNotices.find(characterGuid);
if(emotes!=emoteNotices.end())for(auto const& notice:emotes->second)
record.Controller->OnTextEmote(player,ObjectAccessor::FindPlayer(notice.Source),notice.Emote);
auto helpers=helpNotices.find(characterGuid);
if(helpers!=helpNotices.end())for(auto const& notice:helpers->second)
record.Controller->OnSharedTargetDamage(player,notice.Helper,notice.Target);
record.Controller->Update(player, diff);
}
}
@@ -219,3 +228,43 @@ void ClientlessPlayerMgr::DamageTaken(Player* player, Unit* attacker, uint32 dam
std::lock_guard<std::mutex> lock(_damageMutex);
_damageNotices[player->GetGUID().GetCounter()].push_back({attacker->GetGUID(),damage});
}
void ClientlessPlayerMgr::TextEmote(Player* source, Player* target, uint32 emote)
{
if(!source || !target || source==target || !target->GetSession()->IsClientless())return;
std::lock_guard<std::mutex> lock(_damageMutex);
auto& queue=_emoteNotices[target->GetGUID().GetCounter()];
if(queue.size()<8)queue.push_back({source->GetGUID(),emote});
}
void ClientlessPlayerMgr::MarkActivity(Player* player)
{
if(!player)return;
std::lock_guard<std::mutex> lock(_damageMutex);
auto now=GameTime::GetGameTime();
if(_activity.size()>4096)
{
for(auto it=_activity.begin();it!=_activity.end();)
if(now-it->second>600)it=_activity.erase(it);else ++it;
}
_activity[player->GetGUID()]=now;
}
time_t ClientlessPlayerMgr::LastActivity(ObjectGuid player)
{
std::lock_guard<std::mutex> lock(_damageMutex);
auto it=_activity.find(player);return it==_activity.end()?0:it->second;
}
void ClientlessPlayerMgr::SharedTargetDamage(Unit* victim,Unit* attacker,uint32 damage)
{
auto helper=attacker?attacker->GetControllingPlayer():nullptr;
if(!damage || !helper || !victim || helper==victim)return;
std::lock_guard<std::mutex> lock(_damageMutex);
for(auto unit:victim->getAttackers())
if(auto player=unit->ToPlayer())
if(player!=helper && player->GetSession()->IsClientless())
{
auto& queue=_helpNotices[player->GetGUID().GetCounter()];
if(queue.size()<8)queue.push_back({helper->GetGUID(),victim->GetGUID()});
}
}
@@ -39,6 +39,8 @@ public:
virtual void OnPlayerLogout(Player*) { }
// Notification after health was reduced, never the mutable pre-damage event.
virtual void OnDamageTaken(Player*, Unit*, uint32) { }
virtual void OnTextEmote(Player*, Player*, uint32) { }
virtual void OnSharedTargetDamage(Player*, ObjectGuid, ObjectGuid) { }
};
class TC_GAME_API ClientlessPlayerMgr
@@ -52,6 +54,10 @@ public:
bool IsManaged(uint32 characterGuid) const;
void DamageTaken(Player* player, Unit* attacker, uint32 damage);
void TextEmote(Player* source, Player* target, uint32 emote);
void SharedTargetDamage(Unit* victim,Unit* attacker,uint32 damage);
void MarkActivity(Player* player);
time_t LastActivity(ObjectGuid player);
uint32 GetAccountId(uint32 characterGuid) const;
uint32 GetManagedCount() const { return uint32(_records.size()); }
@@ -79,6 +85,11 @@ private:
};
struct DamageNotice { ObjectGuid Attacker; uint32 Damage; };
struct HelpNotice { ObjectGuid Helper,Target; };
std::unordered_map<uint32,std::deque<HelpNotice>> _helpNotices;
struct EmoteNotice { ObjectGuid Source; uint32 Emote; };
std::unordered_map<uint32,std::deque<EmoteNotice>> _emoteNotices;
std::unordered_map<ObjectGuid,time_t> _activity;
std::mutex _damageMutex;
std::unordered_map<uint32,std::deque<DamageNotice>> _damageNotices;
std::unordered_map<uint32, ManagedPlayer> _records;
@@ -34,6 +34,7 @@ return {
},
personality = {
group_loyalty = 0.35, -- Chance to stay when the scheduled intent changes.
sociability = 0.65,
adventurousness = 0.70,
wanderlust = 0.30,
@@ -18,6 +18,7 @@ return {
},
personality = {
group_loyalty = 0.25, -- Chance to stay when the scheduled intent changes.
sociability = 0.50,
adventurousness = 0.50,
wanderlust = 0.50,
@@ -20,6 +20,9 @@
#include "ClientlessWorldPointMgr.h"
#include "AdventureBehaviorMgr.h"
#include "AdventureCombatMgr.h"
#include "AdventureInteractionMgr.h"
#include "AdventureGeneratorMgr.h"
#include "AdventureSupplyMgr.h"
#include "ClientlessTravelGraphMgr.h"
#include "ClientlessRouteRecorderMgr.h"
#include "ClientlessLuaEngine.h"
@@ -59,6 +62,20 @@
#include <unordered_map>
#include <unordered_set>
namespace
{
bool GenerateBot(ChatHandler* handler,char const* args)
{ return sAdventureGeneratorMgr->Command(handler,args); }
bool InteractionStatus(ChatHandler* handler,char const* args)
{
std::istringstream input(args?args:"");uint32 guid=0;input>>guid;
auto session=sWorld->FindClientlessSession(guid);auto player=session?session->GetPlayer():nullptr;
if(!player || !sClientlessPlayerMgr->IsManaged(guid))
{handler->SendSysMessage("Usage: clientless interaction <online-managed-guid>");return false;}
sAdventureInteractionMgr->Report(handler,player);return true;
}
}
namespace ClientlessCombatPreparation
{
struct Gear {uint8 Slot;uint32 Entry;std::string Enchantments;uint8 RandomType;uint32 RandomId;};
@@ -259,6 +276,13 @@ namespace ClientlessCombatPreparation
for(auto const& aura:c->GetAppliedAuras())handler->PSendSysMessage("Target aura=%u",aura.first);
return true;
}
if(op=="supplies"){sAdventureSupplyMgr->Report(handler,player);return true;}
if(op=="testrecovery" && source>=1 && source<=80 && player->IsAlive() && !player->IsInCombat() && !sAdventureCombatMgr->Busy(player) && !player->IsBeingTeleported() && !player->GetTransport())
{
player->SetHealth(std::max(1u,player->GetMaxHealth()*(100-source)/100));
TC_LOG_INFO("server.adventurebot","SUPPLY_TEST_INJURY guid=%u missingPercent=%u",guid,source);
return true;
}
if(op=="testdeath" && player->IsAlive() && !player->IsBeingTeleported() && !player->GetTransport() && !player->InBattleground() && !player->InArena())
{
TC_LOG_INFO("server.adventurebot","DEATH_TEST_REQUEST guid=%u",guid);
@@ -2496,6 +2520,8 @@ static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*arg
static std::vector<ChatCommand> clientlessCommands =
{
{ "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, "" },
{ "luareload", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessLuaReload, "" },
{ "transport", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessTransport, "" },
{ "login", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessLogin, "" },
@@ -33,9 +33,14 @@ struct Position{
float GetExactDist(Position const* b)const{return GetExactDist(*b);}
};
struct Map{float GetHeight(int,float,float,float,bool,float){return 0;}}map;
struct Creature;
struct Unit:Position{virtual Creature* ToCreature(){return nullptr;}};
struct Creature:Unit{ObjectGuid guid;bool valid=true,alive=true,controlled=false,evading=false,boss=false,phase=true,visible=true,inWorld=true;unsigned level=60;std::string script="mob";Creature(unsigned n):guid{n}{}ObjectGuid GetGUID(){return guid;}Creature* ToCreature()override{return this;}bool GetControllingPlayer(){return controlled;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsEvadingAttacks(){return evading;}bool isWorldBoss(){return boss;}unsigned getLevel(){return level;}std::string GetScriptName(){return script;}};
struct Creature;struct Player;
struct Unit:Position{
ObjectGuid guid;bool valid=true,alive=true,phase=true,visible=true,inWorld=true;
virtual Creature* ToCreature(){return nullptr;}virtual Player* ToPlayer(){return nullptr;}
virtual bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}virtual ObjectGuid GetGUID(){return guid;}
void GetNearPoint(Player*,float&a,float&b,float&c,float,float){a=x;b=y;c=z;}float GetAngle(Player*){return 0;}
};
struct Creature:Unit{bool controlled=false,evading=false,boss=false;unsigned level=60;std::string script="mob";Creature(unsigned n){guid={n};}ObjectGuid GetGUID(){return guid;}Creature* ToCreature()override{return this;}bool GetControllingPlayer(){return controlled;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsEvadingAttacks(){return evading;}bool isWorldBoss(){return boss;}unsigned getLevel(){return level;}std::string GetScriptName(){return script;}};
struct Player;
struct Corpse:Position{
time_t released=100;int saves=0;ObjectGuid GetGUID(){return {700};}time_t GetGhostTime(){return released;}int GetType(){return 0;}
@@ -45,10 +50,10 @@ struct WorldPacket{int opcode;WorldPacket(int o,int):opcode(o){}template<class T
struct Session{Player* owner=nullptr;int releases=0,reclaims=0;void HandleRepopRequestOpcode(WorldPacket&);void HandleReclaimCorpseOpcode(WorldPacket&);};
struct Player:Unit{
bool alive=true,ghost=false,teleport=false,combat=false,melee=true,los=true;unsigned delay=30;int saves=0,revives=0,bones=0,graveyards=0;bool sickness=false;int durability=75;
Session session;Player(){session.owner=this;}ObjectGuid GetGUID(){return {26};}bool IsAlive(){return alive;}bool IsInWorld(){return true;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool GetTransport(){return false;}bool IsInFlight(){return false;}bool IsCharmed(){return false;}
Session session;Player(){session.owner=this;guid={26};}ObjectGuid GetGUID(){return guid;}Player* ToPlayer()override{return this;}bool GetGroup(){return false;}bool IsAlive(){return alive;}bool IsInWorld(){return true;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool GetTransport(){return false;}bool IsInFlight(){return false;}bool IsCharmed(){return false;}
unsigned GetMapId(){return 0;}unsigned GetInstanceId(){return 0;}bool InBattleground(){return false;}bool InArena(){return false;}bool HasFlag(int,int){return ghost;}Session*GetSession(){return &session;}Corpse*GetCorpse(){return &corpse;}
uint32 GetCorpseReclaimDelay(bool){return delay;}Map*GetMap(){return &map;}int GetPhaseShift(){return 0;}
bool IsInPhase(Creature*c){return c->phase;}bool CanSeeOrDetect(Creature*c){return c->visible;}bool IsValidAttackTarget(Creature*c){return c->valid;}unsigned getLevel(){return 60;}bool IsWithinMeleeRange(Creature*){return melee;}float GetCombatReach(){return 1;}float GetAngle(Player*){return 0;}
bool IsInPhase(Unit*c){return c->phase;}bool CanSeeOrDetect(Unit*c){return c->visible;}bool IsValidAttackTarget(Unit*c){return c->valid;}unsigned getLevel(){return 60;}bool IsWithinMeleeRange(Unit*){return melee;}float GetCombatReach(){return 1;}float GetAngle(Player*){return 0;}
void SaveToDB(){++saves;}void RepopAtGraveyard(){++graveyards;teleport=true;Relocate(-100,0,0);}void ResurrectPlayer(float hp,bool sick){assert(hp==0.5f);sickness=sick;alive=true;ghost=false;++revives;}void SpawnCorpseBones(){++bones;}
void StopMoving(){}
};
@@ -59,7 +64,7 @@ void Session::HandleReclaimCorpseOpcode(WorldPacket&){++reclaims;assert(corpse.I
void nearPoint(Creature*,Player*,float&,float&,float&,float,float){}
struct FakeCreature:Creature{using Creature::Creature;void GetNearPoint(Player*,float&x,float&y,float&z,float,float){x=this->x;y=this->y;z=this->z;}float GetAngle(Player*){return 0;}};
namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}}
namespace ObjectAccessor{std::unordered_map<unsigned,Creature*> objects;Creature* GetCreature(Player&,ObjectGuid g){auto i=objects.find(g.id);return i==objects.end()?nullptr:i->second;}}
namespace ObjectAccessor{std::unordered_map<unsigned,Unit*> objects;Unit* GetUnit(Player&,ObjectGuid g){auto i=objects.find(g.id);return i==objects.end()?nullptr:i->second;}}
struct ClientlessPathDiagnostics{bool Complete=true,Suspicious=false;float PathLength=100;};
enum class ClientlessTravelState{Moving,Arrived,Stuck,PathRejected};
struct TravelStatus{ClientlessTravelState State=ClientlessTravelState::Arrived;Position Target;};
@@ -78,11 +83,11 @@ code=code.replace('bool GetControllingPlayer(){return controlled;}bool IsAlive()
# Forward declaration needed in Creature's adapter.
code=code.replace('struct Creature;','struct Creature;struct Player;',1)
code+=header+r'''
class AdventureCombatMgr::Fighter{public:int ticks=0;void Tick(Creature*,uint32){++ticks;}};
class AdventureCombatMgr::Fighter{public:int ticks=0;void Tick(Unit*,uint32){++ticks;}};
bool AdventureCombatMgr::Reachable(Player*p,Creature*)const{return p->los;}
bool AdventureCombatMgr::Reachable(Player*p,Unit*)const{return p->los;}
Creature* AdventureCombatMgr::Proactive(Player*)const{return nullptr;}
bool AdventureCombatMgr::Start(Player*,Record&r,Creature*c,bool train,bool defense,time_t until){r.AI=std::make_shared<Fighter>();r.Target=c->guid;r.Training=train;r.Defending=defense;r.Until=until;return true;}
bool AdventureCombatMgr::Start(Player*,Record&r,Unit*c,bool train,bool defense,time_t until){r.AI=std::make_shared<Fighter>();r.Target=c->GetGUID();r.Training=train;r.Defending=defense;r.Until=until;return true;}
void AdventureCombatMgr::Finish(Player*,Record&r){r.AI.reset();r.Target.Clear();r.Defending=false;r.Training=false;r.Cooldown=10000;}
'''
for name in ('bool AdventureCombatMgr::Valid(', 'void AdventureCombatMgr::DamageTaken(', 'bool AdventureCombatMgr::Death(', 'bool AdventureCombatMgr::Update('):code+='\n'+function(name)
@@ -101,6 +106,9 @@ int main(){
assert(SegmentDistance(5,3,0,0,0,0,10,0,0)==3);
AdventureCombatMgr mgr;Player p;Creature a(1),b(2),c(3);ObjectAccessor::objects={{1,&a},{2,&b},{3,&c}};
Player enemy;enemy.guid={99};ObjectAccessor::objects[99]=&enemy;
assert(mgr.Valid(&p,&enemy,false));assert(!mgr.Valid(&p,&enemy,true));
enemy.valid=false;assert(!mgr.Valid(&p,&enemy,false));enemy.valid=true;
assert(mgr.Valid(&p,&a,false));assert(!mgr.Valid(&p,&a,true));
Creature dummy(4);dummy.script="npc_training_dummy";assert(mgr.Valid(&p,&dummy,true));assert(!mgr.Valid(&p,&dummy,false));
dummy.level=63;assert(!mgr.Valid(&p,&dummy,true));dummy.level=60;
@@ -110,6 +118,7 @@ int main(){
dummy.controlled=true;assert(!mgr.Valid(&p,&dummy,true));dummy.controlled=false;
dummy.valid=false;assert(!mgr.Valid(&p,&dummy,true));
auto&r=mgr._records[26];
mgr.DamageTaken(&p,&enemy,1);assert(r.Damagers.front().id==99);r.Damagers.clear();
mgr.DamageTaken(&p,&a,0);assert(r.Damagers.empty());
mgr.DamageTaken(&p,&a,5);mgr.DamageTaken(&p,&b,5);mgr.Update(&p,500);
assert(r.Target.id==1 && r.Defending); // first actual damager
@@ -117,6 +126,10 @@ int main(){
a.alive=false;mgr.Update(&p,500);assert(!r.AI);mgr.Update(&p,500);assert(r.Target.id==2);
mgr.Finish(&p,r);r.Damagers.clear();mgr.Start(&p,r,&c,true,false,500);
mgr.DamageTaken(&p,&b,1);mgr.Update(&p,500);assert(r.Target.id==2 && r.Defending && !r.Training);
// Actual player attackers use the same first-damager focus and native PvP permission.
mgr.Finish(&p,r);r.Damagers.clear();mgr.DamageTaken(&p,&enemy,1);mgr.Update(&p,500);
assert(r.Target.id==99 && r.Defending);mgr.DamageTaken(&p,&c,1);mgr.Update(&p,500);assert(r.Target.id==99);
enemy.valid=false;mgr.Update(&p,500);assert(!r.AI);r.Damagers.clear();
// Release through native handler and save ghost plus corpse timestamp.
mgr.Finish(&p,r);p.alive=false;p.ghost=false;r.Dead=false;mgr.Death(&p,r);
assert(p.ghost && p.session.releases==1 && p.saves==1 && corpse.saves==1);
@@ -0,0 +1,39 @@
"""Execute production equipment proc affinity, including mana-trinket rejection."""
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(' bool UsefulSpell(');b=s.index('{',a);e=b+1;depth=1
while depth:depth+=(s[e]=='{')-(s[e]=='}');e+=1
code=r'''
#include "AdventureGeneratorPolicy.h"
#include <cassert>
#include <unordered_map>
#include <vector>
using uint32=unsigned;using Archetype=AdventureGeneratorPolicy::Archetype;
enum {ITEM_MOD_INTELLECT=5,POWER_MANA=0,SPELL_AURA_PERIODIC_ENERGIZE=1,SPELL_AURA_MOD_POWER_REGEN,SPELL_AURA_MOD_POWER_REGEN_PERCENT,SPELL_AURA_MOD_INCREASE_ENERGY,SPELL_AURA_MOD_INCREASE_ENERGY_PERCENT,SPELL_EFFECT_ENERGIZE,SPELL_EFFECT_ENERGIZE_PCT,SPELL_AURA_MOD_STAT,SPELL_AURA_MOD_SPELL_HIT_CHANCE,SPELL_AURA_MOD_SPELL_CRIT_CHANCE,SPELL_AURA_MOD_SPELL_CRIT_CHANCE_SCHOOL,SPELL_AURA_MOD_HEALING_DONE,SPELL_AURA_MOD_DAMAGE_DONE,SPELL_AURA_MOD_ATTACK_POWER,SPELL_AURA_MOD_RANGED_ATTACK_POWER};
enum class StatType{Strength=0,Agility=1,Intellect=3};
struct Effect{int ApplyAuraName=0,MiscValue=0,Effect=0;unsigned TriggerSpell=0;};
struct Info{std::vector<Effect>Effects;};
struct Spells{std::unordered_map<unsigned,Info> data;Info*GetSpellInfo(unsigned id){auto i=data.find(id);return i==data.end()?nullptr:&i->second;}}spells;auto sSpellMgr=&spells;
'''+s[a:e]+r'''
int main(){
auto warrior=AdventureGeneratorPolicy::Archetypes()[0],mage=AdventureGeneratorPolicy::Archetypes()[7];
spells.data[1].Effects={{SPELL_AURA_PERIODIC_ENERGIZE,POWER_MANA,0,0}};
assert(!UsefulSpell(1,warrior));assert(UsefulSpell(1,mage));
spells.data[2].Effects={{0,POWER_MANA,SPELL_EFFECT_ENERGIZE,0}};assert(!UsefulSpell(2,warrior));
spells.data[3].Effects={{0,0,0,1}};assert(!UsefulSpell(3,warrior));assert(UsefulSpell(3,mage));
spells.data[4].Effects={{SPELL_AURA_MOD_STAT,int(StatType::Intellect),0,0}};assert(!UsefulSpell(4,warrior));assert(UsefulSpell(4,mage));
spells.data[5].Effects={{SPELL_AURA_MOD_STAT,int(StatType::Strength),0,0}};assert(UsefulSpell(5,warrior));assert(!UsefulSpell(5,mage));
spells.data[6].Effects={{0,0,0,6}};assert(UsefulSpell(6,warrior)); // bounded cyclic trigger metadata
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
}
'''
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 equipment affinity: mana-only/direct/triggered procs, caster/physical stat effects and bounded cycles passed')
@@ -0,0 +1,64 @@
"""Exercise production generator command gates for ownership/retirement."""
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('bool AdventureGeneratorMgr::Command(');b=s.index('{',a);e=b+1;depth=1
while depth:depth+=(s[e]=='{')-(s[e]=='}');e+=1
code=r'''
#include <cassert>
#include "AdventureGeneratorPolicy.h"
#include <cstdint>
#include <memory>
#include <sstream>
#include <string>
using uint32=uint32_t;using uint8=uint8_t;
struct Field{unsigned number=0;std::string str="ready";unsigned GetUInt32(){return number;}unsigned GetUInt8(){return number;}std::string GetString(){return str;}};
struct Row{Field f[6];Field*Fetch(){return f;}bool NextRow(){return false;}};
struct Transaction{template<class...A>void PAppend(char const*,A...){}};
struct Future{bool get(){return true;}};struct Commit{Future m_future;};
struct DB{bool owned=true,persistent=false,online=false;int retired=0;std::string state="ready";
template<class...A>std::shared_ptr<Row>PQuery(char const*,A...){if(!owned)return nullptr;auto row=std::make_shared<Row>();row->f[0].number=6;row->f[1].str=state;row->f[2].number=persistent;row->f[3].number=online;row->f[4].str="warrior";row->f[5].number=42;return row;}
std::shared_ptr<Row>Query(char const*){return nullptr;}auto BeginTransaction(){return std::make_shared<Transaction>();}Commit AsyncCommitTransaction(std::shared_ptr<Transaction>){++retired;return {};}
}CharacterDatabase;
struct ChatHandler{void SendSysMessage(char const*){}template<class...A>void PSendSysMessage(char const*,A...){} };
bool schema=true;bool Schema(){return schema;}
namespace GameTime{unsigned GetGameTime(){return 100;}}
enum class ClientlessPlayerState{Offline,Online,Removing};
constexpr int UNIT_STATE_CASTING=1,POWER_MANA=0;
struct Player{bool combat=false,casting=false;int saves=0;bool IsInWorld(){return true;}bool IsAlive(){return true;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return false;}bool GetTransport(){return false;}bool IsInFlight(){return false;}bool HasUnitState(int){return casting;}unsigned getClass(){return 1;}void UpdateMaxHealth(){}void SetFullHealth(){}void SetFullPower(int){}void SaveToDB(std::shared_ptr<Transaction>){++saves;}};
struct Session{Player player;Player*GetPlayer(){return &player;}};
int equipped=0;bool Equip(Player*,AdventureGeneratorPolicy::Archetype const&,std::mt19937&,std::string&){++equipped;return true;}
struct Behavior{void InterruptForInteraction(Player*){}}behavior;auto sAdventureBehaviorMgr=&behavior;
#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 AdventureGeneratorMgr{
int creates=0;bool Command(ChatHandler*,char const*);
bool Create(ChatHandler*,uint32,uint32,uint8,unsigned,std::string const&,bool){++creates;return true;}
bool Profile(unsigned,std::string&lua){lua="return {}";return true;}
};
'''+s[a:e]+r'''
int main(){
AdventureGeneratorMgr mgr;ChatHandler handler;auto run=[&](char const*args){return mgr.Command(&handler,args);};
schema=false;assert(!run("create 6 1"));schema=true;
assert(run("create 6 1"));assert(mgr.creates==1);assert(run("create 6 1 "));assert(mgr.creates==2);
assert(!run("create 6 0"));assert(!run("create 6 11"));assert(!run("create 6 1 256 any any temporary 1"));assert(mgr.creates==2);
assert(run("create 6 2 60 horde warrior temporary 42"));assert(mgr.creates==4);
CharacterDatabase.owned=false;assert(!run("retire 26"));assert(!run("logout 26"));assert(!run("login 26"));assert(!run("equip 26"));assert(!core.stops && !core.starts && !CharacterDatabase.retired);
CharacterDatabase.owned=true;CharacterDatabase.persistent=true;assert(!run("retire 30"));CharacterDatabase.persistent=false;
CharacterDatabase.online=true;assert(!run("retire 30"));CharacterDatabase.online=false;
world.state=ClientlessPlayerState::Removing;assert(!run("retire 30"));world.state=ClientlessPlayerState::Offline;
assert(run("retire 30"));assert(CharacterDatabase.retired==1);
CharacterDatabase.state="retired";assert(!run("login 30"));assert(!core.starts);CharacterDatabase.state="ready";
assert(run("login 30"));assert(core.starts==1);assert(run("logout 30"));assert(core.stops==1);
assert(!run("equip 30"));world.available=true;world.session.player.combat=true;assert(!run("equip 30"));world.session.player.combat=false;
world.session.player.casting=true;assert(!run("equip 30"));world.session.player.casting=false;
assert(run("equip 30"));assert(equipped==1 && world.session.player.saves==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 generator commands: batch bounds, dedicated ownership, persistent/online/removing retirement protection and retired login refusal passed')
@@ -0,0 +1,50 @@
"""Execute the actual cultural naming/profile generator with real Lua."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
code=r'''
#include "AdventureGeneratorPolicy.h"
#include <lua.hpp>
#include <cassert>
#include <cctype>
#include <set>
int main(){
using namespace AdventureGeneratorPolicy;
std::set<std::string> global;
lua_State* L=luaL_newstate();
for(unsigned race:{1,2,3,4,5,6,7,8,9,10,11,22})for(unsigned gender:{0,1}){
std::set<std::string> cultural;
for(unsigned seed=0;seed<256;++seed){
std::mt19937 r(seed),again(seed);auto name=Name(race,gender,r);assert(name==Name(race,gender,again));
assert(name.size()>=3 && name.size()<=12 && std::isupper(name[0]));for(unsigned i=1;i<name.size();++i)assert(std::islower(name[i]));
cultural.insert(name);global.insert(name);
for(auto const&a:Archetypes()){
auto profile=Profile(name,a,race,"local_tavern","home_region",r);
assert(luaL_loadbuffer(L,profile.data(),profile.size(),"generated-profile")==LUA_OK);assert(lua_pcall(L,0,1,0)==LUA_OK);
lua_getfield(L,-1,"identity");lua_getfield(L,-1,"name");assert(name==lua_tostring(L,-1));lua_pop(L,1);
lua_getfield(L,-1,"background");assert(std::string(lua_tostring(L,-1)).find(Origin(race))!=std::string::npos);lua_pop(L,2);
lua_getfield(L,-1,"professions");lua_getfield(L,-1,"primary");assert(lua_rawlen(L,-1)==2);
lua_rawgeti(L,-1,1);assert(std::string(lua_tostring(L,-1))==AdventureProfessionPolicy::PrimaryForSkill(a.Skills[0]));lua_pop(L,3);
lua_getfield(L,-1,"personality");for(auto key:{"sociability","adventurousness","wanderlust","group_loyalty"}){lua_getfield(L,-1,key);auto value=lua_tonumber(L,-1);assert(value>=.25 && value<=.9);lua_pop(L,1);}lua_pop(L,1);
lua_getfield(L,-1,"behavior");lua_getfield(L,-1,"initial_location");assert(std::string(lua_tostring(L,-1))=="local_tavern");lua_settop(L,0);
}
}
assert(cultural.size()>=50);
}
assert(global.size()>900);
std::string raw="A quote \" slash \\ newline\nreturn os.execute('no')";auto escaped=LuaString(raw);
assert(luaL_dostring(L,("return "+escaped).c_str())==LUA_OK);assert(raw==lua_tostring(L,-1));lua_settop(L,0);
auto warrior=Archetypes()[0];assert(Armor(warrior,39)==3 && Armor(warrior,40)==4);
auto hunter=Archetypes()[2];assert(Armor(hunter,39)==2 && Armor(hunter,40)==3);
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));
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','-I',str(root/'src/server/game/World'),str(p/'test.cpp'),'-o',str(p/'test'),*flags],check=True)
subprocess.run([str(p/'test')],check=True)
print('Production generator: 6144 cultural names and 61440 real Lua profiles, profession coherence, personality ranges, escaping and equipment/armor gates passed')
@@ -0,0 +1,118 @@
"""Run production interaction manager against deterministic native-world adapters."""
from pathlib import Path
import subprocess
import tempfile
root=Path(__file__).resolve().parents[2]
header=(root/'src/server/game/World/AdventureInteractionMgr.h').read_text()
header='\n'.join(line for line in header.splitlines() if not line.startswith('#include')).replace('private:','public:')
source=(root/'src/server/game/World/AdventureInteractionMgr.cpp').read_text()
source='\n'.join(line for line in source.splitlines() if not line.startswith('#include'))
code=r'''
#include "AdventureInteractionPolicy.h"
#include <algorithm>
#include <cassert>
#include <cmath>
#include <ctime>
#include <string>
#include <unordered_map>
#include <vector>
using uint32=unsigned;
#define TC_GAME_API
#define TC_LOG_INFO(...) ((void)0)
struct ObjectGuid { unsigned id=0;unsigned GetCounter()const{return id;}void Clear(){id=0;}explicit operator bool()const{return id!=0;}bool operator==(ObjectGuid b)const{return id==b.id;}bool operator!=(ObjectGuid b)const{return id!=b.id;}};
namespace std { template<>struct hash<ObjectGuid>{size_t operator()(ObjectGuid g)const{return g.id;}}; }
struct Position {
float m_positionX=0,m_positionY=0,m_positionZ=0,o=0;
void Relocate(Position const*p){*this=*p;}
float GetPositionX()const{return m_positionX;}float GetPositionY()const{return m_positionY;}float GetPositionZ()const{return m_positionZ;}float GetOrientation()const{return o;}
float GetExactDist(Position const*p)const{return std::hypot(m_positionX-p->m_positionX,m_positionY-p->m_positionY);}
};
enum {TEXT_EMOTE_RUDE=1,TEXT_EMOTE_THREATEN,TEXT_EMOTE_ROAR,TEXT_EMOTE_GREET,TEXT_EMOTE_HELLO,TEXT_EMOTE_WAVE,TEXT_EMOTE_BOW,TEXT_EMOTE_SALUTE,TEXT_EMOTE_SMILE,TEXT_EMOTE_THANK,EMOTE_ONESHOT_BOW,EMOTE_ONESHOT_WAVE,EMOTE_ONESHOT_NO,UNIT_STAND_STATE_STAND,ALLIANCE,LANG_COMMON,LANG_ORCISH,CHAT_MSG_PARTY,GROUP_REMOVEMETHOD_LEAVE,CMSG_PARTY_INVITE_RESPONSE,UNIT_STATE_CASTING};
struct WorldPacket { explicit WorldPacket(int=0){} };
struct Player;struct Group;
namespace WorldPackets {namespace Party {struct PartyInviteResponse{explicit PartyInviteResponse(WorldPacket&&){}bool Accept=false;};}}
struct Session {Player*owner=nullptr;bool clientless=false;bool IsClientless(){return clientless;}void HandlePartyInviteResponseOpcode(WorldPackets::Party::PartyInviteResponse&);};
struct Map {bool dungeon=false;bool IsDungeon(){return dungeon;}};
struct Unit:Position {Player*victim=nullptr;std::vector<Unit*> attackers;virtual ~Unit()=default;virtual struct Creature*ToCreature(){return nullptr;}Unit*GetVictim(){return reinterpret_cast<Unit*>(victim);}auto&getAttackers(){return attackers;}};
struct Creature:Unit{};
struct Member {Player*player=nullptr;Member*nextMember=nullptr;Player*GetSource(){return player;}Member*next(){return nextMember;}};
struct Player:Unit {
ObjectGuid guid,selection;bool alive=true,inWorld=true,combat=false,teleport=false,transport=false,flight=false,charmed=false,casting=false,afk=false,visible=true,phase=true,los=true;
unsigned mapid=0,team=ALLIANCE;Session session;Group*group=nullptr,*invite=nullptr;Map map;int gestures=0,says=0,posture=0;uint32 lastEmote=0;
explicit Player(unsigned id=26){guid.id=id;session.owner=this;}
ObjectGuid GetGUID(){return guid;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool GetTransport(){return transport;}bool IsInFlight(){return flight;}bool IsCharmed(){return charmed;}bool HasUnitState(int){return casting;}
bool IsInMap(Player*p){return mapid==p->mapid;}bool IsInPhase(Player*p){return phase && p->phase;}bool CanSeeOrDetect(Player*p){return p->visible;}bool IsWithinLOSInMap(Player*p){return los && p->los;}bool IsFriendlyTo(Player*p){return team==p->team;}
Map*GetMap(){return &map;}unsigned GetMapId(){return mapid;}Session*GetSession(){return &session;}Group*GetGroup(){return group;}Group*GetGroupInvite(){return invite;}unsigned GetTeam(){return team;}ObjectGuid GetTarget(){return selection;}bool isAFK(){return afk;}
void SetStandState(int s){posture=s;}void SetFacingToObject(Player*){}void HandleEmoteCommand(uint32 e){++gestures;lastEmote=e;}void Say(char const*,int,Player*){++says;}
void GetNearPoint(Player*,float&x,float&y,float&z,float,float){x=m_positionX;y=m_positionY;z=m_positionZ;}
std::vector<Player*> people;void GetPlayerListInGrid(std::vector<Player*>&out,float){out=people;}
};
struct Group {ObjectGuid leader;Member first;int removals=0,messages=0;ObjectGuid GetLeaderGUID(){return leader;}Member*GetFirstMember(){return first.player?&first:nullptr;}void BroadcastPacket(WorldPacket*,bool){++messages;}void RemoveMember(ObjectGuid,int){++removals;assert(first.player);first.player->group=nullptr;}};
int accepts=0,declines=0;
void Session::HandlePartyInviteResponseOpcode(WorldPackets::Party::PartyInviteResponse&p){assert(owner->invite);if(p.Accept){++accepts;owner->group=owner->invite;}else ++declines;owner->invite=nullptr;}
struct ChatHandler{void SendSysMessage(char const*){}template<class...Args>void PSendSysMessage(char const*,Args...){}static void BuildChatPacket(WorldPacket&,int,int,Player*,Player*,char const*){}};
namespace GameTime {time_t now=100;time_t GetGameTime(){return now;}}
unsigned urand(unsigned a,unsigned){return a;}float roll=0;float frand(float,float){return roll;}
namespace ObjectAccessor {std::unordered_map<ObjectGuid,Player*> players;Player*FindPlayer(ObjectGuid id){auto it=players.find(id);return it==players.end()?nullptr:it->second;}}
struct Profile {std::string Locale="deDE";std::unordered_map<std::string,float> Personality;};
struct Lua {std::unordered_map<unsigned,Profile> profiles;Profile const*GetAdventureProfile(unsigned id){auto it=profiles.find(id);return it==profiles.end()?nullptr:&it->second;}}lua;auto sClientlessLuaEngine=&lua;
struct Behavior {int interrupts=0;std::string intent="social",schedule="social:inn";std::string InteractionIntent(Player*){return intent;}std::string InteractionSchedule(Player*){return schedule;}void InterruptForInteraction(Player*){++interrupts;}}behavior;auto sAdventureBehaviorMgr=&behavior;
struct Combat {bool busy=false;int assists=0;bool Busy(Player*){return busy;}bool Assist(Player*,Unit*){++assists;return true;}}combat;auto sAdventureCombatMgr=&combat;
struct ClientlessAction{std::string Name;std::unordered_map<std::string,std::string> Parameters;};
struct Core {time_t activity=0;int queued=0;time_t LastActivity(ObjectGuid){return activity;}bool QueueAction(unsigned,ClientlessAction){++queued;return true;}}core;auto sClientlessPlayerMgr=&core;
enum class ClientlessTravelState{Moving,Arrived};struct Nav {ClientlessTravelState State=ClientlessTravelState::Arrived;Position Target;};
struct ClientlessPathDiagnostics{bool Complete=true,Suspicious=false;float PathLength=10;};
struct Travel {Nav nav;bool active=false,valid=true;int starts=0,cancels=0;Nav*GetStatus(unsigned){return active?&nav:nullptr;}bool AnalyzePath(Player*,Position const&,ClientlessPathDiagnostics&){return valid;}void StartTravel(Player*,Position const&p){++starts;nav.Target=p;nav.State=ClientlessTravelState::Moving;active=true;}void CancelTravel(unsigned){++cancels;active=false;}}travel;auto sClientlessTravelMgr=&travel;
enum class ClientlessActionExecutionState{Moving,SelectingTarget,Completed};struct Execution {ClientlessActionExecutionState State=ClientlessActionExecutionState::Completed;};
struct Executor {Execution status;Execution*GetStatus(unsigned){return &status;}}executor;auto sClientlessActionExecutorMgr=&executor;
struct Graph {void EnsureLoaded(){}bool FindNearestNodeToPosition(unsigned,float,float,float,std::string&node,float&distance){node="entrance";distance=10;return true;}}graph;auto sClientlessTravelGraphMgr=&graph;
'''+header+'\n'+source+r'''
int main(){
using namespace AdventureInteractionPolicy;
assert(Willingness("adventure",.65f)>Willingness("profession",.65f));assert(Willingness("rest",0)==0);
assert(!LeaderExpired(699,100));assert(LeaderExpired(700,100));assert(!LeaderExpired(99,100));
assert(CanLeave(true,false,false,false));assert(!CanLeave(true,true,false,false));assert(!CanLeave(true,false,true,false));
AdventureInteractionMgr mgr;Player bot(26),leader(21),other(27);bot.session.clientless=true;other.session.clientless=true;
ObjectAccessor::players={{bot.guid,&bot},{leader.guid,&leader},{other.guid,&other}};
lua.profiles[26].Personality["sociability"]=.65f;lua.profiles[26].Personality["group_loyalty"]=0;lua.profiles[27]={};
Group group;group.leader=leader.guid;group.first.player=&bot;
auto tick=[&](time_t now){GameTime::now=now;return mgr.Update(&bot,1000);};
// Native invitation response creates actual membership, then suspends intent.
bot.invite=&group;tick(100);assert(!bot.group);tick(102);assert(bot.group && accepts==1);
assert(mgr._records[26].Grouped && behavior.interrupts==1);
// Follow uses verified local MMAP paths; invalid paths never move the player.
leader.m_positionX=20;tick(103);assert(travel.starts==1);travel.active=false;travel.valid=false;leader.m_positionX=30;tick(104);assert(travel.starts==1);travel.valid=true;
// Changing leader resets both AFK and inactivity timers.
mgr._records[26].AfkSince=1;group.leader=other.guid;tick(105);assert(mgr._records[26].AfkSince==0 && mgr._records[26].LastActivity==105);
group.leader=leader.guid;tick(106);
// Chat/spells count as activity even while the leader stands still.
core.activity=650;tick(650);tick(1249);assert(bot.group);tick(1250);assert(!bot.group && group.removals==1);
// Hour intent change is deferred inside instances and during combat.
core.activity=0;bot.group=&group;behavior.schedule="adventure:road";tick(1300);behavior.schedule="economy:city";
bot.map.dungeon=true;tick(1301);assert(bot.group);bot.map.dungeon=false;bot.combat=true;tick(1302);assert(bot.group);bot.combat=false;tick(1303);assert(!bot.group);
// Native AFK needs a full ten minutes even if manually set; no early exit.
bot.group=&group;tick(1400);leader.afk=true;tick(1401);tick(2000);assert(bot.group);tick(2001);assert(!bot.group);leader.afk=false;
// Grouped bots also answer directed emotes; responses do not create events.
leader.m_positionX=0;bot.group=&group;tick(2100);mgr.Emote(&bot,&leader,TEXT_EMOTE_WAVE);tick(2101);assert(bot.gestures==1);tick(2102);assert(bot.gestures==1);tick(2103);assert(bot.group);
// Friendly greeting versus hostile contact; no combat call is made.
bot.group=nullptr;tick(2120);mgr.Emote(&bot,&other,TEXT_EMOTE_WAVE);tick(2121);assert(bot.lastEmote==EMOTE_ONESHOT_WAVE);tick(2123);
other.team=999;mgr._records[26].Encounters.clear();mgr._records[26].NextSocial=0;mgr.Emote(&bot,&other,TEXT_EMOTE_THREATEN);tick(2124);assert(bot.lastEmote==EMOTE_ONESHOT_NO && combat.assists==0);tick(2126);
// Invisible/distant targets and repeated emotes are ignored.
auto gestures=bot.gestures;other.visible=false;mgr.Emote(&bot,&other,TEXT_EMOTE_WAVE);tick(2130);assert(bot.gestures==gestures);other.visible=true;
// Bot encounters reserve both participants; cooldown prevents new loops.
other.team=ALLIANCE;bot.people={&other};mgr._records.clear();tick(2200);assert(mgr._records[26].Partner==other.guid && mgr._records[27].Partner==bot.guid);
tick(2201);GameTime::now=2203;mgr.Update(&other,1000);assert(other.gestures==1);tick(2205);GameTime::now=2205;mgr.Update(&other,1000);tick(2265);assert(!mgr._records[26].Partner);
// Bot-origin invitations are declined; autonomous bot grouping is deferred.
mgr._records.clear();group.leader=other.guid;bot.invite=&group;tick(2300);tick(2302);assert(!bot.group && declines==1);
// Missing leader map uses the existing graph executor, without teleporting.
group.leader=leader.guid;bot.group=&group;leader.mapid=1;tick(2400);assert(core.queued==1);executor.status.State=ClientlessActionExecutionState::Moving;tick(2416);assert(core.queued==1);
// Pending help is acknowledged only outside combat, after caller's self-care.
mgr.CombatHelp(&bot,leader.guid);leader.mapid=0;executor.status.State=ClientlessActionExecutionState::Completed;bot.combat=true;auto says=bot.says;tick(2417);assert(bot.says==says);bot.combat=false;tick(2418);assert(bot.says==says+1);
mgr.Offline(&bot);assert(!mgr._records.count(26));
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['g++','-std=c++17','-Wall','-Wextra','-I',str(root/'src/server/game/World'),str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('production interaction manager: invitations, factions, reservations, follow, AFK and deferred departure passed')
@@ -0,0 +1,97 @@
"""Execute the production recovery state machine with native-item adapters."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
source=(root/'src/server/game/World/AdventureSupplyMgr.cpp').read_text()
def function(signature):
a=source.index(signature);b=source.index('{',a);e=b+1;depth=1
while depth:depth+=(source[e]=='{')-(source[e]=='}');e+=1
return source[a:e]
header=(root/'src/server/game/World/AdventureSupplyMgr.h').read_text()
header='\n'.join(x for x in header.splitlines() if not x.startswith('#include')).replace('private:','public:')
code=r'''
#include "AdventureSupplyPolicy.h"
#include <algorithm>
#include <cassert>
#include <ctime>
#include <string>
#include <vector>
#include <unordered_map>
using uint32=unsigned;
#define TC_GAME_API
#define TC_LOG_INFO(...) ((void)0)
#define TC_LOG_WARN(...) ((void)0)
constexpr int POWER_MANA=0,POWER_RAGE=1,CURRENT_CHANNELED_SPELL=0,CURRENT_GENERIC_SPELL=1,UNIT_STAND_STATE_STAND=0,UNIT_STAND_STATE_SIT=1;
struct Guid{unsigned GetCounter(){return 26;}};
struct Spell{struct Info{unsigned Id=1;}info;Info* GetSpellInfo(){return &info;}};
struct Player{
unsigned hp=100,maxhp=100,mana=100,maxmana=100,type=POWER_RAGE;bool alive=true,combat=false,casting=false,aura=false,bandageAllowed=true,busy=false,foodAllowed=true;
int posture=0,stops=0;Spell spell;std::unordered_map<unsigned,unsigned> items;
Guid GetGUID(){return {};}bool IsInCombat(){return combat;}bool IsAlive(){return alive;}bool IsBeingTeleported(){return false;}bool GetTransport(){return false;}bool IsInFlight(){return false;}bool IsFlying(){return false;}bool IsCharmed(){return false;}bool IsFalling(){return false;}
unsigned GetHealth(){return hp;}unsigned GetMaxHealth(){return maxhp;}unsigned GetPower(int){return mana;}unsigned GetMaxPower(int){return maxmana;}unsigned GetPowerType(){return type;}
unsigned GetItemCount(unsigned entry){return items[entry];}void StopMoving(){++stops;}void SetStandState(int s){posture=s;}
Spell*GetCurrentSpell(int slot){return casting && slot==0?&spell:nullptr;}
bool HasAura(unsigned id,Guid){return aura && spell.info.Id==id;}
void InterruptNonMeleeSpells(bool,unsigned id){if(spell.info.Id==id)casting=false;}
void RemoveAurasDueToSpell(unsigned id,Guid){if(spell.info.Id==id)aura=false;}
};
namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}}
unsigned today=20261027;unsigned Day(time_t){return today;}
struct Behavior{int interrupts=0;void InterruptForCombat(Player*){++interrupts;}}behavior;auto sAdventureBehaviorMgr=&behavior;
struct Lua{void DismountGroundMount(Player*,char const*){}}lua;auto sClientlessLuaEngine=&lua;
'''+header+r'''
std::vector<AdventureSupplyPolicy::Use> uses;int stocks=0;unsigned savedDay=0;
void AdventureSupplyMgr::Online(Player*p){auto&r=_records[26];r.Loaded=true;r.LastDay=savedDay;}
bool AdventureSupplyMgr::Stock(Player*,Record&r,unsigned date){++stocks;r.LastDay=date;r.AfterCombat=false;savedDay=date;return true;}
AdventureSupplyMgr::UseResult AdventureSupplyMgr::TryUse(Player*p,Record&r,Use use){
uses.push_back(use);if(p->busy)return UseResult::Busy;
if((use==Use::Bandage && !p->bandageAllowed)||(use==Use::Food && !p->foodAllowed))return UseResult::Unavailable;
if(!p->combat){r.Active=use;r.Spell=unsigned(use);p->spell.info.Id=r.Spell;p->aura=true;r.Requested=GameTime::now;}
return UseResult::Started;
}
'''+function('void AdventureSupplyMgr::Cancel(')+'\n'+function('bool AdventureSupplyMgr::Update(')+r'''
int main(){
using U=AdventureSupplyPolicy::Use;using namespace AdventureSupplyPolicy;
assert(!MissingTenPercent(91,100));assert(MissingTenPercent(90,100));assert(!MissingTenPercent(1,0));
assert(!MissingTenPercent(4725,5249));assert(MissingTenPercent(4724,5249));
assert(TopUp(19,20)==1 && TopUp(20,20)==0 && TopUp(21,20)==0);
AdventureSupplyMgr mgr;Player p;auto tick=[&]{GameTime::now+=3;return mgr.Update(&p,500,false);};
assert(!tick() && stocks==1);assert(!tick() && stocks==1);
p.hp=91;assert(!tick() && uses.empty());
// At the exact threshold, bandage has priority over food.
p.hp=90;assert(tick() && uses.back()==U::Bandage);
p.aura=false;p.hp=95;assert(!tick() && uses.size()==1); // no food below10% missing
// New recovery: bandage finishes but health deficit still10% or more.
p.hp=40;assert(tick() && uses.back()==U::Bandage);
p.aura=false;p.hp=75;assert(tick() && uses.back()==U::Food);
auto count=uses.size();p.hp=95;assert(tick() && uses.size()==count); // keep current meal to full
p.hp=100;assert(!tick() && !p.aura && p.posture==UNIT_STAND_STATE_STAND);
// Missing First Aid / native bandage lockout: immediate food fallback.
p.hp=80;p.bandageAllowed=false;uses.clear();assert(tick());assert((uses==std::vector<U>{U::Bandage,U::Food}));
p.hp=100;assert(!tick());
// A busy global cooldown must not count as a failed bandage attempt.
p.bandageAllowed=true;p.hp=80;p.busy=true;uses.clear();assert(tick() && uses.back()==U::Bandage);
assert(!mgr._records[26].BandageTried);p.busy=false;assert(tick() && uses.back()==U::Bandage);
// Incoming combat interrupts only owned recovery; combat never eats/bandages.
p.combat=true;uses.clear();assert(!tick() && uses.back()==U::HealingPotion && !p.aura);
p.hp=100;p.type=POWER_MANA;p.mana=89;uses.clear();assert(!tick() && uses.back()==U::ManaPotion);
// Exactly one top-up after the actual combat interval, not per enemy/tick.
auto previous=stocks;p.combat=false;assert(tick() && stocks==previous+1 && uses.back()==U::Drink);
assert(tick() && stocks==previous+1);p.mana=100;assert(!tick());
// Warriors never drink; mana-only recovery starts at10% missing as well.
p.type=POWER_RAGE;p.mana=0;uses.clear();assert(!tick() && uses.empty());
p.type=POWER_MANA;p.mana=91;assert(!tick());p.mana=90;assert(tick() && uses.back()==U::Drink);
p.mana=100;assert(!tick());
// Persisted day blocks logout/login farming; next day allows a single top-up.
mgr._records.clear();uses.clear();previous=stocks;assert(!tick() && stocks==previous);
++today;assert(!tick() && stocks==previous+1);assert(!tick() && stocks==previous+1);
// Dead or combat-reaction-controlled bots never start a meal.
p.hp=40;p.alive=false;uses.clear();assert(!tick() && uses.empty());p.alive=true;
GameTime::now+=3;assert(!mgr.Update(&p,500,true) && uses.empty());
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['g++','-std=c++17','-I'+str(root/'src/server/game/World'),str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Production supplies: exact10% boundary, bandage-before-food, native lockout fallback, recovery completion, combat potions, mana-only drinking and daily/post-combat top-ups passed.')
@@ -0,0 +1,20 @@
"""Validate additive supply catalogs and persistent daily stamps on SQL fixtures."""
from pathlib import Path
import sqlite3,re
root=Path(__file__).resolve().parents[2]
def sqlite(sql):
sql=re.sub(r' ENGINE=InnoDB[^;]*','',sql)
return sql.replace('INSERT IGNORE','INSERT OR IGNORE').replace('REPLACE INTO','INSERT OR REPLACE INTO')
db=sqlite3.connect(':memory:')
world=(root/'sql/custom/world/2026_10_27_10_adventure_supplies.sql').read_text()
characters=(root/'sql/custom/characters/2026_10_27_01_adventure_supplies.sql').read_text()
db.executescript(sqlite(world));db.executescript(sqlite(characters))
roles={r[0] for r in db.execute('SELECT DISTINCT role FROM vcore_adventure_supply')}
assert roles=={'bandage','healing_potion','mana_potion','food','drink'}
assert db.execute("SELECT min_level FROM vcore_adventure_supply WHERE role='bandage' AND item_id=21991").fetchone()==(60,)
for role in roles:assert db.execute('SELECT COUNT(*) FROM vcore_adventure_supply WHERE role=? AND min_level<=60',(role,)).fetchone()[0]>0
db.execute('UPDATE vcore_adventure_supply SET enabled=0 WHERE item_id=22829')
db.execute('UPDATE vcore_adventure_supply SET min_level=59 WHERE item_id=21991')
db.execute('INSERT INTO vcore_adventure_supply_state VALUES(26,20261027)')
before=list(db.iterdump());db.executescript(sqlite(world));db.executescript(sqlite(characters));assert list(db.iterdump())==before
print('Supply migrations: all five roles, level60 netherweave tier, editable/disabled catalog preservation and persistent daily stamp passed.')
@@ -0,0 +1,124 @@
"""Execute production item selection/use and secondary-rank gates."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
source=(root/'src/server/game/World/AdventureSupplyMgr.cpp').read_text()
def function(signature):
a=source.index(signature);b=source.index('{',a);e=b+1;depth=1
while depth:depth+=(source[e]=='{')-(source[e]=='}');e+=1
return source[a:e]
header=(root/'src/server/game/World/AdventureSupplyMgr.h').read_text()
header='\n'.join(x for x in header.splitlines() if not x.startswith('#include')).replace('private:','public:')
code=r'''
#include "AdventureSupplyPolicy.h"
#include <algorithm>
#include <array>
#include <cassert>
#include <memory>
#include <ctime>
#include <string>
#include <vector>
#include <unordered_map>
#include <unordered_set>
using uint32=unsigned;
#define TC_GAME_API
#define TC_LOG_INFO(...) ((void)0)
constexpr int ITEM_CLASS_CONSUMABLE=0,ITEM_SUBCLASS_BANDAGE=7,ITEM_SUBCLASS_FOOD=5,ITEM_SUBCLASS_POTION=1,EQUIP_ERR_OK=0,UNIT_STATE_CASTING=1,ITEM_SPELLTRIGGER_ON_USE=0,TRIGGERED_NONE=0,TARGET_FLAG_UNIT=2,SPELL_EFFECT_LEARN_SPELL=36,SPELL_EFFECT_SKILL_STEP=44;
constexpr unsigned SKILL_FIRST_AID=129,SKILL_COOKING=185,SKILL_FISHING=356,SKILL_ARCHAEOLOGY=794;
struct Guid{unsigned id=26;unsigned GetCounter(){return id;}};
struct Effect{int Trigger=0,SpellID=0,Effect=0;unsigned TriggerSpell=0;int MiscValue=0,value=0;int CalcValue()const{return value;}};
struct ItemTemplate{
unsigned id,level,subclass,skill=0,rank=0;std::vector<Effect> Effects;
unsigned GetId()const{return id;}unsigned GetRequiredLevel()const{return level;}unsigned GetClass()const{return 0;}unsigned GetSubClass()const{return subclass;}bool IsPotion()const{return subclass==1;}
};
struct Item{ItemTemplate*proto;unsigned count=20;unsigned GetBagSlot(){return 19;}unsigned GetSlot(){return 0;}Guid GetGUID(){return {proto->id};}};
struct SpellInfo{unsigned Id=0,SpellLevel=0,BaseLevel=0;bool combat=true,autoCast=true;std::vector<Effect>Effects;bool CanBeUsedInCombat(){return combat;}};
struct Rank{unsigned skill,step,maximum;};
struct Player;
struct SpellMgr{
std::unordered_map<unsigned,SpellInfo>spells;std::unordered_map<unsigned,Rank>ranks;
SpellInfo*GetSpellInfo(unsigned id){auto i=spells.find(id);return i==spells.end()?nullptr:&i->second;}
Rank*GetSpellLearnSkill(unsigned id){auto i=ranks.find(id);return i==ranks.end()?nullptr:&i->second;}
static bool IsSpellValid(SpellInfo*,Player*,bool){return true;}
}spellMgr;auto sSpellMgr=&spellMgr;
struct ObjectMgr{std::unordered_map<unsigned,ItemTemplate>items;ItemTemplate*GetItemTemplate(unsigned id){auto i=items.find(id);return i==items.end()?nullptr:&i->second;}}objects;auto sObjectMgr=&objects;
struct RC{int MinLevel=1;}rc;
struct DBC{RC*GetSkillRaceClassInfo(unsigned,unsigned,unsigned){return &rc;}}sDBCManager;
struct Field{unsigned value=0;std::string GetString(){return {};};unsigned GetUInt32(){return value;}unsigned GetUInt8(){return value;}unsigned GetUInt16(){return value;}};
struct Row{std::vector<std::array<Field,3>> rows;size_t index=0;Field*Fetch(){return rows[index].data();}bool NextRow(){return ++index<rows.size();}};
struct Database{bool active=false;Row fixture;Row*Query(char const*){fixture.index=0;return active?&fixture:nullptr;}}WorldDatabase;
namespace GameTime{time_t GetGameTime(){return 100;}}
struct History{bool cooldown=false,gcd=false;bool HasGlobalCooldown(SpellInfo*){return gcd;}bool IsReady(SpellInfo*,unsigned){return !cooldown;}};
namespace WorldPackets{namespace Spells{struct SpellCastRequest{unsigned SpellID=0;struct{unsigned Flags=0;Guid Unit;}Target;};}}
struct SpellCastRequestItemData{unsigned bag,slot;Guid guid;SpellCastRequestItemData(unsigned b,unsigned s,Guid g):bag(b),slot(s),guid(g){}};
struct PendingSpellCastRequest{WorldPackets::Spells::SpellCastRequest request;SpellCastRequestItemData item;PendingSpellCastRequest(WorldPackets::Spells::SpellCastRequest&&r,SpellCastRequestItemData i):request(r),item(i){}};
struct Player{
unsigned level=60;bool combat=false,casting=false;History history;int requests=0;std::unordered_map<unsigned,unsigned>skills,caps,steps;std::unordered_set<unsigned>known;
std::unordered_map<unsigned,std::unique_ptr<Item>>inventory;
Guid GetGUID(){return {};}unsigned getLevel(){return level;}unsigned getRace(){return 1;}unsigned getClass(){return 1;}
bool IsInWorld(){return true;}
bool IsSpellFitByClassAndRace(unsigned){return true;}bool HasSpell(unsigned id){return known.count(id);}
unsigned GetPureSkillValue(unsigned skill){return skills[skill];}unsigned GetPureMaxSkillValue(unsigned skill){return caps[skill];}unsigned GetSkillStep(unsigned skill){return steps[skill];}
void LearnSpell(unsigned id,bool){known.insert(id);auto rank=spellMgr.GetSpellLearnSkill(id);if(rank){caps[rank->skill]=rank->maximum;steps[rank->skill]=rank->step;}
else for(auto e:spellMgr.GetSpellInfo(id)->Effects)if(e.Effect==SPELL_EFFECT_SKILL_STEP){caps[e.MiscValue]=e.value*75;steps[e.MiscValue]=e.value;}}
void SetSkill(unsigned id,unsigned step,unsigned val,unsigned max){skills[id]=val;caps[id]=max;steps[id]=step;}
int CanUseItem(ItemTemplate const*p){return p->level<=level && (!p->skill || skills[p->skill]>=p->rank)?0:1;}
int CanUseItem(Item*i){return CanUseItem(i->proto);}
Item*GetItemByEntry(unsigned id){auto i=inventory.find(id);return i==inventory.end()?nullptr:i->second.get();}
unsigned GetItemCount(unsigned id){auto i=GetItemByEntry(id);return i?i->count:0;}
bool IsInCombat(){return combat;}bool IsNonMeleeSpellCast(bool){return casting;}bool HasUnitState(int){return casting;}
History*GetSpellHistory(){return &history;}bool CanRequestSpellCast(SpellInfo*){return true;}
void RequestSpellCast(std::unique_ptr<PendingSpellCastRequest>r,SpellInfo*){
assert(r->request.Target.Flags==TARGET_FLAG_UNIT && r->request.Target.Unit.id==26 && r->item.bag==19 && r->item.slot==0);
auto i=GetItemByEntry(r->item.guid.id);assert(i && i->count);++requests;
if(--i->count==0)inventory.erase(r->item.guid.id); // last item may disappear immediately
}
void Add(ItemTemplate*p,unsigned count=20){inventory[p->id]=std::make_unique<Item>(Item{p,count});}
};
struct Spell{Player*p;SpellInfo*info;Item*m_CastItem=nullptr;Spell(Player*q,SpellInfo*i,int):p(q),info(i){}bool CanAutoCast(Player*q){assert(q==p && m_CastItem);return info->autoCast;}};
struct ClientlessCharacterFactory{static bool PrepareSkillRank(Player*,unsigned,unsigned){assert(false);return false;}};
'''+header+'\n'+function(' bool RoleMatches(')+'\n'+function('ItemTemplate const* AdventureSupplyMgr::Select(')+'\n'+function('AdventureSupplyMgr::UseResult AdventureSupplyMgr::TryUse(')+'\n'+function('void AdventureSupplyMgr::LearnSecondaries(')+r'''
int main(){
using U=AdventureSupplyPolicy::Use;AdventureSupplyMgr mgr;Player p;auto&r=mgr._records[26];
objects.items.emplace(21991,ItemTemplate{21991,0,7,129,325,{{0,100}}});
objects.items.emplace(14530,ItemTemplate{14530,0,7,129,225,{{0,101}}});
objects.items.emplace(22829,ItemTemplate{22829,55,1,0,0,{{0,102}}});
objects.items.emplace(57191,ItemTemplate{57191,80,1,0,0,{{0,103}}});
objects.items.emplace(27854,ItemTemplate{27854,55,5,0,0,{{0,104}}});
mgr._catalog={{"healing_potion",57191,80},{"bandage",21991,60},{"bandage",14530,45},{"healing_potion",22829,55},{"food",27854,55}};
for(unsigned id=100;id<=104;++id)spellMgr.spells[id].Id=id;
spellMgr.spells[104].combat=false;
assert(mgr.Select(&p,"bandage")->id==21991); // owning future tier never grants skill
p.Add(&objects.items[21991]);assert(mgr.TryUse(&p,r,U::Bandage)==AdventureSupplyMgr::UseResult::Unavailable && p.requests==0);
p.skills[129]=225;assert(mgr.Select(&p,"bandage")->id==14530);
p.skills[129]=375;assert(mgr.Select(&p,"bandage")->id==21991);
p.history.gcd=true;assert(mgr.TryUse(&p,r,U::Bandage)==AdventureSupplyMgr::UseResult::Busy);p.history.gcd=false;
p.history.cooldown=true;assert(mgr.TryUse(&p,r,U::Bandage)==AdventureSupplyMgr::UseResult::Unavailable);p.history.cooldown=false;
spellMgr.spells[100].autoCast=false;assert(mgr.TryUse(&p,r,U::Bandage)==AdventureSupplyMgr::UseResult::Unavailable && p.requests==0);spellMgr.spells[100].autoCast=true;
p.inventory[21991]->count=1;assert(mgr.TryUse(&p,r,U::Bandage)==AdventureSupplyMgr::UseResult::Started && p.requests==1 && p.GetItemCount(21991)==0);
p.Add(&objects.items[27854]);p.combat=true;assert(mgr.TryUse(&p,r,U::Food)==AdventureSupplyMgr::UseResult::Unavailable);
assert(mgr.Select(&p,"healing_potion")->id==22829);p.Add(&objects.items[22829]);assert(mgr.TryUse(&p,r,U::HealingPotion)==AdventureSupplyMgr::UseResult::Started && p.GetItemCount(22829)==19);
// Native trainer gates, native skill caps and explicit secondary-only ranks.
mgr._secondaryRanks.clear();p.skills.clear();p.caps.clear();p.steps.clear();p.known.clear();
for(unsigned skill:{129u,185u,356u,794u})for(unsigned step=1;step<=6;++step){unsigned id=skill*10+step;spellMgr.ranks[id]={skill,step,step<=4?step*75:step==5?375u:450u};spellMgr.spells[id].Id=id;mgr._secondaryRanks.push_back({skill,id,step,step==6?65u:step==5?50u:1u,step==1?0u:(step-1)*75-25});}
mgr.LearnSecondaries(&p);
for(unsigned skill:{129u,185u,356u,794u})assert(p.GetPureSkillValue(skill)==375 && p.GetPureMaxSkillValue(skill)==375 && !p.HasSpell(skill*10+6));
auto known=p.known;mgr.LearnSecondaries(&p);assert(p.known==known);
// Real profession rank representation: trainer wrapper -> learned effect44.
Player fresh;AdventureSupplyMgr discovered;
SpellInfo wrapper;wrapper.Id=3279;wrapper.Effects={{0,0,36,3273}};spellMgr.spells[3279]=wrapper;
SpellInfo apprentice;apprentice.Id=3273;apprentice.Effects={{0,0,44,0,129,1}};spellMgr.spells[3273]=apprentice;
SpellInfo primary;primary.Id=2575;primary.Effects={{0,0,44,0,186,1}};spellMgr.spells[2575]=primary;
WorldDatabase.active=true;WorldDatabase.fixture.rows={{{{3279},{0},{0}}},{{{2575},{0},{0}}}};
discovered.LearnSecondaries(&fresh);
assert(fresh.GetPureSkillValue(129)==75 && fresh.GetPureMaxSkillValue(129)==75);
assert(discovered._secondaryRanks.size()==1 && !fresh.skills.count(186));
assert(!p.skills.count(186) && !p.skills.count(164)); // never invent primary professions
}
'''
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 native-item delegation: usable bandage tiers, native cooldown/lockout gates, real inventory consumption, self targeting and level60 secondary caps passed.')
@@ -0,0 +1,90 @@
"""Execute the production native factory with transactional/publishing adapters."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
source=(root/'src/server/game/World/ClientlessCharacterFactory.cpp').read_text()
source=source[:source.index('bool ClientlessCharacterFactory::PrepareSkillRank(')]
source='\n'.join(line for line in source.splitlines() if not line.startswith('#include'))
header=(root/'src/server/game/World/ClientlessCharacterFactory.h').read_text()
header='\n'.join(line for line in header.splitlines() if not line.startswith('#include'))
code=r'''
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <functional>
#include <memory>
#include <random>
#include <string>
using uint8=uint8_t;using uint32=uint32_t;
#define TC_GAME_API
constexpr int SEC_PLAYER=0,GENDER_FEMALE=1,CLASS_DEATH_KNIGHT=6,CONFIG_MAX_PLAYER_LEVEL=0,CONFIG_CHARACTERS_PER_REALM=1,LOCALE_enUS=0,CHAR_NAME_SUCCESS=0,TOTAL_LOCALES=4,POWER_MANA=0,LOGIN_REP_REALM_CHARACTERS=0;
using LocaleConstant=int;
struct ObjectGuid{unsigned id;unsigned GetCounter()const{return id;}};enum class HighGuid{Player};
struct Field {uint32 value=0;std::string text="BOT";uint8 GetUInt8(){return value;}uint32 GetUInt32(){return value;}std::string GetString(){return text;}};
struct Row{Field fields[3];Field*Fetch(){return fields;}};using QueryResult=std::shared_ptr<Row>;
struct Transaction{int characterRows=0,profileRows=0;};using CharacterDatabaseTransaction=std::shared_ptr<Transaction>;
struct Future{bool success=true;bool get(){return success;}};struct Commit{Future m_future;};
struct Statement{void setUInt32(int,uint32){}};
struct Database {
bool account=true,duplicate=false,success=true;uint32 count=1;int commits=0,savedCharacters=0,savedProfiles=0,realmWrites=0;Statement stmt;
template<class...A>QueryResult PQuery(char const*sql,A...){
std::string query(sql);if(query.find("SELECT 1")!=std::string::npos)return duplicate?std::make_shared<Row>():nullptr;
if(query.find("COUNT(*)")!=std::string::npos){auto row=std::make_shared<Row>();row->fields[0].value=count;return row;}
if(!account)return nullptr;auto row=std::make_shared<Row>();row->fields[1].value=3;return row;
}
void EscapeString(std::string&){}CharacterDatabaseTransaction BeginTransaction(){return std::make_shared<Transaction>();}
Commit AsyncCommitTransaction(CharacterDatabaseTransaction tx){++commits;if(success){savedCharacters+=tx->characterRows;savedProfiles+=tx->profileRows;}return {{success}};}
Statement*GetPreparedStatement(int){return &stmt;}void Execute(Statement*){++realmWrites;}
}CharacterDatabase,LoginDatabase;
struct CharacterCreateInfo {protected:std::string Name;uint8 Race=0,Class=0,Gender=0,Skin=0,Face=0,HairStyle=0,HairColor=0,FacialHair=0;};
struct Motion {void Initialize(){}};
struct WorldSession {enum class WorldSessionMode{Clientless};template<class...T>WorldSession(T...){} };
struct Player {
static inline bool createSuccess=true;static inline int cleaned=0;unsigned level=1;ObjectGuid guid{0};Motion motion;
explicit Player(WorldSession*){}void CleanupsBeforeDelete(){++cleaned;}static bool ValidateAppearance(uint8,uint8,uint8,uint8,uint8,uint8,uint8,uint8,bool){return true;}
Motion*GetMotionMaster(){return &motion;}bool Create(unsigned id,CharacterCreateInfo*){guid={id};return createSuccess;}void setCinematic(int){}unsigned getLevel(){return level;}void SetLevel(unsigned l){level=l;}void InitStatsForLevel(bool){}void InitTalentForLevel(){}void InitTaxiNodesForLevel(){}void InitGlyphsForLevel(){}void SetCanModifyStats(bool){}void UpdateMaxHealth(){}void UpdateMaxPower(int){}void SetMoney(int){}void LearnDefaultSkills(){}void UpdateSkillsForLevel(){}void SetFullHealth(){}void SetFullPower(int){}
void SaveToDB(CharacterDatabaseTransaction tx,bool creating){assert(creating);++tx->characterRows;}ObjectGuid GetGUID(){return guid;}
};
struct World {bool connected=false;bool FindSession(uint32){return connected;}unsigned getIntConfig(int c){return c==CONFIG_MAX_PLAYER_LEVEL?85:10;}}world;auto sWorld=&world;
namespace AccountMgr {int security=0;int GetSecurity(uint32,int){return security;}}
struct Realm {struct{int Realm=1;}Id;}realm;
bool normalizePlayerName(std::string&s){return !s.empty();}
struct Info{unsigned Race_related=0,Required_expansion=0;};struct Store {Info info;Info*LookupEntry(unsigned){return &info;}}sChrRacesStore,sChrClassesStore;
struct ObjectMgr {
bool valid=true,reserved=false;unsigned nextGuid=30;Info info;
Info*GetPlayerInfo(unsigned,unsigned){return valid?&info:nullptr;}bool IsReservedName(std::string const&){return reserved;}
static int CheckPlayerName(std::string const&,int,bool){return CHAR_NAME_SUCCESS;}
struct Generator{unsigned Generate(){return 30;}}generator;template<HighGuid G>Generator&GetGenerator(){return generator;}
}objects;auto sObjectMgr=&objects;
struct Cache {bool duplicate=false;int published=0;bool GetCharacterCacheByName(std::string const&){return duplicate;}template<class...T>void AddCharacterCacheEntry(T...){++published;}}cache;auto sCharacterCache=&cache;
struct ScriptMgr {int creates=0;void OnPlayerCreate(Player*){++creates;}}scripts;auto sScriptMgr=&scripts;
'''+header+'\n'+source+r'''
int main(){
using Factory=ClientlessCharacterFactory;Factory::Definition d{"Aldren",1,1,0,60,42};int prepared=0,appended=0;
auto prepare=[&](Player*p,std::string&){++prepared;assert(p->getLevel()==60);return true;};
auto append=[&](CharacterDatabaseTransaction const&tx,Player*){++appended;++tx->profileRows;};
auto run=[&](){return Factory::Create(6,d,prepare,append);};
auto ok=run();assert(ok.Guid==30 && ok.Error.empty());assert(cache.published==1 && scripts.creates==1 && appended==1 && prepared==1);
assert(CharacterDatabase.savedCharacters==1 && CharacterDatabase.savedProfiles==1 && Player::cleaned==1 && LoginDatabase.realmWrites==1);
// Preparation failure never invokes save, appends profile, publishes cache or hooks.
auto commits=CharacterDatabase.commits;auto failed=Factory::Create(6,d,[](Player*,std::string&e){e="gear unavailable";return false;},append);
assert(!failed.Guid && failed.Error=="gear unavailable" && CharacterDatabase.commits==commits && appended==1 && cache.published==1);
// DB rollback cannot publish either half of the generated identity.
CharacterDatabase.success=false;auto rollback=run();assert(!rollback.Guid && cache.published==1 && scripts.creates==1);
assert(CharacterDatabase.savedCharacters==1 && CharacterDatabase.savedProfiles==1 && LoginDatabase.realmWrites==1);CharacterDatabase.success=true;
// Ordinary active players, privileged accounts and exhausted accounts are protected.
auto previous=prepared;world.connected=true;assert(!run().Guid);world.connected=false;
AccountMgr::security=3;assert(!run().Guid);AccountMgr::security=0;
CharacterDatabase.count=10;assert(!run().Guid);CharacterDatabase.count=1;
objects.valid=false;assert(!run().Guid);objects.valid=true;objects.reserved=true;assert(!run().Guid);objects.reserved=false;
cache.duplicate=true;assert(!run().Guid);cache.duplicate=false;CharacterDatabase.duplicate=true;assert(!run().Guid);CharacterDatabase.duplicate=false;
LoginDatabase.account=false;assert(!run().Guid);LoginDatabase.account=true;
d.Level=86;assert(!run().Guid);d.Level=54;d.Class=CLASS_DEATH_KNIGHT;assert(!run().Guid);d.Level=60;d.Class=1;
assert(prepared==previous);assert(cache.published==1 && scripts.creates==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('Production native factory: character/profile atomicity, failed preparation/commit, cache publication order, account/security/name/cap gates passed')
@@ -0,0 +1,40 @@
"""Execute native tier-based skill preparation before world insertion."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/ClientlessCharacterFactory.cpp').read_text()
a=s.index('bool ClientlessCharacterFactory::PrepareSkillRank(')
code=r'''
#include <cassert>
#include <unordered_map>
using uint32=unsigned;constexpr unsigned MAX_SKILL_STEP=16;
struct RC{unsigned MinLevel=5,SkillTierID=1;}rc;
struct DBC {bool available=true;RC*GetSkillRaceClassInfo(unsigned,unsigned,unsigned){return available?&rc:nullptr;}}sDBCManager;
struct Tier{unsigned Value[16]={75,150,225,300,375,450,525};}tier;
struct Store{bool available=true;Tier*LookupEntry(unsigned){return available?&tier:nullptr;}}sSkillTiersStore;
struct Player{
bool inWorld=false;unsigned level=60,step=0,value=0,cap=0;int writes=0;
bool IsInWorld(){return inWorld;}unsigned getRace(){return 1;}unsigned getClass(){return 1;}unsigned getLevel(){return level;}
unsigned GetSkillStep(unsigned){return step;}unsigned GetPureSkillValue(unsigned){return value;}unsigned GetPureMaxSkillValue(unsigned){return cap;}
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'''
int main(){
Player p;using F=ClientlessCharacterFactory;
assert(F::PrepareSkillRank(&p,186,1));assert(p.step==1 && p.value==75 && p.cap==75);
assert(F::PrepareSkillRank(&p,186,5));assert(p.step==5 && p.value==375 && p.cap==375);
auto previous=p.writes;assert(F::PrepareSkillRank(&p,186,1));assert(p.step==5 && p.writes==previous);
assert(!F::PrepareSkillRank(&p,186,0));assert(!F::PrepareSkillRank(&p,186,17));assert(!F::PrepareSkillRank(&p,186,8));
p.inWorld=true;assert(!F::PrepareSkillRank(&p,186,6));p.inWorld=false;
p.level=4;assert(!F::PrepareSkillRank(&p,186,6));p.level=60;
sDBCManager.available=false;assert(!F::PrepareSkillRank(&p,186,6));sDBCManager.available=true;
sSkillTiersStore.available=false;assert(!F::PrepareSkillRank(&p,186,6));
assert(p.writes==previous);assert(!F::PrepareSkillRank(nullptr,186,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('Production offline skill preparation: native tier caps, no downgrading, rank/level/race/tier gates and no modification of online players passed')
@@ -0,0 +1,56 @@
"""Exercise production map-worker social/helper queues concurrently."""
from pathlib import Path
import subprocess
import tempfile
root=Path(__file__).resolve().parents[2]
source=(root/'src/server/game/World/ClientlessPlayerMgr.cpp').read_text()
def function(signature):
a=source.index(signature);b=source.index('{',a);e=b+1;depth=1
while depth:depth+=(source[e]=='{')-(source[e]=='}');e+=1
return source[a:e]
code=r'''
#include <cassert>
#include <deque>
#include <mutex>
#include <thread>
#include <unordered_map>
#include <vector>
using uint32=unsigned;
struct ObjectGuid{unsigned id;unsigned GetCounter()const{return id;}bool operator==(ObjectGuid b)const{return id==b.id;}};
struct Player;
struct Unit {ObjectGuid guid;Player*owner=nullptr;std::vector<Unit*> attackers;ObjectGuid GetGUID(){return guid;}virtual Player*ToPlayer(){return nullptr;}Player*GetControllingPlayer(){return owner;}auto&getAttackers(){return attackers;}};
struct Session {bool clientless=true;bool IsClientless(){return clientless;}};
struct Player:Unit {Session session;Player(){owner=this;}Player*ToPlayer()override{return this;}Session*GetSession(){return &session;}};
struct ClientlessPlayerMgr {
struct EmoteNotice{ObjectGuid Source;uint32 Emote;};struct HelpNotice{ObjectGuid Helper,Target;};
std::mutex _damageMutex;std::unordered_map<uint32,std::deque<EmoteNotice>> _emoteNotices;std::unordered_map<uint32,std::deque<HelpNotice>> _helpNotices;
void TextEmote(Player*,Player*,uint32);void SharedTargetDamage(Unit*,Unit*,uint32);
};
'''+function('void ClientlessPlayerMgr::TextEmote(')+'\n'+function('void ClientlessPlayerMgr::SharedTargetDamage(')+r'''
int main(){
ClientlessPlayerMgr mgr;std::vector<Player> bots(16);std::vector<std::thread> threads;
Player helper;helper.guid={77};helper.session.clientless=false;
for(unsigned i=0;i<bots.size();++i){bots[i].guid={i+1};threads.emplace_back([&,i]{
Unit target;target.guid={900+i};target.attackers={&bots[i],&helper};
for(unsigned n=1;n<=1000;++n){mgr.TextEmote(&helper,&bots[i],n);mgr.SharedTargetDamage(&target,&helper,n);}
});}
for(auto&t:threads)t.join();
for(unsigned i=0;i<bots.size();++i){
auto const&emotes=mgr._emoteNotices[i+1];assert(emotes.size()==8);
auto const&helps=mgr._helpNotices[i+1];assert(helps.size()==8);
for(unsigned n=0;n<8;++n){assert(emotes[n].Source.id==77 && emotes[n].Emote==n+1);assert(helps[n].Helper.id==77 && helps[n].Target.id==900+i);}
}
mgr._emoteNotices.clear();mgr.TextEmote(nullptr,&bots[0],1);mgr.TextEmote(&helper,&helper,1);mgr.TextEmote(&bots[0],&helper,1);assert(mgr._emoteNotices.empty());
mgr._helpNotices.clear();Unit target;target.guid={1000};target.attackers={&bots[0]};
mgr.SharedTargetDamage(&target,&helper,0);mgr.SharedTargetDamage(&target,nullptr,10);assert(mgr._helpNotices.empty());
Unit pet;pet.guid={800};pet.owner=&helper;mgr.SharedTargetDamage(&target,&pet,10);assert(mgr._helpNotices[1].front().Helper.id==77);
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['g++','-std=c++17','-pthread',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
unit=(root/'src/server/game/Entities/Unit/Unit.cpp').read_text();start=unit.index('uint32 Unit::DealDamage(')
hook=unit.index('sClientlessPlayerMgr->SharedTargetDamage(',start)
assert unit.index('GetCommandStatus(CHEAT_GOD)',start)<hook<unit.index('Unit::Kill(attacker, victim, durabilityLoss);',hook)
print('Production social/helper queues: concurrent GUID-only delivery, bounded queues, human/self/zero-damage gates, pet attribution and lethal-cleanup ordering passed')
@@ -0,0 +1,34 @@
"""Execute the native pure-skill-cap getter; populated skills must not return0."""
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('uint16 Player::GetPureMaxSkillValue(');b=s.index('{',a);e=b+1;depth=1
while depth:depth+=(s[e]=='{')-(s[e]=='}');e+=1
code=r'''
#include <cassert>
#include <cstdint>
#include <unordered_map>
using uint16=uint16_t;using uint8=uint8_t;using uint32=uint32_t;
constexpr int SKILL_DELETED=1,PLAYER_SKILL_MAX_RANK_0=100;
struct Status{unsigned pos=0;int uState=0;};using SkillStatusMap=std::unordered_map<unsigned,Status>;
struct Player{
SkillStatusMap mSkillStatus;unsigned rank=1;uint16 cap=375;
unsigned GetSkillRank(unsigned)const{return rank;}
uint16 GetUInt16Value(unsigned field,unsigned offset)const{assert(field==101 && offset==1);return cap;}
uint16 GetPureMaxSkillValue(uint32)const;
};
'''+s[a:e]+r'''
int main(){
Player p;assert(p.GetPureMaxSkillValue(0)==0);assert(p.GetPureMaxSkillValue(129)==0);
p.mSkillStatus[129]={3,0};assert(p.GetPureMaxSkillValue(129)==375);
p.cap=75;assert(p.GetPureMaxSkillValue(129)==75);
p.rank=0;assert(p.GetPureMaxSkillValue(129)==0);p.rank=1;
p.mSkillStatus[129].uState=SKILL_DELETED;assert(p.GetPureMaxSkillValue(129)==0);
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['g++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Native pure skill cap: learned skills return stored cap; missing/deleted/zero-rank skills return0.')