Add profile-driven AdventureBot character generation
This commit is contained in:
1 parent
e333f2f9fc
commit
04aa87fcfc
33 files changed
+3465
-2
No files matched your search
@@ -1,3 +1,79 @@
|
||||
# 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.
|
||||
|
||||
@@ -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,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,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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "AdventureSupplyMgr.h"
|
||||
#include "ClientlessCharacterFactory.h"
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "ClientlessLuaEngine.h"
|
||||
#include "DatabaseEnv.h"
|
||||
@@ -86,6 +87,7 @@ void AdventureSupplyMgr::LearnSecondaries(Player* p)
|
||||
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)
|
||||
{
|
||||
@@ -306,3 +308,8 @@ void AdventureSupplyMgr::Report(ChatHandler* handler,Player* p)
|
||||
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);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ 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);
|
||||
|
||||
@@ -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
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "AdventureCombatMgr.h"
|
||||
#include "AdventureInteractionMgr.h"
|
||||
#include "AdventureGeneratorMgr.h"
|
||||
#include "AdventureSupplyMgr.h"
|
||||
#include "AdventureSocialActions.h"
|
||||
#include "Config.h"
|
||||
@@ -335,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",
|
||||
@@ -384,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;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "AdventureCombatMgr.h"
|
||||
#include "AdventureInteractionMgr.h"
|
||||
#include "AdventureGeneratorMgr.h"
|
||||
#include "AdventureSupplyMgr.h"
|
||||
#include "ClientlessTravelGraphMgr.h"
|
||||
#include "ClientlessRouteRecorderMgr.h"
|
||||
@@ -63,6 +64,8 @@
|
||||
|
||||
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;
|
||||
@@ -2518,6 +2521,7 @@ static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*arg
|
||||
{
|
||||
{ "combat", rbac::RBAC_PERM_COMMAND_GM, true, &ClientlessCombatPreparation::Command, "" },
|
||||
{ "interaction", rbac::RBAC_PERM_COMMAND_GM, true, &InteractionStatus, "" },
|
||||
{ "generate", rbac::RBAC_PERM_COMMAND_GM, true, &GenerateBot, "" },
|
||||
{ "luareload", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessLuaReload, "" },
|
||||
{ "transport", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessTransport, "" },
|
||||
{ "login", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessLogin, "" },
|
||||
|
||||
@@ -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')
|
||||
@@ -56,6 +56,7 @@ 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;}
|
||||
@@ -75,6 +76,7 @@ struct Player{
|
||||
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];
|
||||
|
||||
@@ -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')
|
||||
Reference in new issue
Block a user