Add AdventureBot combat reactions, training and corpse recovery
GCC PCH / build (push) Canceled after 0s
clang PCH / build (push) Canceled after 0s

This commit is contained in:
Nico committed 2026-10-27 23:17:05 +01:00
1 parent e18a416bf4
commit c0c799c4b6
37 files changed
+1568 -18

No files matched your search

+117
View File
@@ -1,3 +1,120 @@
# Aktuell: Training, echte Gegner und Todesverhalten abgeschlossen
Stand 27.10.2026. Auf feature/clientless-player-core bleiben alle bisherigen
Änderungen erhalten. Debug-Build mit -j10 erfolgreich installiert; alle 51
Testskripte bestanden. Testserver PID 45545 läuft mit frisch geprüftem Ready
und Port 8085. Nutzer hat den lokalen Commit aller Änderungen beauftragt; kein Push.
Neue AdventureCombatMgr und AdventureCombatPolicy überlagern den Adventure-
Controller während Kampf und Tod. Tatsächlich angewandter Kreaturenschaden
wird über eine threadsichere GUID-Queue geordnet an den Controller übergeben.
Der erste Angreifer bleibt Fokus; weitere folgen nach dessen Tod. Abenteuer
berücksichtigt Gegner am aktiven MMAP-Pfad, Kürschnerei nur Tiere bei bestehendem
Profilrecht. Arthoran bleibt Bergbau/Schmied. Konservative Erstangriffsfilter:
höchstens +2 Level, mindestens 60% HP und gegebenenfalls 30% Mana, keine Elites,
Weltbosse oder Gegnergruppen. Andere Intents jagen nicht.
Migration 09 ergänzt leisure_training für 60–180 Sekunden an echten erreichbaren
Puppen im lokalen 60-Yards-Umkreis. Bereits in Test-DB angewendet und trotz
SQL-Ignore gezielt vorgemerkt. Ursprüngliche Intentfristen bleiben erhalten.
Todesablauf: native Freigabe, gespeicherter Leichenzeitpunkt, MMAP-Punkte innerhalb
39 Yards und native Aufnahmefrist. Ohne passenden Pfad nach 300 Sekunden native
Friedhofsrückkehr und Wiederbelebung ohne Geistheiler-Mali.
Live erfolgreich geprüft:
- Trainingsszene: MMAP-Anreise, native Angriffe und reguläres Ende nach 60 Sekunden.
- Echter Bär, Entry 42336 / Spawn 300425, Stufe 58: besiegt, native Respawnzeile
bestätigt. Tatsächlicher Gegentreffer löste den Selbstverteidigungsfokus aus.
- Expliziter Testtod 1: Freigabe um 1793138872, kein akzeptierter Rückweg;
volle 300 Sekunden tatsächlich abgewartet. Friedhofswiederbelebung, anschließend
lebend gespeichert, keine Leiche oder Wiederbelebungskrankheit.
- Expliziter Testtod 2 nahe Friedhof: tatsächlicher MMAP-Geisterlauf von 37,40
Yards und native Leichenaufnahme. Danach HP 4340 gespeichert, keine Leiche
oder Aura 15007. Kein natürlicher Kampftod behauptet.
- Administrative Offline-Positionierung diente nur isolierten Tests; kein Beleg
für Fernanreise. Mehrfachangreifer und Neustart während Geisterlauf bislang
durch Produktionsadapter geprüft, nicht durch entsprechende Live-Abnahme.
Arthoran ist wieder angemeldet, ohne Fernauftrag am letzten Rücklaufpunkt:
Karte 0, (-12187.9, -2603.74, 1.5777), Verwüstete Lande. Er reagiert auf Angreifer.
Normales Lua-Profil bytegleich restauriert; kein temporärer Override vorhanden.
Befehle: clientless combat train <guid> [seconds], attack <guid> <spawn-id>,
testdeath <guid>, stop <guid>. Testdeath ist ein markierter nativer Diagnosetod
für einen verwalteten Bot ohne zusätzlichen Haltbarkeitsverlust.
Belege: doc/clientless/checkpoints/2026-10-27-combat/.
Logs: /tmp/vcore-reactive-combat-build-final.log und
/tmp/vcore-reactive-tests-final.log. DB-Helfer: /tmp/vcore-instance-db.py,
Modus combat_live_state. Vorherige Ausrüstung/Reparatur-WIP bleibt erhalten.
Pfadfindungsüberarbeitung bleibt vertagt. Weitere Klassenrotationen und konkrete
Spieler-/Bot-Interaktionsregeln sind noch auszuarbeiten.
---
# Wiederaufnahme: Kampfausstattung und Reparaturtest
Stand 27.10.2026: Branch feature/clientless-player-core, bestehende Änderungen
bewahren. Debug-Build erfolgreich, installiert; Testserver PID41714 frisch ready
und Port8085 bestätigt. 48 Testskripte bestanden. Kein Push oder neuer Commit.
Arthoran26 trägt neun native Kopien von Allitester21 (Zorn und Ashkandi),
26/26 native Waffen-Talentpunkte Baum746, stufengerechte Klassenfähigkeiten.
Originalquelle unverändert, alte Ausrüstung in Taschen. Begrenzte Trainingsübung
an echter Puppe314147 erfolgreich: native angenommene Fähigkeiten protokolliert.
Phase169 ist Standard; keine Phasenänderungen. Ursprüngliches Problem war LOS.
Volle autonome Kampf-/Todes-/Interaktionssteuerung bleibt zukünftige Umsetzung.
Malusfreie Friedhofswiederbelebung nach5min ohne Pfad ist verbindliche Vorgabe.
NEU: economy_repair, Migration08 bereits Test-DB angewendet. Liveprüfung läuft:
Arthoran ausgeloggt, nur seine neun Kopien auf halbe Haltbarkeit gesetzt,
Position administrativ an poi_tele_954 (Handelsdistrikt), wieder angemeldet.
Intent economy am Anker angekommen; Mikroaktion economy_repair gestartet,
echter Händler1287/GUID1879, native MMAP-Anreise. Abschluss bestätigt: ECONOMY_REPAIR_COMPLETE; nach saveall alle neun Kopien
wieder auf voller Haltbarkeit, Geld weiterhin0, Allitester unverändert.
Ausgangswerte /tmp/vcore-repair-before.tsv.
Normales Lua-Profil bytegleich restauriert, kein temporärer Override.
Werkzeuge /tmp/vcore-instance-db.py gear_verify (prüft auch Allitester-Hash),
repair_test_prepare NICHT WIEDERHOLEN. Konsole saveall nach Reparaturabschluss,
ECONOMY_REPAIR_COMPLETE in Server.log prüfen. Gegebenenfalls Szene beobachten.
Build-/Testlogs /tmp/vcore-combat-preparation-{build,tests-final}.log.
Migration08 wird von *.sql ignoriert, gezielt force-add für Erhalt erforderlich.
Reparaturprüfung abgeschlossen; Arthoran darf den Wirtschaftsintent weiter ausführen.
---
# Neuer Arbeitsbereich: Kampf, Reaktionen und Interaktionen
Alle sieben Grundintents sind abgeschlossen und die bisherigen Änderungen
lokal committet (e18a416bf4). Der Nutzer hat die visuelle Instanzabnahme erlassen.
Jetzt Kampf, reaktives Verhalten, Bot-Spieler und Bot-Bot gemeinsam ausarbeiten.
Neue Regeln/Vorprüfung in `combat-reactions.md` dauerhaft festgehalten.
Nutzer: Selbstverteidigung fokussiert erste tatsächlich schädigende Kreatur bis
zu deren Tod; weitere Treffer wechseln den Fokus nicht. Erstangriffe wählen
nächstgelegenes gültiges intentspezifisches Ziel: Abenteuer nahe Feindkreatur auf
aktuellem Weg; Beruf/Lederjagd nahes geeignetes Tier. Stärkecheck verhindert
leichtsinnige Angriffe auf deutlich stärkere Gegner. Exakte Grenzwerte noch offen.
Tod: Leichenrücklauf als Geist, native Wiederbelebungsreichweite; ohne Pfad nach
fünf Minuten Friedhofswiederbelebung. Native Handler/39-Yards-Radius vorhanden.
Timerherkunft und weitere Randfälle als Auslegung/Vorschlag markieren.
Arthoran bleibt mining/blacksmithing; keine stillschweigende skinning-Freigabe.
Sammeln weiterhin performativ, neu gewünschter Kampf real.
PlayerAI.cpp gründlich geprüft. Viel vorhandene Klassen-/Zauberlogik, aber
Charm-Steuerung muss entkoppelt werden. Technisch nicht ungenutzt: Unit::UpdateCharmAI
und mehrere Bossskripte verwenden sie. Unit::DealDamage liefert UnitAI-Callbacks
und UnitScript-OnDamage VOR endgültigem HP-Abzug: tatsächlichen Erstschaden
sauber bestätigen, keine Nullschaden-/Godmode-Fokussierung.
Noch keine Kampfimplementierung oder Livekampftests in dieser Vorprüfung.
Nur Dokumentation neu/uncommittet; keine weiteren Commits ohne konkreten Bedarf.
Details zu Flucht, Zielverlust, verbleibenden Angreifern, Erstangriffsschwellen
und konkreten Spieler-/Bot-Reaktionen noch nicht als Nutzerentscheidungen ausgeben.
---
# Aktuell: Instanzintent implementiert und technisch live geprüft
Stand 27.10.2026, letzter Testserverprozess PID **35865**, frisches Ready beim
@@ -0,0 +1,60 @@
TrinityCore rev. e18a416bf445+ 2026-10-27 20:10:13 +0100 (feature/clientless-player-core branch) (Unix, Debug, Static) (worldserver-daemon) ready...
COMBAT_START guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' training=1 defending=0 until=1793138792
COMBAT_CAST guid=26 target='GUID Full: 0x000000000000001a Type: Player Low: 26' spell=6673
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=78
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=772
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=12294
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=1464
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=1464
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=1464
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=12294
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=78
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=772
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=7384
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=1464
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=7384
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=12294
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=1464
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=772
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=78
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=7384
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=12294
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=78
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=7384
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=772
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=78
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=7384
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=12294
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=7384
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=1464
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=772
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=78
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=12294
COMBAT_CAST guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' spell=78
COMBAT_END guid=26 target='GUID Full: 0xf1307f9a0000050b Type: Creature Entry: 32666 Low: 1291' training=1
COMBAT_START guid=26 target='GUID Full: 0xf130a56000000b17 Type: Creature Entry: 42336 Low: 2839' training=0 defending=0 until=0
COMBAT_END guid=26 target='GUID Full: 0xf130a56000000b17 Type: Creature Entry: 42336 Low: 2839' training=0
COMBAT_START guid=26 target='GUID Full: 0xf130a56000000b17 Type: Creature Entry: 42336 Low: 2839' training=0 defending=1 until=0
COMBAT_CAST guid=26 target='GUID Full: 0xf130a56000000b17 Type: Creature Entry: 42336 Low: 2839' spell=12294
COMBAT_CAST guid=26 target='GUID Full: 0xf130a56000000b17 Type: Creature Entry: 42336 Low: 2839' spell=78
COMBAT_CAST guid=26 target='GUID Full: 0xf130a56000000b17 Type: Creature Entry: 42336 Low: 2839' spell=772
COMBAT_END guid=26 target='GUID Full: 0xf130a56000000b17 Type: Creature Entry: 42336 Low: 2839' training=0
DEATH_TEST_REQUEST guid=26
DEATH_RELEASE guid=26
DEATH_CEMETERY_REVIVE guid=26
COMBAT_START guid=26 target='GUID Full: 0xf130b03f00000ae8 Type: Creature Entry: 45119 Low: 2792' training=0 defending=1 until=0
COMBAT_CAST guid=26 target='GUID Full: 0x000000000000001a Type: Player Low: 26' spell=6673
COMBAT_CAST guid=26 target='GUID Full: 0xf130b03f00000ae8 Type: Creature Entry: 45119 Low: 2792' spell=78
COMBAT_CAST guid=26 target='GUID Full: 0xf130b03f00000ae8 Type: Creature Entry: 45119 Low: 2792' spell=772
COMBAT_CAST guid=26 target='GUID Full: 0xf130b03f00000ae8 Type: Creature Entry: 45119 Low: 2792' spell=7384
COMBAT_CAST guid=26 target='GUID Full: 0xf130b03f00000ae8 Type: Creature Entry: 45119 Low: 2792' spell=12294
COMBAT_END guid=26 target='GUID Full: 0xf130b03f00000ae8 Type: Creature Entry: 45119 Low: 2792' training=0
COMBAT_START guid=26 target='GUID Full: 0xf130b03f00000ae7 Type: Creature Entry: 45119 Low: 2791' training=0 defending=1 until=0
COMBAT_CAST guid=26 target='GUID Full: 0xf130b03f00000ae7 Type: Creature Entry: 45119 Low: 2791' spell=12294
COMBAT_CAST guid=26 target='GUID Full: 0xf130b03f00000ae7 Type: Creature Entry: 45119 Low: 2791' spell=772
COMBAT_CAST guid=26 target='GUID Full: 0xf130b03f00000ae7 Type: Creature Entry: 45119 Low: 2791' spell=7384
COMBAT_END guid=26 target='GUID Full: 0xf130b03f00000ae7 Type: Creature Entry: 45119 Low: 2791' training=0
DEATH_TEST_REQUEST guid=26
DEATH_RELEASE guid=26
DEATH_CORPSE_RUN guid=26 path=37.40
DEATH_RECLAIM guid=26
@@ -0,0 +1,16 @@
2026-10-27_23:05:32 INFO [server.adventurebot.navigation] NAV_PLAN seq=1 guid=26 name='Arthoran' map=0 segment=0 start=(-8744.788 319.742 100.549) target=(-8745.762 312.810 100.206) type=0x1 calculated=1 complete=1 suspicious=0 points=3 direct=7.01 path=7.02 maxSegment=4.00 maxZDelta=0.31 finalDistance=0.00 repaths=0 reason='ok'
2026-10-27_23:05:32 INFO [server.adventurebot.navigation] NAV_EXEC seq=1 guid=26 name='Arthoran' segment=1 splinePoints=3 splineLength=7.07 segmentEnd=(-8745.762 312.810 100.206) target=(-8745.762 312.810 100.206) walk=0
2026-10-27_23:05:32 INFO [server.adventurebot.navigation] NAV_START guid=26 name='Arthoran' map=0 target=(-8745.762 312.810 100.206) remaining=5.51 state=Moving
2026-10-27_23:05:33 DEBUG [server.adventurebot.navigation] NAV_SAMPLE seq=1 guid=26 segment=1 pos=(-8745.745 312.927 100.193) remaining=0.00 deviation=0.00 splineActive=1 repaths=0
2026-10-27_23:06:54 INFO [server.adventurebot.navigation] NAV_PLAN seq=2 guid=26 name='Arthoran' map=0 segment=0 start=(-11831.100 -2510.480 -20.070) target=(-11841.850 -2510.480 -20.025) type=0x1 calculated=1 complete=1 suspicious=0 points=4 direct=10.75 path=10.79 maxSegment=4.03 maxZDelta=0.46 finalDistance=0.00 repaths=0 reason='ok'
2026-10-27_23:06:54 INFO [server.adventurebot.navigation] NAV_EXEC seq=2 guid=26 name='Arthoran' segment=1 splinePoints=4 splineLength=10.85 segmentEnd=(-11841.850 -2510.480 -20.025) target=(-11841.850 -2510.480 -20.025) walk=0
2026-10-27_23:06:54 INFO [server.adventurebot.navigation] NAV_START guid=26 name='Arthoran' map=0 target=(-11841.850 -2510.480 -20.025) remaining=9.25 state=Moving
2026-10-27_23:06:55 DEBUG [server.adventurebot.navigation] NAV_SAMPLE seq=2 guid=26 segment=1 pos=(-11838.028 -2510.480 -19.686) remaining=2.34 deviation=0.00 splineActive=1 repaths=0
2026-10-27_23:13:16 INFO [server.adventurebot.navigation] NAV_PLAN seq=3 guid=26 name='Arthoran' map=0 segment=0 start=(-12186.000 -2566.580 3.966) target=(-12187.900 -2603.740 1.578) type=0x1 calculated=1 complete=1 suspicious=0 points=11 direct=37.29 path=37.40 maxSegment=4.06 maxZDelta=0.68 finalDistance=0.00 repaths=0 reason='ok'
2026-10-27_23:13:16 INFO [server.adventurebot.navigation] NAV_EXEC seq=3 guid=26 name='Arthoran' segment=1 splinePoints=11 splineLength=37.40 segmentEnd=(-12187.900 -2603.740 1.578) target=(-12187.900 -2603.740 1.578) walk=0
2026-10-27_23:13:16 INFO [server.adventurebot.navigation] NAV_START guid=26 name='Arthoran' map=0 target=(-12187.900 -2603.740 1.578) remaining=35.79 state=Moving
2026-10-27_23:13:17 DEBUG [server.adventurebot.navigation] NAV_SAMPLE seq=3 guid=26 segment=1 pos=(-12186.536 -2577.073 3.698) remaining=25.29 deviation=0.00 splineActive=1 repaths=0
2026-10-27_23:13:18 DEBUG [server.adventurebot.navigation] NAV_SAMPLE seq=3 guid=26 segment=1 pos=(-12187.064 -2587.424 2.146) remaining=14.85 deviation=0.00 splineActive=1 repaths=0
2026-10-27_23:13:19 DEBUG [server.adventurebot.navigation] NAV_SAMPLE seq=3 guid=26 segment=1 pos=(-12187.603 -2597.920 1.355) remaining=4.33 deviation=0.00 splineActive=1 repaths=0
2026-10-27_23:13:20 DEBUG [server.adventurebot.navigation] NAV_SAMPLE seq=3 guid=26 segment=1 pos=(-12187.900 -2603.740 1.578) remaining=0.00 deviation=0.00 splineActive=0 repaths=0
2026-10-27_23:13:20 INFO [server.adventurebot.navigation] NAV_COMPLETE seq=3 guid=26 name='Arthoran' remaining=0.00 repaths=0 segments=1 maxDeviation=0.00
@@ -0,0 +1,8 @@
Final Debug build -j10 and installation succeeded.
51 tests/clientless/test_*.py scripts passed.
Live native training: 60 seconds, valid real dummy, normal MMAP approach and regular end.
Real combat: spawn300425 entry42336 level58; native hostile/LOS/phase/strength/path checks; creature killed with native respawn record.
Death fixture1: explicit native testdeath, normal release at1793138872; no acceptable MMAP route; no accelerated clock; cemetery resurrection after1793139172 without sickness.
Death fixture2: explicit native testdeath at nearby native ground point; ghost path37.40 yards; native reclaim; saved aliveHP4340, no corpse or sickness.
Administrative offline positioning used for isolated tests, not evidence of long-distance travel.
Native restarts during a ghost run and multi-attacker focus were exercised by production adapter tests, not live multi-actor gameplay.
+189
View File
@@ -0,0 +1,189 @@
# Kampf, Reaktionen und Interaktionen – vereinbarte Grundlage
Stand 27.10.2026. Neue Nutzervorgaben nach Abschluss der sieben Grundintents.
Historische Vereinbarungen und aktueller Umsetzungsstand; siehe Nachtrag unten.
Kampf, reaktives Verhalten, Bot-Spieler- und Bot-Bot-Interaktion gehören für den
Nutzer zusammen. Bestehende Intent-, Reise- und Szeneninfrastruktur erhalten;
DungeonBots bleiben unabhängig. Allgemeine Hinderniserkennung bleibt vertagt.
## Verbindliche Zielregeln
- Selbstverteidigung: die Kreatur, die zuerst tatsächlich Schaden am Bot
verursacht, wird Fokusziel. Weitere Angreifer verdrängen sie nicht. Fokus bis
zum Tod dieser Kreatur beibehalten. Ein zufälliger Zielwechsel ist unerwünscht.
- Proaktiver Angriff: nächstgelegenes gültiges und zum aktuellen Intent passendes
Ziel wählen. Die Intentregeln bestimmen zuerst die zulässige Zielmenge.
- Abenteuer: nahe feindliche Kreaturen auf dem aktuellen Weg berücksichtigen;
keine wahllose Jagd auf alles im allgemeinen Umkreis.
- Beruf/Lederjagd: ein nahes geeignetes Tier wählen. Die bestehende Freigabe des
betreffenden Hauptberufs im Lua-Profil gilt weiter. Arthoran hat derzeit nur
mining und blacksmithing; diese Absprache erteilt ihm nicht automatisch
skinning. Das tatsächliche Sammeln/Kürschnern bleibt gemäß bisheriger Regel
performativ; der neu beauftragte Kampf selbst ist real.
- Vor einem eigenständigen Angriff Eignung/Stärke prüfen, insbesondere deutlich
überlegene Gegner ausschließen. Exakte Level-, Distanz-, Gesundheits- und
Ressourcen-Grenzen sind noch nicht vom Nutzer festgelegt.
Noch auszugestalten: Verhalten, wenn ein Fokusziel despawnt, phasen-/kartenfremd
oder dauerhaft unerreichbar wird; Selbstverteidigung gegen überlegene Gegner;
weitere verbleibende Angreifer nach Tod des Erstangreifers. Nicht als bereits
vereinbarte Zielwechsel-/Fluchtregeln ausgeben.
## Vom Nutzer bestätigte Ergänzungen
- Bei Berufsarbeit einen nahen feindlichen Gegner berücksichtigen, der den
unmittelbaren Zugang zur konkret gewählten Ressource/Arbeitsstelle blockiert.
- Konservativer Erstangriffsfilter: native Angriffserlaubnis, lebendig, richtige
Phase, lokaler erreichbarer Kampfplatz, kein Weltboss; Elites zunächst meiden.
Zusätzlich Leveldifferenz, aktuelle HP/Ressourcen und nahe weitere Gegner
berücksichtigen. Grenzwerte als anpassbare Regeln führen.
- Geselligkeit, Ruhe, Freizeit, Wirtschaften und der abstrahierte Instanzaufenthalt
begründen von sich aus keine Jagd. Selbstverteidigung ist eine überlagernde
Reaktion und benötigt keinen Wechsel zum Abenteuerintent.
## Verbindlicher Todesablauf
Seele freigeben, als Geist vom Friedhof zurück zur eigenen Leiche laufen und
sich wiederbeleben, sobald native Reichweite und übrige native Bedingungen
vorliegen. Ist kein Pfad verfügbar, ist nach fünf Minuten die Wiederbelebung
am Friedhof erlaubt. Ein langsamer, aber funktionierender Leichenrücklauf ist
nicht allein wegen Überschreitens von fünf Minuten ein fehlender Pfad.
Technische Ableitung: nach erreichbaren Punkten innerhalb des erlaubten
Leichenradius suchen, nicht nur nach einem Pfad zur exakten Leichenposition.
Die fünf Minuten als absolute Frist ab Freigabe/Ankunft am Friedhof behandeln,
nicht bei Pfadversuchen oder Neustart neu beginnen. Die genaue Timerauslegung
ist eine Umsetzungsauslegung des Friedhof-Fallbacks, keine zusätzliche explizite
Antwort des Nutzers. Nach Wiederbelebung ausstehendes Verhalten sicher fortsetzen;
keinen Endzeitblock durch die Todesphase künstlich verlängern.
## Bestätigte Quellgrundlagen
- `AI/PlayerAI/PlayerAI.cpp` und `.h`: gemeinsame PlayerAI-Helfer für Rollen,
bekannte Zauberränge, native Wirkprüfung, Autoangriffe und Formen. Die große
Klassen-/Spezialisierungsauswahl liegt derzeit privat in SimpleCharmedPlayerAI.
Sie enthält situative Unterbrechungen, DoTs, Kombopunkte und Schutzfähigkeiten.
- SimpleCharmedPlayerAI ist technisch eine Charm-KI mit Charmer-Zielwahl und
Unterstützung dieses Charmers. Der Core installiert sie in Unit::UpdateCharmAI;
mehrere Bossskripte leiten davon ab. Also nicht als völlig ungenutzt behandeln.
Wiederverwendung erfordert Entkopplung dieser konkreten Charm-Steuerung;
keine Unterstellung, der Nutzer wolle sie unverändert übernehmen.
- Unit::DealDamage ruft DamageTaken/DamageDealt bei vorhandener UnitAI sowie
den UnitScript-OnDamage-Hook auf. Diese Aufrufe liegen VOR dem abschließenden
HP-Abzug, und nachfolgende Bedingungen können Schaden verhindern. Für die
Regel 'zuerst tatsächlich Schaden verursacht' genügt ein ungeprüftes
Vorab-Ereignis daher nicht. Nullschaden/Godmode/abgebrochene Treffer dürfen
keinen falschen Erstangreifer festschreiben.
- WorldSession::HandleRepopRequestOpcode führt BuildPlayerRepop und
RepopAtGraveyard aus. HandleReclaimCorpseOpcode prüft Geiststatus,
GetCorpseReclaimDelay, gleiche Karte und CORPSE_RECLAIM_RADIUS (aktuell 39 Yards),
dann native Wiederbelebung und Corpse-Bones. Keine eigene großzügigere Distanz.
- Der Nutzer lehnt den normalen Geistheiler ausdrücklich ab: beim Fallback
nach fünf Minuten keine Wiederbelebungskrankheit und kein zusätzlicher
Haltbarkeitsverlust. Nicht HandleSpiritHealerActivateOpcode/SendSpiritResurrect
verwenden. Die native ResurrectPlayer-Funktion ohne Sickness und die saubere
Corpse-Bones-/Geistzustandsbereinigung können dafür wiederverwendet werden.
Der normale Leichenrücklauf bleibt zuerst vorgesehen.
- PlayerScript bietet Chat- und gerichtete Textemote-Ereignisse. Vorhandene
AdventureSocialActions stellen bereits Begrüßung, Gespräch und Reaktionen dar.
Konkrete Regeln für Spieler-/Bot-Interaktionen sind noch weiter auszugestalten.
In dieser Vorprüfung wurden keine Kampf-/Todesabläufe implementiert oder live
getestet und keine Server-/DB-Zustände verändert.
## Nachtrag: Reparaturbesuch und Kampfausstattung
Nutzerauftrag: zusätzliche Wirtschafts-Mikroaktion bei einem echten Händler mit
Reparaturfähigkeit. Sichtbare Szene rein performativ, keine Händler-/Kauf-/
Reparaturpakete, kein Geldabzug. Nach erfolgreich abgeschlossenem Besuch repariert
Hintergrundlogik alle Ausrüstungsgegenstände auf volle Haltbarkeit. Dies ist die
explizit gewünschte Ausnahme von der bisherigen reinen Darstellung wirtschaftlicher
Besuche; keine Reparaturkosten-/Zustandsplanung nötig.
Arthoran soll Kopien von Allitesters angelegter Rüstung und Waffen erhalten,
alle stufengerechten Klassenfähigkeiten lernen, Talentpunkte sinnvoll innerhalb
der Stufe-60-Grenze verteilen und passende Angriffe vorführen. Allitesters
Originale bleiben unverändert. Beide sind Krieger; Vorprüfung ergab Zorn-Set und
Ashkandi, passend zu einem Waffen-Build. Quelle ist Stufe70, Arthoran bleibt60.
Die native Level-60-Talenttabelle erlaubt 26 Punkte, keine überhöhte Freischaltung
aller Talentbäume. Kampfübung an einer tatsächlich vorhandenen Stufe-60-Puppe;
damit wird noch keine vollständige autonome Kampf-/Reaktions-KI behauptet.
## Technischer Umsetzungsstand nach Kampfausstattung
Reparaturaktion implementiert; native IsArmorer-Prüfung, erreichbare freundliche
NPCs und bestehende Ankunfts-/Szenenprüfung. Einmal nach erfolgreichem Besuch
DurabilityRepairAll(false, 1.0f, false), ohne Kosten oder Interaktionspakete.
Arthoran erhielt neun neue Iteminstanzen aus Allitesters angelegtem Zorn-Set
und Ashkandi. Frühere Gegenstände bleiben im Inventar; Allitesters vollständiger
Inventar-/Item-/Zauber-/Talentsnapshot blieb unverändert. Native Klassen- und
Stufenprüfung, Plattenkompetenz und Haltungen sowie 26 Waffen-Talentpunkte
über LearnTalent, Baum 746. Keine fremden Spezialisierungsfähigkeiten freischalten.
Begrenzte Trainingsübung über PlayerAI-Helfer; nur tatsächliche passende
Trainingspuppen, native Sicht-/Phasen-/Angriffserlaubnis. Keine Phasenänderung.
Phase169 ist DEFAULT_PHASE; die erste Fehlposition hatte fehlende Sichtlinie.
Nach administrativer Offline-Positionierung vor der echten Puppe314147 waren
Phase, Sichtbarkeit und Sichtlinie gültig. Live wurden unter anderem Heroischer
Stoß78, Verwunden772, Tödlicher Stoß12294 und Überwältigen7384 von Spell::prepare
mit SPELL_CAST_OK(255) angenommen. Das belegt die begrenzte Übung, keine
fertige autonome Selbstverteidigungs-, Todes- oder Interaktionssteuerung.
Debug-Build erfolgreich installiert; 48 Testskripte erfolgreich.
Live-Reparatur erfolgreich: reguläre lokale MMAP-Anreise zu Marda Weller1287,
vollständige Szene und ECONOMY_REPAIR_COMPLETE. Nach saveall alle neun zuvor
halbierten Kopien wieder voll, Geld0 und Allitesters Snapshot unverändert.
## Ausbau: Training, Erstangriffe und Todesreaktion
AdventureCombatMgr überlagert den Lua-Adventure-Controller während Kampf/Tod.
Generischer ClientlessPlayerMgr übergibt ausschließlich tatsächlich nach HP-Abzug
entstandene Schäden als GUID-Ereignisse. Mutex-Queue trennt Map-Worker und
World-Thread; andere Controller bleiben bei ihrer bisherigen Standardfunktion.
Der erste tatsächliche Kreaturenangreifer wird festgehalten. Weitere Angreifer
warten in Schadensreihenfolge, bis der Fokus tot ist. Phasenverlust/Despawn bzw.
20 Sekunden unerreichbarer Fokus beenden eine nicht mehr ausführbare Verfolgung.
Spieler und von Spielern kontrollierte Kreaturen sind nicht Gegenstand dieses Ausbaus.
Automatischer Erstangriff im Abenteuer nur auf nächstgelegenen passenden Gegner
im lokalen20Yards-Umkreis und8Yards-Korridor des tatsächlichen aktiven MMAP-Wegs.
Für Kürschnerei nur bei Profilrecht und lebendem BEAST mit nativer SkinLootId;
Arthoran erhält dieses Recht weiterhin nicht. Andere Intents lösen keine Jagd aus.
Vorläufiger konservativer Filter: höchstens+2Level, mindestens60%HP und bei
Manaklassen30%Mana, keine Elites/Weltbosse, höchstens ein Gegner im12Yards-Umfeld,
kein bereits fremd bekämpfter Gegner. Native Phase/Sicht/Angriffserlaubnis und
vollständiger unverdächtiger MMAP-Anschluss bleiben Voraussetzung.
Kampf nutzt native Autoangriffe und für Krieger die stufengelernten Fähigkeiten,
Ressourcen und Abklingzeiten. Keine künstlichen Ressourcen-/Schadenswerte.
Andere Klassen erhalten hier zunächst Autoangriffe; eigene Rotationen folgen.
Training ist Freizeit-Aktion leisure_training60–180Sekunden an echter erreichbarer
Puppe im60Yards-Umkreis, begrenzt zusätzlich durch aktuelle Intentfrist.
Echte Selbstverteidigung kann Training unterbrechen. Keine Dummy-Spawns/Phasenänderungen.
Tod über native Freigabe- und Leichenaufnahmehandler. Persistierter GhostTime
bestimmt sowohl native Aufnahmefrist als auch Fünf-Minuten-Fallback; Charakter
und Leichenzeitpunkt werden nach Freigabe gespeichert. Mehrere mögliche Ziele
innerhalb39Yards werden geprüft. Gescheiterte Bewegungspunkte werden für diesen
Leichenlauf gemieden. Bei funktionierendem Pfad keine pauschale Zeitbegrenzung.
Fallback: erst nach300Sekunden ohne passenden Pfad native Friedhofsrückkehr,
danach ResurrectPlayer(.5,false) und SpawnCorpseBones, keine Geistheiler-Interaktion,
keine Wiederbelebungskrankheit oder zusätzliche Haltbarkeitsänderung. Normale
Kampfschäden bleiben erhalten. Intent-Endzeiten werden durch Reaktionen nicht verlängert.
Diagnose: clientless combat train <guid> [seconds], attack <guid> <spawn-id>,
testdeath <guid>, stop <guid>. Testdeath nutzt ausdrücklich einen nativen Testtod
für einen verwalteten Bot, ohne zusätzlichen Haltbarkeitsverlust.
51 Testskripte bestanden; Live-Ergebnisse separat ergänzen.
Live-Abschluss: Trainingsszene regulär nach60Sekunden beendet; Stufe58-Bär42336
(spawn300425) nativ besiegt und Respawn registriert. Erster Testtod: mangels
akzeptiertem MMAP-Rückweg volle300Sekunden gewartet, malusfreie Friedhofs-
wiederbelebung bestätigt. Zweiter Testtod naheFriedhof: tatsächlicher37.40Yards-
Geisterlauf und nativeLeichenaufnahme. NachLogout lebendHP4340 ohneLeiche oder
Aura15007 gespeichert. KeineUhrabkürzung, keinNormalreisebeleg durchTestpositionierung.
Belege: checkpoints/2026-10-27-combat/. Mehrfachangreifer undNeustart während
Leichenlauf produktiv mitAdaptern geprüft, nochkeine entsprechendeLiveabnahme.
+5
View File
@@ -11,6 +11,7 @@ Transaktionshandler aufgerufen und keine wirtschaftlichen Fenster simuliert.
| Aktion | Ablauf |
|---|---|
| `economy_repair` | Einen echten reparaturfähigen Händler aufsuchen; nach abgeschlossener Szene sämtliche Ausrüstung kostenlos nativ reparieren. Keine Händlerinteraktion. |
| `economy_vendor_visit` | Einen freundlichen erreichbaren Händler aufsuchen, kurz umsehen, verabschieden und zurücktreten. |
| `economy_goods` | Beim Händler länger Waren betrachten. Die Darstellung richtet sich zum tatsächlichen Händler; eine passende Auslage ist keine Voraussetzung. |
| `economy_trade` | Begrüßen, Handelsgespräch darstellen, zuhören, nicken und verabschieden. |
@@ -160,3 +161,7 @@ Die anschließende Abenteuer-Migration überführt die allgemeinen Wirtschafts-
gewichte in ausdrückliche Location-/Intent-Katalogeinträge. Ein im Code
versteckter Kategorie-Fallback entfällt damit; vorhandene angepasste Gewichte
einschließlich Null behalten Vorrang.
Ergänzung: `2026_10_27_08_adventure_repair.sql` ergänzt den Reparaturbesuch.
Die vollständige Haltbarkeitswiederherstellung ist die ausdrücklich gewünschte
Ausnahme von der rein performativen Wirtschaftsdarstellung.
+6
View File
@@ -11,6 +11,7 @@ auswählen (Standard 0,5); vorhandene gespeicherte Tagespläne bleiben bestehen.
| Schlüssel | Ablauf |
|---|---|
| `leisure_training` | Eine echte sichtbare und erreichbare Trainingspuppe im lokalen Umkreis aufsuchen und 60–180 Sekunden mit nativen Angriffen trainieren. |
| `leisure_walk` | Zusammenhängender MMAP-Hinweg zu einem entfernten Außenpunkt, danach dieselben Wegpunkte zurück; Gehmodus und kurze Pause am Wendepunkt. |
| `leisure_roam` | Eine lokale Spazierstrecke auswählen, vollständig verfolgen und auf demselben Weg zurückkehren. |
| `leisure_pet_walk` | Eigenen Begleiter beschwören, gegebenenfalls anleinen, eine zusammenhängende Strecke entspannt hin und zurück gehen, zurückkehren und eigene Begleiterressourcen aufräumen. |
@@ -281,3 +282,8 @@ Die abschließende visuelle Beurteilung erfolgt im Spiel.
Der Nutzer hat anschließend sowohl die zusammenhängenden Fußspaziergänge als
auch das flüssige Haustierausführen nach der Tempokorrektur visuell bestätigt.
Schlafdarstellung, Ausritte, Freizeitflüge und Angeln sind dadurch nicht abgenommen.
Training ergänzt Migration `2026_10_27_09_adventure_training.sql`. Es nutzt
die Kampfreaktion, sucht höchstens 60 Yards lokal und respektiert das Ende des
aktuellen Zeitblocks. Echte Angreifer unterbrechen das Training zugunsten der
Selbstverteidigung. Kein Dummy-Spawn, Phasenwechsel oder künstlicher Schaden.
@@ -0,0 +1,7 @@
-- A real native repairer is required. Only the scene is acted; completion
-- restores durability for free without invoking trade/vendor handlers.
INSERT IGNORE INTO vcore_adventure_action
(action_key,kind,min_seconds,max_seconds,requirement,emote_id)
VALUES ('economy_repair','social',25,45,'repair',0);
INSERT IGNORE INTO vcore_adventure_action_intent(action_key,intent,weight)
VALUES ('economy_repair','economy',2);
@@ -0,0 +1,6 @@
-- Training is a leisure scene, only at a real, visible, reachable training dummy.
INSERT IGNORE INTO vcore_adventure_action
(action_key,kind,min_seconds,max_seconds,requirement,emote_id)
VALUES ('leisure_training','leisure',60,180,'training',0);
INSERT IGNORE INTO vcore_adventure_action_intent(action_key,intent,weight)
VALUES ('leisure_training','leisure',2);
+3
View File
@@ -67,6 +67,7 @@
#include "PetAI.h"
#include "PhasingHandler.h"
#include "Player.h"
#include "ClientlessPlayerMgr.h"
#include "PlayerAI.h"
#include "QuestDef.h"
#include "ReputationMgr.h"
@@ -902,6 +903,8 @@ bool Unit::HasBreakableByDamageCrowdControlAura(Unit* excludeCasterChannel) cons
victim->ToPlayer()->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_TOTAL_DAMAGE_RECEIVED, damage);
victim->ModifyHealth(-(int32)damage);
if (damage && victim->IsPlayer())
sClientlessPlayerMgr->DamageTaken(victim->ToPlayer(), attacker, damage);
if (damagetype == DIRECT_DAMAGE || damagetype == SPELL_DIRECT_DAMAGE)
victim->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::NonPeriodicDamage, spellProto);
@@ -1,4 +1,5 @@
#include "AdventureBehaviorMgr.h"
#include "AdventureCombatMgr.h"
#include "AdventureLocationMgr.h"
#include "AdventureEconomyPolicy.h"
#include "AdventureRestSpace.h"
@@ -556,6 +557,7 @@ bool AdventureBehaviorMgr::LeisureAvailable(Player* p,Record const& r,AdventureM
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
auto location=sAdventureLocationMgr->Find(r.Location);
if (!profile || !location) return false;
if (a.Requirement=="training") return sAdventureCombatMgr->TrainingAvailable(p);
if (a.Requirement=="fish")
{
auto rod=p->GetWeaponForAttack(BASE_ATTACK,true);
@@ -640,6 +642,8 @@ bool AdventureBehaviorMgr::PlanLeisureWalk(Player* p, Record& r)
bool AdventureBehaviorMgr::StartLeisure(Player* p,Record& r,AdventureMicroAction const& a)
{
if (!LeisureAvailable(p,r,a)) return false;
if (a.Requirement=="training")
return sAdventureCombatMgr->Train(p,r.MicroTimer,r.Deadline);
sClientlessLuaEngine->DismountGroundMount(p,"leisure-scene-start");
auto& s=r.Leisure;s={};s.Kind=a.Requirement;s.Origin.Relocate(p);
if (!p->IsOutdoors() && (s.Kind=="walk" || s.Kind=="roam" || s.Kind=="pet_walk"))
@@ -1739,3 +1743,23 @@ bool AdventureBehaviorMgr::UpdateInstance(Player* player, Record& r, time_t now)
}
return true;
}
void AdventureBehaviorMgr::InterruptForCombat(Player* player)
{
auto it=_records.find(player->GetGUID().GetCounter());
if(it==_records.end()) return;
auto& r=it->second;
ResetMicro(player,r);
sClientlessPlayerMgr->ClearQueuedActions(player->GetGUID().GetCounter());
sClientlessActionExecutorMgr->Clear(player->GetGUID().GetCounter());
sClientlessTravelMgr->CancelTravel(player->GetGUID().GetCounter());
if(!r.Instance.Active) {r.State=Phase::Idle;r.Dirty=true;r.Queued=false;}
r.Detail="combat/death reaction; original deadline retained";
}
std::string AdventureBehaviorMgr::CombatIntent(Player* player) const
{
auto it=_records.find(player->GetGUID().GetCounter());
if(it==_records.end() || !it->second.Enabled || it->second.Instance.Active ||
(it->second.State!=Phase::Travel && it->second.State!=Phase::OnSite)) return {};
return it->second.Intent;
}
@@ -29,6 +29,8 @@ public:
void IntentChanged(Player* player, ClientlessIntent const* intent);
void Update(Player* player, uint32 diff);
bool Command(ChatHandler* handler, char const* args);
void InterruptForCombat(Player* player);
std::string CombatIntent(Player* player) const;
private:
enum class Phase {Idle, Travel, OnSite, Switching, Handover};
struct RecoveryScene
@@ -0,0 +1,287 @@
#include "AdventureCombatMgr.h"
#include "AdventureCombatPolicy.h"
#include "AdventureBehaviorMgr.h"
#include "ClientlessLuaEngine.h"
#include "ClientlessPlayerMgr.h"
#include "ClientlessActionExecutorMgr.h"
#include "ClientlessTravelMgr.h"
#include "Creature.h"
#include "Corpse.h"
#include "Player.h"
#include "PlayerAI.h"
#include "ObjectAccessor.h"
#include "MotionMaster.h"
#include "MoveSpline.h"
#include "Map.h"
#include "Spell.h"
#include "SpellAuras.h"
#include "WorldSession.h"
#include "WorldPacket.h"
#include "Opcodes.h"
#include "GameTime.h"
#include "Log.h"
#include <algorithm>
#include <cmath>
// Native player spell checks, cooldowns, rage and autoattacks. No charmer/fake AI
// installation and no generated resources or synthetic damage.
class AdventureCombatMgr::Fighter : public PlayerAI
{
public:
explicit Fighter(Player* player):PlayerAI(player) { }
void UpdateAI(uint32) override { }
void Tick(Creature* target,uint32 diff)
{
_rend=_rend>diff?_rend-diff:0;_filler=_filler>diff?_filler-diff:0;
me->SetFacingToObject(target);
if(me->HasUnitState(UNIT_STATE_CASTING))return;
if(me->getClass()==CLASS_WARRIOR && !me->HasAura(2457) && Cast(2457,me))return;
if(!me->IsWithinMeleeRange(target))return; // MMAP approach is owned by manager.
if(me->GetVictim()!=target)me->Attack(target,true);
DoAutoAttackIfReady();
if(me->getClass()!=CLASS_WARRIOR)return;
if(!me->HasAura(6673) && Cast(6673,me))return;
if(Cast(12294,target))return;
if(!_rend && !target->GetAuraApplicationOfRankedSpell(772,me->GetGUID()) && Cast(772,target))
{_rend=12000;return;}
if(!_filler)for(uint32 id:{7384u,1464u,78u})if(Cast(id,target)){_filler=3000;break;}
}
private:
bool Cast(uint32 id,Unit* target)
{
auto cast=VerifySpellCast(id,target);if(!cast)return false;
uint32 actual=cast.first->GetSpellInfo()->Id;
SpellCastTargets targets;targets.SetUnitTarget(cast.second);
auto result=cast.first->prepare(targets);
if(result==SPELL_CAST_OK)TC_LOG_INFO("server.adventurebot","COMBAT_CAST guid=%u target='%s' spell=%u",me->GetGUID().GetCounter(),target->GetGUID().ToString().c_str(),actual);
return result==SPELL_CAST_OK;
}
uint32 _rend=0,_filler=0;
};
AdventureCombatMgr* AdventureCombatMgr::instance(){static AdventureCombatMgr mgr;return &mgr;}
bool AdventureCombatMgr::Valid(Player* p,Creature* c,bool training) const
{
return c && c->IsAlive() && c->IsInWorld() && p->IsInPhase(c) && p->CanSeeOrDetect(c) &&
p->IsValidAttackTarget(c) && !c->GetControllingPlayer() && !c->IsEvadingAttacks() &&
(training?(c->GetScriptName()=="npc_training_dummy" && c->getLevel()<=p->getLevel()+2 && !c->isWorldBoss()):c->GetScriptName()!="npc_training_dummy");
}
bool AdventureCombatMgr::Reachable(Player* p,Creature* c) const
{
if(!p->IsWithinLOSInMap(c))return false;
if(p->IsWithinMeleeRange(c))return true;
Position end;c->GetNearPoint(p,end.m_positionX,end.m_positionY,end.m_positionZ,0.5f,c->GetAngle(p));
ClientlessPathDiagnostics path;
return sClientlessTravelMgr->AnalyzePath(p,end,path) && path.Complete && !path.Suspicious && path.PathLength<=100;
}
bool AdventureCombatMgr::Safe(Player* p,Creature* c) const
{
if(!Valid(p,c,false) || p->GetExactDist(c)>40 || (c->GetVictim() && c->GetVictim()!=p))return false;
std::vector<Creature*> others;p->GetCreatureListWithEntryInGrid(others,0,35);
unsigned hostiles=0;
for(auto other:others)if(other->IsAlive() && p->IsInPhase(other) && p->IsHostileTo(other) &&
other->GetExactDist(c)<12 && !other->GetControllingPlayer())++hostiles;
// Include neutral quarry itself in the risk count.
if(!p->IsHostileTo(c))++hostiles;
auto maxMana=p->GetMaxPower(POWER_MANA);
return AdventureCombatPolicy::SafeFirstAttack(p->getLevel(),c->getLevel(),p->GetHealthPct(),
c->isElite(),c->isWorldBoss(),hostiles,p->GetPowerType()==POWER_MANA,
maxMana?100.0f*p->GetPower(POWER_MANA)/maxMana:0) && Reachable(p,c);
}
Creature* AdventureCombatMgr::Dummy(Player* p) const
{
if(!p->IsAlive() || p->IsInCombat() || p->GetTransport() || p->IsInFlight() || p->IsBeingTeleported())return nullptr;
Creature* best=nullptr;std::vector<Creature*> all;p->GetCreatureListWithEntryInGrid(all,0,60);
for(auto c:all)if(Valid(p,c,true) && (!best || p->GetExactDist(c)<p->GetExactDist(best)) && Reachable(p,c))best=c;
return best;
}
bool AdventureCombatMgr::TrainingAvailable(Player* p) const{return Dummy(p)!=nullptr;}
bool AdventureCombatMgr::Start(Player* p,Record& r,Creature* c,bool training,bool defending,time_t until)
{
if(!Valid(p,c,training) || !p->IsAlive() || p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsCharmed())return false;
sAdventureBehaviorMgr->InterruptForCombat(p);
sClientlessLuaEngine->DismountGroundMount(p,"combat-start");
p->AttackStop();p->CastStop();p->StopMoving();p->GetMotionMaster()->Clear();p->SetStandState(UNIT_STAND_STATE_STAND);
r.AI=std::make_shared<Fighter>(p);r.Target=c->GetGUID();r.Training=training;r.Defending=defending;
r.Suspended=true;r.Until=until;r.Timer=0;r.UnreachableMs=0;
p->SetSelection(c->GetGUID());
TC_LOG_INFO("server.adventurebot","COMBAT_START guid=%u target='%s' training=%u defending=%u until=%llu",p->GetGUID().GetCounter(),c->GetGUID().ToString().c_str(),training,defending,static_cast<unsigned long long>(until));
return true;
}
bool AdventureCombatMgr::Train(Player* p,uint32 durationMs,time_t deadline)
{
auto c=Dummy(p);if(!c)return false;
auto now=GameTime::GetGameTime();auto until=now+std::max(10u,std::min(durationMs/1000,180u));
if(deadline && deadline<until)until=deadline;
if(until<=now)return false;
return Start(p,_records[p->GetGUID().GetCounter()],c,true,false,until);
}
bool AdventureCombatMgr::Attack(Player* p,uint32 spawn)
{
auto c=p->GetMap()->GetCreatureBySpawnId(spawn);
return !p->IsInCombat() && Safe(p,c) && Start(p,_records[p->GetGUID().GetCounter()],c,false,false,0);
}
void AdventureCombatMgr::DamageTaken(Player* p,Unit* attacker,uint32 damage)
{
auto c=attacker?attacker->ToCreature():nullptr;
if(!damage || !p->IsAlive() || !c || c->GetControllingPlayer())return;
auto& r=_records[p->GetGUID().GetCounter()];
if(std::find(r.Damagers.begin(),r.Damagers.end(),c->GetGUID())==r.Damagers.end())r.Damagers.push_back(c->GetGUID());
}
void AdventureCombatMgr::Finish(Player* p,Record& r)
{
TC_LOG_INFO("server.adventurebot","COMBAT_END guid=%u target='%s' training=%u",p->GetGUID().GetCounter(),r.Target.ToString().c_str(),r.Training);
bool training=r.Training;
p->AttackStop();p->CastStop();p->StopMoving();p->GetMotionMaster()->Clear();p->SetSelection(ObjectGuid::Empty);
sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());
if(training && std::all_of(p->getAttackers().begin(),p->getAttackers().end(),[](Unit* u)
{auto c=u->ToCreature();return c && c->GetScriptName()=="npc_training_dummy";}))p->CombatStop(true); // Only the bounded dummy scene may end its own combat.
r.AI.reset();r.Target.Clear();r.Training=false;r.Defending=false;r.UnreachableMs=0;r.Cooldown=10000;
}
void AdventureCombatMgr::Stop(Player* p)
{
auto it=_records.find(p->GetGUID().GetCounter());if(it!=_records.end())Finish(p,it->second);
}
void AdventureCombatMgr::Offline(Player* p){_records.erase(p->GetGUID().GetCounter());}
Creature* AdventureCombatMgr::Proactive(Player* p) const
{
auto intent=sAdventureBehaviorMgr->CombatIntent(p);
if(intent!="adventure" && intent!="profession")return nullptr;
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
if(intent=="profession" && (!profile || !profile->PrimaryProfessions.count("skinning")))return nullptr;
if(intent=="adventure" && p->movespline->Finalized())return nullptr;
Creature* best=nullptr;std::vector<Creature*> all;p->GetCreatureListWithEntryInGrid(all,0,AdventureCombatPolicy::SearchRadius);
for(auto c:all)
{
if(best && p->GetExactDist(c)>=p->GetExactDist(best))continue;
if(intent=="adventure")
{
if(!p->IsHostileTo(c))continue;
bool nearPath=false;auto path=sClientlessTravelMgr->GetActivePath(p->GetGUID().GetCounter());
if(!path)continue;
auto const& points=*path;
for(std::size_t i=1;i<points.size();++i)
if(AdventureCombatPolicy::SegmentDistance(c->GetPositionX(),c->GetPositionY(),c->GetPositionZ(),points[i-1].GetPositionX(),points[i-1].GetPositionY(),points[i-1].GetPositionZ(),points[i].GetPositionX(),points[i].GetPositionY(),points[i].GetPositionZ())<=AdventureCombatPolicy::PathCorridor){nearPath=true;break;}
if(!nearPath)continue;
}
else if(c->GetCreatureTemplate()->type!=CREATURE_TYPE_BEAST || !c->GetCreatureTemplate()->SkinLootId)continue;
if(Safe(p,c))best=c;
}
return best;
}
bool AdventureCombatMgr::Death(Player* p,Record& r)
{
if(!r.Dead)
{
if(r.AI)Finish(p,r);
sAdventureBehaviorMgr->InterruptForCombat(p);r.Damagers.clear();r.Dead=true;r.Suspended=true;
TC_LOG_INFO("server.adventurebot","DEATH_RELEASE guid=%u",p->GetGUID().GetCounter());
}
if(p->InBattleground() || p->InArena() || p->IsBeingTeleported())return true;
if(!p->HasFlag(PLAYER_FLAGS,PLAYER_FLAGS_GHOST))
{
WorldPacket release(CMSG_REPOP_REQUEST,1);release<<uint8(0);p->GetSession()->HandleRepopRequestOpcode(release);
if(auto corpse=p->GetCorpse())corpse->SaveToDB();
p->SaveToDB();
return true;
}
if(r.CemeteryPending)
{
r.CemeteryPending=false;
p->ResurrectPlayer(0.5f,false);p->SpawnCorpseBones();
TC_LOG_INFO("server.adventurebot","DEATH_CEMETERY_REVIVE guid=%u",p->GetGUID().GetCounter());
return true;
}
auto corpse=p->GetCorpse();if(!corpse)return true;
auto now=GameTime::GetGameTime();
if(corpse->IsWithinDistInMap(p,CORPSE_RECLAIM_RADIUS,true))
{
if(now<corpse->GetGhostTime()+p->GetCorpseReclaimDelay(corpse->GetType()==CORPSE_RESURRECTABLE_PVP))return true;
WorldPacket reclaim(CMSG_RECLAIM_CORPSE,8);reclaim<<corpse->GetGUID();p->GetSession()->HandleReclaimCorpseOpcode(reclaim);
if(p->IsAlive())TC_LOG_INFO("server.adventurebot","DEATH_RECLAIM guid=%u",p->GetGUID().GetCounter());
return true;
}
auto travel=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter());
if(travel && travel->State==ClientlessTravelState::Moving)return true;
if(travel && (travel->State==ClientlessTravelState::Stuck || travel->State==ClientlessTravelState::PathRejected))
{
r.FailedCorpsePoints.push_back(travel->Target);
sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());
}
if(now<r.NextCorpseSearch)return true;
r.NextCorpseSearch=now+5;
// Try a set of legal reclaim points, including near-side points around the
// body; an unreachable exact corpse position alone never triggers fallback.
bool pathFound=false;Position best;float shortest=1e30f;
if(p->GetMapId()==corpse->GetMapId() && p->GetInstanceId()==corpse->GetInstanceId())for(unsigned ring=0;ring<3;++ring)for(unsigned i=0;i<(ring?12u:1u);++i)
{
Position end;float radius=ring==1?20.0f:ring==2?32.0f:0.0f;float angle=i*6.2831853f/12;
float x=corpse->GetPositionX()+radius*std::cos(angle),y=corpse->GetPositionY()+radius*std::sin(angle);
float z=p->GetMap()->GetHeight(p->GetPhaseShift(),x,y,corpse->GetPositionZ()+3,true,8);
if(z<=INVALID_HEIGHT)continue;
end.Relocate(x,y,z);
if(end.GetExactDist(corpse)>CORPSE_RECLAIM_RADIUS-2)continue;
if(std::any_of(r.FailedCorpsePoints.begin(),r.FailedCorpsePoints.end(),[&](Position const& failed){return failed.GetExactDist(end)<3;}))continue;
ClientlessPathDiagnostics path;
if(sClientlessTravelMgr->AnalyzePath(p,end,path) && path.Complete && !path.Suspicious && path.PathLength<shortest)
{pathFound=true;best=end;shortest=path.PathLength;}
}
if(pathFound)
{
sClientlessTravelMgr->StartTravel(p,best);
TC_LOG_INFO("server.adventurebot","DEATH_CORPSE_RUN guid=%u path=%.2f",p->GetGUID().GetCounter(),shortest);
}
else if(AdventureCombatPolicy::CemeteryFallback(now,corpse->GetGhostTime(),true))
{
// Native nearest graveyard relocation. No healer interaction, sickness,
// durability repair/loss or resource cheat; native resurrection at 50%.
r.CemeteryPending=true;
p->RepopAtGraveyard();
}
return true;
}
bool AdventureCombatMgr::Update(Player* p,uint32 diff)
{
if(!p->IsInWorld())return false;
auto& r=_records[p->GetGUID().GetCounter()];
r.Cooldown=r.Cooldown>diff?r.Cooldown-diff:0;
if(p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsCharmed())return r.AI || !p->IsAlive();
if(r.Timer>diff){r.Timer-=diff;return r.AI || !p->IsAlive() || r.Dead;}
r.Timer=500;
if(!p->IsAlive())return Death(p,r);
if(r.Dead){r.Dead=false;r.Suspended=false;r.NextCorpseSearch=0;r.FailedCorpsePoints.clear();r.Cooldown=30000;r.Timer=0;return false;}
// First actual damager wins, including interrupting a proactive/training
// target. Keep all subsequent damagers ordered for after that focus dies.
if(!r.Defending)while(!r.Damagers.empty())
{
auto id=r.Damagers.front();r.Damagers.pop_front();auto c=ObjectAccessor::GetCreature(*p,id);
if(!Valid(p,c,false))continue;
if(r.AI)Finish(p,r);
Start(p,r,c,false,true,0);break;
}
if(!r.AI && !p->IsInCombat() && !r.Cooldown)
if(auto c=Proactive(p))Start(p,r,c,false,false,0);
if(!r.AI)return false;
auto c=ObjectAccessor::GetCreature(*p,r.Target);
if(!Valid(p,c,r.Training) || (r.Until && GameTime::GetGameTime()>=r.Until) || p->GetExactDist(c)>80)
{Finish(p,r);return true;}
if(!Reachable(p,c))
{
r.UnreachableMs+=500;
if(r.UnreachableMs>=20000){TC_LOG_WARN("server.adventurebot","COMBAT_TARGET_UNREACHABLE guid=%u",p->GetGUID().GetCounter());Finish(p,r);}
return true;
}
r.UnreachableMs=0;
if(!p->IsWithinMeleeRange(c))
{
auto nav=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter());
if(!nav || nav->State!=ClientlessTravelState::Moving || nav->Target.GetExactDist(c)>5)
{
Position end;c->GetNearPoint(p,end.m_positionX,end.m_positionY,end.m_positionZ,0.5f,c->GetAngle(p));
sClientlessTravelMgr->StartTravel(p,end);
}
return true;
}
sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());
p->StopMoving();r.AI->Tick(c,500);
return true;
}
@@ -0,0 +1,48 @@
#ifndef VCORE_ADVENTURE_COMBAT_MGR_H
#define VCORE_ADVENTURE_COMBAT_MGR_H
#include "Common.h"
#include "ObjectGuid.h"
#include <ctime>
#include <deque>
#include <memory>
#include <unordered_map>
#include "Position.h"
#include <vector>
class Player;
class Unit;
class Creature;
class TC_GAME_API AdventureCombatMgr
{
public:
static AdventureCombatMgr* instance();
bool Update(Player* player,uint32 diff);
void DamageTaken(Player* player,Unit* attacker,uint32 damage);
void Offline(Player* player);
bool TrainingAvailable(Player* player) const;
bool Train(Player* player,uint32 durationMs,time_t deadline=0);
bool Attack(Player* player,uint32 creatureSpawnId);
void Stop(Player* player);
private:
class Fighter;
struct Record
{
std::shared_ptr<Fighter> AI;
ObjectGuid Target;
std::deque<ObjectGuid> Damagers;
bool Training=false,Defending=false,Suspended=false,Dead=false,CemeteryPending=false;
time_t Until=0,NextCorpseSearch=0;
std::vector<Position> FailedCorpsePoints;
uint32 Timer=0,UnreachableMs=0,Cooldown=0;
};
bool Start(Player* player,Record& record,Creature* target,bool training,bool defending,time_t until);
bool Valid(Player* player,Creature* target,bool training) const;
bool Safe(Player* player,Creature* target) const;
bool Reachable(Player* player,Creature* target) const;
Creature* Dummy(Player* player) const;
Creature* Proactive(Player* player) const;
bool Death(Player* player,Record& record);
void Finish(Player* player,Record& record);
std::unordered_map<uint32,Record> _records;
};
#define sAdventureCombatMgr AdventureCombatMgr::instance()
#endif
@@ -0,0 +1,27 @@
#ifndef VCORE_ADVENTURE_COMBAT_POLICY_H
#define VCORE_ADVENTURE_COMBAT_POLICY_H
#include <cstdint>
#include <cmath>
namespace AdventureCombatPolicy
{
// Conservative initial tuning. Self-defence deliberately bypasses this filter.
constexpr unsigned MaxLevelAdvantage=2;
constexpr float MinimumHealth=60, MinimumMana=30, SearchRadius=20, PathCorridor=8;
inline bool SafeFirstAttack(unsigned level,unsigned enemy,float health,bool elite,
bool boss,unsigned nearbyHostiles,bool manaUser,float mana)
{
return enemy<=level+MaxLevelAdvantage && health>=MinimumHealth && !elite && !boss &&
nearbyHostiles<=1 && (!manaUser || mana>=MinimumMana);
}
inline bool CemeteryFallback(std::int64_t now,std::int64_t released,bool noPath)
{return noPath && released>0 && now>=released && now-released>=300;}
inline float SegmentDistance(float x,float y,float z,float ax,float ay,float az,float bx,float by,float bz)
{
float dx=bx-ax,dy=by-ay,dz=bz-az,length=dx*dx+dy*dy+dz*dz;
float t=length>0?((x-ax)*dx+(y-ay)*dy+(z-az)*dz)/length:0;
t=t<0?0:t>1?1:t;
dx=x-ax-t*dx;dy=y-ay-t*dy;dz=z-az-t*dz;
return std::sqrt(dx*dx+dy*dy+dz*dz);
}
}
#endif
@@ -0,0 +1,31 @@
#ifndef VCORE_ADVENTURE_COMBAT_PREPARATION_POLICY_H
#define VCORE_ADVENTURE_COMBAT_PREPARATION_POLICY_H
#include <array>
#include <cstdint>
namespace AdventureCombatPreparationPolicy
{
// Level-60 solo Arms: damage, bleed/Overpower synergy, self-recovery and
// sensible rage/cooldown support. All ranks still pass Player::LearnTalent.
struct Talent {uint32_t Id, Ranks;};
inline std::array<Talent,13> const& Arms60()
{
static std::array<Talent,13> const talents={{{10134,3},{11163,2},{8176,3},{8190,2},
{10138,3},{10741,2},{11418,2},{9662,2},{11223,1},{10520,3},{8208,1},{8214,1},{8194,1}}};
return talents;
}
template<class Learn,class Remaining>
unsigned SpendArms60(Learn learn,Remaining remaining)
{
unsigned spent=0;
for (unsigned pass=0;pass<40 && remaining();++pass)
{
bool progress=false;
for (auto const& talent:Arms60())
for (unsigned rank=0;rank<talent.Ranks && remaining();++rank)
if (learn(talent.Id,rank)) {++spent;progress=true;}
if (!progress) break;
}
return spent;
}
}
#endif
@@ -6,10 +6,11 @@
namespace AdventureEconomyPolicy
{
enum class Service { None, Vendor, Bank, Auction, Any };
enum class Service { None, Vendor, Bank, Auction, Repair, Any };
inline Service RequiredService(std::string const& kind)
{
if (kind=="vendor_visit" || kind=="goods" || kind=="trade" || kind=="vendor_compare") return Service::Vendor;
if (kind=="repair") return Service::Repair;
if (kind=="bank_visit" || kind=="banking") return Service::Bank;
if (kind=="auction_visit" || kind=="auctions") return Service::Auction;
if (kind=="service_wait" || kind=="service_finish") return Service::Any;
@@ -26,14 +27,15 @@ inline bool Economic(std::string const& kind)
{
return RequiredService(kind)!=Service::None || kind=="supplies";
}
inline bool Matches(std::string const& kind, bool vendor, bool banker, bool auctioneer)
inline bool Matches(std::string const& kind, bool vendor, bool banker, bool auctioneer, bool repairer=false)
{
switch (RequiredService(kind))
{
case Service::Vendor: return vendor;
case Service::Bank: return banker;
case Service::Auction: return auctioneer;
case Service::Any: return vendor || banker || auctioneer;
case Service::Repair: return repairer;
case Service::Any: return vendor || banker || auctioneer || repairer;
default: return false;
}
}
@@ -40,7 +40,7 @@ namespace
bool ServiceMatches(Unit* unit, std::string const& kind)
{
return unit && AdventureEconomyPolicy::Matches(kind,unit->IsVendor(),unit->IsBanker(),
unit->HasFlag(UNIT_NPC_FLAGS,UNIT_NPC_FLAG_AUCTIONEER));
unit->HasFlag(UNIT_NPC_FLAGS,UNIT_NPC_FLAG_AUCTIONEER),unit->IsArmorer());
}
bool TargetVisible(Player* player, WorldObject* target, std::string const& kind)
{
@@ -66,7 +66,7 @@ namespace
if (snapshot) for (auto const& entity:snapshot->Entities)
{
if (entity.Type!=ClientlessEntityType::Creature || !entity.Alive ||
!(entity.Capabilities & (CLIENTLESS_CAP_VENDOR|CLIENTLESS_CAP_BANKER|CLIENTLESS_CAP_AUCTIONEER))) continue;
!(entity.Capabilities & (CLIENTLESS_CAP_VENDOR|CLIENTLESS_CAP_BANKER|CLIENTLESS_CAP_AUCTIONEER|CLIENTLESS_CAP_REPAIR))) continue;
auto unit=ObjectAccessor::GetCreature(*player,ObjectGuid(HighGuid::Unit,entity.Entry,entity.Guid));
if (Friendly(player,unit,c) && ServiceMatches(unit,kind) &&
(c.ServiceKind.empty() || ServiceMatches(unit,c.ServiceKind))) people.push_back(unit);
@@ -772,6 +772,14 @@ bool AdventureSocialActions::Update(Player * p, AdventureSocialScene & s, uint32
}
if (AdventureEconomyPolicy::RequiredService(s.Kind)!=AdventureEconomyPolicy::Service::None)
{
if (s.Kind=="repair" && !s.RepairDone)
{
// The visit is performative. No vendor packet, transaction or money
// operation; the completed, validated scene restores durability.
p->DurabilityRepairAll(false,1.0f,false);
s.RepairDone=true;
TC_LOG_INFO("server.adventurebot","ECONOMY_REPAIR_COMPLETE guid=%u target='%s'",p->GetGUID().GetCounter(),s.TargetKey.c_str());
}
s.Destination=c.Anchor;
if (!c.Move(s.Destination,350)) return false;
s.Departing=true;
@@ -14,6 +14,7 @@ struct AdventureMicroAction;
struct AdventureLocation;
struct AdventureSocialScene
{
bool RepairDone=false;
bool Active=false, Approaching=false, Casting=false, WaitingChair=false, Seated=false, Departing=false;
std::string Action, Kind, TargetKey;
ObjectGuid Target, PreviousSelection;
@@ -4,6 +4,7 @@
#include "ClientlessActionExecutorMgr.h"
#include "ClientlessTravelMgr.h"
#include "AdventureBehaviorMgr.h"
#include "AdventureCombatMgr.h"
#include "AdventureSocialActions.h"
#include "Config.h"
#include "Log.h"
@@ -218,9 +219,11 @@ public:
void OnIntentChanged(Player* player, ClientlessIntent const* intent) override
{ sAdventureBehaviorMgr->IntentChanged(player, intent); }
void Update(Player* player, uint32 diff) override
{ sAdventureBehaviorMgr->Update(player, diff); }
{ if (!sAdventureCombatMgr->Update(player, diff)) sAdventureBehaviorMgr->Update(player, diff); }
void OnDamageTaken(Player* player, Unit* attacker, uint32 damage) override
{ sAdventureCombatMgr->DamageTaken(player, attacker, damage); }
void OnPlayerLogout(Player* player) override
{ sAdventureBehaviorMgr->Offline(player); }
{ sAdventureCombatMgr->Offline(player); sAdventureBehaviorMgr->Offline(player); }
};
}
@@ -6,6 +6,7 @@
#include "ClientlessFlightMgr.h"
#include "Log.h"
#include "Player.h"
#include "ObjectAccessor.h"
#include "World.h"
#include "WorldSession.h"
@@ -150,6 +151,11 @@ std::deque<ClientlessAction> const* ClientlessPlayerMgr::GetQueuedActions(uint32
void ClientlessPlayerMgr::Update(uint32 diff)
{
// Damage can arrive from different map workers. Hand GUID-only notices to
// the world-thread controller, preserving order within each character.
decltype(_damageNotices) damageNotices;
{ std::lock_guard<std::mutex> lock(_damageMutex); damageNotices.swap(_damageNotices); }
// CLIENTLESS_PLAYER_CORE_C8_2_TRAVEL_ROBUSTNESS
sClientlessTravelMgr->Update(diff);
// CLIENTLESS_PLAYER_CORE_C8_3_LOCAL_PERCEPTION
@@ -189,7 +195,12 @@ void ClientlessPlayerMgr::Update(uint32 diff)
}
}
if (record.Controller)
{
auto notices=damageNotices.find(characterGuid);
if(notices!=damageNotices.end())for(auto const& notice:notices->second)
record.Controller->OnDamageTaken(player,ObjectAccessor::GetUnit(*player,notice.Attacker),notice.Damage);
record.Controller->Update(player, diff);
}
}
else if (state == ClientlessPlayerState::Removing && !record.LogoutNotified)
{
@@ -201,3 +212,10 @@ void ClientlessPlayerMgr::Update(uint32 diff)
++itr;
}
}
void ClientlessPlayerMgr::DamageTaken(Player* player, Unit* attacker, uint32 damage)
{
if(!damage || !attacker || !player->GetSession()->IsClientless())return;
std::lock_guard<std::mutex> lock(_damageMutex);
_damageNotices[player->GetGUID().GetCounter()].push_back({attacker->GetGUID(),damage});
}
@@ -10,8 +10,11 @@
#include <memory>
#include <string>
#include <unordered_map>
#include <mutex>
#include "ObjectGuid.h"
class Player;
class Unit;
struct TC_GAME_API ClientlessIntent
{
@@ -34,6 +37,8 @@ public:
virtual void OnIntentChanged(Player*, ClientlessIntent const*) { }
virtual void Update(Player*, uint32) { }
virtual void OnPlayerLogout(Player*) { }
// Notification after health was reduced, never the mutable pre-damage event.
virtual void OnDamageTaken(Player*, Unit*, uint32) { }
};
class TC_GAME_API ClientlessPlayerMgr
@@ -46,6 +51,7 @@ public:
bool StopPlayer(uint32 characterGuid);
bool IsManaged(uint32 characterGuid) const;
void DamageTaken(Player* player, Unit* attacker, uint32 damage);
uint32 GetAccountId(uint32 characterGuid) const;
uint32 GetManagedCount() const { return uint32(_records.size()); }
@@ -72,6 +78,9 @@ private:
bool LogoutNotified = false;
};
struct DamageNotice { ObjectGuid Attacker; uint32 Damage; };
std::mutex _damageMutex;
std::unordered_map<uint32,std::deque<DamageNotice>> _damageNotices;
std::unordered_map<uint32, ManagedPlayer> _records;
};
@@ -656,3 +656,9 @@ void ClientlessTravelMgr::Update(uint32 diff)
LaunchPath(player, record);
}
}
std::vector<Position> const* ClientlessTravelMgr::GetActivePath(uint32 guid) const
{
auto it=_records.find(guid);
return it!=_records.end() && it->second.Status.State==ClientlessTravelState::Moving ? &it->second.ActiveSpline : nullptr;
}
@@ -62,6 +62,7 @@ public:
void CancelTravel(uint32 characterGuid);
ClientlessTravelStatus const* GetStatus(uint32 characterGuid) const;
void Update(uint32 diff);
std::vector<Position> const* GetActivePath(uint32 characterGuid) const;
static char const* StateName(ClientlessTravelState state);
@@ -19,6 +19,7 @@
#include "ClientlessActionExecutorMgr.h"
#include "ClientlessWorldPointMgr.h"
#include "AdventureBehaviorMgr.h"
#include "AdventureCombatMgr.h"
#include "ClientlessTravelGraphMgr.h"
#include "ClientlessRouteRecorderMgr.h"
#include "ClientlessLuaEngine.h"
@@ -36,6 +37,16 @@
#include "TemporarySummon.h"
#include "Unit.h"
#include "World.h"
#include "Item.h"
#include "ItemTemplate.h"
#include "PlayerAI.h"
#include "PhasingHandler.h"
#include "PhaseShift.h"
#include "Map.h"
#include "Spell.h"
#include "SpellMgr.h"
#include "Log.h"
#include "AdventureCombatPreparationPolicy.h"
#include <algorithm>
#include <cmath>
@@ -44,6 +55,253 @@
#include <string>
#include <cctype>
#include <vector>
#include <memory>
#include <unordered_map>
#include <unordered_set>
namespace ClientlessCombatPreparation
{
struct Gear {uint8 Slot;uint32 Entry;std::string Enchantments;uint8 RandomType;uint32 RandomId;};
bool Prepare(ChatHandler* handler,Player* bot,uint32 sourceGuid)
{
if (bot->getClass()!=CLASS_WARRIOR || bot->getLevel()!=60 || bot->IsInCombat() ||
bot->IsBeingTeleported() || bot->GetTransport() || bot->GetGUID().GetCounter()==sourceGuid)
{handler->SendSysMessage("Preparation requires an idle level-60 warrior and a different source character.");return false;}
auto source=CharacterDatabase.PQuery("SELECT class,online FROM characters WHERE guid=%u",sourceGuid);
if (!source || source->Fetch()[0].GetUInt8()!=bot->getClass())
{handler->SendSysMessage("Source character class differs or source is missing.");return false;}
auto sourcePlayer=ObjectAccessor::FindConnectedPlayer(ObjectGuid(HighGuid::Player,sourceGuid));
if (source->Fetch()[1].GetUInt8() && !sourcePlayer)
{handler->SendSysMessage("Online source is not locally accessible.");return false;}
// Snapshot first. Never load source Item objects from DB: their load
// correction path can write back to the original character's items.
std::vector<Gear> gear;
if (sourcePlayer)
{
for(uint8 slot=EQUIPMENT_SLOT_START;slot<EQUIPMENT_SLOT_END;++slot)
if(auto item=sourcePlayer->GetItemByPos(INVENTORY_SLOT_BAG_0,slot))
{
std::ostringstream enchantments;
for(unsigned i=0;i<MAX_ENCHANTMENT_SLOT;++i)
enchantments<<item->GetEnchantmentId(EnchantmentSlot(i))<<' '<<item->GetEnchantmentDuration(EnchantmentSlot(i))<<' '<<item->GetEnchantmentCharges(EnchantmentSlot(i))<<' ';
auto random=item->GetItemRandomEnchantmentId();
gear.push_back({slot,item->GetEntry(),enchantments.str(),uint8(random.Type),random.Id});
}
}
else if(auto rows=CharacterDatabase.PQuery("SELECT ci.slot,i.itemEntry,i.enchantments,i.randomPropertyType,i.randomPropertyId "
"FROM character_inventory ci JOIN item_instance i ON i.guid=ci.item WHERE ci.guid=%u AND ci.bag=0 AND ci.slot<19 ORDER BY ci.slot",sourceGuid)) do
{
auto f=rows->Fetch();gear.push_back({f[0].GetUInt8(),f[1].GetUInt32(),f[2].GetString(),f[3].GetUInt8(),f[4].GetUInt32()});
}while(rows->NextRow());
if (gear.empty()) {handler->SendSysMessage("No source equipment found.");return false;}
for(auto const& g:gear)
{
auto proto=sObjectMgr->GetItemTemplate(g.Entry);
if(!proto || proto->GetRequiredLevel()>bot->getLevel())
{handler->PSendSysMessage("Item %u is missing or exceeds level 60; nothing copied.",g.Entry);return false;}
}
bot->LearnDefaultSkills();bot->UpdateSkillsForLevel();
if (bot->GetPrimaryTalentTree(bot->GetActiveSpec())) bot->ResetTalents(true);
bot->InitTalentForLevel();
if(!bot->LearnPrimaryTalentSpecialization(0))
{handler->SendSysMessage("Could not choose Arms specialization.");return false;}
auto tabs=sDBCManager.GetTalentTabPages(bot->getClass());
std::unordered_set<uint32> otherSpecSpells;
if(tabs)for(unsigned i=1;i<MAX_TALENT_TABS;++i)
if(auto spells=sDBCManager.GetTalentTreePrimarySpells(tabs[i]))
otherSpecSpells.insert(spells->begin(),spells->end());
uint32 learned=0;
// Armor/weapon proficiencies are generic-family, often level-zero
// spells. Their native race/class/min-level skill rule gates learning.
for(auto ability:sSkillLineAbilityStore)
{
auto skill=sSkillLineStore.LookupEntry(ability->SkillLine);
auto rc=sDBCManager.GetSkillRaceClassInfo(ability->SkillLine,bot->getRace(),bot->getClass());
auto spell=sSpellMgr->GetSpellInfo(ability->Spell);
if(!skill || (skill->CategoryID!=SKILL_CATEGORY_ARMOR && skill->CategoryID!=SKILL_CATEGORY_WEAPON) ||
!rc || rc->MinLevel>bot->getLevel() || !spell || spell->SpellLevel>bot->getLevel() ||
!bot->IsSpellFitByClassAndRace(spell->Id) || !SpellMgr::IsSpellValid(spell,bot,false))continue;
if(!bot->HasSpell(spell->Id)){bot->LearnSpell(spell->Id,false);++learned;}
}
for(auto stance:std::array<std::pair<uint32,uint32>,3>{{{2457,1},{71,10},{2458,30}}})
if(bot->getLevel()>=stance.second && !bot->HasSpell(stance.first))
if(auto spell=sSpellMgr->GetSpellInfo(stance.first))
if(bot->IsSpellFitByClassAndRace(spell->Id) && SpellMgr::IsSpellValid(spell,bot,false))
{bot->LearnSpell(spell->Id,false);++learned;}
auto cls=sChrClassesStore.LookupEntry(bot->getClass());
for(auto ability:sSkillLineAbilityStore)
{
auto spell=sSpellMgr->GetSpellInfo(ability->Spell);
if(!spell || !spell->SpellLevel || spell->SpellLevel>bot->getLevel() ||
spell->SpellFamilyName!=cls->SpellClassSet || otherSpecSpells.count(spell->Id) ||
!bot->IsSpellFitByClassAndRace(spell->Id) ||
sDBCManager.GetTalentSpellCost(spell->GetFirstRankSpell()->Id)>0 ||
!SpellMgr::IsSpellValid(spell,bot,false))continue;
if(!bot->HasSpell(spell->Id)){bot->LearnSpell(spell->Id,false);++learned;}
}
auto spent=AdventureCombatPreparationPolicy::SpendArms60(
[&](uint32 id,unsigned rank){return bot->LearnTalent(id,rank);},[&](){return bot->GetFreeTalentPoints();});
if(bot->GetFreeTalentPoints())
{handler->PSendSysMessage("Talent plan could not spend %u points; gear not copied.",bot->GetFreeTalentPoints());return false;}
// Create new GUIDs through the native item allocator. Displaced gear is
// stored in free bag slots; it is never destroyed or overwritten in DB.
uint32 copied=0;
for(auto const& g:gear)
{
if(auto equipped=bot->GetItemByPos(INVENTORY_SLOT_BAG_0,g.Slot))
if(equipped->GetEntry()==g.Entry)continue; // repeatable setup
auto discard=[&](Item* p){if(p){RemoveItemFromUpdateQueueOf(p,bot);delete p;}};
std::unique_ptr<Item,decltype(discard)> item(Item::CreateItem(g.Entry,1,bot),discard);
if(!item)return false;
item->SetItemRandomProperties({ItemRandomEnchantmentType(g.RandomType),g.RandomId});
std::istringstream enchantments(g.Enchantments);
for(unsigned i=0;i<MAX_ENCHANTMENT_SLOT;++i)
{
uint32 id=0,duration=0,charges=0;
if(!(enchantments>>id>>duration>>charges))return false;
if(id)item->SetEnchantment(EnchantmentSlot(i),id,duration,charges);
}
uint16 destination=0;
auto equipResult=bot->CanEquipItem(g.Slot,destination,item.get(),true);
if(equipResult!=EQUIP_ERR_OK)
{handler->PSendSysMessage("Native equip check rejected item %u at slot %u: error=%u armorMask=%u weaponMask=%u.",g.Entry,g.Slot,uint32(equipResult),bot->GetArmorProficiency(),bot->GetWeaponProficiency());return false;}
if(auto previous=bot->GetItemByPos(INVENTORY_SLOT_BAG_0,g.Slot))
{
ItemPosCountVec storage;
if(bot->CanStoreItem(NULL_BAG,NULL_SLOT,storage,previous,false)!=EQUIP_ERR_OK)
{handler->SendSysMessage("No bag space for displaced equipment; stopped without discarding it.");return false;}
bot->RemoveItem(INVENTORY_SLOT_BAG_0,g.Slot,true);
bot->StoreItem(storage,previous,true);
}
if(!bot->EquipItem(destination,item.get(),true))return false;
item.release();++copied;
}
bot->DurabilityRepairAll(false,1.0f,false);
bot->SetHealth(bot->GetMaxHealth());
bot->SendTalentsInfoData(false);
auto tx=CharacterDatabase.BeginTransaction();bot->SaveToDB(tx);
auto commit=CharacterDatabase.AsyncCommitTransaction(tx);
if(!commit.m_future.get()){handler->SendSysMessage("Character save failed.");return false;}
handler->PSendSysMessage("Prepared level-60 Arms warrior: %u copied items, %u newly learned spells, %u talent points spent, %u remaining. Original source unchanged.",copied,learned,spent,bot->GetFreeTalentPoints());
TC_LOG_INFO("server.adventurebot","COMBAT_PREPARED guid=%u source=%u copied=%u learned=%u talents=%u primaryTree=%u health=%u",
bot->GetGUID().GetCounter(),sourceGuid,copied,learned,spent,bot->GetPrimaryTalentTree(bot->GetActiveSpec()),bot->GetMaxHealth());
return true;
}
// A bounded training exercise, not a replacement for the future autonomous
// combat/interaction policy. It reuses actual PlayerAI spell validation.
class Exercise : public PlayerAI
{
public:
Exercise(Player* p,Creature* target,uint32 seconds):PlayerAI(p),_target(target->GetGUID()),_remaining(seconds*1000){}
void UpdateAI(uint32 diff) override {Tick(diff);}
Player* Owner()const{return me;}
bool Tick(uint32 diff)
{
auto target=ObjectAccessor::GetCreature(*me,_target);
if(!me->IsAlive() || me->IsCharmed() || !target || !target->IsAlive() ||
target->GetScriptName()!="npc_training_dummy" || !me->IsInPhase(target) ||
me->GetExactDist(target)>60 || _remaining<=diff)return false;
_remaining-=diff;
_rendDelay=_rendDelay>diff?_rendDelay-diff:0;
_fillerDelay=_fillerDelay>diff?_fillerDelay-diff:0;
if(_timer>diff){_timer-=diff;return true;}_timer=500;
if(me->HasUnitState(UNIT_STATE_CASTING))return true;
me->SetFacingToObject(target);
if(!me->HasAura(2457))
if(auto cast=VerifySpellCast(2457,me)){Cast(cast,2457);return true;}
if(!me->IsWithinMeleeRange(target))
{
if(auto cast=VerifySpellCast(100,target))
{Cast(cast,100);return true;}
if(!me->GetVictim()) AttackStart(target);
return true;
}
if(me->GetVictim()!=target)AttackStart(target);
DoAutoAttackIfReady();
if(!me->HasAura(6673))if(auto cast=VerifySpellCast(6673,me)){Cast(cast,6673);return true;}
if(auto cast=VerifySpellCast(12294,target)){Cast(cast,12294);return true;}
if(!_rendDelay && !target->GetAuraApplicationOfRankedSpell(772,me->GetGUID()))
if(auto cast=VerifySpellCast(772,target)){_rendDelay=12000;Cast(cast,772);return true;}
if(!_fillerDelay)for(uint32 id:{7384u,1464u,78u})
if(auto cast=VerifySpellCast(id,target)){_fillerDelay=3000;Cast(cast,id);break;}
return true;
}
void Stop(){me->AttackStop();me->CastStop();me->GetMotionMaster()->Clear();me->StopMoving();me->SetSelection(ObjectGuid::Empty);}
private:
void Cast(TargetedSpell cast,uint32 spell)
{
SpellCastTargets targets;targets.SetUnitTarget(cast.second);
uint32 actual=cast.first->GetSpellInfo()->Id;
auto result=cast.first->prepare(targets);
TC_LOG_INFO("server.adventurebot",result==SPELL_CAST_OK?"COMBAT_EXERCISE_CAST guid=%u spell=%u actual=%u result=%u":"COMBAT_EXERCISE_REJECT guid=%u spell=%u actual=%u result=%u",
me->GetGUID().GetCounter(),spell,actual,uint32(result));
}
ObjectGuid _target;uint32 _remaining=0,_timer=0,_rendDelay=0,_fillerDelay=0;
};
std::unordered_map<uint32,std::unique_ptr<Exercise>> Exercises;
bool Command(ChatHandler* handler,char const* args)
{
std::istringstream in(args?args:"");std::string op;uint32 guid=0,source=0;in>>op>>guid>>source;
auto session=sWorld->FindClientlessSession(guid);auto player=session?session->GetPlayer():nullptr;
if(!player || !player->IsInWorld() || !sClientlessPlayerMgr->IsManaged(guid))
{handler->SendSysMessage("Use an online managed clientless player.");return false;}
if(op=="inspect")
{
auto c=player->GetMap()->GetCreatureBySpawnId(source);
handler->PSendSysMessage("Bot position=(%.3f %.3f %.3f) phases=%s",player->GetPositionX(),player->GetPositionY(),player->GetPositionZ(),PhasingHandler::FormatPhases(player->GetPhaseShift()).c_str());
if(!c){handler->SendSysMessage("Requested creature spawn is not loaded.");return false;}
handler->PSendSysMessage("Target entry=%u level=%u script='%s' distance=%.2f phase=%u visible=%u attackable=%u los=%u boss=%u flags=%u phases=%s",
c->GetEntry(),c->getLevel(),c->GetScriptName().c_str(),player->GetExactDist(c),
player->IsInPhase(c)?1:0,player->CanSeeOrDetect(c)?1:0,player->IsValidAttackTarget(c)?1:0,
player->IsWithinLOSInMap(c)?1:0,c->isWorldBoss()?1:0,c->GetUInt32Value(UNIT_FIELD_FLAGS),PhasingHandler::FormatPhases(c->GetPhaseShift()).c_str());
for(auto const& aura:c->GetAppliedAuras())handler->PSendSysMessage("Target aura=%u",aura.first);
return true;
}
if(op=="testdeath" && player->IsAlive() && !player->IsBeingTeleported() && !player->GetTransport() && !player->InBattleground() && !player->InArena())
{
TC_LOG_INFO("server.adventurebot","DEATH_TEST_REQUEST guid=%u",guid);
Unit::Kill(nullptr,player,false);
handler->SendSysMessage("Native test death triggered without extra durability loss.");
return true;
}
if(op=="attack")
{
bool ok=sAdventureCombatMgr->Attack(player,source);
handler->SendSysMessage(ok?"Native opponent combat started.":"Target rejected: requires a nearby reachable valid opponent within strength limits.");
return ok;
}
if(op=="train")return sAdventureCombatMgr->Train(player,std::max(10u,std::min(source?source:60u,180u))*1000);
if(op=="prepare")return Prepare(handler,player,source);
if(op=="stop")
{sAdventureCombatMgr->Stop(player);auto it=Exercises.find(guid);if(it!=Exercises.end()){it->second->Stop();Exercises.erase(it);}return true;}
if(op=="exercise" && player->getClass()==CLASS_WARRIOR && !player->IsInCombat() && !player->IsBeingTeleported() && !player->GetTransport())
{
std::vector<Creature*> targets;player->GetCreatureListWithEntryInGrid(targets,0,50);
Creature* best=nullptr;
for(auto c:targets)if(c->GetScriptName()=="npc_training_dummy" && c->getLevel()<=player->getLevel()+2 && !c->isWorldBoss() && c->IsAlive() && player->IsInPhase(c) && player->IsValidAttackTarget(c) && player->IsWithinLOSInMap(c))
if(!best || player->GetExactDist(c)<player->GetExactDist(best))best=c;
if(!best){handler->SendSysMessage("No native training dummy nearby.");return false;}
// Explicit exercise owns movement only while the usual intent is stopped.
sAdventureBehaviorMgr->Command(handler,("stop "+std::to_string(guid)).c_str());
sClientlessPlayerMgr->ClearQueuedActions(guid);sClientlessActionExecutorMgr->Clear(guid);sClientlessTravelMgr->CancelTravel(guid);
Exercises[guid]=std::make_unique<Exercise>(player,best,std::max(10u,std::min(source?source:60u,180u)));
handler->PSendSysMessage("Training exercise started at native dummy %u.",best->GetEntry());return true;
}
handler->SendSysMessage("Usage: clientless combat prepare <guid> <source-guid> | exercise <guid> [seconds] | train <guid> [seconds] | attack <guid> <creature-spawn> | inspect <guid> <creature-spawn> | stop <guid>");return false;
}
void Update(uint32 diff)
{
for(auto it=Exercises.begin();it!=Exercises.end();)
{
auto session=sWorld->FindClientlessSession(it->first);auto p=session?session->GetPlayer():nullptr;
if(!p || p!=it->second->Owner()){it=Exercises.erase(it);continue;}
if(!it->second->Tick(diff)){it->second->Stop();TC_LOG_INFO("server.adventurebot","COMBAT_EXERCISE_END guid=%u",it->first);it=Exercises.erase(it);}
else ++it;
}
}
}
namespace VianoBotCommands
{
@@ -2237,6 +2495,7 @@ static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*arg
static std::vector<ChatCommand> clientlessCommands =
{
{ "combat", rbac::RBAC_PERM_COMMAND_GM, true, &ClientlessCombatPreparation::Command, "" },
{ "luareload", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessLuaReload, "" },
{ "transport", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessTransport, "" },
{ "login", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessLogin, "" },
@@ -2307,6 +2566,7 @@ class ClientlessInstanceVisitResume : public WorldScript
{
public:
ClientlessInstanceVisitResume() : WorldScript("ClientlessInstanceVisitResume") { }
void OnUpdate(uint32 diff) override {ClientlessCombatPreparation::Update(diff);}
void OnStartup() override
{
if (!CharacterDatabase.Query("SHOW TABLES LIKE 'vcore_adventure_instance_visit'")) return;
@@ -0,0 +1,62 @@
"""Execute the production bounded PlayerAI exercise and native selection and cleanup."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/scripts/Custom/viano_bot_commands.cpp').read_text()
a=s.index(' class Exercise : public PlayerAI');b=s.index(' std::unordered_map<uint32,std::unique_ptr<Exercise>>',a)
code=r'''
#include <cassert>
#include <cstdint>
#include <set>
#include <string>
#include <vector>
using uint32=uint32_t;
#define TC_LOG_INFO(...) ((void)0)
constexpr int UNIT_STATE_CASTING=1;
struct ObjectGuid{int value=1;bool operator==(ObjectGuid const&o)const{return value==o.value;}static const ObjectGuid Empty;};const ObjectGuid ObjectGuid::Empty{0};
struct Creature{ObjectGuid guid;bool alive=true,phase=true,rend=false;std::string script="npc_training_dummy";ObjectGuid GetGUID(){return guid;}bool IsAlive(){return alive;}std::string GetScriptName(){return script;}bool GetAuraApplicationOfRankedSpell(uint32,ObjectGuid){return rend;}}target;
struct Motion{int clears=0;void Clear(){++clears;}};
struct Player{
bool alive=true,charmed=false,casting=false;float distance=12;std::set<uint32> auras,phases{1,169};std::vector<uint32> casts;Creature* victim=nullptr;int swings=0,stops=0;Motion motion;
bool IsAlive(){return alive;}bool IsCharmed(){return charmed;}bool IsInPhase(Creature*c){return c->phase;}
float GetExactDist(Creature*){return distance;}bool HasUnitState(int){return casting;}void SetFacingToObject(Creature*){}
bool HasAura(uint32 id){return auras.count(id);}bool IsWithinMeleeRange(Creature*){return distance<4;}
Creature* GetVictim(){return victim;}ObjectGuid GetGUID(){return {26};}
void AttackStop(){++stops;victim=nullptr;}void CastStop(){casting=false;}void StopMoving(){}Motion* GetMotionMaster(){return &motion;}
void SetSelection(ObjectGuid){}
};
namespace ObjectAccessor{Creature* GetCreature(Player&,ObjectGuid){return &target;}}
struct PhasingHandler{static void RemovePhase(Player* p,uint32 id,bool){p->phases.erase(id);}};
constexpr int SPELL_CAST_OK=0;
struct SpellCastTargets{void SetUnitTarget(void*){}};
struct Spell{
Player* player;std::set<uint32>* available;struct Info{uint32 Id;}info;
auto GetSpellInfo(){return &info;}
int prepare(SpellCastTargets const&){auto id=info.Id;player->casts.push_back(id);available->erase(id);if(id==2457||id==6673)player->auras.insert(id);if(id==100)player->distance=2;if(id==772)target.rend=true;delete this;return SPELL_CAST_OK;}
};
struct PlayerAI{
Player* me;explicit PlayerAI(Player*p):me(p){}virtual ~PlayerAI()=default;virtual void UpdateAI(uint32)=0;
struct TargetedSpell{Spell* first=nullptr;void* second=nullptr;explicit operator bool()const{return first!=nullptr;}};
std::set<uint32> available{2457,100,6673,12294,772,7384,1464,78};
template<class T>TargetedSpell VerifySpellCast(uint32 id,T* t){return available.count(id)?TargetedSpell{new Spell{me,&available,{id}},t}:TargetedSpell{};}
void AttackStart(Creature*c){me->victim=c;}void DoAutoAttackIfReady(){++me->swings;}
};
'''+s[a:b]+r'''
int main(){
Player p;Exercise exercise(&p,&target,20);
for(unsigned i=0;i<24;++i)assert(exercise.Tick(500));
assert((p.casts==std::vector<uint32>{2457,100,6673,12294,772,7384,1464,78}));assert(p.swings>0);
p.casting=true;auto count=p.casts.size();assert(exercise.Tick(500)&&p.casts.size()==count);
p.distance=61;assert(!exercise.Tick(500));exercise.Stop();
assert(p.phases.count(169)&&p.phases.count(1)&&p.stops==1&&p.motion.clears==1);
Player q;q.phases={1,169};Exercise withoutAddedPhase(&q,&target,10);withoutAddedPhase.Stop();assert(q.phases.count(169));
Player hostile;target.script="not_a_training_dummy";Exercise rejected(&hostile,&target,10);assert(!rejected.Tick(500)&&hostile.casts.empty());
target.script="npc_training_dummy";hostile.charmed=true;assert(!rejected.Tick(500));
hostile.charmed=false;hostile.alive=false;assert(!rejected.Tick(500));
}
'''
with tempfile.TemporaryDirectory() as tmp:
src=Path(tmp)/'exercise.cpp';exe=Path(tmp)/'exercise';src.write_text(code)
subprocess.run(['g++','-std=c++17',str(src),'-o',str(exe)],check=True)
subprocess.run([str(exe)],check=True)
print('Production exercise: stance/charge/melee/casts, native validation delegation, bounded lifetime, invalid targets and unchanged native phases passed.')
@@ -0,0 +1,56 @@
"""Run the level-60 Arms plan through production Player::LearnTalent."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/Entities/Player/Player.cpp').read_text()
a=s.index('bool Player::LearnTalent(');b=s.index('\nvoid Player::LearnPetTalent',a)
# Native 4.3.4 Talent.dbc rows used by this plan; Impale depends on Deep Wounds 3/3.
rows=[(10134,0,[84570,84571,84572]),(11163,0,[84579,84580]),(8176,1,[12834,12849,12867]),(8190,1,[29834,29838]),
(10138,2,[56636,56637,56638]),(10741,2,[16493,16494]),(11418,3,[86655,12330]),(9662,3,[29836,29859]),
(11223,3,[85730]),(10520,4,[84583,84587,84588]),(8208,4,[64976]),(8214,4,[29723,29725]),(8194,5,[46867,56611])]
code=r'''
#include "AdventureCombatPreparationPolicy.h"
#include <cassert>
#include <cstdint>
#include <set>
#include <unordered_map>
using uint32=uint32_t;using uint8=uint8_t;using int8=int8_t;
#define TC_LOG_ERROR(...) ((void)0)
#define TC_LOG_DEBUG(...) ((void)0)
constexpr int MAX_TALENT_RANK=5,REQ_PRIMARY_TREE_TALENTS=31;
struct TalentEntry{uint32 ID=0,TabID=746,TierID=0,ColumnIndex=0,SpellRank[5]={},PrereqTalent[3]={},PrereqRank[3]={};};
struct TalentTabEntry{uint32 ClassMask=1;};
struct Store{std::unordered_map<uint32,TalentEntry> rows;TalentEntry const* LookupEntry(uint32 id){auto i=rows.find(id);return i==rows.end()?nullptr:&i->second;}uint32 GetNumRows(){return 12000;}}sTalentStore;
struct Tabs{TalentTabEntry tab;auto LookupEntry(uint32){return &tab;}}sTalentTabStore;
struct Player{
uint32 points=26,mask=1;std::set<uint32> spells;
uint32 GetFreeTalentPoints(){return points;}uint32 getClassMask(){return mask;}uint32 GetPrimaryTalentTree(int){return 746;}int GetActiveSpec(){return 0;}
bool HasSpell(uint32 spell){return spells.count(spell);}
void LearnSpell(uint32 spell,bool){for(auto const& [id,t]:sTalentStore.rows)for(auto rank:t.SpellRank)if(rank==spell){for(auto old:t.SpellRank)spells.erase(old);}spells.insert(spell);}
void AddTalent(uint32,int,bool){}void SetFreeTalentPoints(uint32 p){points=p;}
bool LearnTalent(uint32,uint32);
};
'''+s[a:b]+'\nint main(){\n'
for id,tier,spells in rows:
ranks=spells+[0]*(5-len(spells))
code+=f'sTalentStore.rows[{id}]={{{id},746,{tier},0,{{'+','.join(map(str,ranks))+'},{0,0,0},{0,0,0}};\n'
code+=r'''
sTalentStore.rows[10741].PrereqTalent[0]=8176;sTalentStore.rows[10741].PrereqRank[0]=2;
Player p;
assert(!p.LearnTalent(10741,0)); // cannot skip native dependency/tier gates
unsigned spent=AdventureCombatPreparationPolicy::SpendArms60([&](uint32 id,uint32 rank){return p.LearnTalent(id,rank);},[&]{return p.points;});
assert(spent==26&&p.points==0);
assert(p.HasSpell(84572)&&p.HasSpell(84580)&&p.HasSpell(12867)&&p.HasSpell(29838));
assert(p.HasSpell(56638)&&p.HasSpell(16494)&&p.HasSpell(46867));
assert(!p.HasSpell(46924)); // Bladestorm requires 31 points; level60 does not get it
assert(AdventureCombatPreparationPolicy::SpendArms60([&](uint32 id,uint32 rank){return p.LearnTalent(id,rank);},[&]{return p.points;})==0);
Player wrongClass;wrongClass.mask=128;
assert(AdventureCombatPreparationPolicy::SpendArms60([&](uint32 id,uint32 rank){return wrongClass.LearnTalent(id,rank);},[&]{return wrongClass.points;})==0);
assert(wrongClass.points==26&&wrongClass.spells.empty());
}
'''
with tempfile.TemporaryDirectory() as tmp:
src=Path(tmp)/'talents.cpp';exe=Path(tmp)/'talents';src.write_text(code)
subprocess.run(['g++','-std=c++17','-I'+str(root/'src/server/game/World'),str(src),'-o',str(exe)],check=True)
subprocess.run([str(exe)],check=True)
print('Production talent learner: native tiers/dependencies/class gates, all 26 legal level60 points, no capstone bypass and repeat safety passed.')
@@ -0,0 +1,143 @@
"""Execute production reaction/death state machine with native-world adapters."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
source=(root/'src/server/game/World/AdventureCombatMgr.cpp').read_text()
def function(signature):
a=source.index(signature);b=source.index('{',a);e=b+1;depth=1
while depth:depth+=(source[e]=='{')-(source[e]=='}');e+=1
return source[a:e]
header=(root/'src/server/game/World/AdventureCombatMgr.h').read_text()
header='\n'.join(x for x in header.splitlines() if not x.startswith('#include')).replace('private:','public:')
code=r'''
#include "AdventureCombatPolicy.h"
#include <cassert>
#include <string>
#include <deque>
#include <vector>
#include <memory>
#include <unordered_map>
#include <algorithm>
#include <ctime>
using uint32=unsigned;using uint8=unsigned char;
#define TC_GAME_API
#define TC_LOG_INFO(...) ((void)0)
#define TC_LOG_WARN(...) ((void)0)
constexpr int PLAYER_FLAGS=0,PLAYER_FLAGS_GHOST=1,CMSG_REPOP_REQUEST=2,CMSG_RECLAIM_CORPSE=3,CORPSE_RESURRECTABLE_PVP=1;
constexpr float CORPSE_RECLAIM_RADIUS=39,INVALID_HEIGHT=-100000;
struct ObjectGuid{unsigned id=0;unsigned GetCounter()const{return id;}void Clear(){id=0;}std::string ToString()const{return std::to_string(id);}bool operator==(ObjectGuid b)const{return id==b.id;}bool operator!=(ObjectGuid b)const{return id!=b.id;}};
struct Position{
float x=0,y=0,z=0;void Relocate(float a,float b,float c){x=a;y=b;z=c;}
float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}
float GetExactDist(Position const& b)const{return std::sqrt((x-b.x)*(x-b.x)+(y-b.y)*(y-b.y)+(z-b.z)*(z-b.z));}
float GetExactDist(Position const* b)const{return GetExactDist(*b);}
};
struct Map{float GetHeight(int,float,float,float,bool,float){return 0;}}map;
struct Creature;
struct Unit:Position{virtual Creature* ToCreature(){return nullptr;}};
struct Creature:Unit{ObjectGuid guid;bool valid=true,alive=true,controlled=false,evading=false,boss=false,phase=true,visible=true,inWorld=true;unsigned level=60;std::string script="mob";Creature(unsigned n):guid{n}{}ObjectGuid GetGUID(){return guid;}Creature* ToCreature()override{return this;}bool GetControllingPlayer(){return controlled;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsEvadingAttacks(){return evading;}bool isWorldBoss(){return boss;}unsigned getLevel(){return level;}std::string GetScriptName(){return script;}};
struct Player;
struct Corpse:Position{
time_t released=100;int saves=0;ObjectGuid GetGUID(){return {700};}time_t GetGhostTime(){return released;}int GetType(){return 0;}
unsigned GetMapId(){return 0;}unsigned GetInstanceId(){return 0;}Map* GetMap(){return &map;}bool IsWithinDistInMap(Player*,float,bool);void SaveToDB(){++saves;}
}corpse;
struct WorldPacket{int opcode;WorldPacket(int o,int):opcode(o){}template<class T>WorldPacket&operator<<(T){return *this;}};
struct Session{Player* owner=nullptr;int releases=0,reclaims=0;void HandleRepopRequestOpcode(WorldPacket&);void HandleReclaimCorpseOpcode(WorldPacket&);};
struct Player:Unit{
bool alive=true,ghost=false,teleport=false,combat=false,melee=true,los=true;unsigned delay=30;int saves=0,revives=0,bones=0,graveyards=0;bool sickness=false;int durability=75;
Session session;Player(){session.owner=this;}ObjectGuid GetGUID(){return {26};}bool IsAlive(){return alive;}bool IsInWorld(){return true;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool GetTransport(){return false;}bool IsInFlight(){return false;}bool IsCharmed(){return false;}
unsigned GetMapId(){return 0;}unsigned GetInstanceId(){return 0;}bool InBattleground(){return false;}bool InArena(){return false;}bool HasFlag(int,int){return ghost;}Session*GetSession(){return &session;}Corpse*GetCorpse(){return &corpse;}
uint32 GetCorpseReclaimDelay(bool){return delay;}Map*GetMap(){return &map;}int GetPhaseShift(){return 0;}
bool IsInPhase(Creature*c){return c->phase;}bool CanSeeOrDetect(Creature*c){return c->visible;}bool IsValidAttackTarget(Creature*c){return c->valid;}unsigned getLevel(){return 60;}bool IsWithinMeleeRange(Creature*){return melee;}float GetCombatReach(){return 1;}float GetAngle(Player*){return 0;}
void SaveToDB(){++saves;}void RepopAtGraveyard(){++graveyards;teleport=true;Relocate(-100,0,0);}void ResurrectPlayer(float hp,bool sick){assert(hp==0.5f);sickness=sick;alive=true;ghost=false;++revives;}void SpawnCorpseBones(){++bones;}
void StopMoving(){}
};
bool Corpse::IsWithinDistInMap(Player*p,float r,bool){return GetExactDist(p)<=r;}
void Session::HandleRepopRequestOpcode(WorldPacket&){++releases;owner->ghost=true;owner->Relocate(-100,0,0);}
void Session::HandleReclaimCorpseOpcode(WorldPacket&){++reclaims;assert(corpse.IsWithinDistInMap(owner,39,true));owner->alive=true;owner->ghost=false;}
// Update's local combat approach adapter.
void nearPoint(Creature*,Player*,float&,float&,float&,float,float){}
struct FakeCreature:Creature{using Creature::Creature;void GetNearPoint(Player*,float&x,float&y,float&z,float,float){x=this->x;y=this->y;z=this->z;}float GetAngle(Player*){return 0;}};
namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}}
namespace ObjectAccessor{std::unordered_map<unsigned,Creature*> objects;Creature* GetCreature(Player&,ObjectGuid g){auto i=objects.find(g.id);return i==objects.end()?nullptr:i->second;}}
struct ClientlessPathDiagnostics{bool Complete=true,Suspicious=false;float PathLength=100;};
enum class ClientlessTravelState{Moving,Arrived,Stuck,PathRejected};
struct TravelStatus{ClientlessTravelState State=ClientlessTravelState::Arrived;Position Target;};
struct Travel{
TravelStatus status;bool present=false,path=true,nearOnly=false;int starts=0,analyses=0;
auto GetStatus(uint32)->TravelStatus*{return present?&status:nullptr;}
void CancelTravel(uint32){present=false;}
bool AnalyzePath(Player*,Position const&end,ClientlessPathDiagnostics& d){++analyses;d.PathLength=std::fabs(end.x+100);return path && (!nearOnly || end.x<-15);}
bool StartTravel(Player*,Position const&end){++starts;present=true;status.State=ClientlessTravelState::Moving;status.Target=end;return true;}
}travel;auto sClientlessTravelMgr=&travel;
struct Behavior{int interrupts=0;void InterruptForCombat(Player*){++interrupts;}}behavior;auto sAdventureBehaviorMgr=&behavior;
'''
# production Update uses direct Position members and creature near point.
code=code.replace('float x=0,y=0,z=0;','union{float x=0;float m_positionX;};union{float y=0;float m_positionY;};union{float z=0;float m_positionZ;};',1)
code=code.replace('bool GetControllingPlayer(){return controlled;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsEvadingAttacks(){return evading;}bool isWorldBoss(){return boss;}unsigned getLevel(){return level;}std::string GetScriptName(){return script;}};', 'bool GetControllingPlayer(){return controlled;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsEvadingAttacks(){return evading;}bool isWorldBoss(){return boss;}unsigned getLevel(){return level;}std::string GetScriptName(){return script;}float GetAngle(Player*){return 0;}void GetNearPoint(Player*,float&a,float&b,float&c,float,float){a=x;b=y;c=z;}};')
# Forward declaration needed in Creature's adapter.
code=code.replace('struct Creature;','struct Creature;struct Player;',1)
code+=header+r'''
class AdventureCombatMgr::Fighter{public:int ticks=0;void Tick(Creature*,uint32){++ticks;}};
bool AdventureCombatMgr::Reachable(Player*p,Creature*)const{return p->los;}
Creature* AdventureCombatMgr::Proactive(Player*)const{return nullptr;}
bool AdventureCombatMgr::Start(Player*,Record&r,Creature*c,bool train,bool defense,time_t until){r.AI=std::make_shared<Fighter>();r.Target=c->guid;r.Training=train;r.Defending=defense;r.Until=until;return true;}
void AdventureCombatMgr::Finish(Player*,Record&r){r.AI.reset();r.Target.Clear();r.Defending=false;r.Training=false;r.Cooldown=10000;}
'''
for name in ('bool AdventureCombatMgr::Valid(', 'void AdventureCombatMgr::DamageTaken(', 'bool AdventureCombatMgr::Death(', 'bool AdventureCombatMgr::Update('):code+='\n'+function(name)
code+=r'''
int main(){
using namespace AdventureCombatPolicy;
assert(SafeFirstAttack(60,62,60,false,false,1,false,0));
assert(!SafeFirstAttack(60,63,100,false,false,1,false,0));
assert(!SafeFirstAttack(60,60,59,false,false,1,false,0));
assert(!SafeFirstAttack(60,60,100,true,false,1,false,0));
assert(!SafeFirstAttack(60,60,100,false,true,1,false,0));
assert(!SafeFirstAttack(60,60,100,false,false,2,false,0));
assert(!SafeFirstAttack(60,60,100,false,false,1,true,29));
assert(!CemeteryFallback(399,100,true));assert(CemeteryFallback(400,100,true));
assert(!CemeteryFallback(5000,100,false));assert(!CemeteryFallback(99,100,true));
assert(SegmentDistance(5,3,0,0,0,0,10,0,0)==3);
AdventureCombatMgr mgr;Player p;Creature a(1),b(2),c(3);ObjectAccessor::objects={{1,&a},{2,&b},{3,&c}};
assert(mgr.Valid(&p,&a,false));assert(!mgr.Valid(&p,&a,true));
Creature dummy(4);dummy.script="npc_training_dummy";assert(mgr.Valid(&p,&dummy,true));assert(!mgr.Valid(&p,&dummy,false));
dummy.level=63;assert(!mgr.Valid(&p,&dummy,true));dummy.level=60;
dummy.boss=true;assert(!mgr.Valid(&p,&dummy,true));dummy.boss=false;
dummy.phase=false;assert(!mgr.Valid(&p,&dummy,true));dummy.phase=true;
dummy.visible=false;assert(!mgr.Valid(&p,&dummy,true));dummy.visible=true;
dummy.controlled=true;assert(!mgr.Valid(&p,&dummy,true));dummy.controlled=false;
dummy.valid=false;assert(!mgr.Valid(&p,&dummy,true));
auto&r=mgr._records[26];
mgr.DamageTaken(&p,&a,0);assert(r.Damagers.empty());
mgr.DamageTaken(&p,&a,5);mgr.DamageTaken(&p,&b,5);mgr.Update(&p,500);
assert(r.Target.id==1 && r.Defending); // first actual damager
mgr.DamageTaken(&p,&c,5);mgr.Update(&p,500);assert(r.Target.id==1);
a.alive=false;mgr.Update(&p,500);assert(!r.AI);mgr.Update(&p,500);assert(r.Target.id==2);
mgr.Finish(&p,r);r.Damagers.clear();mgr.Start(&p,r,&c,true,false,500);
mgr.DamageTaken(&p,&b,1);mgr.Update(&p,500);assert(r.Target.id==2 && r.Defending && !r.Training);
// Release through native handler and save ghost plus corpse timestamp.
mgr.Finish(&p,r);p.alive=false;p.ghost=false;r.Dead=false;mgr.Death(&p,r);
assert(p.ghost && p.session.releases==1 && p.saves==1 && corpse.saves==1);
// Native reclaim radius and native delay both apply.
p.Relocate(38,0,0);GameTime::now=129;mgr.Death(&p,r);assert(p.session.reclaims==0);
GameTime::now=130;mgr.Death(&p,r);assert(p.alive && p.session.reclaims==1);
// Exact corpse point inaccessible, near-side point in legal radius reachable.
p.alive=false;p.ghost=true;p.Relocate(-100,0,0);travel={};travel.nearOnly=true;r.NextCorpseSearch=0;
GameTime::now=500;mgr.Death(&p,r);assert(travel.starts==1 && travel.status.Target.x<-15);
assert(p.graveyards==0 && !p.alive); // time alone never revives reachable ghost
GameTime::now=900;mgr.Death(&p,r);assert(p.graveyards==0);
// No path: wait all five minutes from original native persisted ghost time.
travel={};travel.path=false;r.NextCorpseSearch=0;GameTime::now=399;mgr.Death(&p,r);assert(!p.alive && p.graveyards==0);
// Simulate process restart: volatile record gone; corpse timestamp survives.
AdventureCombatMgr::Record resumed;resumed.Dead=true;GameTime::now=400;mgr.Death(&p,resumed);
assert(p.graveyards==1 && p.teleport && !p.alive && resumed.CemeteryPending);
p.teleport=false;mgr.Death(&p,resumed);assert(p.alive && !p.sickness && p.revives==1 && p.bones==1 && p.durability==75);
}
'''
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 combat focus, damage ordering, risk filters, corpse radius/delay, alternate paths and malus-free persisted fallback passed.')
+10 -9
View File
@@ -25,8 +25,8 @@ struct WorldObject{
bool IsInWorld()const{return inWorld;}unsigned GetMapId()const{return map;}float GetPositionX()const{return x;}float GetPositionY()const{return 0;}float GetPositionZ()const{return z;}float GetCollisionHeight()const{return 2;}
};
struct Unit:WorldObject{
bool vendor=false,banker=false,auction=false,inn=false,alive=true,combat=false,flying=false,transit=false,critter=false,totem=false,friendly=true,spline=false;Guid owner{0},guid;
bool IsVendor(){return vendor;}bool IsBanker(){return banker;}bool HasFlag(int,int){return auction;}bool IsInnkeeper(){return inn;}
bool repair=false,vendor=false,banker=false,auction=false,inn=false,alive=true,combat=false,flying=false,transit=false,critter=false,totem=false,friendly=true,spline=false;Guid owner{0},guid;
bool IsArmorer(){return repair;}bool IsVendor(){return vendor;}bool IsBanker(){return banker;}bool HasFlag(int,int){return auction;}bool IsInnkeeper(){return inn;}
bool IsAlive(){return alive;}bool IsInCombat(){return combat;}bool IsInFlight(){return transit;}bool IsFlying(){return flying;}bool GetTransport(){return transit;}
bool IsCritter(){return critter;}bool IsTotem(){return totem;}Guid GetCharmerOrOwnerGUID(){return owner;}Guid GetGUID(){return guid;}bool IsSplineEnabled(){return spline;}
};
@@ -44,13 +44,13 @@ struct Player:Unit{
struct Anchor{float GetExactDist(WorldObject const*u)const{return std::hypot(u->x,u->z);}float GetPositionZ()const{return 0;}};
struct AdventureSocialContext{Anchor Anchor;unsigned MapId=0;std::string ServiceKind,Intent="economy";};
enum class ClientlessEntityType{Creature,Player};
constexpr unsigned CLIENTLESS_CAP_VENDOR=1,CLIENTLESS_CAP_BANKER=8,CLIENTLESS_CAP_AUCTIONEER=32;
constexpr unsigned CLIENTLESS_CAP_REPAIR=256,CLIENTLESS_CAP_VENDOR=1,CLIENTLESS_CAP_BANKER=8,CLIENTLESS_CAP_AUCTIONEER=32;
struct Entity{ClientlessEntityType Type=ClientlessEntityType::Creature;bool Alive=true;unsigned Capabilities=0,Entry=0,Guid=0;};
struct Snapshot{float Radius=0;unsigned ScanAgeMs=1000;std::vector<Entity> Entities;};
Player* observer=nullptr;
struct Perception{Snapshot snapshot;Snapshot* GetSnapshot(unsigned){return &snapshot;}
void ScanNow(Player*p,float radius){observer=p;snapshot={};snapshot.Radius=radius;
for(auto u:p->creatures)if(std::hypot(u->x,u->z)<=radius) snapshot.Entities.push_back({ClientlessEntityType::Creature,u->alive,unsigned(u->vendor?1:0)|unsigned(u->banker?8:0)|unsigned(u->auction?32:0),0,unsigned(u->guid.value)});
for(auto u:p->creatures)if(std::hypot(u->x,u->z)<=radius) snapshot.Entities.push_back({ClientlessEntityType::Creature,u->alive,unsigned(u->vendor?1:0)|unsigned(u->banker?8:0)|unsigned(u->auction?32:0)|unsigned(u->repair?256:0),0,unsigned(u->guid.value)});
}}perception;auto sClientlessPerceptionMgr=&perception;
namespace ObjectAccessor{Creature* GetCreature(Player&p,ObjectGuid guid){for(auto u:p.creatures)if(u->guid.value==guid.value)return u;return nullptr;}}
struct Managed{bool IsManaged(unsigned){return true;}}managed;auto sClientlessPlayerMgr=&managed;
@@ -59,15 +59,15 @@ for sig in ('bool InBounds(', 'bool Friendly(', 'bool ServiceMatches(', 'bool Ta
code+=r'''
int main(){
using namespace AdventureEconomyPolicy;using namespace AdventureSocialFlow;
Player p,bot;Creature vendor,banker,auction,inn,guard;
vendor.vendor=true;banker.banker=true;auction.auction=true;inn.inn=true;
p.creatures={&vendor,&banker,&auction,&inn,&guard};int nextGuid=10;for(auto u:p.creatures)u->guid.value=++nextGuid;p.players={&bot};AdventureSocialContext c;
Player p,bot;Creature vendor,banker,auction,inn,guard,repair;
repair.repair=true;vendor.vendor=true;banker.banker=true;auction.auction=true;inn.inn=true;
p.creatures={&vendor,&banker,&auction,&inn,&guard,&repair};int nextGuid=10;for(auto u:p.creatures)u->guid.value=++nextGuid;p.players={&bot};AdventureSocialContext c;
for(auto kind:{"vendor_visit","goods","trade","vendor_compare"}){
auto people=People(&p,kind,c);assert(people.size()==1&&people[0]==&vendor);
}
for(auto kind:{"bank_visit","banking"})assert(People(&p,kind,c)==std::vector<Unit*>{&banker});
for(auto kind:{"auction_visit","auctions"})assert(People(&p,kind,c)==std::vector<Unit*>{&auction});
for(auto kind:{"service_wait","service_finish"})assert(People(&p,kind,c).size()==3);
for(auto kind:{"service_wait","service_finish"})assert(People(&p,kind,c).size()==4);
c.ServiceKind=LocationRequirement("economy_bank");
assert(People(&p,"goods",c).empty());assert(People(&p,"service_wait",c)==std::vector<Unit*>{&banker});
c.ServiceKind=LocationRequirement("economy_auction");
@@ -89,7 +89,8 @@ int main(){
nativeMap.clear=false;assert(!TargetVisible(&p,&banker,"banking")); // A real wall still blocks the scene.
nativeMap.clear=true;p.los=true;
assert(People(&p,"order",c)==std::vector<Unit*>{&inn}); // Preserve ordinary innkeeper interactions.
for(auto kind:{"vendor_visit","goods","trade","vendor_compare","bank_visit","banking","auction_visit","auctions"}){
assert(People(&p,"repair",c)==std::vector<Unit*>{&repair});
for(auto kind:{"repair","vendor_visit","goods","trade","vendor_compare","bank_visit","banking","auction_visit","auctions"}){
assert(Economic(kind));auto steps=Steps(kind,20000);
assert(steps.front().Action==Cue::Greet&&steps.back().Action==Cue::Farewell);
for(auto step:steps)assert(step.Action!=Cue::Fire&&step.Action!=Cue::Dance&&step.Action!=Cue::Seat);
+1 -1
View File
@@ -59,7 +59,7 @@ struct AdventureBehaviorMgr{
void FinishInstance(Player* p,Record&r){++finishes;r.Instance={};p->SetStandState(0);}
bool UpdateInstance(Player*,Record&,time_t);
};
'''+s[start:]+r'''
'''+s[start:s.index("void AdventureBehaviorMgr::InterruptForCombat(")]+r'''
using Stage=AdventureInstancePolicy::Phase;
AdventureBehaviorMgr::Record fresh(){AdventureBehaviorMgr::Record r;r.Instance.Active=true;r.Instance.Entry=78;r.Instance.Exit=121;r.Instance.Map=36;r.Instance.BlockEnd=3600;r.Instance.LossPercent=50;return r;}
int main(){
@@ -10,7 +10,8 @@ code=r'''
#include <memory>
#include <vector>
using uint32=uint32_t;
struct WorldScript{WorldScript(char const*){}virtual void OnStartup(){}};
struct WorldScript{WorldScript(char const*){}virtual void OnStartup(){}virtual void OnUpdate(uint32){}};
namespace ClientlessCombatPreparation{int ticks=0;void Update(uint32){++ticks;}}
struct Field{uint32 value;uint32 GetUInt32(){return value;}};
struct Rows{std::vector<std::vector<Field>> data;unsigned index=0;Field* Fetch(){return data[index].data();}bool NextRow(){return ++index<data.size();}};
struct Database{bool schema=true;std::shared_ptr<Rows> rows=std::make_shared<Rows>();
@@ -74,6 +74,7 @@ struct ClientlessCompanionMgr{
};
struct Profile{bool CanGroundMount=true;float GroundMountMinDistance=45;std::vector<int> GroundMounts{1};uint32 CompanionSpell=10673,CompanionLeashItem=37460;}profile;
struct Lua{int mounts=0,dismounts=0;Profile* GetAdventureProfile(uint32){return &profile;}bool PrepareGroundMount(Player*,float,char const*){++mounts;return true;}void DismountGroundMount(Player*,char const*){++dismounts;}}lua;auto sClientlessLuaEngine=&lua;
struct CombatMgr{bool available=true;int starts=0;bool TrainingAvailable(Player*){return available;}bool Train(Player*,uint32 duration,time_t){assert(duration>0);++starts;return available;}}combatMgr;auto sAdventureCombatMgr=&combatMgr;
struct AdventureMicroAction{std::string Requirement;};
struct Location{struct{std::string Category="leisure";float O=0;}Point;bool IsArea=false;uint32 AreaId=0,ZoneId=1519;}location;
struct Locations{Location* Find(std::string const&){return &location;}}locations;auto sAdventureLocationMgr=&locations;
@@ -214,6 +215,11 @@ int main(){
assert(!mgr.LeisureAvailable(&p,r,{"fish"}));p.hasRod=true;p.water=true;
assert(!mgr.LeisureAvailable(&p,r,{"fish"}));p.water=false;
r.Intent="leisure";combatMgr.available=false;assert(!mgr.LeisureAvailable(&p,r,{"training"}));
combatMgr.available=true;assert(mgr.LeisureAvailable(&p,r,{"training"}));
assert(mgr.StartLeisure(&p,r,{"training"}) && combatMgr.starts==1);
r.Intent="adventure";assert(!mgr.LeisureAvailable(&p,r,{"training"}));
}
'''
with tempfile.TemporaryDirectory(prefix='vcore-leisure-') as folder:
@@ -0,0 +1,16 @@
"""Apply repair additions and verify editable weights survive repeated migration."""
from pathlib import Path
import re,sqlite3
root=Path(__file__).resolve().parents[2]
db=sqlite3.connect(':memory:')
base=(root/'sql/custom/world/2026_10_26_03_adventure_behavior.sql').read_text()
for table in ('vcore_adventure_action','vcore_adventure_action_intent'):
body=re.search(r'CREATE TABLE IF NOT EXISTS '+table+r' \((.*?)\) ENGINE=',base,re.S)[1]
db.execute('CREATE TABLE '+table+' ('+body+')')
sql=(root/'sql/custom/world/2026_10_27_08_adventure_repair.sql').read_text().replace('INSERT IGNORE','INSERT OR IGNORE')
db.executescript(sql)
assert db.execute("SELECT kind,min_seconds,max_seconds,requirement FROM vcore_adventure_action WHERE action_key='economy_repair'").fetchone()==('social',25,45,'repair')
assert db.execute("SELECT intent,weight FROM vcore_adventure_action_intent WHERE action_key='economy_repair'").fetchone()==('economy',2)
db.execute("UPDATE vcore_adventure_action_intent SET weight=0 WHERE action_key='economy_repair'")
before=list(db.iterdump());db.executescript(sql);assert list(db.iterdump())==before
print('Repair action catalog: proper native service requirement, economy-only intent and preserved administrator weights passed.')
+16
View File
@@ -52,6 +52,7 @@ struct Position{float m_positionX=0,m_positionY=0,m_positionZ=0;void Relocate(Pl
struct NativeMap{float GetHeight(int,float,float,float,bool,float){return 0;}}nativeMap;
struct Player:Unit{
Player(){x=2;}
int repairs=0;void DurabilityRepairAll(bool cost,float,bool guild){assert(!cost&&!guild);++repairs;}
std::vector<int> emotes;int faces=0,casts=0;bool casting=false,sit=false,los=true;float distance=2;Session session;
ObjectGuid selection;
bool visible=true;bool CanSeeOrDetect(GameObject*){return visible;}
@@ -67,6 +68,7 @@ struct Player:Unit{
void Position::Relocate(Player*p){m_positionX=p->x;m_positionY=p->y;m_positionZ=p->z;}
struct AdventureSocialContext{bool AllowCreateFire=true;std::string Intent="social";AdventureProfessionPolicy::Professions PrimaryProfessions;Position Anchor;std::deque<std::string> TargetHistory;int departures=0;bool reachable=true;bool Move(Position const&,float){++departures;return reachable;}};
struct AdventureSocialScene{
bool RepairDone=false;
bool Active=true,Approaching=false,Casting=false,WaitingChair=false,Seated=false,Departing=false;
std::string Action="social_npc",Kind="npc",TargetKey;ObjectGuid Target{1},PreviousSelection;Position Destination,Origin;float Facing=0;
uint32 Elapsed=0,CastElapsed=0,CheckTimer=0,Repaths=0,ServiceVisits=0,ServiceDuration=30000;Sequence Sequence;
@@ -211,6 +213,20 @@ int main(){
// Displacement while working must reapproach without advancing the cue.
p.distance=8;s.Approaching=false;travel.status.State=ClientlessTravelState::Moving;
assert(AdventureSocialActions::Update(&p,s,1000,c)&&s.Approaching&&p.states.empty());
// Repair requires arrival and a completed valid service scene. No packets/casts.
Unit repairer;repairer.service=true;target=&repairer;c.Intent="economy";
p.distance=2;p.los=true;friendly=true;approach=true;
s={};s.Kind="repair";s.Action="economy_repair";s.Target.id=1;s.Approaching=true;
s.Sequence.Start(Steps(s.Kind,15000));travel.status.State=ClientlessTravelState::Moving;
int beforeRepairs=p.repairs;oldCasts=p.casts;oldUses=p.session.uses;
assert(AdventureSocialActions::Update(&p,s,1000,c)&&p.repairs==beforeRepairs);
travel.status.State=ClientlessTravelState::Arrived;
active=true;for(int i=0;i<80&&active;++i)active=AdventureSocialActions::Update(&p,s,1000,c);
assert(p.repairs==beforeRepairs+1&&s.RepairDone&&p.casts==oldCasts&&p.session.uses==oldUses);
assert(!AdventureSocialActions::Update(&p,s,1000,c)&&p.repairs==beforeRepairs+1);
// Lost repair permission/target must never repair in the background.
s={};s.Kind="repair";s.Target.id=1;s.Sequence.Start(Steps(s.Kind,15000));repairer.service=false;
assert(!AdventureSocialActions::Update(&p,s,1000,c)&&p.repairs==beforeRepairs+1);
}
'''
@@ -0,0 +1,16 @@
"""Apply training additions and verify editable weights survive repeated migration."""
from pathlib import Path
import re,sqlite3
root=Path(__file__).resolve().parents[2]
db=sqlite3.connect(':memory:')
base=(root/'sql/custom/world/2026_10_26_03_adventure_behavior.sql').read_text()
for table in ('vcore_adventure_action','vcore_adventure_action_intent'):
body=re.search(r'CREATE TABLE IF NOT EXISTS '+table+r' \((.*?)\) ENGINE=',base,re.S)[1]
db.execute('CREATE TABLE '+table+' ('+body+')')
sql=(root/'sql/custom/world/2026_10_27_09_adventure_training.sql').read_text().replace('INSERT IGNORE','INSERT OR IGNORE')
db.executescript(sql)
assert db.execute("SELECT kind,min_seconds,max_seconds,requirement FROM vcore_adventure_action WHERE action_key='leisure_training'").fetchone()==('leisure',60,180,'training')
assert db.execute("SELECT intent,weight FROM vcore_adventure_action_intent WHERE action_key='leisure_training'").fetchone()==('leisure',2)
db.execute("UPDATE vcore_adventure_action_intent SET weight=0 WHERE action_key='leisure_training'")
before=list(db.iterdump());db.executescript(sql);assert list(db.iterdump())==before
print('Training catalog: real dummy requirement, leisure-only intent, bounded duration and preserved administrator weights passed.')
@@ -0,0 +1,44 @@
"""Execute the production map-worker damage enqueue concurrently, without pointers."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/ClientlessPlayerMgr.cpp').read_text()
a=s.index('void ClientlessPlayerMgr::DamageTaken(');b=s.index('{',a);e=b+1;depth=1
while depth:depth+=(s[e]=='{')-(s[e]=='}');e+=1
code=r'''
#include <cassert>
#include <deque>
#include <unordered_map>
#include <mutex>
#include <thread>
#include <vector>
using uint32=unsigned;
struct ObjectGuid{unsigned id;unsigned GetCounter(){return id;}};
struct Unit{ObjectGuid guid;ObjectGuid GetGUID(){return guid;}};
struct Session{bool clientless=true;bool IsClientless(){return clientless;}};
struct Player:Unit{Session session;Session* GetSession(){return &session;}};
struct ClientlessPlayerMgr{
struct DamageNotice{ObjectGuid Attacker;uint32 Damage;};
std::mutex _damageMutex;std::unordered_map<uint32,std::deque<DamageNotice>> _damageNotices;
void DamageTaken(Player*,Unit*,uint32);
};
'''+s[a:e]+r'''
int main(){
ClientlessPlayerMgr mgr;std::vector<Player> players(16);std::vector<std::thread> threads;
for(unsigned i=0;i<players.size();++i){players[i].guid={i+1};threads.emplace_back([&,i]{for(unsigned n=1;n<=1000;++n){Unit attacker{{n}};mgr.DamageTaken(&players[i],&attacker,n);}});}
for(auto& t:threads)t.join();
assert(mgr._damageNotices.size()==16);
for(unsigned i=1;i<=16;++i){auto const&q=mgr._damageNotices[i];assert(q.size()==1000);for(unsigned n=1;n<=1000;++n)assert(q[n-1].Attacker.id==n && q[n-1].Damage==n);}
Player ordinary;ordinary.guid={100};ordinary.session.clientless=false;Unit attacker{{100}};
mgr.DamageTaken(&ordinary,&attacker,100);assert(!mgr._damageNotices.count(100));
ordinary.session.clientless=true;mgr.DamageTaken(&ordinary,&attacker,0);mgr.DamageTaken(&ordinary,nullptr,100);assert(!mgr._damageNotices.count(100));
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['g++','-std=c++17','-pthread',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
unit=(root/'src/server/game/Entities/Unit/Unit.cpp').read_text();start=unit.index('uint32 Unit::DealDamage(')
hook=unit.index('sClientlessPlayerMgr->DamageTaken(',start)
assert unit.index('GetCommandStatus(CHEAT_GOD)',start)<unit.index('victim->ModifyHealth(-(int32)damage);',start)<hook
print('Production damage handoff: 16 concurrent map-worker writers, ordered GUID-only events, clientless-only/positive damage gates and post-health notification passed.')
+45
View File
@@ -0,0 +1,45 @@
"""Execute native phase visibility rules: implicit/default phase 169 is shared."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/Phasing/PhaseShift.cpp').read_text()
def function(signature):
a=s.index(signature);b=s.index('{',a);e=b+1;depth=1
while depth:depth+=(s[e]=='{')-(s[e]=='}');e+=1
return s[a:e]
code=r'''
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <vector>
using uint32=uint32_t;using int32=int32_t;using ObjectGuid=int;
constexpr uint32 DEFAULT_PHASE=169;
enum class PhaseShiftFlags:uint32{None=0,AlwaysVisible=1,Inverse=2,InverseUnphased=4,Unphased=8,NoCosmetic=16};
enum class PhaseFlags:uint32{None=0,Cosmetic=1,Personal=2};
template<class T>struct EnumFlag{uint32 bits;EnumFlag(T v):bits(uint32(v)){}EnumFlag(uint32 v):bits(v){}bool HasFlag(T f)const{return (bits&uint32(f))!=0;}EnumFlag operator~()const{return EnumFlag(~bits);}EnumFlag& operator|=(T f){bits|=uint32(f);return *this;}EnumFlag& operator|=(EnumFlag f){bits|=f.bits;return *this;}EnumFlag& operator&=(EnumFlag f){bits&=f.bits;return *this;}};
struct PhaseShift{
struct PhaseRef{uint32 Id;EnumFlag<PhaseFlags> Flags{PhaseFlags::None};int References=0;bool operator==(PhaseRef const& p)const{return Id==p.Id;}};
using PhaseContainer=std::vector<PhaseRef>;PhaseContainer Phases;
EnumFlag<PhaseShiftFlags> Flags{PhaseShiftFlags::Unphased};int PersonalGuid=0,NonCosmeticReferences=0,CosmeticReferences=0,DefaultReferences=0;bool IsDbPhaseShift=false;
bool CanSee(PhaseShift const&)const;void ModifyPhasesReferences(PhaseContainer::iterator,int32);void UpdateUnphasedFlag();
void Add(uint32 id){Phases.push_back({id});ModifyPhasesReferences(Phases.end()-1,1);}
};
namespace Trinity{namespace Containers{
template<class I,class J,class P>bool Intersects(I a,I b,J c,J d,P p){for(;a!=b;++a)for(auto x=c;x!=d;++x)if(*a==*x&&p(*a,*x))return true;return false;}
}}
'''
for sig in ('bool PhaseShift::CanSee(', 'void PhaseShift::ModifyPhasesReferences(', 'void PhaseShift::UpdateUnphasedFlag('):code+=function(sig)+'\n'
code+=r'''
int main(){PhaseShift player,standard;standard.Add(169);
assert(player.CanSee(standard)&&standard.CanSee(player)); // no explicit membership required
PhaseShift custom;custom.Add(170);assert(!player.CanSee(custom));
custom.Add(169);assert(player.CanSee(custom));
custom.ModifyPhasesReferences(custom.Phases.begin()+1,-1);custom.Phases.pop_back();assert(!player.CanSee(custom));
PhaseShift anotherDefault;assert(player.CanSee(anotherDefault));
}
'''
with tempfile.TemporaryDirectory() as tmp:
src=Path(tmp)/'phase.cpp';exe=Path(tmp)/'phase';src.write_text(code)
subprocess.run(['g++','-std=c++17',str(src),'-o',str(exe)],check=True)
subprocess.run([str(exe)],check=True)
print('Native PhaseShift: implicit/default 169 visibility, genuine custom-phase separation and reference updates passed.')