Compare commits
3
Commits
c0c799c4b6
...
04aa87fcfc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04aa87fcfc | ||
|
|
e333f2f9fc | ||
|
|
39a515dbd1 |
No files matched your search
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ↦}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 ↦}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=⊥
|
||||
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.')
|
||||
Reference in new issue
Block a user