Add AdventureBot combat reactions, training and corpse recovery
This commit is contained in:
1 parent
e18a416bf4
commit
c0c799c4b6
37 files changed
+1568
-18
No files matched your search
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
@@ -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 ⌖}}
|
||||
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 ↦}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 ↦}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.')
|
||||
@@ -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);
|
||||
|
||||
@@ -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.')
|
||||
@@ -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.')
|
||||
@@ -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.')
|
||||
Reference in new issue
Block a user