Add profile-driven AdventureBot character generation
clang PCH / build (push) Waiting to run
GCC PCH / build (push) Canceled after 0s

This commit is contained in:
Nico committed 2026-10-28 11:30:47 +01:00
1 parent e333f2f9fc
commit 04aa87fcfc
33 files changed
+3465 -2

No files matched your search

+76
View File
@@ -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.
+151
View File
@@ -0,0 +1,151 @@
# AdventureBot generation
The GM/console command `clientless generate` creates complete new native player
characters on an existing normal bot account. No source player is cloned and no
authentication account/password is created or changed. The native realm-character
counter is updated for that account after a successful character commit.
## Commands
```
clientless generate create <account> <count>
clientless generate create <account> <count> <level> <faction> <archetype> <persistence> <seed>
clientless generate list
clientless generate profile <guid>
clientless generate login <guid>
clientless generate logout <guid>
clientless generate equip <guid>
clientless generate retire <guid>
```
The short create form defaults to level60, any faction/archetype, persistent,
and a time-based seed. When specifying options, supply all five optional values.
Count is1–10 per request, level10–85 (also limited by server maximum), faction is
`any`, `alliance` or `horde`, and persistence is `persistent` or `temporary`.
Each character is committed separately. A failed batch reports its actual
completed count and retains already completed characters.
Available archetypes: `warrior`, `paladin`, `hunter`, `rogue`, `priest`,
`death_knight` (minimum55), `shaman`, `mage`, `warlock`, `druid`, or `any`.
Native race/class data and account expansion restrictions determine eligible
combinations. Both factions can use the administrative bot-account pool; this
is not a normal client's character-creation request.
`equip` refreshes the equipment of an idle online generator-owned bot using its
stored archetype and seed. Native checks still apply, casting/combat/transit are
rejected, and replaced items remain in bags. It requires sufficient bag space;
an error can stop an online refresh partway through. The initial offline create
pipeline is the atomic operation.
Creation leaves bots offline. Explicit login uses the existing AdventureBot Lua
controller. Initially they perform a local social intent for one hour at their
home. Enable the existing daily planner with `clientless adventure schedule
<guid> on` when desired. Rotation and automatic population targeting are not
activated by this change.
## Identity and profile
Names use curated cultural syllable combinations for the supported races and
sexes. They contain no numbers, bot markers or numeric collision suffixes.
Native name validation, reserved-name checks, character cache/database checks,
and reservation of hand-written named Lua profiles apply. An exhausted pool
fails cleanly instead of manufacturing a non-roleplay name.
Profiles include a racial origin, vocation, background, two coherent primary
professions, personality values and regional/activity affinities. The stored
seed makes choices reproducible against unchanged catalogs and name availability.
Existing name collisions legitimately advance the random stream. Equipment
varies within a set of similar high-scoring legal options.
Generated Lua is stored in the character database, avoiding runtime writes to
source-controlled profile files. The ordinary sandbox/parser loads it with
source `generated:<guid>`. Existing hand-written named Lua profiles retain
precedence; ordinary non-generated characters keep the generic fallback.
`identity.background` and `identity.vocation` are also available in the parsed
profile. Generated strings are Lua-escaped, then escaped separately for SQL.
Home points come from enabled, faction-compatible social tavern/settlement
locations nearest the native racial starting region. Mature goblins and worgen
use mainland orc/human home regions, rather than phased introductory maps.
Death knights also choose racial mainland homes. Map relocation happens before
native talent calculation, so introductory map609 does not reduce their talent
budget. This is administrative creation, not a claim that these bots played or
completed introductory quests.
## Native preparation
`ClientlessCharacterFactory` is generic infrastructure with preparation and
transaction-extension callbacks. It has no AdventureBot policy dependency.
It uses native `Player::Create`, validated random appearance, level/skill setup,
new player/item GUID allocators and native `SaveToDB`. Requested levels are
initialized as birth state through native stat/talent/taxi/glyph initialization,
avoiding earned-level reward mails/hooks before the character transaction. Network-active accounts,
privileged accounts, unavailable names and the configured per-realm character
limit are rejected.
The AdventureBot layer supplies:
- Native class-trainer spells, using the same read-only `CanTeachSpell` predicate
as ordinary native teaching; weapon/armor proficiencies are race/class/level
checked. Owned weapon skills are raised to their native level cap.
- A solo damage specialization and valid talent allocation through native talent
prerequisites, tiers, ranks and primary-tree gates. Arms uses the existing
explicit build, other archetypes use a legal tier-ordered baseline. These
generic builds are not claimed to be optimized raid/PvP builds.
- Coherent primary profession pairs, trained to their legal native rank/cap;
all four available secondary professions through existing supply preparation.
Before world insertion, effect44 learning casts cannot be relied on to apply
their target effects. The factory therefore initializes validated skill ranks
with native SkillRaceClass/SkillTiers caps and `Player::SetSkill`. This helper
refuses online players; ordinary live supply learning keeps its native casts.
This does not teach every profession recipe or synthesize quest rewards.
- Four native Embersilk Bags and class/stat/level-matching equipment. Candidate
items are normal/uncommon/rare; native use/equip checks remain authoritative.
Armor transitions at40 are respected. Temporary, holiday, reputation-bound,
restricted-map/area and random-affix equipment is excluded. Caster/physical
stat affinity and mana-only/triggered utility effects are checked, so a
warrior does not receive a mana-restoration trinket. Identical equipped
entries are not duplicated, in addition to native unique-equipment checks. Initial items
are retained in bags rather than destroyed.
- Existing daily/post-combat supply restocking upon login. Consumables are native
items and continue to use the existing actual item/cooldown logic.
No mounts, pets or optimized class rotations are introduced here. Existing
non-warrior combat limitations remain. Being fully trained is distinct from
having class-specific automated combat/support AI.
## Persistence and future rotation
Apply `sql/custom/characters/2026_10_28_00_adventure_generator.sql` before using
commands. The additive migration is idempotent. Native character state, inventory,
professions, Lua profile, homebind and generated ownership are committed in one
character-database transaction. Cache publication and create hooks happen only
after confirmed success. Failure before/at commit does not publish a ready bot.
The registry explicitly records ownership, seed, archetype, creation time,
persistence and state. Existing characters such as Arthoran are not adopted.
Only offline temporary generator-owned characters may be retired. Retirement
marks metadata only; the native character, items and profile remain available
for audit. Generator login refuses retired entries. Persistent characters are
protected. There is no automatic deletion, replacement, new-account allocation
or restart autologin. A later rotating population controller must define those
policies and account/name capacity management explicitly.
## Verification
Production-code adapter tests cover factory transaction failure/publication,
account/name/security/character-cap gates, retirement ownership protections,
command bounds, cultural naming, real Lua profile execution, personality/skill
coherence, string escaping and equipment/armor eligibility.
Compilation and adapters do not establish visible in-game behavior. Live-created
character identities, gear, profession/talent/spell rows and login/profile
results are recorded in the task checkpoint after native server checks.
Live checkpoint (2026-10-28): four level60 characters are online: Aelion29
(night elf warrior), Zanzan30 (troll warrior), Lenara31 (worgen mage), Aldren32
(forsaken death knight). Their generated profiles were reloaded after a server
restart, and native equipment refresh was saved for all four. Earlier temporary
fixtures27/28 remain archived and offline. Existing inventory ownership for
Allitester21 and Arthoran26 matched the pre-generation snapshot. All62 clientless
test scripts passed. See checkpoints/2026-10-28-generator/ for native data/logs.
@@ -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;
+2 -1
View File
@@ -75,10 +75,11 @@ namespace Trainer
Type GetTrainerType() const { return _type; }
uint32 GetTrainerRequirement() const { return _requirement; }
bool IsTrainerValidForPlayer(Player const* player) const;
// Read-only eligibility, shared by native teaching and character provisioning.
bool CanTeachSpell(Player const* player, Spell const* trainerSpell) const;
private:
Spell const* GetSpell(uint32 spellId) const;
bool CanTeachSpell(Player const* player, Spell const* trainerSpell) const;
SpellState GetSpellState(Player const* player, Spell const* trainerSpell) const;
void SendTeachFailure(Creature const* npc, Player const* player, uint32 spellId, FailReason reason) const;
void SendTeachSucceeded(Creature const* npc, Player const* player, uint32 spellId) const;
@@ -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')