Add persistent instance visits and validate sleeping spaces
This commit is contained in:
1 parent
440c53acd7
commit
e18a416bf4
22 files changed
+1621
-31
No files matched your search
@@ -1,3 +1,202 @@
|
|||||||
|
# Aktuell: Instanzintent implementiert und technisch live geprüft
|
||||||
|
|
||||||
|
Stand 27.10.2026, letzter Testserverprozess PID **35865**, frisches Ready beim
|
||||||
|
letzten Restart und Port 8085 geprüft. Nutzer beauftragte Umsetzung einschließlich
|
||||||
|
zufälliger Verletzung (0–50 Prozent Max-HP) nach Rückkehr. Umsetzung fertig,
|
||||||
|
Der Nutzer hat ausdrücklich auf eine weitere visuelle Abnahme verzichtet.
|
||||||
|
Alle vorhandenen Änderungen sollen gemeinsam lokal committet werden; kein Push.
|
||||||
|
Details: `instance-intent.md`.
|
||||||
|
|
||||||
|
Implementierung:
|
||||||
|
- Intent `instance`, eigener Tagesplanwert (Default .5), feste Phasen statt
|
||||||
|
zufälliger Innenraum-Mikroaktionen. Anreise verbraucht Blockzeit; zehn Sekunden
|
||||||
|
Vorbereitung; tatsächlicher nativer Eintritt; Warten direkt dahinter; Rückkehr
|
||||||
|
zu Blockende minus fünf Minuten; Nachruhe bis zum ursprünglichen Blockende.
|
||||||
|
- Neue native Außenwelt-Aliase `instance_visit_at_<Trigger-ID>` und editierbare
|
||||||
|
Ein-/Ausgangszuordnung. 108 Eingänge in Test-DB, insgesamt 2.973 Locations /
|
||||||
|
3.966 Worldpoints. Innenräume bleiben reine native Karten/Graphknoten.
|
||||||
|
- Charakterpersistenz enthält Phase, Trigger, Instanz-ID, absolute Frist,
|
||||||
|
einmal gezogenen Verletzungswert und injury_applied. Gemeinsame Transaktion
|
||||||
|
mit Player::SaveToDB über ASYNC-Worker, bestätigter Commit vor Fortschritt.
|
||||||
|
- Optionales WorldScript in viano_bot_commands.cpp meldet nur gespeicherte
|
||||||
|
Besucher beim Serverstart automatisch mit dem Adventure-Controller an.
|
||||||
|
Wiederaufnahme hat Vorrang vor normalem Lua-Initialintent.
|
||||||
|
- Getrennten Hinterausgang aus nativen Daten bevorzugen, expliziter Override
|
||||||
|
möglich. Verborgene Dungeon-Durchquerung wird innen bis zum echten Ausgang
|
||||||
|
abstrahiert, danach nativer Trigger. Fehlender Ausgang: Eingang. Keine Kämpfe,
|
||||||
|
Beute/Inventar-/Geldänderung, keine DungeonBot-Abhängigkeit.
|
||||||
|
|
||||||
|
Geprüft:
|
||||||
|
- Debug-Build -j10, installiert, **44 Testskripte im vollständigen abschließenden
|
||||||
|
Lauf bestanden** (/tmp/vcore-instance-tests-final.log).
|
||||||
|
- Beide additiven Migrationen angewendet. Tabellen-Collation unicode_ci passt
|
||||||
|
zum bestehenden Schema; Dateien wegen *.sql-Ignore gezielt mit git add -f
|
||||||
|
vorgemerkt, alle übrigen Änderungen uncommittet. Kein Push.
|
||||||
|
- Verlies-Eingang 101, Ausgang 503, Karte34/Instanz-ID1 tatsächlich betreten.
|
||||||
|
Echter Serverneustart während Phase3: automatischer Login, gleiche Instanz-ID1,
|
||||||
|
ursprüngliche absolute Frist beibehalten. Normales Lua-Profil war restauriert.
|
||||||
|
- Für schnelle Rückkehrprüfung wurde ausschließlich nach sauberem Serverstopp
|
||||||
|
die Test-Besuchsfrist verkürzt. Original in
|
||||||
|
/tmp/vcore-instance-live-original-deadline.tsv. Keine normale Codefrist geändert.
|
||||||
|
- Native Rückkehr nach Karte0 (-8766.11,845.499,87.9952), zunächst zweimal echte
|
||||||
|
0%-Würfe, nächster regulärer kurzer Testblock 17%: **2165/2609 HP**.
|
||||||
|
Charakter-DB bestätigte Phase5, loss_percent17, injury_applied1 und HP2165.
|
||||||
|
- Erneuter Logout/Login während Phase5: gleiche Frist, injuryApplied1, kein
|
||||||
|
Neuwurf/keine zweite Verletzung. Aktuell Nachruhe vor dem Verlies bis
|
||||||
|
Deadline1793128202; anschließend endet der manuelle Testauftrag. Zustand bei
|
||||||
|
Wiederaufnahme neu prüfen; keinen weiteren Test automatisch starten.
|
||||||
|
- Normales Profil in Quelle UND Installation bytegleich normal (SHA256
|
||||||
|
26b11c3108e6ecc5dbc59d1852b4103ca4e818148603a34c02a470c969dc659b).
|
||||||
|
Kein temporärer Lua-Override. Nach saveall Geld-/Inventar-SHA256 unverändert
|
||||||
|
8d40931782d05219466ba9fd077ce7acd3e02bd53eb63f6e96115042c620d26a.
|
||||||
|
|
||||||
|
Wichtig: Fernanreise NICHT live erfolgreich verifiziert. Vorhandene Graph-
|
||||||
|
Recovery-Schleifen führten erst nach Goldhain, dann durch Sturmwind. Nutzer hat
|
||||||
|
Pfadfindung/Hinderniserkennung ausdrücklich auf später verschoben. Daher wurde
|
||||||
|
für den isolierten Instanztest nur der ausgeloggte Testbot administrativ an den
|
||||||
|
realen Außeneingang gestellt; anschließender lokaler MMAP-Anschluss und Eintritt
|
||||||
|
waren regulär. Kein Instanzteleport als erfolgreicher Eintritt ausgegeben.
|
||||||
|
Todesminen-Hinterausgang und zwei unabhängig gleichzeitig betretene Kopien nur
|
||||||
|
produktive Adaptertests/Quellprüfung, noch kein entsprechender Livevergleich.
|
||||||
|
|
||||||
|
Erster Liveversuch fand meinen falschen SYNCH-Transaktionsweg; Debugserver
|
||||||
|
beendete sich vor der Anreise. Fehler korrigiert auf AsyncCommitTransaction mit
|
||||||
|
Commitresultat. Nachfolgende native Saves/Neustarts erfolgreich, Fehlerlog
|
||||||
|
/tmp/vcore-instance-first-live-DBErrors.log erhalten. Nicht als aktuelles Problem
|
||||||
|
missverstehen. Aktuell keine neuen Instanz-SQL-/Startfehler beobachtet.
|
||||||
|
|
||||||
|
Alle Schlaf-WIP-Änderungen von vorher erhalten. Bett-/Hinderniserkennung bleibt
|
||||||
|
auf Nutzerwunsch zurückgestellt. Nur feature/clientless-player-core verwenden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Aktueller Auftrag: Instanzbesuche – Nutzervorgaben konkretisiert
|
||||||
|
|
||||||
|
Wirtschaftstest vom Nutzer als ausreichend beendet; nächster Intent sind
|
||||||
|
Instanzbesuche. Details verbindlich in `instance-intent.md` festgehalten.
|
||||||
|
Rest des gewählten Location-Zeitblocks, letzte fünf Minuten Nachruhe draußen.
|
||||||
|
Beispiel 60 Minuten: 40 Anreise, 15 innen, 5 Nachruhe. Tatsächlicher Eintritt,
|
||||||
|
Warten direkt hinter dem Eingang in nativer eigener Instanzkopie, keine
|
||||||
|
Dungeon-Durchquerung/Kampf. Neustart: am Eingang innen wieder einloggen,
|
||||||
|
ursprüngliche absolute Frist weiterverwenden. Rückkehr eigener Ausgang,
|
||||||
|
sonst Eingang. Instanzinnenräume NICHT zu normalen Locations machen.
|
||||||
|
|
||||||
|
World-DB geprüft: areatrigger_teleport enthält 78 Deadmines Entrance Target,
|
||||||
|
119 Exit Front Target, 121 Rear Exit Target. Alle sechs source/target-Knoten
|
||||||
|
in vcore_clientless_travel_node vorhanden, Kommentare nur Triggernummern.
|
||||||
|
Semantik aus nativen Namen und DBC-Quell-/DB-Zielkarten ableiten.
|
||||||
|
|
||||||
|
Noch keine Implementierung/Instanzbetretung in dieser Vorprüfung. Core-Befund:
|
||||||
|
Online startet heute flüchtigen Behavior-Record/Profil neu; dauerhafte
|
||||||
|
Besuchsphase/Frist und Wiederaufnahme müssen ergänzt werden. Native Instanz-ID,
|
||||||
|
Solobindung und Instanzspeicherung für Restart berücksichtigen und testen.
|
||||||
|
Eigener Ausgang hinten erfordert Rückkehrbehandlung ohne Dungeon-Durchquerung.
|
||||||
|
Randfall Ankunft mit weniger als fünf Minuten: noch keine ausdrückliche
|
||||||
|
Nutzerentscheidung, sinnvolle Ableitung ist Eintritt auslassen/Nachruhe.
|
||||||
|
|
||||||
|
Keine Code-/DB-/Serveränderung bei dieser Vorprüfung. Vorheriger Wirtschaftsintent
|
||||||
|
kann auf dem Testserver weiterhin laufen; vor Implementierung Zustand prüfen.
|
||||||
|
Schlaf-/Hindernisthemen bleiben ausdrücklich vertagt. Vorhandenes WIP bewahren.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Aktueller Nutzerauftrag: freier Wirtschaftsintent im Handelsdistrikt
|
||||||
|
|
||||||
|
Bett-/Geometriesuche ausdrücklich vom Nutzer zurückgestellt. Ebenso spätere
|
||||||
|
Überarbeitung der Pfadfindung/Hinderniserkennung (beobachtetes Durchlaufen von
|
||||||
|
Zäunen und Erhöhungen). Diese offenen Themen sollen die weitere Entwicklung
|
||||||
|
NICHT blockieren; jetzt daran nicht weiterarbeiten.
|
||||||
|
|
||||||
|
Arthoran GUID 26 hat auf Nutzerwunsch den autonomen Auftrag:
|
||||||
|
`clientless adventure intent 26 economy poi_tele_954 3600`.
|
||||||
|
`poi_tele_954` ist der vorhandene allgemeine Sturmwind-Anker im Handelsdistrikt
|
||||||
|
(-8833.07,622.778,93.9317), Kategorie landmark; keine Händler-/Bank-/AH-Rollensperre
|
||||||
|
wie bei den spezialisierten Testankern. Kein gezielter Mikroaktionszwang.
|
||||||
|
INTENT_ARRIVED economy und selbst gewählter Start `economy_vendor_visit`
|
||||||
|
gegen echten Händler Entry 277, GUID Low 1325, sind bestätigt. Szene läuft.
|
||||||
|
Kein erneuter Build/Restart oder Lua-Profilwechsel für diesen Auftrag.
|
||||||
|
|
||||||
|
Vorher `saveall`; Geld-/Inventar-SHA256 weiterhin
|
||||||
|
8d40931782d05219466ba9fd077ce7acd3e02bd53eb63f6e96115042c620d26a.
|
||||||
|
Wirtschaften bleibt rein performativ: keine echten Transaktionen.
|
||||||
|
Nutzer beobachtet nun die freie Ausführung im Spiel; keine neue visuelle
|
||||||
|
Abnahme behaupten. Schlafboden am ersten Anker war vom Nutzer bestätigt.
|
||||||
|
|
||||||
|
Alle vorherigen WIP-Änderungen erhalten, Branch feature/clientless-player-core,
|
||||||
|
kein Push. Normales Lua-Profil weiterhin unverändert. Schlaf-Testanker und
|
||||||
|
bisheriger Ausbau bleiben erhalten, siehe historische Notizen unten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Neuer Liveauftrag: Suche bei Allitesters Position
|
||||||
|
|
||||||
|
Nach dem Flächenausbau wollte der Nutzer eine erneute Suche nahe den beiden
|
||||||
|
Betten an seiner aktuellen Position. `saveall`, danach Allitester GUID 21:
|
||||||
|
Map 0, (-8877.360, 673.489, 104.950), O=0.649831. Separater aktiver manueller
|
||||||
|
Suchanker `gilded_rose_sleep_room_test` in Test-DB angelegt, Kategorie rest_sleep,
|
||||||
|
Metadata von Sturmwind; bestehende Punkte nicht verschoben.
|
||||||
|
|
||||||
|
Neustart war nötig: adventure reload erneuert NICHT den Reisepunkt-Cache,
|
||||||
|
weshalb der erste Anreiseversuch den neuen Punkt nicht fand und zurückfiel.
|
||||||
|
Server jetzt PID 32004, frisches Ready nach elf Sekunden, Port 8085 geprüft.
|
||||||
|
Einmaliges Loginprofil rest/room_test danach bytegleich restauriert, installierte
|
||||||
|
Profilkopie ebenfalls normal. Arthoran erreichte den neuen Anker regulär.
|
||||||
|
|
||||||
|
Erneute Suche dort: beds=0 raised=0 floor=46 (erster Lauf floor=34).
|
||||||
|
REST_CONFIRMED am Anker (-8877.360,673.489,104.950), O=.650, zuletzt 143 Sekunden.
|
||||||
|
Die sichtbaren Betten werden also weiterhin NICHT als Bett/erhöhte Fläche erkannt;
|
||||||
|
Ursache der konkreten geometrischen Ablehnung ist NICHT gemessen. Nicht behaupten,
|
||||||
|
dass nur der bisher zu kleine Radius schuld war.
|
||||||
|
|
||||||
|
Die gewichtete Auswahl wählte zwischenzeitlich wieder den alten Schlafanker.
|
||||||
|
Für den gezielten Versuch wurde NUR `gilded_rose_sleep_upstairs` kurz enabled=0
|
||||||
|
gesetzt, Katalog geladen, micro rest_sleep ausgeführt, dann enabled=1 restauriert
|
||||||
|
und Katalog erneut geladen. Kein Punkt blieb vorübergehend deaktiviert.
|
||||||
|
DB-Helfer /tmp/vcore-sleep-room-test.py, Positionssnapshot daneben als JSON.
|
||||||
|
Nutzer hat neuen Bodenplatz am alten Anker sichtbar bestätigt; Bettdarstellung
|
||||||
|
und tatsächliche Bettnutzung weiterhin offen. Kein neuer Code/Build in diesem
|
||||||
|
Folgeversuch. Vor weiterer Arbeit laufende Szene/Position neu prüfen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Aktuell: Schlafplatzsuche am 27.10.2026
|
||||||
|
|
||||||
|
Dieser Abschnitt hat Vorrang vor den historischen Checkpoints unten.
|
||||||
|
Der Nutzer nahm den Schlaftest wieder auf, sah Arthoran mitten im oberen Stuhl
|
||||||
|
und beauftragte eine Flächensuche: Betten/geeignete erhöhte Flächen bevorzugen,
|
||||||
|
sonst freier Gasthausboden, auch für Gasthäuser ohne Betten.
|
||||||
|
|
||||||
|
Implementiert, gebaut (`-j10`), installiert und live getestet, noch uncommittet:
|
||||||
|
`AdventureRestSpace.h`, Schlafadapter in `AdventureBehaviorMgr.cpp`, gespeichertes
|
||||||
|
Szenenziel in `.h`; zwei neue Regressionen und erweiterter Ruhetest. Insgesamt
|
||||||
|
39 Testskripte erfolgreich (38er-Lauf plus neuer nativer Adaptertest; danach
|
||||||
|
betroffene erweiterte Tests erneut erfolgreich). Details: `rest-intent.md`.
|
||||||
|
|
||||||
|
Schlafanker sind jetzt Suchbereiche, keine ungeprüften Liegepunkte. Native
|
||||||
|
Bettobjekte vor erhöhten Flächen mit abrupten Seitenkanten vor freiem Boden.
|
||||||
|
Geometrie ist keine sichere Bettsemantik; passende Tische können ähnlich sein.
|
||||||
|
Modellabhängige Liegefläche, Höhen-/Kollisionsraster, native Stuhlsperre,
|
||||||
|
Spieler/Kreaturenbelegung, Innenraum und Stockwerksgrenze. Tatsächlicher
|
||||||
|
MMAP-Endpunkt muss passen, kein Teleport/Klettern auf unerreichbare Möbel.
|
||||||
|
Erneute Prüfung vor Hinlegen und alle zwei Sekunden. Keine neue SQL-Migration.
|
||||||
|
|
||||||
|
Live: Prozess 31446, frisches Ready nach elf Sekunden, Port 8085 bestätigt.
|
||||||
|
Arthoran GUID 26 im Ruheauftrag `gilded_rose_sleep_upstairs`. Die neue Suche
|
||||||
|
fand `beds=0 raised=0 floor=18`; er ging 4,03 Yards zu
|
||||||
|
(-8868.534, 682.800, 104.949), Ausrichtung 3,740, Endpunktfehler null.
|
||||||
|
`REST_CONFIRMED` für 243 Sekunden. Die sichtbare Abnahme des neuen Bodenplatzes
|
||||||
|
ist offen, ebenso ein positiver Live-Betttest; Fixturetests decken Betten ab.
|
||||||
|
Normales Lua-Profil in Quelle UND Installation bytegleich wiederhergestellt,
|
||||||
|
mining/blacksmithing unverändert. Kein temporäres Override, kein Push.
|
||||||
|
|
||||||
|
Die zuvor bereits vorhandene Änderung an `rest-intent.md` zur ersten
|
||||||
|
Schlafvorführung wurde erhalten und ergänzt. Branch ausschließlich
|
||||||
|
`feature/clientless-player-core`. Build/Install/Testlogs: `/tmp/vcore-sleep-*.log`.
|
||||||
|
Prozess-/Szenenzustand beim Fortsetzen neu prüfen. Nicht automatisch den
|
||||||
|
abgeschlossenen Berufs-Ausbau oder die vertagte Instanzarbeit beginnen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Fortsetzung am 27.10.2026 – aktueller Stand hat Vorrang
|
# Fortsetzung am 27.10.2026 – aktueller Stand hat Vorrang
|
||||||
|
|
||||||
Der unten erhaltene Sitzungswechsel-Checkpoint ist historisch. Der Berufs-Ausbau
|
Der unten erhaltene Sitzungswechsel-Checkpoint ist historisch. Der Berufs-Ausbau
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
# Instanzbesuche
|
||||||
|
|
||||||
|
Stand: 27.10.2026. Verbindliche Nutzervorgaben und Umsetzung des Intents `instance`. Eigenständiger AdventureBot-Intent,
|
||||||
|
getrennt vom offenen Abenteuer und von DungeonBots. Keine Kampf-KI nötig.
|
||||||
|
|
||||||
|
## Zeitmodell
|
||||||
|
|
||||||
|
Die Besuchsdauer ergibt sich aus dem verbleibenden Zeitblock, für den der
|
||||||
|
Instanzeingang als Location gewählt wurde. Es gibt keine zusätzliche volle
|
||||||
|
Aufenthaltsdauer ab Ankunft. Anreise und Vorbereitung verbrauchen Blockzeit.
|
||||||
|
Die letzten fünf Minuten sind für Rückkehr, scheinbare Orientierung und Rast
|
||||||
|
nach dem Besuch reserviert.
|
||||||
|
|
||||||
|
Nutzerbeispiel: ein Stundenblock, 40 Minuten Anreise, 15 Minuten Aufenthalt
|
||||||
|
in der Instanz, anschließend fünf Minuten Orientierung/Rast draußen.
|
||||||
|
Maßgeblich bleiben absolute Zeitpunkte: Blockende und Beginn der Nachruhe
|
||||||
|
(Blockende minus 300 Sekunden). Serverausfall/Offlinezeit verlängern den Block
|
||||||
|
nicht; ein Neustart startet den Besuch nicht erneut.
|
||||||
|
|
||||||
|
Noch technisch auszugestaltender Randfall: Ist bei Ankunft bereits die
|
||||||
|
Nachruhephase erreicht, darf kein negativer oder künstlich verlängerter
|
||||||
|
Instanzaufenthalt entstehen. Naheliegende Umsetzung: Eintritt auslassen und
|
||||||
|
verbleibende Zeit zur Nachruhe nutzen. Das ist eine Implementierungsableitung,
|
||||||
|
keine zusätzliche ausdrücklich beantwortete Nutzerentscheidung.
|
||||||
|
|
||||||
|
## Sichtbarer und verborgener Ablauf
|
||||||
|
|
||||||
|
Reguläre Anreise zum Eingang, kurze passende Vorbereitung, tatsächlicher
|
||||||
|
bestätigter Eintritt über den vorhandenen Übergang. Fehlgeschlagener Eintritt
|
||||||
|
startet keine verborgene Aufenthaltsphase.
|
||||||
|
|
||||||
|
Arthoran wartet direkt hinter dem Eingang in seiner nativen Instanzkopie.
|
||||||
|
Er durchläuft den Dungeon nicht, kämpft nicht und simuliert keine Beute oder
|
||||||
|
Erfolge. Unabhängige Spieler/Gruppen sollen ihn beim eigenen Eintritt nicht
|
||||||
|
sehen. Native Instanzzuweisung und vorhandene Bindungen beachten; bloß die
|
||||||
|
Karten-ID zu vergleichen genügt nicht, die Instanz-ID ist ebenfalls relevant.
|
||||||
|
Keine gemeinsame Bot-Gruppe oder sonstige geteilte Bindung für diesen Solobesuch
|
||||||
|
neu erzeugen. Bestehende DungeonBots bleiben unabhängig.
|
||||||
|
|
||||||
|
Beim Neustart loggt er am gespeicherten Instanzstandort wieder ein und wartet
|
||||||
|
nur bis zum ursprünglichen Rückkehrzeitpunkt. Dazu müssen Besuchsphase,
|
||||||
|
Blockende, Instanz-/Eingangs-/Ausgangszuordnung erhalten bleiben; ist der
|
||||||
|
Zeitpunkt inzwischen verstrichen, folgt die Rückkehr ohne neue Wartezeit.
|
||||||
|
|
||||||
|
## Orte und Rückkehr
|
||||||
|
|
||||||
|
Die Instanzinnenräume werden keine normalen Aufenthaltslocations.
|
||||||
|
Die vorhandenen Eingangs-/Ausgangsknoten des TravelGraph liefern die
|
||||||
|
Übergänge; der Außenwelteingang ist das wählbare Reiseziel.
|
||||||
|
|
||||||
|
Hat die Instanz einen eigenen Ausgang, ergibt sich die Rückkehrposition aus
|
||||||
|
diesem Ausgang. Fehlt ein gesonderter Ausgang, erfolgt die Rückkehr am Eingang.
|
||||||
|
Kein willkürlicher Stadt-/Heimatteleport. Ein entfernter Hinterausgang benötigt
|
||||||
|
bei abstrahiertem Aufenthalt eine ausdrückliche technische Rückkehrbehandlung;
|
||||||
|
eine vollständige MMAP-Durchquerung des Dungeons ist nicht Teil des Auftrags.
|
||||||
|
|
||||||
|
## Bestätigte Daten und bestehender Core
|
||||||
|
|
||||||
|
Die Test-World-DB `areatrigger_teleport` enthält lesbare Bezeichnungen:
|
||||||
|
|
||||||
|
| Trigger | Name | Zielkarte | Zielposition |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 78 | Deadmines Entrance Target | 36 | -14.5732, -385.475, 62.4561 |
|
||||||
|
| 119 | Deadmines Exit Front Target | 0 | -11208, 1676.41, 24.5716 |
|
||||||
|
| 121 | Deadmines Rear Exit Target | 0 | -11341, 1576.55, 93.7284 |
|
||||||
|
|
||||||
|
Alle sechs Knoten `portal_at_{78,119,121}_{source,target}` sind bereits in
|
||||||
|
`vcore_clientless_travel_node` vorhanden. Ihre Kommentare enthalten derzeit
|
||||||
|
nur `Auto-imported AreaTrigger <ID> source/target`. Die semantischen Namen
|
||||||
|
bleiben in der nativen World-Tabelle verfügbar. Quelle der Triggerpositionen
|
||||||
|
ist zusätzlich `AreaTrigger.dbc`; Quelle/Zielkarte dienen zur belastbaren
|
||||||
|
Richtungszuordnung. Namen sind Hinweise, keine alleinige Zugangsfreigabe.
|
||||||
|
|
||||||
|
`MapManager::CreateMapForPlayer` verwendet vorhandene Spieler-/Gruppenbindungen
|
||||||
|
und sonst eine neue Instanz-ID. Bei Login wird eine gespeicherte Instanz-ID
|
||||||
|
nur mit verfügbarer Karte beziehungsweise passender Instanzspeicherung
|
||||||
|
wiederhergestellt. `InstanceMap::AddPlayerToMap` erzeugt native Instanzspeicher
|
||||||
|
und Solobindungen. Neustart und abgelaufene Instanzspeicherung müssen deshalb
|
||||||
|
auch mit einem echten Wiederanmeldetest geprüft werden.
|
||||||
|
|
||||||
|
Vor diesem Ausbau erzeugte `AdventureBehaviorMgr::Online` einen neuen
|
||||||
|
flüchtigen Record und startete gegebenenfalls das Lua-Initialintent. Jetzt
|
||||||
|
hat die dauerhafte Wiederaufnahme des Besuchs davor Vorrang.
|
||||||
|
|
||||||
|
Der Phasen-/Persistenzplan und die gezielten Tests sind im folgenden
|
||||||
|
Implementierungsabschnitt beschrieben. Der historische Datenbefund allein
|
||||||
|
belegt keine Gameplay-Abnahme.
|
||||||
|
|
||||||
|
|
||||||
|
## Implementierung und Bedienung
|
||||||
|
|
||||||
|
Der Verhaltensmanager führt feste Phasen statt zufälliger Dungeon-Mikroaktionen
|
||||||
|
aus: Anreise, zehn Sekunden Vorbereitung am echten Eingang, Eintritt, Aufenthalt,
|
||||||
|
Rückkehr und Nachruhe. Es gibt keine Kampf-/Beute-/Dungeon-Routenlogik.
|
||||||
|
Eine zu späte Ankunft überspringt den Eintritt und nutzt die verbleibende Zeit
|
||||||
|
zur Rast. Während eines Aufenthalts wird ein Intentwechsel erst nach der
|
||||||
|
Rückkehr ausgeführt. Die ursprüngliche Besuchsfrist wird dadurch nicht verlängert.
|
||||||
|
|
||||||
|
`sql/custom/world/2026_10_27_07_adventure_instances.sql` erzeugt aus vorhandenen
|
||||||
|
nativen Teleporten, Instanzvorlagen und Graphquellknoten Außenwelt-Eingangsorte
|
||||||
|
`instance_visit_at_<Trigger-ID>` sowie einen editierbaren Zuordnungskatalog
|
||||||
|
`vcore_adventure_instance`. Auf der Test-DB ergaben sich 108 Eingangsaliase.
|
||||||
|
Die alten Portalpunkte/Graphknoten bleiben unverändert. Instanzkarten werden
|
||||||
|
weiterhin nicht als normale Aufenthaltsorte ausgewählt.
|
||||||
|
|
||||||
|
`exit_trigger=0` lässt den Laufzeitadapter einen passenden nativen Ausgang zur
|
||||||
|
Außenkarte des Eingangs suchen. Benannte Hinterausgänge haben Vorrang vor dem
|
||||||
|
Vorderausgang; Quelle/Zielkarte und vorhandene Graphknoten werden geprüft.
|
||||||
|
Eine ausdrückliche Triggerzuordnung im Katalog hat Vorrang vor der Namensheuristik.
|
||||||
|
Native Zugangsbedingungen bleiben wirksam. Gruppierte Bots sowie bereits mit
|
||||||
|
anderen Spielern oder Gruppen geteilte Instanzbindungen werden zurückgewiesen.
|
||||||
|
|
||||||
|
Die verborgene Durchquerung bis zu einem entfernten Ausgang wird abstrahiert:
|
||||||
|
Am Rückkehrzeitpunkt wird innerhalb derselben Instanzkopie zum realen
|
||||||
|
Ausgangstrigger umgesetzt und dessen nativer Handler ausgeführt. Das ist kein
|
||||||
|
behaupteter MMAP-Lauf durch den Dungeon. Ohne Ausgang oder bei nativer
|
||||||
|
Wiederanmelde-Rettung aus einer abgelaufenen Instanz wird die zugeordnete
|
||||||
|
Außenposition verwendet. Ein verweigerter Ausgang gilt nicht als Rückkehr.
|
||||||
|
|
||||||
|
Nach bestätigter Rückkehr wird einmal ein bei Besuchsbeginn gezogener
|
||||||
|
Zufallswert von 0–50 Prozent der maximalen HP dargestellt. Bei vorher voller
|
||||||
|
Gesundheit bleiben 50–100 Prozent HP. Bereits vorhandene Verletzungen werden
|
||||||
|
nicht geheilt; die Darstellung tötet den Bot nicht. Normale Regeneration bleibt
|
||||||
|
aktiv. Der Wert wird gespeichert und beim Neustart nicht neu gewürfelt.
|
||||||
|
|
||||||
|
Die additive Charakter-Migration
|
||||||
|
`sql/custom/characters/2026_10_27_00_adventure_instance_visit.sql` speichert
|
||||||
|
Besuchsphase, absolute Frist, Trigger, Instanz-ID und Verletzungsmarker.
|
||||||
|
Charakterzustand und Besuchsphase/Verletzungsmarker werden gemeinsam über die
|
||||||
|
native asynchrone Transaktionsverbindung gespeichert. Erst deren bestätigter
|
||||||
|
Abschluss erlaubt das Fortschreiten. Die synchrone Verbindung kann die
|
||||||
|
ASYNC-only-Statements des nativen Charakterspeichers nicht ausführen.
|
||||||
|
|
||||||
|
Ein WorldScript im optionalen Adventure-Skript meldet beim Serverstart nur
|
||||||
|
Charaktere mit gespeichertem Besuch erneut mit dem Adventure-Controller an.
|
||||||
|
Die Wiederaufnahme hat Vorrang vor dem normalen Lua-Initialintent. Fehlt die
|
||||||
|
Besuchstabelle, bleibt diese Wiederanmeldung inaktiv; andere Clientless-Systeme
|
||||||
|
werden davon nicht abhängig.
|
||||||
|
|
||||||
|
Serverkonsole:
|
||||||
|
|
||||||
|
```text
|
||||||
|
clientless adventure intent 26 instance instance_visit_at_101 3600
|
||||||
|
clientless adventure status 26
|
||||||
|
```
|
||||||
|
|
||||||
|
Trigger 101 ist das Verlies in Sturmwind, Trigger 78 die Todesminen.
|
||||||
|
Der Tagesplan kennt `instance` mit eigener Intentgewichtung (Standard 0,5);
|
||||||
|
bestehende gespeicherte Tagespläne werden nicht umgeschrieben.
|
||||||
|
|
||||||
|
Regressionen: `test_adventure_instance_begin.py` prüft die produktive Anreise
|
||||||
|
über den vorhandenen Ortsplaner mit lokalem MMAP-Anschluss;
|
||||||
|
`test_adventure_instance.py` führt den produktiven Phasenablauf
|
||||||
|
mit nativen Übergangsadaptern aus; `test_adventure_instance_catalog.py` führt
|
||||||
|
die Migrationen auf Schemafixtures aus; `test_adventure_instance_persistence.py`
|
||||||
|
führt produktives Speichern/Wiederherstellen mit Transaktionsadapter aus;
|
||||||
|
`test_adventure_instance_autoresume.py` führt das tatsächliche Startup-Skript aus.
|
||||||
|
|
||||||
|
|
||||||
|
## Technischer Live-Test am 27.10.2026
|
||||||
|
|
||||||
|
- Debug-Build mit `-j10` und Installation erfolgreich. Abschließender vollständiger
|
||||||
|
Lauf: **44 Testskripte bestanden**, Log `/tmp/vcore-instance-tests-final.log`.
|
||||||
|
- Beide Migrationen auf Test-DB angewendet; Collation der neuen Tabellen stimmt
|
||||||
|
mit `utf8mb4_unicode_ci` der vorhandenen Ortsdaten überein. 108 neue Aliase,
|
||||||
|
insgesamt 2.973 geladene Locations / 3.966 Worldpoints.
|
||||||
|
- Die anfängliche Instanzanreise über den isolierten Portalnode führte in
|
||||||
|
vorhandene Graph-Recovery-Schleifen. Die Anreise verwendet jetzt den bestehenden
|
||||||
|
Ortsplaner mit lokalem MMAP-Anschluss. Auch dort trat auf dieser Strecke eine
|
||||||
|
bestehende Graphschleife auf. Dieses Reiseproblem wurde ausdrücklich nicht
|
||||||
|
repariert. **Die reguläre Fernanreise ist nicht erfolgreich live verifiziert.**
|
||||||
|
- Für den isolierten Instanztest wurde ausschließlich der ausgeloggte Testbot
|
||||||
|
administrativ am echten Außenwelt-Knoten `portal_at_101_source` positioniert
|
||||||
|
(-8761.85, 848.557, 87.8052). Danach liefen lokaler MMAP-Anschluss,
|
||||||
|
zehn Sekunden Vorbereitung und der tatsächliche native Trigger 101 regulär.
|
||||||
|
- `INSTANCE_ENTERED guid=26 map=34 instance=1` bestätigt das Verlies. Gespeicherter
|
||||||
|
Charakterstand direkt hinter dem Eingang: (48.9849, 0.483882, -16.3942).
|
||||||
|
- Echter geordneter Serverneustart während Phase 3. Der Startup-Hook meldete
|
||||||
|
Arthoran automatisch mit normalem Lua-Profil wieder an; `INSTANCE_RESUME`
|
||||||
|
bestätigte erneut Karte 34, **Instanz-ID 1**, dieselbe absolute Frist und
|
||||||
|
unveränderten Verletzungsmarker. Keine manuelle Anmeldung für diesen Restart.
|
||||||
|
- Für die zeitnahe Rückkehrprüfung wurde nach sauberem Serverstopp nur das
|
||||||
|
gespeicherte Blockende des bestätigten Innenaufenthalts auf einen verkürzten
|
||||||
|
Testblock gesetzt. Ursprünglicher Wert in
|
||||||
|
`/tmp/vcore-instance-live-original-deadline.tsv`. Der Produktivablauf verlängert
|
||||||
|
oder verkürzt normale Nutzerblöcke nicht.
|
||||||
|
- Rückkehr über nativen Ausgang 503 auf Karte 0, Position
|
||||||
|
(-8766.11, 845.499, 87.9952). Zuerst zweimal gültiger Zufallswert 0 Prozent;
|
||||||
|
ein weiterer regulär gestarteter sechsminütiger Testblock ergab **17 Prozent**:
|
||||||
|
`INSTANCE_RETURNED ... lossPercent=17 health=2165 maxHealth=2609`.
|
||||||
|
Die Charakter-DB bestätigte gleichzeitig Phase 5, `injury_applied=1` und 2165 HP.
|
||||||
|
- Erneute Anmeldung während der Nachruhe bestätigte `INSTANCE_RESUME phase=5`,
|
||||||
|
dieselbe Frist und `injuryApplied=1`, ohne Neuwurf oder erneute Verletzung. Normale Gesundheitsregeneration
|
||||||
|
bleibt aktiv. Nachruhe wird bei neuem explizitem Auftrag sauber beendet;
|
||||||
|
die ersten beiden Nachruhen wurden für die weiteren Tests bewusst unterbrochen.
|
||||||
|
- Normales Profil in Quelle und Installation ist bytegleich restauriert
|
||||||
|
(SHA256 `26b11c3108e6ecc5dbc59d1852b4103ca4e818148603a34c02a470c969dc659b`).
|
||||||
|
Keine temporären Lua-Overrides. Geld/Inventar nach abschließendem `saveall` unverändert: SHA256
|
||||||
|
`8d40931782d05219466ba9fd077ce7acd3e02bd53eb63f6e96115042c620d26a`.
|
||||||
|
|
||||||
|
Beim ersten Liveversuch wurde ein falscher Transaktionsweg erkannt: nativer
|
||||||
|
Charakterspeicher plus synchrone Verbindung führte zum Debug-Abbruch. Dieser
|
||||||
|
Fehler ist behoben: `AsyncCommitTransaction` führt die native Transaktion auf der
|
||||||
|
passenden Worker-Verbindung aus und liefert den bestätigten Abschluss zurück.
|
||||||
|
Regression und nachfolgende echte Speicher-/Neustarttests bestanden. Historisches
|
||||||
|
Fehlerlog: `/tmp/vcore-instance-first-live-DBErrors.log`.
|
||||||
|
|
||||||
|
Der Nutzer verzichtet ausdrücklich auf eine weitere visuelle Abnahme.
|
||||||
|
Ein echter Todesminen-Hinterausgang und
|
||||||
|
zwei voneinander unabhängige gleichzeitig betretene Instanzkopien wurden noch
|
||||||
|
nicht live getestet; Quell-/Zielzuordnung, geteilte Bindungssperre, Hinterausgang
|
||||||
|
und fehlender Ausgang sind durch produktive Adaptertests abgedeckt.
|
||||||
@@ -31,13 +31,13 @@ wirtschaftlichen Besuche und Aktivitäten.
|
|||||||
## Implementierungsstand und offene Ausgestaltung
|
## Implementierungsstand und offene Ausgestaltung
|
||||||
|
|
||||||
Der bestehende Verhaltensmanager kennt aktuell die technischen Intentnamen
|
Der bestehende Verhaltensmanager kennt aktuell die technischen Intentnamen
|
||||||
`social`, `adventure`, `profession`, `rest`, `leisure` und `economy`. Der [Ruhe-Intent](rest-intent.md)
|
`social`, `adventure`, `profession`, `rest`, `leisure`, `economy` und `instance`. Der [Ruhe-Intent](rest-intent.md)
|
||||||
ist ergänzt, ebenso der [Freizeit-Intent](leisure-intent.md) und der
|
ist ergänzt, ebenso der [Freizeit-Intent](leisure-intent.md) und der
|
||||||
[Wirtschaften-Intent](economy-intent.md). Der [Abenteuer-Ausbau](adventure-intent.md)
|
[Wirtschaften-Intent](economy-intent.md). Der [Abenteuer-Ausbau](adventure-intent.md)
|
||||||
ergänzt Erkundung und Objektuntersuchung. Der [Berufs-Ausbau](profession-intent.md)
|
ergänzt Erkundung und Objektuntersuchung. Der [Berufs-Ausbau](profession-intent.md)
|
||||||
ergänzt profilgebundene Arbeitsdarstellungen, echte Ressourcen-/Arbeitsplatzprüfung
|
ergänzt profilgebundene Arbeitsdarstellungen, echte Ressourcen-/Arbeitsplatzprüfung
|
||||||
und progressive Gebietssuche. Instanzbesuche bleiben konzeptionell
|
und progressive Gebietssuche. Der [Instanzintent](instance-intent.md) ergänzt bestätigten Eintritt,
|
||||||
festgehalten; ihr technischer Name und Detailablauf sind noch nicht umgesetzt. Der ausführliche bisherige
|
gespeicherten Aufenthalt, native Rückkehr und einmalige Verletzungsdarstellung. Der ausführliche bisherige
|
||||||
[Mikroaktionskatalog](mikroaktions-katalog.md) bleibt als Grundlage erhalten.
|
[Mikroaktionskatalog](mikroaktions-katalog.md) bleibt als Grundlage erhalten.
|
||||||
|
|
||||||
Der Geselligkeitsausbau ist implementiert. Die Sitzposition auf nativen
|
Der Geselligkeitsausbau ist implementiert. Die Sitzposition auf nativen
|
||||||
|
|||||||
@@ -178,8 +178,12 @@ keine zufällig neben anderen Gesten auswählbare Mikroaktion.
|
|||||||
|
|
||||||
Vorgeschlagener Ablauf: Anreise → Vorbereitung → bestätigter Eintritt →
|
Vorgeschlagener Ablauf: Anreise → Vorbereitung → bestätigter Eintritt →
|
||||||
abstrahierter Aufenthalt → Rückkehr → kurze Nachbereitung.
|
abstrahierter Aufenthalt → Rückkehr → kurze Nachbereitung.
|
||||||
Zeitmodell, technische Unterbringung, Neustartverhalten und Rückkehrposition
|
Zeitmodell, Unterbringung, Neustartverhalten und Rückkehrposition wurden am
|
||||||
werden vor der Umsetzung festgelegt. Ein fehlgeschlagener Eintritt darf die
|
27.10.2026 vom Nutzer konkretisiert: verbleibender Location-Zeitblock mit fünf
|
||||||
|
Minuten Nachruhe, Warten direkt hinter dem Eingang in der eigenen Instanzkopie,
|
||||||
|
Wiederaufnahme mit ursprünglicher Frist und Rückkehr über den zugehörigen
|
||||||
|
Ausgang beziehungsweise Eingang. Details und technische Vorprüfung stehen in
|
||||||
|
[instance-intent.md](instance-intent.md). Ein fehlgeschlagener Eintritt darf die
|
||||||
Aufenthaltsphase nicht starten.
|
Aufenthaltsphase nicht starten.
|
||||||
|
|
||||||
## Ruhe
|
## Ruhe
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Ruhemöglichkeit löst die Suche nach einer Ersatzlocation aus.
|
|||||||
| Schlüssel | Ausführung |
|
| Schlüssel | Ausführung |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `rest_seat` | Erreichbaren nativen Stuhl oder eine Bank aufsuchen, regulär benutzen und 60–100 Sekunden sitzen. |
|
| `rest_seat` | Erreichbaren nativen Stuhl oder eine Bank aufsuchen, regulär benutzen und 60–100 Sekunden sitzen. |
|
||||||
| `rest_sleep` | Einen ausdrücklich markierten Schlafplatz aufsuchen und 120–300 Sekunden mit nativer Schlafhaltung ruhen. |
|
| `rest_sleep` | Einen geprüften Schlafplatz nahe dem Schlafanker oder im Gasthaus aufsuchen und 120–300 Sekunden mit nativer Schlafhaltung ruhen. |
|
||||||
| `rest_ground` | Einen ausdrücklich markierten Bodenrastplatz aufsuchen und 60–120 Sekunden sitzen. |
|
| `rest_ground` | Einen ausdrücklich markierten Bodenrastplatz aufsuchen und 60–120 Sekunden sitzen. |
|
||||||
| `rest_drink` | Eine vorhandene native Sitzgelegenheit benutzen und Trinken darstellen. |
|
| `rest_drink` | Eine vorhandene native Sitzgelegenheit benutzen und Trinken darstellen. |
|
||||||
| `rest_eat` | Eine vorhandene native Sitzgelegenheit benutzen und Essen darstellen. |
|
| `rest_eat` | Eine vorhandene native Sitzgelegenheit benutzen und Essen darstellen. |
|
||||||
@@ -55,10 +55,11 @@ Die vorhandene Weltpunkt-Tabelle bleibt der Ortskatalog. Zusätzliche Kategorien
|
|||||||
- `rest_sleep`: ausdrücklich festgelegter Schlafplatz mit Position und Ausrichtung.
|
- `rest_sleep`: ausdrücklich festgelegter Schlafplatz mit Position und Ausrichtung.
|
||||||
- `rest_ground`: ausdrücklich festgelegter Bodenrastplatz.
|
- `rest_ground`: ausdrücklich festgelegter Bodenrastplatz.
|
||||||
|
|
||||||
Ein Schlafplatz ist keine automatische Bettinteraktion. Der Bot läuft zur
|
Ein Schlafanker ist keine automatische Bettinteraktion. Der Bot sucht heute
|
||||||
markierten Position und nimmt dort die native Schlafhaltung ein. Dekorative
|
eine freie Liegefläche in seiner Nähe, läuft regulär dorthin und nimmt die native
|
||||||
Betten werden weder als benutzbar vorausgesetzt noch durch Teleports betreten.
|
Schlafhaltung ein (Details und Grenzen im Abschnitt zur Flächensuche unten).
|
||||||
Neue Schlafplätze müssen räumlich und visuell geprüft werden.
|
Dekorative Betten werden weder als benutzbar vorausgesetzt noch durch Teleports
|
||||||
|
betreten. Neue Schlafplätze benötigen weiterhin eine visuelle Abnahme.
|
||||||
|
|
||||||
Die Migration ergänzt einen Ruheplatz im Obergeschoss der Goldhainer Taverne
|
Die Migration ergänzt einen Ruheplatz im Obergeschoss der Goldhainer Taverne
|
||||||
(`goldshire_sleep_upstairs`) und einen Anlaufpunkt bei einer vorhandenen
|
(`goldshire_sleep_upstairs`) und einen Anlaufpunkt bei einer vorhandenen
|
||||||
@@ -188,3 +189,93 @@ Stadtkennzeichen unter anderem für Sturmwind und deren Vererbung an das
|
|||||||
selbst unmarkierte Untergebiet 1617. Der Testserver erreichte erneut `ready...`;
|
selbst unmarkierte Untergebiet 1617. Der Testserver erreichte erneut `ready...`;
|
||||||
Prozess und Port 8085 wurden geprüft. Arthoran ist wieder im Ruhe-Intent in
|
Prozess und Port 8085 wurden geprüft. Arthoran ist wieder im Ruhe-Intent in
|
||||||
der Gilded Rose, sein temporär angepasstes Startprofil ist zurückgesetzt.
|
der Gilded Rose, sein temporär angepasstes Startprofil ist zurückgesetzt.
|
||||||
|
|
||||||
|
## Schlafvorführung am 27.10.2026
|
||||||
|
|
||||||
|
Auf ausdrücklichen Nutzerwunsch wurde die Schlafprüfung wieder aufgenommen.
|
||||||
|
Arthoran reiste regulär von Elwynn zur Gilded Rose. Der alte Schlafmarker lag
|
||||||
|
1,72 Yards vom nativen MMAP-Endpunkt entfernt und wurde deshalb von der strengen
|
||||||
|
lokalen Ankunftsprüfung zurückgewiesen. Die native Navigation erreichte über
|
||||||
|
die Treppe (-8865.066, 684.800, 105.517); der Marker wurde gezielt dorthin
|
||||||
|
korrigiert (Migration `_06`, auf Test-DB bereits ausgeführt). Keine Lockerung
|
||||||
|
der Ankunftsprüfung, kein Teleport. Der kurz verwendete Lua-Testoverride wurde
|
||||||
|
bytegleich wiederhergestellt.
|
||||||
|
|
||||||
|
`REST_CONFIRMED guid=26 point='gilded_rose_sleep_upstairs' posture='sleep'
|
||||||
|
durationMs=168000` bestätigt die gestartete native Schlafhaltung. Die sichtbare
|
||||||
|
Abnahme durch den Nutzer steht noch aus. Der Ruheauftrag gilt für diesen
|
||||||
|
Testlauf; das normale Lua-Profil blieb unverändert.
|
||||||
|
|
||||||
|
|
||||||
|
## Flächensuche statt ungeprüfter Schlafposition
|
||||||
|
|
||||||
|
Die Sichtprüfung vom 27.10.2026 zeigte Arthoran mitten im oberen Stuhl.
|
||||||
|
`REST_CONFIRMED` belegte zuvor nur Navigation und Haltung, keine freie Liegefläche.
|
||||||
|
Die Migration `_06` wird deshalb nicht weiter als räumlich abgenommener Platz
|
||||||
|
betrachtet. Ihr Punkt dient jetzt als Suchanker; vorhandene Koordinaten bleiben erhalten.
|
||||||
|
|
||||||
|
Schlaf sucht bis acht Yards um einen geeigneten `rest_sleep`-Anker; ohne nutzbaren
|
||||||
|
Schlafanker dürfen freigegebene Tavernen als Suchanker dienen. Explizite
|
||||||
|
Schlafanker haben Vorrang. Die Suche bleibt innerhalb von 1,5 Yards zur
|
||||||
|
Ankerhöhe und im Innenraum. Sie ersetzt keine Anreise und teleportiert nicht.
|
||||||
|
|
||||||
|
Reihenfolge innerhalb der Suche:
|
||||||
|
|
||||||
|
1. Phasenrichtige, gespawnte native Bettobjekte, deren Name ein vollständiges
|
||||||
|
Bett-/Bedroll-/Bunk-/Cot-Wort enthält. Ein eigener nativer Betttyp existiert
|
||||||
|
nicht; auch Betten vom Typ Chair werden nicht als Stuhl benutzt.
|
||||||
|
2. Geometrisch mögliche erhöhte Liegeflächen mit abrupten Kanten auf beiden
|
||||||
|
Seiten (mindestens 0,35 Yards Abfall auf 0,15 Yards Strecke, höchstens ein
|
||||||
|
Yard Höhendifferenz). Das ist eine Heuristik, keine sichere Bettklassifikation;
|
||||||
|
ein passender Tisch kann geometrisch gleich aussehen.
|
||||||
|
3. Ein freier ebener Bodenplatz, auch in Gasthäusern ohne Bett.
|
||||||
|
|
||||||
|
Für jede Ausrichtung wird eine modellgrößenabhängige Körperfläche geprüft:
|
||||||
|
mindestens 3,2 × 1,3 Yards, größere Charaktermodelle entsprechend größer.
|
||||||
|
45 Stützstellen müssen eine gemeinsame ebene, trockene Fläche ergeben
|
||||||
|
(maximal 0,18 Yards Abweichung). Native VMAP-/dynamische Sichtlinien auf
|
||||||
|
mehreren Körperhöhen und senkrechte Abfragen prüfen Möbel, Wände und niedrige
|
||||||
|
Überbauten. Native Stühle werden zusätzlich mit Sicherheitsabstand ausgeschlossen,
|
||||||
|
auch wenn ihr Modell keine nutzbare Kollision besitzt. Andere Spieler und
|
||||||
|
Kreaturen dürfen die Körperfläche nicht belegen. Dekoration ohne Kollisionsdaten
|
||||||
|
und ohne eigene erkennbare Objektkennung bleibt technisch nicht sicher erfassbar.
|
||||||
|
|
||||||
|
Der gewählte Platz und seine Ausrichtung werden getrennt vom Suchanker in der
|
||||||
|
Szene gespeichert. MMAP muss ihn vollständig und unverdächtig erreichen;
|
||||||
|
maximale Endpunktabweichung 0,3 Yards, tatsächliche Ankunft höchstens 0,35 Yards.
|
||||||
|
Ein Pfad auf dem Boden unter einer Bettfläche gilt damit nicht als Bettankunft.
|
||||||
|
Die Flächenprüfung wiederholt sich unmittelbar vor der Schlafhaltung und alle
|
||||||
|
zwei Sekunden während des Schlafens. Änderungen beenden die Szene über das
|
||||||
|
vorhandene Aufräumen. Bodenrast (`rest_ground`) behält ihr bisheriges Verhalten.
|
||||||
|
|
||||||
|
Neue Regressionen: `test_adventure_rest_space.py` (produktive Flächen-/Kantenregeln),
|
||||||
|
`test_adventure_rest_targets.py` (produktive native Objektauswahl und Suchadapter).
|
||||||
|
`test_adventure_rest.py` prüft zusätzlich den vom Anker abweichenden Zielpunkt,
|
||||||
|
die erneute Flächenprüfung und den Abbruch bei nachträglicher Belegung.
|
||||||
|
|
||||||
|
|
||||||
|
Liveprüfung des Flächenausbaus am 27.10.2026:
|
||||||
|
- Debug-Build (`-j10`) erfolgreich, sauber gestoppt, installiert und neu gestartet.
|
||||||
|
Frische Ready-Meldung nach elf Sekunden, Prozess 31446 und Port 8085 bestätigt.
|
||||||
|
- Alle 38 zunächst vorhandenen/ergänzten Testskripte bestanden; der danach
|
||||||
|
ergänzte native Zieladaptertest ebenfalls, insgesamt 39. Die anschließend
|
||||||
|
erweiterten Ruhe-, Flächen- und Zieladaptertests erneut erfolgreich.
|
||||||
|
- Arthoran erreichte den oberen Suchanker regulär. Suche: `beds=0 raised=0 floor=18`.
|
||||||
|
Gewählt wurde `(-8868.534, 682.800, 104.949)`, Ausrichtung 3,740.
|
||||||
|
Native Geh-Anreise über 4,03 Yards, Endpunktfehler 0,00, danach
|
||||||
|
`REST_CONFIRMED` mit 243 Sekunden Schlafdauer. Kein Teleport/Objektbenutzen.
|
||||||
|
- Source- und installierte Profilkopie entsprechen wieder dem unveränderten
|
||||||
|
Normalprofil (SHA256 `26b11c3108e6ecc5dbc59d1852b4103ca4e818148603a34c02a470c969dc659b`).
|
||||||
|
- Bett-/erhöhte-Flächen-Präferenz ist durch produktive Adapterfixtures geprüft,
|
||||||
|
noch nicht positiv live an einem realen Bett. Sichtprüfung des neuen
|
||||||
|
Bodenplatzes durch den Nutzer bleibt offen. Keine neue DB-Migration nötig.
|
||||||
|
|
||||||
|
|
||||||
|
Erneute Suche auf Nutzerwunsch bei Allitester: separater Testanker
|
||||||
|
`gilded_rose_sleep_room_test`, (-8877.360, 673.489, 104.950), O=0,649831.
|
||||||
|
Arthoran erreichte den Punkt nach frischem Laden des Reisepunkt-Caches regulär.
|
||||||
|
Auch hier `beds=0 raised=0`, zuletzt `floor=46`; Schlafhaltung am Boden bestätigt
|
||||||
|
(143 Sekunden). Die sichtbaren Zimmerbetten sind damit weiterhin nicht erkannt;
|
||||||
|
der genaue geometrische Ablehnungsgrund wurde noch nicht instrumentiert.
|
||||||
|
Der ältere Anker wurde für die gezielte Auswahl kurz ausgeblendet und danach
|
||||||
|
vollständig wieder aktiviert. Normalprofil unverändert wiederhergestellt.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-- Same database/transaction as characters.health and saved instance position.
|
||||||
|
CREATE TABLE IF NOT EXISTS vcore_adventure_instance_visit (
|
||||||
|
character_guid int unsigned NOT NULL,
|
||||||
|
location_key varchar(100) NOT NULL,
|
||||||
|
entry_trigger int unsigned NOT NULL,
|
||||||
|
exit_trigger int unsigned NOT NULL,
|
||||||
|
instance_map int unsigned NOT NULL,
|
||||||
|
instance_id int unsigned NOT NULL DEFAULT 0,
|
||||||
|
block_end bigint unsigned NOT NULL,
|
||||||
|
prepare_until bigint unsigned NOT NULL DEFAULT 0,
|
||||||
|
phase tinyint unsigned NOT NULL,
|
||||||
|
loss_percent tinyint unsigned NOT NULL,
|
||||||
|
injury_applied tinyint unsigned NOT NULL DEFAULT 0,
|
||||||
|
schedule tinyint unsigned NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY(character_guid)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
-- Native outside entrance aliases only. Instance interiors stay graph nodes.
|
||||||
|
CREATE TABLE IF NOT EXISTS vcore_adventure_instance (
|
||||||
|
location_key varchar(100) NOT NULL,
|
||||||
|
entry_trigger int unsigned NOT NULL,
|
||||||
|
exit_trigger int unsigned NOT NULL DEFAULT 0 COMMENT '0: native exit discovery, separate exit preferred',
|
||||||
|
enabled tinyint unsigned NOT NULL DEFAULT 1,
|
||||||
|
PRIMARY KEY(location_key), UNIQUE KEY(entry_trigger)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO vcore_clientless_world_point
|
||||||
|
(point_key,category,map,position_x,position_y,position_z,orientation,enabled,comment)
|
||||||
|
SELECT CONCAT('instance_visit_at_',a.ID),'instance_entrance',w.map,
|
||||||
|
w.position_x,w.position_y,w.position_z,w.orientation,1,
|
||||||
|
CONCAT('Native instance entrance: ',a.Name)
|
||||||
|
FROM areatrigger_teleport a
|
||||||
|
JOIN instance_template i ON i.map=a.target_map
|
||||||
|
JOIN vcore_clientless_travel_node n ON n.node_key=CONCAT('portal_at_',a.ID,'_source') AND n.enabled=1
|
||||||
|
JOIN vcore_clientless_world_point w ON w.point_key=n.world_point_key AND w.enabled=1
|
||||||
|
LEFT JOIN instance_template inside_source ON inside_source.map=w.map
|
||||||
|
WHERE inside_source.map IS NULL;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO vcore_adventure_location (point_key,display_name,source_type,source_id)
|
||||||
|
SELECT CONCAT('instance_visit_at_',a.ID),a.Name,'instance_entrance',a.ID
|
||||||
|
FROM areatrigger_teleport a
|
||||||
|
JOIN vcore_clientless_world_point w ON w.point_key=CONCAT('instance_visit_at_',a.ID) AND w.category='instance_entrance';
|
||||||
|
INSERT IGNORE INTO vcore_adventure_instance(location_key,entry_trigger)
|
||||||
|
SELECT point_key,source_id FROM vcore_adventure_location WHERE source_type='instance_entrance';
|
||||||
|
INSERT IGNORE INTO vcore_adventure_location_intent(category,intent,weight)
|
||||||
|
VALUES ('instance_entrance','instance',5);
|
||||||
|
-- Other intent/category weights and existing plans are deliberately preserved.
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "AdventureBehaviorMgr.h"
|
#include "AdventureBehaviorMgr.h"
|
||||||
#include "AdventureLocationMgr.h"
|
#include "AdventureLocationMgr.h"
|
||||||
#include "AdventureEconomyPolicy.h"
|
#include "AdventureEconomyPolicy.h"
|
||||||
|
#include "AdventureRestSpace.h"
|
||||||
#include "ClientlessLuaEngine.h"
|
#include "ClientlessLuaEngine.h"
|
||||||
#include "ClientlessPlayerMgr.h"
|
#include "ClientlessPlayerMgr.h"
|
||||||
#include "ClientlessActionExecutorMgr.h"
|
#include "ClientlessActionExecutorMgr.h"
|
||||||
@@ -23,6 +24,10 @@
|
|||||||
#include "TerrainMgr.h"
|
#include "TerrainMgr.h"
|
||||||
#include "World.h"
|
#include "World.h"
|
||||||
#include "WorldSession.h"
|
#include "WorldSession.h"
|
||||||
|
#include "ObjectMgr.h"
|
||||||
|
#include "InstanceSaveMgr.h"
|
||||||
|
#include "Opcodes.h"
|
||||||
|
#include "WorldPacket.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
@@ -39,9 +44,95 @@ namespace
|
|||||||
}
|
}
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
bool SleepSpace(Player* player, Position const& destination)
|
||||||
|
{
|
||||||
|
std::vector<GameObject*> objects;
|
||||||
|
player->GetGameObjectListWithEntryInGrid(objects, 0, 45);
|
||||||
|
std::vector<Player*> people;
|
||||||
|
player->GetPlayerListInGrid(people, 45);
|
||||||
|
std::vector<Creature*> creatures;
|
||||||
|
player->GetCreatureListWithEntryInGrid(creatures, 0, 45);
|
||||||
|
auto map = player->GetMap();
|
||||||
|
return AdventureRestSpace::Clear({destination.GetPositionX(), destination.GetPositionY(),
|
||||||
|
destination.GetPositionZ(), destination.GetOrientation()}, player->GetCollisionHeight(),
|
||||||
|
[&](float x, float y, float z)
|
||||||
|
{
|
||||||
|
float h = map->GetHeight(player->GetPhaseShift(), x, y, z, true, 3);
|
||||||
|
return map->GetWaterLevel(player->GetPhaseShift(), x, y) > h + 0.1f ? INVALID_HEIGHT : h;
|
||||||
|
},
|
||||||
|
[&](float x, float y, float z, float a, float b, float c)
|
||||||
|
{
|
||||||
|
return map->isInLineOfSight(player->GetPhaseShift(), x,y,z,a,b,c,
|
||||||
|
LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags::Nothing);
|
||||||
|
},
|
||||||
|
[&](float x, float y, float z)
|
||||||
|
{
|
||||||
|
for (auto go : objects)
|
||||||
|
if (go->isSpawned() && player->IsInPhase(go) && go->GetGoType() == GAMEOBJECT_TYPE_CHAIR &&
|
||||||
|
!AdventureRestSpace::BedName(go->GetName()) && std::fabs(go->GetPositionZ()-z) < 1.8f &&
|
||||||
|
go->GetExactDist2d(x,y) < std::max(0.9f,0.9f*go->GetObjectScale())) return true;
|
||||||
|
for (auto other : people)
|
||||||
|
if (other != player && player->IsInPhase(other) && std::fabs(other->GetPositionZ()-z) < 2 &&
|
||||||
|
other->GetExactDist2d(x,y) < 0.8f) return true;
|
||||||
|
for (auto other : creatures)
|
||||||
|
if (player->IsInPhase(other) && std::fabs(other->GetPositionZ()-z) < 2 &&
|
||||||
|
other->GetExactDist2d(x,y) < std::max(0.8f, other->GetBoundaryRadius())) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Position> SleepCandidates(Player* player, Position const& anchor, bool indoor)
|
||||||
|
{
|
||||||
|
std::vector<Position> beds, raised, floor;
|
||||||
|
std::vector<GameObject*> objects;
|
||||||
|
player->GetGameObjectListWithEntryInGrid(objects,0,40);
|
||||||
|
auto add = [&](float x,float y,float probe,float o,bool bed)
|
||||||
|
{
|
||||||
|
if (anchor.GetExactDist(x,y,probe) > 10) return;
|
||||||
|
float z=player->GetMap()->GetHeight(player->GetPhaseShift(),x,y,probe,true,3);
|
||||||
|
if (z <= INVALID_HEIGHT+1 || std::fabs(z-anchor.GetPositionZ()) > 1.5f) return;
|
||||||
|
PositionFullTerrainStatus terrain;
|
||||||
|
player->GetMap()->GetTerrain()->GetFullTerrainStatusForPosition(player->GetPhaseShift(),
|
||||||
|
player->GetMapId(),x,y,z+0.2f,terrain);
|
||||||
|
if (indoor && terrain.outdoors) return;
|
||||||
|
// A raised surface needs abrupt low edges on both sides. It is only
|
||||||
|
// a geometric candidate, never a claim that static geometry is a bed.
|
||||||
|
float left=player->GetMap()->GetHeight(player->GetPhaseShift(),x-std::sin(o)*1.2f,y+std::cos(o)*1.2f,z+0.2f,true,3);
|
||||||
|
float right=player->GetMap()->GetHeight(player->GetPhaseShift(),x+std::sin(o)*1.2f,y-std::cos(o)*1.2f,z+0.2f,true,3);
|
||||||
|
bool elevated=AdventureRestSpace::Raised({x,y,z,o},[&](float a,float b,float c)
|
||||||
|
{return player->GetMap()->GetHeight(player->GetPhaseShift(),a,b,c,true,3);});
|
||||||
|
// Never accept an arbitrary table/chair top as the floor fallback.
|
||||||
|
if (!bed && !elevated && (z-left>0.25f || z-right>0.25f)) return;
|
||||||
|
Position p(x,y,z,o);
|
||||||
|
if (!SleepSpace(player,p)) return;
|
||||||
|
(bed?beds:elevated?raised:floor).push_back(p);
|
||||||
|
};
|
||||||
|
for (auto go : objects)
|
||||||
|
if (go->isSpawned() && player->IsInPhase(go) && AdventureRestSpace::BedName(go->GetName()))
|
||||||
|
for (unsigned turn=0;turn<4;++turn)
|
||||||
|
add(go->GetPositionX(),go->GetPositionY(),go->GetPositionZ()+1.2f,
|
||||||
|
go->GetOrientation()+turn*1.5707963f,true);
|
||||||
|
// Stay on the designated storey. Bounded sampling cannot select the
|
||||||
|
// room below through a floor or wander outside the inn for a fallback.
|
||||||
|
for (unsigned ring=0;ring<=4;++ring)
|
||||||
|
for (unsigned spoke=0;spoke<(ring?12u:1u);++spoke)
|
||||||
|
{
|
||||||
|
float angle=spoke*6.2831853f/12;
|
||||||
|
float x=anchor.GetPositionX()+ring*2*std::cos(angle);
|
||||||
|
float y=anchor.GetPositionY()+ring*2*std::sin(angle);
|
||||||
|
for (unsigned turn=0;turn<4;++turn)
|
||||||
|
add(x,y,anchor.GetPositionZ()+1.1f,anchor.GetOrientation()+turn*1.5707963f,false);
|
||||||
|
}
|
||||||
|
TC_LOG_INFO("server.adventurebot", "REST_SPACE_SEARCH guid=%u beds=%u raised=%u floor=%u",
|
||||||
|
player->GetGUID().GetCounter(),uint32(beds.size()),uint32(raised.size()),uint32(floor.size()));
|
||||||
|
beds.insert(beds.end(),raised.begin(),raised.end());
|
||||||
|
beds.insert(beds.end(),floor.begin(),floor.end());
|
||||||
|
return beds;
|
||||||
|
}
|
||||||
|
|
||||||
bool Known(std::string const & intent)
|
bool Known(std::string const & intent)
|
||||||
{
|
{
|
||||||
return intent == "social" || intent == "adventure" || intent == "profession" || intent == "rest" || intent == "leisure" || intent == "economy";
|
return intent == "social" || intent == "adventure" || intent == "profession" || intent == "rest" || intent == "leisure" || intent == "economy" || intent == "instance";
|
||||||
}
|
}
|
||||||
bool Busy(uint32 guid)
|
bool Busy(uint32 guid)
|
||||||
{
|
{
|
||||||
@@ -98,6 +189,7 @@ void AdventureBehaviorMgr::Online(Player * player)
|
|||||||
r.Schedule = profile->DailySchedule;
|
r.Schedule = profile->DailySchedule;
|
||||||
r.Random.seed(player->GetGUID().GetCounter() ^ uint32(GameTime::GetGameTime()));
|
r.Random.seed(player->GetGUID().GetCounter() ^ uint32(GameTime::GetGameTime()));
|
||||||
auto & record = _records.insert_or_assign(player->GetGUID().GetCounter(), std::move(r)).first->second;
|
auto & record = _records.insert_or_assign(player->GetGUID().GetCounter(), std::move(r)).first->second;
|
||||||
|
if (RestoreInstance(player,record)) return;
|
||||||
if (! record.Schedule && Known(profile->InitialIntent)) SetWanted(player, record, profile->InitialIntent, profile->InitialLocation, GameTime::GetGameTime() + profile->InitialDurationSeconds);
|
if (! record.Schedule && Known(profile->InitialIntent)) SetWanted(player, record, profile->InitialIntent, profile->InitialLocation, GameTime::GetGameTime() + profile->InitialDurationSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +198,7 @@ void AdventureBehaviorMgr::Offline(Player * player)
|
|||||||
auto i = _records.find(player->GetGUID().GetCounter());
|
auto i = _records.find(player->GetGUID().GetCounter());
|
||||||
if (i != _records.end())
|
if (i != _records.end())
|
||||||
{
|
{
|
||||||
|
if (i->second.Instance.Active) SaveInstance(player,i->second);
|
||||||
ResetMicro(player, i->second);
|
ResetMicro(player, i->second);
|
||||||
_records.erase(i);
|
_records.erase(i);
|
||||||
}
|
}
|
||||||
@@ -178,14 +271,14 @@ void AdventureBehaviorMgr::Generate(Player * player, Record & record, uint32 dat
|
|||||||
{
|
{
|
||||||
std::vector < double > weights
|
std::vector < double > weights
|
||||||
{
|
{
|
||||||
Affinity(profile->ActivityAffinity, "social", 1) * Affinity(profile->Personality, "sociability", 0.5f) + Affinity(profile->ActivityAffinity, "tavern", 0.5f) * Affinity(profile->Personality, "tavern_affinity", 0.5f), Affinity(profile->ActivityAffinity, "adventure", 1) * Affinity(profile->Personality, "adventurousness", 0.5f), Affinity(profile->ActivityAffinity, "profession", 0.5f), Affinity(profile->ActivityAffinity, "rest", 0.5f), Affinity(profile->ActivityAffinity, "leisure", 0.5f), Affinity(profile->ActivityAffinity, "economy", 0.5f)
|
Affinity(profile->ActivityAffinity, "social", 1) * Affinity(profile->Personality, "sociability", 0.5f) + Affinity(profile->ActivityAffinity, "tavern", 0.5f) * Affinity(profile->Personality, "tavern_affinity", 0.5f), Affinity(profile->ActivityAffinity, "adventure", 1) * Affinity(profile->Personality, "adventurousness", 0.5f), Affinity(profile->ActivityAffinity, "profession", 0.5f), Affinity(profile->ActivityAffinity, "rest", 0.5f), Affinity(profile->ActivityAffinity, "leisure", 0.5f), Affinity(profile->ActivityAffinity, "economy", 0.5f), Affinity(profile->ActivityAffinity, "instance", 0.5f)
|
||||||
};
|
};
|
||||||
auto i = Choose(weights, record.Random);
|
auto i = Choose(weights, record.Random);
|
||||||
return std::array < std::string, 6 >
|
return std::array < std::string, 7 >
|
||||||
{
|
{
|
||||||
"social", "adventure", "profession", "rest", "leisure", "economy"
|
"social", "adventure", "profession", "rest", "leisure", "economy", "instance"
|
||||||
}
|
}
|
||||||
[std::min(i, std::size_t(5))];
|
[std::min(i, std::size_t(6))];
|
||||||
};
|
};
|
||||||
record.Plan = AdventureBehaviorPolicy::Generate(record.Random, Affinity(profile->Personality, "wanderlust", 0.3f), pick,[&](std::string const & intent)
|
record.Plan = AdventureBehaviorPolicy::Generate(record.Random, Affinity(profile->Personality, "wanderlust", 0.3f), pick,[&](std::string const & intent)
|
||||||
{
|
{
|
||||||
@@ -209,6 +302,17 @@ void AdventureBehaviorMgr::SetWanted(Player * player, Record & r, std::string co
|
|||||||
r.Deadline = deadline;
|
r.Deadline = deadline;
|
||||||
r.Dirty = true;
|
r.Dirty = true;
|
||||||
r.Tried.clear();
|
r.Tried.clear();
|
||||||
|
if (r.Instance.Active)
|
||||||
|
{
|
||||||
|
if (r.Instance.Stage>=AdventureInstancePolicy::Phase::Entering &&
|
||||||
|
r.Instance.Stage<=AdventureInstancePolicy::Phase::Returning)
|
||||||
|
{
|
||||||
|
r.Instance.Abort=true;
|
||||||
|
r.Detail="returning from instance before intent change";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FinishInstance(player,r);
|
||||||
|
}
|
||||||
// A sightseeing flight must land and return before a new intent takes over.
|
// A sightseeing flight must land and return before a new intent takes over.
|
||||||
if (r.Leisure.Active && r.Leisure.Kind=="flight")
|
if (r.Leisure.Active && r.Leisure.Kind=="flight")
|
||||||
{
|
{
|
||||||
@@ -291,7 +395,12 @@ void AdventureBehaviorMgr::Begin(Player * player, Record & r)
|
|||||||
intent.Parameters["location"] = r.Location;
|
intent.Parameters["location"] = r.Location;
|
||||||
sClientlessPlayerMgr->SetIntent(player->GetGUID().GetCounter(), std::move(intent));
|
sClientlessPlayerMgr->SetIntent(player->GetGUID().GetCounter(), std::move(intent));
|
||||||
r.Publishing = false;
|
r.Publishing = false;
|
||||||
if (l->IsArea)
|
if (r.Intent=="instance")
|
||||||
|
{
|
||||||
|
if (!ResolveInstance(player,r)) {Fallback(player,r);return;}
|
||||||
|
Queue(player,r,"travel_to_location","id",r.Location,true);
|
||||||
|
}
|
||||||
|
else if (l->IsArea)
|
||||||
{
|
{
|
||||||
auto node = AreaNode(player, * l);
|
auto node = AreaNode(player, * l);
|
||||||
if (node.empty())
|
if (node.empty())
|
||||||
@@ -309,6 +418,7 @@ void AdventureBehaviorMgr::Begin(Player * player, Record & r)
|
|||||||
|
|
||||||
void AdventureBehaviorMgr::Fallback(Player * player, Record & r)
|
void AdventureBehaviorMgr::Fallback(Player * player, Record & r)
|
||||||
{
|
{
|
||||||
|
if (r.Instance.Active && r.Instance.Stage==AdventureInstancePolicy::Phase::Travel) FinishInstance(player,r);
|
||||||
r.Tried.insert(r.Location);
|
r.Tried.insert(r.Location);
|
||||||
AdventureLocation const * nearest = nullptr;
|
AdventureLocation const * nearest = nullptr;
|
||||||
double best = std::numeric_limits < double >::infinity();
|
double best = std::numeric_limits < double >::infinity();
|
||||||
@@ -777,7 +887,7 @@ bool AdventureBehaviorMgr::RecoveryAvailable(Player* player, Record const& r, Ad
|
|||||||
for (auto const& location : sAdventureLocationMgr->Locations())
|
for (auto const& location : sAdventureLocationMgr->Locations())
|
||||||
{
|
{
|
||||||
auto const& p = location.Point;
|
auto const& p = location.Point;
|
||||||
if (p.Category != category || !Allowed(player, location, "rest") || p.MapId != r.AnchorMap) continue;
|
if ((p.Category != category && !(action.Requirement=="sleep" && p.Category=="tavern")) || !Allowed(player, location, "rest") || p.MapId != r.AnchorMap) continue;
|
||||||
if (r.Anchor.GetExactDist(p.X, p.Y, p.Z) <= 30 && std::fabs(p.Z-r.Anchor.GetPositionZ()) <= 12) return true;
|
if (r.Anchor.GetExactDist(p.X, p.Y, p.Z) <= 30 && std::fabs(p.Z-r.Anchor.GetPositionZ()) <= 12) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -792,30 +902,53 @@ bool AdventureBehaviorMgr::StartRecovery(Player* player, Record& r, AdventureMic
|
|||||||
for (auto const& location : sAdventureLocationMgr->Locations())
|
for (auto const& location : sAdventureLocationMgr->Locations())
|
||||||
{
|
{
|
||||||
auto const& p = location.Point;
|
auto const& p = location.Point;
|
||||||
if (p.Category != category || !Allowed(player, location, "rest") || p.MapId != r.AnchorMap ||
|
if ((p.Category != category && !(action.Requirement=="sleep" && p.Category=="tavern")) || !Allowed(player, location, "rest") || p.MapId != r.AnchorMap ||
|
||||||
r.Anchor.GetExactDist(p.X, p.Y, p.Z) > 30 || std::fabs(p.Z-r.Anchor.GetPositionZ()) > 12) continue;
|
r.Anchor.GetExactDist(p.X, p.Y, p.Z) > 30 || std::fabs(p.Z-r.Anchor.GetPositionZ()) > 12) continue;
|
||||||
choices.push_back(&location);
|
choices.push_back(&location);
|
||||||
weights.push_back(Novelty(r.TargetHistory, "rest:"+p.Id)/(1+player->GetExactDist(p.X,p.Y,p.Z)));
|
weights.push_back(Novelty(r.TargetHistory, "rest:"+p.Id)/(1+player->GetExactDist(p.X,p.Y,p.Z)));
|
||||||
}
|
}
|
||||||
while (!choices.empty())
|
while (!choices.empty())
|
||||||
{
|
{
|
||||||
auto i = Choose(weights, r.Random);
|
auto preferred=weights;
|
||||||
|
if (action.Requirement=="sleep" && std::any_of(choices.begin(),choices.end(),
|
||||||
|
[](AdventureLocation const* l){return l->Point.Category=="rest_sleep";}))
|
||||||
|
for (std::size_t n=0;n<choices.size();++n)
|
||||||
|
if (choices[n]->Point.Category=="tavern") preferred[n]=0;
|
||||||
|
auto i = Choose(preferred, r.Random);
|
||||||
if (i >= choices.size()) break;
|
if (i >= choices.size()) break;
|
||||||
auto const& p = choices[i]->Point;
|
auto const& p = choices[i]->Point;
|
||||||
Position destination(p.X,p.Y,p.Z,p.O);
|
Position designated(p.X,p.Y,p.Z,p.O);
|
||||||
|
auto candidates = action.Requirement=="sleep" ? SleepCandidates(player,designated,true) : std::vector<Position>{designated};
|
||||||
|
for (Position const& destination : candidates)
|
||||||
|
{
|
||||||
std::vector<Player*> occupants;
|
std::vector<Player*> occupants;
|
||||||
player->GetPlayerListInGrid(occupants, 60);
|
player->GetPlayerListInGrid(occupants, 60);
|
||||||
bool occupied = false;
|
bool occupied = false;
|
||||||
for (auto other : occupants)
|
for (auto other : occupants)
|
||||||
if (other != player && player->IsInPhase(other) && other->GetExactDist(destination) < 1.5f) occupied = true;
|
if (other != player && player->IsInPhase(other) && other->GetExactDist(destination) < 1.5f) occupied = true;
|
||||||
if (!occupied && LocalMove(player, r, destination, 120))
|
if (occupied) continue;
|
||||||
|
if (action.Requirement=="sleep")
|
||||||
|
{
|
||||||
|
ClientlessPathDiagnostics path;
|
||||||
|
// Reject a mesh endpoint on the floor beneath an elevated surface.
|
||||||
|
if (!sClientlessTravelMgr->AnalyzePath(player,destination,path) || !path.Complete ||
|
||||||
|
path.Suspicious || path.FinalDistance>0.3f) continue;
|
||||||
|
}
|
||||||
|
if (LocalMove(player, r, destination, 120))
|
||||||
{
|
{
|
||||||
r.Recovery = {true,true,p.Id,action.Requirement,duration,0};
|
r.Recovery = {true,true,p.Id,action.Requirement,duration,0};
|
||||||
|
r.Recovery.Destination=destination;
|
||||||
|
r.Recovery.HasDestination=true;
|
||||||
|
if (action.Requirement=="sleep")
|
||||||
|
TC_LOG_INFO("server.adventurebot", "REST_SPACE_SELECTED guid=%u anchor='%s' pos=(%.3f %.3f %.3f) orientation=%.3f",
|
||||||
|
player->GetGUID().GetCounter(),p.Id.c_str(),destination.GetPositionX(),destination.GetPositionY(),
|
||||||
|
destination.GetPositionZ(),destination.GetOrientation());
|
||||||
Remember(r.TargetHistory, "rest:"+p.Id);
|
Remember(r.TargetHistory, "rest:"+p.Id);
|
||||||
r.Detail = "walking to designated resting spot";
|
r.Detail = "walking to designated resting spot";
|
||||||
TC_LOG_INFO("server.adventurebot", "REST_APPROACH guid=%u point='%s' posture='%s'", player->GetGUID().GetCounter(),p.Id.c_str(),action.Requirement.c_str());
|
TC_LOG_INFO("server.adventurebot", "REST_APPROACH guid=%u point='%s' posture='%s'", player->GetGUID().GetCounter(),p.Id.c_str(),action.Requirement.c_str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
choices.erase(choices.begin()+i);
|
choices.erase(choices.begin()+i);
|
||||||
weights.erase(weights.begin()+i);
|
weights.erase(weights.begin()+i);
|
||||||
}
|
}
|
||||||
@@ -828,28 +961,42 @@ bool AdventureBehaviorMgr::UpdateRecovery(Player* player, Record& r, uint32 diff
|
|||||||
auto location = sAdventureLocationMgr->Find(scene.Point);
|
auto location = sAdventureLocationMgr->Find(scene.Point);
|
||||||
if (!scene.Active || !location || player->GetMapId() != r.AnchorMap || player->IsInWater() ||
|
if (!scene.Active || !location || player->GetMapId() != r.AnchorMap || player->IsInWater() ||
|
||||||
player->IsFalling() || !Allowed(player,*location,"rest") ||
|
player->IsFalling() || !Allowed(player,*location,"rest") ||
|
||||||
location->Point.Category != (scene.Posture=="sleep" ? "rest_sleep" : "rest_ground")) return false;
|
(location->Point.Category != (scene.Posture=="sleep" ? "rest_sleep" : "rest_ground") &&
|
||||||
|
!(scene.Posture=="sleep" && location->Point.Category=="tavern"))) return false;
|
||||||
auto const& p = location->Point;
|
auto const& p = location->Point;
|
||||||
|
Position designated(p.X,p.Y,p.Z,p.O);
|
||||||
|
Position const& destination=scene.HasDestination?scene.Destination:designated;
|
||||||
|
if (destination.GetExactDist(designated)>10) return false;
|
||||||
if (scene.Approaching)
|
if (scene.Approaching)
|
||||||
{
|
{
|
||||||
scene.ApproachElapsed += diff;
|
scene.ApproachElapsed += diff;
|
||||||
if (scene.ApproachElapsed > 60000) return false;
|
if (scene.ApproachElapsed > 60000) return false;
|
||||||
auto travel = sClientlessTravelMgr->GetStatus(player->GetGUID().GetCounter());
|
auto travel = sClientlessTravelMgr->GetStatus(player->GetGUID().GetCounter());
|
||||||
if (travel && travel->State == ClientlessTravelState::Moving) return true;
|
if (travel && travel->State == ClientlessTravelState::Moving) return true;
|
||||||
if (!travel || travel->State != ClientlessTravelState::Arrived || player->GetExactDist(p.X,p.Y,p.Z) > 1) return false;
|
if (!travel || travel->State != ClientlessTravelState::Arrived || player->GetExactDist(destination) > (scene.Posture=="sleep"?0.35f:1.0f)) return false;
|
||||||
std::vector<Player*> occupants;
|
std::vector<Player*> occupants;
|
||||||
player->GetPlayerListInGrid(occupants, 3);
|
player->GetPlayerListInGrid(occupants, 3);
|
||||||
for (auto other : occupants)
|
for (auto other : occupants)
|
||||||
if (other != player && player->IsInPhase(other) && other->GetExactDist(p.X,p.Y,p.Z) < 1.5f) return false;
|
if (other != player && player->IsInPhase(other) && other->GetExactDist(destination) < 1.5f) return false;
|
||||||
player->SetFacingTo(p.O);
|
if (scene.Posture=="sleep" && !SleepSpace(player,destination)) return false;
|
||||||
|
player->SetFacingTo(destination.GetOrientation());
|
||||||
player->SetStandState(scene.Posture=="sleep" ? UNIT_STAND_STATE_SLEEP : UNIT_STAND_STATE_SIT);
|
player->SetStandState(scene.Posture=="sleep" ? UNIT_STAND_STATE_SLEEP : UNIT_STAND_STATE_SIT);
|
||||||
scene.Approaching = false;
|
scene.Approaching = false;
|
||||||
r.Detail = scene.Posture=="sleep" ? "sleeping at designated resting spot" : "resting at designated ground spot";
|
r.Detail = scene.Posture=="sleep" ? "sleeping at designated resting spot" : "resting at designated ground spot";
|
||||||
TC_LOG_INFO("server.adventurebot", "REST_CONFIRMED guid=%u point='%s' posture='%s' durationMs=%u",player->GetGUID().GetCounter(),p.Id.c_str(),scene.Posture.c_str(),scene.Remaining);
|
TC_LOG_INFO("server.adventurebot", "REST_CONFIRMED guid=%u point='%s' posture='%s' durationMs=%u",player->GetGUID().GetCounter(),p.Id.c_str(),scene.Posture.c_str(),scene.Remaining);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (player->GetExactDist(p.X,p.Y,p.Z) > 1 ||
|
if (player->GetExactDist(destination) > (scene.Posture=="sleep"?0.35f:1.0f) ||
|
||||||
player->GetStandState() != (scene.Posture=="sleep" ? UNIT_STAND_STATE_SLEEP : UNIT_STAND_STATE_SIT)) return false;
|
player->GetStandState() != (scene.Posture=="sleep" ? UNIT_STAND_STATE_SLEEP : UNIT_STAND_STATE_SIT)) return false;
|
||||||
|
if (scene.Posture=="sleep")
|
||||||
|
{
|
||||||
|
if (scene.SpaceCheckMs<=diff)
|
||||||
|
{
|
||||||
|
if (!SleepSpace(player,destination)) return false;
|
||||||
|
scene.SpaceCheckMs=2000;
|
||||||
|
}
|
||||||
|
else scene.SpaceCheckMs-=diff;
|
||||||
|
}
|
||||||
if (scene.Remaining <= diff) return false;
|
if (scene.Remaining <= diff) return false;
|
||||||
scene.Remaining -= diff;
|
scene.Remaining -= diff;
|
||||||
return true;
|
return true;
|
||||||
@@ -1085,6 +1232,7 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff)
|
|||||||
}
|
}
|
||||||
r.Timer = 1000;
|
r.Timer = 1000;
|
||||||
time_t now = GameTime::GetGameTime();
|
time_t now = GameTime::GetGameTime();
|
||||||
|
if (r.Instance.Active && UpdateInstance(player,r,now)) return;
|
||||||
auto local = Local(now);
|
auto local = Local(now);
|
||||||
uint32 date = DateOf(local);
|
uint32 date = DateOf(local);
|
||||||
if (r.Schedule)
|
if (r.Schedule)
|
||||||
@@ -1177,6 +1325,7 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff)
|
|||||||
}
|
}
|
||||||
if (r.State == Phase::OnSite)
|
if (r.State == Phase::OnSite)
|
||||||
{
|
{
|
||||||
|
if (r.Intent=="instance") {UpdateInstance(player,r,now);return;}
|
||||||
if (r.Recovery.Active)
|
if (r.Recovery.Active)
|
||||||
{
|
{
|
||||||
if (UpdateRecovery(player,r,1000)) return;
|
if (UpdateRecovery(player,r,1000)) return;
|
||||||
@@ -1330,3 +1479,263 @@ bool AdventureBehaviorMgr::Command(ChatHandler * handler, char const * args)
|
|||||||
handler->SendSysMessage("Usage: clientless adventure import|repairimport|reload|status <guid>|plan <guid>|schedule <guid> on/off|intent <guid> <intent> <location> [seconds]|micro <guid> <scene-action>|stop <guid>");
|
handler->SendSysMessage("Usage: clientless adventure import|repairimport|reload|status <guid>|plan <guid>|schedule <guid> on/off|intent <guid> <intent> <location> [seconds]|micro <guid> <scene-action>|stop <guid>");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool AdventureBehaviorMgr::SaveInstance(Player* player, Record& r)
|
||||||
|
{
|
||||||
|
auto const& v=r.Instance;
|
||||||
|
if (!v.Active || player->IsBeingTeleported()) return false;
|
||||||
|
std::string key=v.Location;
|
||||||
|
CharacterDatabase.EscapeString(key);
|
||||||
|
auto tx=CharacterDatabase.BeginTransaction();
|
||||||
|
player->SaveToDB(tx);
|
||||||
|
tx->PAppend("REPLACE INTO vcore_adventure_instance_visit "
|
||||||
|
"(character_guid,location_key,entry_trigger,exit_trigger,instance_map,instance_id,block_end,prepare_until,phase,loss_percent,injury_applied,schedule) "
|
||||||
|
"VALUES (%u,'%s',%u,%u,%u,%u,%llu,%llu,%u,%u,%u,%u)",
|
||||||
|
player->GetGUID().GetCounter(),key.c_str(),v.Entry,v.Exit,v.Map,v.InstanceId,
|
||||||
|
static_cast<unsigned long long>(v.BlockEnd),static_cast<unsigned long long>(v.PrepareUntil),
|
||||||
|
uint32(v.Stage),v.LossPercent,v.InjuryApplied?1:0,r.Schedule?1:0);
|
||||||
|
// Player::SaveToDB uses ASYNC-only prepared statements. Execute on that
|
||||||
|
// worker connection, and wait for its transaction result before moving on.
|
||||||
|
auto commit=CharacterDatabase.AsyncCommitTransaction(tx);
|
||||||
|
bool saved=commit.m_future.get();
|
||||||
|
if (!saved) TC_LOG_ERROR("server.adventurebot","INSTANCE_SAVE_FAILED guid=%u",player->GetGUID().GetCounter());
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AdventureBehaviorMgr::RestoreInstance(Player* player, Record& r)
|
||||||
|
{
|
||||||
|
if (!CharacterDatabase.Query("SHOW TABLES LIKE 'vcore_adventure_instance_visit'")) return false;
|
||||||
|
auto row=CharacterDatabase.PQuery("SELECT location_key,entry_trigger,exit_trigger,instance_map,instance_id,block_end,prepare_until,phase,loss_percent,injury_applied,schedule "
|
||||||
|
"FROM vcore_adventure_instance_visit WHERE character_guid=%u",player->GetGUID().GetCounter());
|
||||||
|
if (!row) return false;
|
||||||
|
auto f=row->Fetch();
|
||||||
|
auto& v=r.Instance;
|
||||||
|
if (f[7].GetUInt8()>uint8(AdventureInstancePolicy::Phase::Rest) || f[8].GetUInt8()>50) return false;
|
||||||
|
v.Active=true;v.Location=f[0].GetString();v.Entry=f[1].GetUInt32();v.Exit=f[2].GetUInt32();
|
||||||
|
v.Map=f[3].GetUInt32();v.InstanceId=f[4].GetUInt32();v.BlockEnd=time_t(f[5].GetUInt64());
|
||||||
|
v.PrepareUntil=time_t(f[6].GetUInt64());v.Stage=AdventureInstancePolicy::Phase(f[7].GetUInt8());
|
||||||
|
v.LossPercent=f[8].GetUInt8();v.InjuryApplied=f[9].GetUInt8()!=0;r.Schedule=f[10].GetUInt8()!=0;
|
||||||
|
r.Intent=r.WantedIntent="instance";r.Location=r.WantedLocation=v.Location;r.Deadline=v.BlockEnd;
|
||||||
|
r.State=v.Stage==AdventureInstancePolicy::Phase::Travel?Phase::Idle:Phase::OnSite;
|
||||||
|
r.Dirty=v.Stage==AdventureInstancePolicy::Phase::Travel;
|
||||||
|
r.Detail="resuming saved instance visit";
|
||||||
|
TC_LOG_INFO("server.adventurebot","INSTANCE_RESUME guid=%u phase=%u map=%u instance=%u deadline=%llu injuryApplied=%u",
|
||||||
|
player->GetGUID().GetCounter(),uint32(v.Stage),player->GetMapId(),player->GetInstanceId(),
|
||||||
|
static_cast<unsigned long long>(v.BlockEnd),v.InjuryApplied?1:0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AdventureBehaviorMgr::FinishInstance(Player* player, Record& r)
|
||||||
|
{
|
||||||
|
if (!r.Instance.Active) return;
|
||||||
|
auto tx=CharacterDatabase.BeginTransaction();
|
||||||
|
player->SetStandState(UNIT_STAND_STATE_STAND);
|
||||||
|
player->SaveToDB(tx);
|
||||||
|
tx->PAppend("DELETE FROM vcore_adventure_instance_visit WHERE character_guid=%u",player->GetGUID().GetCounter());
|
||||||
|
auto commit=CharacterDatabase.AsyncCommitTransaction(tx);
|
||||||
|
if (!commit.m_future.get())
|
||||||
|
{
|
||||||
|
r.Detail="instance final save failed; retrying";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TC_LOG_INFO("server.adventurebot","INSTANCE_VISIT_END guid=%u phase=%u",player->GetGUID().GetCounter(),uint32(r.Instance.Stage));
|
||||||
|
r.Instance={};
|
||||||
|
if (r.Dirty) r.State=Phase::Idle;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AdventureBehaviorMgr::ResolveInstance(Player* player, Record& r)
|
||||||
|
{
|
||||||
|
if (r.Instance.Active) return r.Instance.Location==r.Location;
|
||||||
|
auto key=r.Location;WorldDatabase.EscapeString(key);
|
||||||
|
auto row=WorldDatabase.PQuery("SELECT entry_trigger,exit_trigger FROM vcore_adventure_instance WHERE location_key='%s' AND enabled=1",key.c_str());
|
||||||
|
if (!row) {r.Detail="instance entrance mapping unavailable";return false;}
|
||||||
|
uint32 entry=row->Fetch()[0].GetUInt32(),exit=row->Fetch()[1].GetUInt32();
|
||||||
|
auto source=sAreaTriggerStore.LookupEntry(entry);
|
||||||
|
auto target=sObjectMgr->GetAreaTrigger(entry);
|
||||||
|
auto map=target?sMapStore.LookupEntry(target->target_mapId):nullptr;
|
||||||
|
auto outside=source?sMapStore.LookupEntry(source->ContinentID):nullptr;
|
||||||
|
sClientlessTravelGraphMgr->EnsureLoaded();
|
||||||
|
if (!source || !map || !map->IsDungeon() || !outside || outside->MapType!=MAP_COMMON ||
|
||||||
|
!sClientlessTravelGraphMgr->FindNode("portal_at_"+std::to_string(entry)+"_source") ||
|
||||||
|
!sClientlessTravelGraphMgr->FindNode("portal_at_"+std::to_string(entry)+"_target") || player->GetGroup())
|
||||||
|
{r.Detail="solo instance entrance is not a valid native graph transition";return false;}
|
||||||
|
auto validExit=[&](uint32 id)
|
||||||
|
{
|
||||||
|
auto at=sAreaTriggerStore.LookupEntry(id);auto dest=sObjectMgr->GetAreaTrigger(id);
|
||||||
|
return at && dest && uint32(at->ContinentID)==target->target_mapId &&
|
||||||
|
dest->target_mapId==uint32(source->ContinentID) &&
|
||||||
|
sClientlessTravelGraphMgr->FindNode("portal_at_"+std::to_string(id)+"_source") &&
|
||||||
|
sClientlessTravelGraphMgr->FindNode("portal_at_"+std::to_string(id)+"_target");
|
||||||
|
};
|
||||||
|
if (exit && !validExit(exit)) {r.Detail="configured native instance exit invalid";return false;}
|
||||||
|
if (!exit)
|
||||||
|
{
|
||||||
|
// Native map pairing is authoritative. Names only prioritize a separate
|
||||||
|
// rear exit over the front door; an explicit catalog override wins.
|
||||||
|
if (auto rows=WorldDatabase.Query("SELECT ID FROM areatrigger_teleport ORDER BY "
|
||||||
|
"(LOWER(Name) REGEXP 'rear|back|hinter') DESC,ID")) do
|
||||||
|
{
|
||||||
|
auto id=rows->Fetch()[0].GetUInt32();
|
||||||
|
if (validExit(id)) {exit=id;break;}
|
||||||
|
} while(rows->NextRow());
|
||||||
|
}
|
||||||
|
r.Instance.Active=true;r.Instance.Location=r.Location;r.Instance.Entry=entry;r.Instance.Exit=exit;
|
||||||
|
r.Instance.Map=target->target_mapId;r.Instance.BlockEnd=r.Deadline;
|
||||||
|
r.Instance.LossPercent=std::uniform_int_distribution<uint32>(0,50)(r.Random);
|
||||||
|
if (!SaveInstance(player,r)) {r.Instance={};return false;}
|
||||||
|
TC_LOG_INFO("server.adventurebot","INSTANCE_PLAN guid=%u entry=%u exit=%u map=%u deadline=%llu lossPercent=%u",
|
||||||
|
player->GetGUID().GetCounter(),entry,exit,target->target_mapId,
|
||||||
|
static_cast<unsigned long long>(r.Deadline),r.Instance.LossPercent);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AdventureBehaviorMgr::UpdateInstance(Player* player, Record& r, time_t now)
|
||||||
|
{
|
||||||
|
using Stage=AdventureInstancePolicy::Phase;
|
||||||
|
auto& v=r.Instance;
|
||||||
|
if (!v.Active) return false;
|
||||||
|
if (Unsafe(player)) {r.Detail="instance visit waiting for safe native state";return true;}
|
||||||
|
auto entry=sAreaTriggerStore.LookupEntry(v.Entry);
|
||||||
|
auto entryTarget=sObjectMgr->GetAreaTrigger(v.Entry);
|
||||||
|
auto exit=v.Exit?sAreaTriggerStore.LookupEntry(v.Exit):nullptr;
|
||||||
|
auto exitTarget=v.Exit?sObjectMgr->GetAreaTrigger(v.Exit):nullptr;
|
||||||
|
if (!entry || !entryTarget || (v.Exit && (!exit || !exitTarget)))
|
||||||
|
{r.Detail="saved native instance transition unavailable";return true;}
|
||||||
|
if (v.Stage==Stage::Travel)
|
||||||
|
{
|
||||||
|
if (now>=AdventureInstancePolicy::ReturnAt(v.BlockEnd))
|
||||||
|
{
|
||||||
|
sClientlessPlayerMgr->ClearQueuedActions(player->GetGUID().GetCounter());
|
||||||
|
sClientlessActionExecutorMgr->Clear(player->GetGUID().GetCounter());
|
||||||
|
sClientlessTravelMgr->CancelTravel(player->GetGUID().GetCounter());
|
||||||
|
v.Stage=Stage::Rest;r.State=Phase::OnSite;r.Dirty=false;
|
||||||
|
if (!SaveInstance(player,r)) return true;
|
||||||
|
r.Detail="instance arrival too late; resting without entering";
|
||||||
|
}
|
||||||
|
else if (r.State!=Phase::OnSite) return false;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
v.Stage=Stage::Preparing;v.PrepareUntil=now+10;
|
||||||
|
if (!SaveInstance(player,r)) return true;
|
||||||
|
player->SetFacingTo(player->GetAngle(entry->Pos.X,entry->Pos.Y));
|
||||||
|
player->HandleEmoteCommand(EMOTE_ONESHOT_KNEEL);
|
||||||
|
r.Detail="looking at instance entrance and preparing equipment";
|
||||||
|
TC_LOG_INFO("server.adventurebot","INSTANCE_PREPARE guid=%u entry=%u",player->GetGUID().GetCounter(),v.Entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (v.Stage==Stage::Preparing)
|
||||||
|
{
|
||||||
|
if (!AdventureInstancePolicy::MayEnter(now,v.BlockEnd))
|
||||||
|
{v.Stage=Stage::Rest;if (!SaveInstance(player,r)) return true;}
|
||||||
|
else if (now<v.PrepareUntil) return true;
|
||||||
|
else {v.Stage=Stage::Entering;if (!SaveInstance(player,r)) return true;}
|
||||||
|
}
|
||||||
|
if (v.Stage==Stage::Entering)
|
||||||
|
{
|
||||||
|
if (player->GetMapId()==v.Map && player->GetInstanceId() &&
|
||||||
|
player->GetExactDist(entryTarget->target_X,entryTarget->target_Y,entryTarget->target_Z)<=5)
|
||||||
|
{
|
||||||
|
v.InstanceId=player->GetInstanceId();v.Stage=Stage::Inside;
|
||||||
|
if (!SaveInstance(player,r)) {v.Stage=Stage::Entering;return true;}
|
||||||
|
r.Detail="waiting just inside private instance entrance";
|
||||||
|
TC_LOG_INFO("server.adventurebot","INSTANCE_ENTERED guid=%u map=%u instance=%u returnAt=%llu",
|
||||||
|
player->GetGUID().GetCounter(),v.Map,v.InstanceId,
|
||||||
|
static_cast<unsigned long long>(AdventureInstancePolicy::ReturnAt(v.BlockEnd)));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (v.Abort || !AdventureInstancePolicy::MayEnter(now,v.BlockEnd))
|
||||||
|
{v.Stage=Stage::Rest;if (!SaveInstance(player,r)) return true;}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto map=sMapStore.LookupEntry(v.Map);
|
||||||
|
auto bind=player->GetBoundInstance(v.Map,player->GetDifficulty(map->IsRaid()));
|
||||||
|
bool shared=false;
|
||||||
|
if (bind && bind->save)
|
||||||
|
{
|
||||||
|
uint32 id=bind->save->GetInstanceId();
|
||||||
|
shared=bind->save->GetPlayerCount()>1 || bind->save->GetGroupCount()>0 ||
|
||||||
|
bool(CharacterDatabase.PQuery("SELECT guid FROM character_instance WHERE instance=%u AND guid<>%u LIMIT 1",id,player->GetGUID().GetCounter())) ||
|
||||||
|
bool(CharacterDatabase.PQuery("SELECT guid FROM group_instance WHERE instance=%u LIMIT 1",id));
|
||||||
|
}
|
||||||
|
if (player->GetGroup() || shared || player->GetMapId()!=uint32(entry->ContinentID) || !player->IsInAreaTriggerRadius(entry))
|
||||||
|
{
|
||||||
|
r.Detail="native instance entrance not reached or private solo binding unavailable";
|
||||||
|
v.Stage=Stage::Rest;SaveInstance(player,r);return true;
|
||||||
|
}
|
||||||
|
if (!SaveInstance(player,r)) return true;
|
||||||
|
WorldPacket packet(CMSG_AREATRIGGER,4);packet<<v.Entry;
|
||||||
|
sClientlessLuaEngine->DismountGroundMount(player,"instance-entry");
|
||||||
|
player->GetSession()->HandleAreaTriggerOpcode(packet);
|
||||||
|
if (player->IsBeingTeleported()) player->GetSession()->CompleteClientlessTeleport();
|
||||||
|
if (player->GetMapId()!=v.Map || !player->GetInstanceId() ||
|
||||||
|
player->GetExactDist(entryTarget->target_X,entryTarget->target_Y,entryTarget->target_Z)>5)
|
||||||
|
{
|
||||||
|
TC_LOG_WARN("server.adventurebot","INSTANCE_ENTRY_DENIED guid=%u entry=%u",player->GetGUID().GetCounter(),v.Entry);
|
||||||
|
v.Stage=Stage::Rest;SaveInstance(player,r);r.Detail="native instance entry denied; no visit started";
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (v.Stage==Stage::Inside)
|
||||||
|
{
|
||||||
|
if (!v.Abort && !player->GetGroup() && now<AdventureInstancePolicy::ReturnAt(v.BlockEnd) &&
|
||||||
|
player->GetMapId()==v.Map && player->GetInstanceId()==v.InstanceId)
|
||||||
|
{r.Detail="waiting just inside private instance entrance";return true;}
|
||||||
|
v.Stage=Stage::Returning;
|
||||||
|
if (!SaveInstance(player,r)) {v.Stage=Stage::Inside;return true;}
|
||||||
|
}
|
||||||
|
if (v.Stage==Stage::Returning)
|
||||||
|
{
|
||||||
|
uint32 outside=exitTarget?exitTarget->target_mapId:uint32(entry->ContinentID);
|
||||||
|
Position destination=exitTarget?Position(exitTarget->target_X,exitTarget->target_Y,exitTarget->target_Z,exitTarget->target_Orientation):
|
||||||
|
Position(entry->Pos.X,entry->Pos.Y,entry->Pos.Z,0);
|
||||||
|
bool arrived=player->GetMapId()==outside && player->GetExactDist(destination)<=5;
|
||||||
|
if (!arrived)
|
||||||
|
{
|
||||||
|
if (player->GetMapId()==v.Map && exit)
|
||||||
|
{
|
||||||
|
// The hidden stay abstracts the dungeon traversal. Stage only
|
||||||
|
// at the real exit inside this same instance, then use its
|
||||||
|
// normal trigger and checks. Never claim a walked dungeon route.
|
||||||
|
if (!player->IsInAreaTriggerRadius(exit))
|
||||||
|
{
|
||||||
|
TC_LOG_INFO("server.adventurebot","INSTANCE_ABSTRACT_EXIT_STAGE guid=%u exit=%u instance=%u",player->GetGUID().GetCounter(),v.Exit,player->GetInstanceId());
|
||||||
|
player->TeleportTo(v.Map,exit->Pos.X,exit->Pos.Y,exit->Pos.Z,0);
|
||||||
|
if (player->IsBeingTeleported()) player->GetSession()->CompleteClientlessTeleport();
|
||||||
|
}
|
||||||
|
if (player->IsBeingTeleported() || !player->IsInAreaTriggerRadius(exit)) return true;
|
||||||
|
WorldPacket packet(CMSG_AREATRIGGER,4);packet<<v.Exit;
|
||||||
|
player->GetSession()->HandleAreaTriggerOpcode(packet);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No separate exit, or native login recovery already moved the
|
||||||
|
// player out of an expired instance: restore the mapped exit.
|
||||||
|
TC_LOG_INFO("server.adventurebot","INSTANCE_RETURN_RECOVERY guid=%u outside=%u",player->GetGUID().GetCounter(),outside);
|
||||||
|
player->TeleportTo(outside,destination.GetPositionX(),destination.GetPositionY(),destination.GetPositionZ(),destination.GetOrientation());
|
||||||
|
}
|
||||||
|
if (player->IsBeingTeleported()) player->GetSession()->CompleteClientlessTeleport();
|
||||||
|
arrived=player->IsInWorld() && !player->IsBeingTeleported() && player->GetMapId()==outside && player->GetExactDist(destination)<=5;
|
||||||
|
}
|
||||||
|
if (!arrived) {r.Detail="waiting for confirmed native instance exit";return true;}
|
||||||
|
if (!v.InjuryApplied)
|
||||||
|
{
|
||||||
|
player->SetHealth(AdventureInstancePolicy::ReturnHealth(player->GetHealth(),player->GetMaxHealth(),v.LossPercent));
|
||||||
|
v.InjuryApplied=true;
|
||||||
|
}
|
||||||
|
v.Stage=Stage::Rest;
|
||||||
|
if (!SaveInstance(player,r)) {v.Stage=Stage::Returning;return true;}
|
||||||
|
TC_LOG_INFO("server.adventurebot","INSTANCE_RETURNED guid=%u exit=%u lossPercent=%u health=%u maxHealth=%u deadline=%llu",
|
||||||
|
player->GetGUID().GetCounter(),v.Exit,v.LossPercent,player->GetHealth(),player->GetMaxHealth(),static_cast<unsigned long long>(v.BlockEnd));
|
||||||
|
r.Detail="outside instance: orientation and recovery rest";
|
||||||
|
}
|
||||||
|
if (v.Stage==Stage::Rest)
|
||||||
|
{
|
||||||
|
if (v.Abort || now>=v.BlockEnd)
|
||||||
|
{FinishInstance(player,r);return false;}
|
||||||
|
player->SetStandState(UNIT_STAND_STATE_SIT);
|
||||||
|
r.Detail="outside instance: orientation and recovery rest";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "ObjectGuid.h"
|
#include "ObjectGuid.h"
|
||||||
#include "AdventureBehaviorPolicy.h"
|
#include "AdventureBehaviorPolicy.h"
|
||||||
#include "AdventureSocialActions.h"
|
#include "AdventureSocialActions.h"
|
||||||
|
#include "AdventureInstancePolicy.h"
|
||||||
#include "ClientlessCompanionMgr.h"
|
#include "ClientlessCompanionMgr.h"
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <random>
|
#include <random>
|
||||||
@@ -35,6 +36,9 @@ private:
|
|||||||
bool Active=false, Approaching=false;
|
bool Active=false, Approaching=false;
|
||||||
std::string Point, Posture;
|
std::string Point, Posture;
|
||||||
uint32 Remaining=0, ApproachElapsed=0;
|
uint32 Remaining=0, ApproachElapsed=0;
|
||||||
|
Position Destination;
|
||||||
|
bool HasDestination=false;
|
||||||
|
uint32 SpaceCheckMs=0;
|
||||||
};
|
};
|
||||||
struct Record
|
struct Record
|
||||||
{
|
{
|
||||||
@@ -49,6 +53,7 @@ private:
|
|||||||
std::deque<std::string> ActionHistory, VariantHistory;
|
std::deque<std::string> ActionHistory, VariantHistory;
|
||||||
std::deque<std::string> TargetHistory;
|
std::deque<std::string> TargetHistory;
|
||||||
AdventureSocialScene Social;
|
AdventureSocialScene Social;
|
||||||
|
AdventureInstancePolicy::Visit Instance;
|
||||||
RecoveryScene Recovery;
|
RecoveryScene Recovery;
|
||||||
struct LeisureScene
|
struct LeisureScene
|
||||||
{
|
{
|
||||||
@@ -67,6 +72,11 @@ private:
|
|||||||
uint32 WorkEmote=0;
|
uint32 WorkEmote=0;
|
||||||
bool WorkPending=false;
|
bool WorkPending=false;
|
||||||
};
|
};
|
||||||
|
bool ResolveInstance(Player* player, Record& record);
|
||||||
|
bool SaveInstance(Player* player, Record& record);
|
||||||
|
bool RestoreInstance(Player* player, Record& record);
|
||||||
|
bool UpdateInstance(Player* player, Record& record, time_t now);
|
||||||
|
void FinishInstance(Player* player, Record& record);
|
||||||
void Generate(Player* player, Record& record, uint32 date);
|
void Generate(Player* player, Record& record, uint32 date);
|
||||||
void SetWanted(Player* player, Record& record, std::string const& intent, std::string const& location, time_t deadline);
|
void SetWanted(Player* player, Record& record, std::string const& intent, std::string const& location, time_t deadline);
|
||||||
void Begin(Player* player, Record& record);
|
void Begin(Player* player, Record& record);
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#ifndef VCORE_ADVENTURE_INSTANCE_POLICY_H
|
||||||
|
#define VCORE_ADVENTURE_INSTANCE_POLICY_H
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <ctime>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace AdventureInstancePolicy
|
||||||
|
{
|
||||||
|
enum class Phase : uint8_t { Travel, Preparing, Entering, Inside, Returning, Rest };
|
||||||
|
struct Visit
|
||||||
|
{
|
||||||
|
bool Active=false, InjuryApplied=false, Abort=false;
|
||||||
|
Phase Stage=Phase::Travel;
|
||||||
|
std::string Location;
|
||||||
|
uint32_t Entry=0, Exit=0, Map=0, InstanceId=0, LossPercent=0;
|
||||||
|
time_t BlockEnd=0, PrepareUntil=0;
|
||||||
|
};
|
||||||
|
inline time_t ReturnAt(time_t end) {return end>300?end-300:0;}
|
||||||
|
inline bool MayEnter(time_t now,time_t end) {return now<ReturnAt(end);}
|
||||||
|
inline uint32_t ReturnHealth(uint32_t current,uint32_t maximum,uint32_t loss)
|
||||||
|
{
|
||||||
|
// Never heal an already injured bot, kill it, or subtract injury twice.
|
||||||
|
uint32_t target=uint32_t(uint64_t(maximum)*(100-std::min(loss,50u))/100);
|
||||||
|
return std::max(1u,std::min(current,target));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#ifndef VCORE_ADVENTURE_REST_SPACE_H
|
||||||
|
#define VCORE_ADVENTURE_REST_SPACE_H
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <string>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
namespace AdventureRestSpace
|
||||||
|
{
|
||||||
|
struct Spot { float X, Y, Z, O; };
|
||||||
|
|
||||||
|
inline bool BedName(std::string name)
|
||||||
|
{
|
||||||
|
for (char& c : name) c = std::isalpha(static_cast<unsigned char>(c)) ?
|
||||||
|
char(std::tolower(static_cast<unsigned char>(c))) : ' ';
|
||||||
|
name = " " + name + " ";
|
||||||
|
return name.find(" bed ") != std::string::npos || name.find(" bunk ") != std::string::npos ||
|
||||||
|
name.find(" bedroll ") != std::string::npos || name.find(" cot ") != std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both side edges must drop abruptly, not gradually down a slope. This
|
||||||
|
// discovers possible static furniture surfaces without inventing object IDs.
|
||||||
|
template<class Height>
|
||||||
|
bool Raised(Spot const& p, Height height)
|
||||||
|
{
|
||||||
|
for (float side : {-1.0f, 1.0f})
|
||||||
|
{
|
||||||
|
float previous=p.Z;
|
||||||
|
bool edge=false;
|
||||||
|
for (unsigned step=0;step<=5;++step)
|
||||||
|
{
|
||||||
|
float offset=side*(0.75f+step*0.15f);
|
||||||
|
float z=height(p.X-std::sin(p.O)*offset,p.Y+std::cos(p.O)*offset,p.Z+0.2f);
|
||||||
|
if (!std::isfinite(z) || std::fabs(z-p.Z)>1.0f) return false;
|
||||||
|
if (step && previous-z>=0.35f && p.Z-z>=0.35f) {edge=true;break;}
|
||||||
|
previous=z;
|
||||||
|
}
|
||||||
|
if (!edge) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A conservative full-body rectangle, independent of the standing collision
|
||||||
|
// cylinder. Height samples alone miss chair backs and table undersides.
|
||||||
|
template<class Height, class Ray, class Blocked>
|
||||||
|
bool Clear(Spot const& p, float bodyHeight, Height height, Ray ray, Blocked blocked)
|
||||||
|
{
|
||||||
|
float length = std::max(1.6f, bodyHeight * 0.9f);
|
||||||
|
float width = std::max(0.65f, bodyHeight * 0.3f);
|
||||||
|
float co = std::cos(p.O), si = std::sin(p.O);
|
||||||
|
for (int i = -4; i <= 4; ++i)
|
||||||
|
for (int j = -2; j <= 2; ++j)
|
||||||
|
{
|
||||||
|
float along = length * i / 4, across = width * j / 2;
|
||||||
|
float x = p.X + co * along - si * across;
|
||||||
|
float y = p.Y + si * along + co * across;
|
||||||
|
float z = height(x, y, p.Z + 0.3f);
|
||||||
|
if (!std::isfinite(z) || std::fabs(z - p.Z) > 0.18f || blocked(x, y, p.Z)) return false;
|
||||||
|
for (float level : {0.15f, 0.45f, 0.9f})
|
||||||
|
if (!ray(p.X, p.Y, p.Z + level, x, y, p.Z + level)) return false;
|
||||||
|
if (!ray(x, y, p.Z + 0.12f, x, y, p.Z + 1.1f)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -2302,7 +2302,24 @@ static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*arg
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restart recovery belongs to the optional AdventureBot script layer.
|
||||||
|
class ClientlessInstanceVisitResume : public WorldScript
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ClientlessInstanceVisitResume() : WorldScript("ClientlessInstanceVisitResume") { }
|
||||||
|
void OnStartup() override
|
||||||
|
{
|
||||||
|
if (!CharacterDatabase.Query("SHOW TABLES LIKE 'vcore_adventure_instance_visit'")) return;
|
||||||
|
if (auto rows=CharacterDatabase.Query("SELECT c.account,c.guid FROM vcore_adventure_instance_visit v JOIN characters c ON c.guid=v.character_guid ORDER BY c.guid")) do
|
||||||
|
{
|
||||||
|
auto f=rows->Fetch();
|
||||||
|
sClientlessPlayerMgr->StartPlayer(f[0].GetUInt32(),f[1].GetUInt32(),CreateClientlessAdventureLuaController());
|
||||||
|
} while(rows->NextRow());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
void AddSC_viano_bot_commands()
|
void AddSC_viano_bot_commands()
|
||||||
{
|
{
|
||||||
new VianoBotCommands::VianoBotCommandScript();
|
new VianoBotCommands::VianoBotCommandScript();
|
||||||
|
new ClientlessInstanceVisitResume();
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
"""Execute production instance phases with native-transfer/persistence adapters."""
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess,tempfile
|
||||||
|
root=Path(__file__).resolve().parents[2]
|
||||||
|
s=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text()
|
||||||
|
start=s.index('bool AdventureBehaviorMgr::UpdateInstance(')
|
||||||
|
code=r'''
|
||||||
|
#include "AdventureInstancePolicy.h"
|
||||||
|
#include <cassert>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cmath>
|
||||||
|
using uint32=uint32_t;using uint8=uint8_t;
|
||||||
|
#define TC_LOG_INFO(...) ((void)0)
|
||||||
|
#define TC_LOG_WARN(...) ((void)0)
|
||||||
|
constexpr int UNIT_STAND_STATE_STAND=0,UNIT_STAND_STATE_SIT=1,EMOTE_ONESHOT_KNEEL=16,CMSG_AREATRIGGER=1;
|
||||||
|
struct Position{float x=0,y=0,z=0,o=0;Position()=default;Position(float a,float b,float c,float d=0):x(a),y(b),z(c),o(d){}float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}float GetOrientation()const{return o;}};
|
||||||
|
struct Trigger{uint32 ContinentID=0;struct{float X=0,Y=0,Z=0;}Pos;};
|
||||||
|
struct Target{uint32 target_mapId=36;float target_X=0,target_Y=0,target_Z=0,target_Orientation=0;};
|
||||||
|
struct Triggers{Trigger entrance,exit;Triggers(){exit.ContinentID=36;exit.Pos.X=100;}Trigger* LookupEntry(uint32 id){return id==78?&entrance:id==121?&exit:nullptr;}}sAreaTriggerStore;
|
||||||
|
struct Targets{Target entrance,exit;Targets(){exit.target_mapId=0;exit.target_X=500;}Target* GetAreaTrigger(uint32 id){return id==78?&entrance:id==121?&exit:nullptr;}}targets;auto sObjectMgr=&targets;
|
||||||
|
struct MapEntry{bool IsRaid(){return false;}}mapEntry;
|
||||||
|
struct Maps{auto LookupEntry(uint32){return &mapEntry;}}sMapStore;
|
||||||
|
struct WorldPacket{uint32 trigger=0;WorldPacket(int,int){}WorldPacket& operator<<(uint32 id){trigger=id;return *this;}};
|
||||||
|
struct Save{uint32 GetInstanceId(){return 7;}unsigned GetPlayerCount(){return 1;}unsigned GetGroupCount(){return 0;}}save;
|
||||||
|
struct Bind{Save* save=nullptr;};
|
||||||
|
struct Session;
|
||||||
|
struct Player:Position{
|
||||||
|
uint32 map=0,instance=0,health=100,maximum=100;int stand=0,damages=0,entryCalls=0,exitCalls=0,teleports=0;
|
||||||
|
bool safe=true,group=false,deny=false;Bind bind;Session* session=nullptr;
|
||||||
|
struct Guid{uint32 GetCounter(){return 26;}};Guid GetGUID(){return {};}
|
||||||
|
uint32 GetMapId(){return map;}uint32 GetInstanceId(){return instance;}bool IsInWorld(){return true;}
|
||||||
|
bool IsBeingTeleported(){return false;}bool GetGroup(){return group;}
|
||||||
|
Bind* GetBoundInstance(uint32,int){return bind.save?&bind:nullptr;}int GetDifficulty(bool){return 0;}
|
||||||
|
bool IsInAreaTriggerRadius(Trigger* t){return map==t->ContinentID&&std::fabs(x-t->Pos.X)<3;}
|
||||||
|
float GetAngle(float,float){return 0;}void SetFacingTo(float){}void HandleEmoteCommand(int){}
|
||||||
|
void SetStandState(int v){stand=v;}uint32 GetHealth(){return health;}uint32 GetMaxHealth(){return maximum;}
|
||||||
|
void SetHealth(uint32 h){health=h;++damages;}Session* GetSession(){return session;}
|
||||||
|
float GetExactDist(float a,float b,float c){return std::sqrt((x-a)*(x-a)+(y-b)*(y-b)+(z-c)*(z-c));}
|
||||||
|
float GetExactDist(Position const& p){return GetExactDist(p.x,p.y,p.z);}
|
||||||
|
bool TeleportTo(uint32 m,float a,float b,float c,float o){++teleports;map=m;instance=m==36?7:0;x=a;y=b;z=c;this->o=o;return true;}
|
||||||
|
};
|
||||||
|
struct Session{Player* p;void CompleteClientlessTeleport(){}void HandleAreaTriggerOpcode(WorldPacket const& packet){
|
||||||
|
if(packet.trigger==78){++p->entryCalls;if(!p->deny){p->map=36;p->instance=7;p->x=0;}}
|
||||||
|
else{++p->exitCalls;if(!p->deny){p->map=0;p->instance=0;p->x=500;}}
|
||||||
|
}};
|
||||||
|
bool Unsafe(Player* p){return !p->safe;}
|
||||||
|
struct Database{bool shared=false;template<class... T>bool PQuery(char const*,T...){return shared;}}CharacterDatabase;
|
||||||
|
struct Manager{void ClearQueuedActions(uint32){}}manager;auto sClientlessPlayerMgr=&manager;
|
||||||
|
struct Executor{void Clear(uint32){}}executor;auto sClientlessActionExecutorMgr=&executor;
|
||||||
|
struct Travel{void CancelTravel(uint32){}}travel;auto sClientlessTravelMgr=&travel;
|
||||||
|
struct Lua{void DismountGroundMount(Player*,char const*){}}lua;auto sClientlessLuaEngine=&lua;
|
||||||
|
struct AdventureBehaviorMgr{
|
||||||
|
enum class Phase{Idle,Travel,OnSite};
|
||||||
|
struct Record{AdventureInstancePolicy::Visit Instance;Phase State=Phase::OnSite;bool Dirty=false;std::string Detail;};
|
||||||
|
int saves=0,finishes=0;bool saveOK=true;
|
||||||
|
bool SaveInstance(Player*,Record&){++saves;return saveOK;}
|
||||||
|
void FinishInstance(Player* p,Record&r){++finishes;r.Instance={};p->SetStandState(0);}
|
||||||
|
bool UpdateInstance(Player*,Record&,time_t);
|
||||||
|
};
|
||||||
|
'''+s[start:]+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(){
|
||||||
|
assert(AdventureInstancePolicy::ReturnAt(3600)==3300);
|
||||||
|
assert(AdventureInstancePolicy::MayEnter(3299,3600)&&!AdventureInstancePolicy::MayEnter(3300,3600));
|
||||||
|
assert(AdventureInstancePolicy::ReturnHealth(100,100,0)==100);
|
||||||
|
assert(AdventureInstancePolicy::ReturnHealth(100,100,50)==50);
|
||||||
|
assert(AdventureInstancePolicy::ReturnHealth(40,100,50)==40); // no healing
|
||||||
|
assert(AdventureInstancePolicy::ReturnHealth(UINT32_MAX,UINT32_MAX,50)==2147483647u);
|
||||||
|
AdventureBehaviorMgr mgr;auto r=fresh();Player p;Session session{&p};p.session=&session;
|
||||||
|
// 40 minutes' travel leaves 15 minutes inside and 5 outside.
|
||||||
|
assert(mgr.UpdateInstance(&p,r,2400)&&r.Instance.Stage==Stage::Preparing&&p.entryCalls==0);
|
||||||
|
assert(mgr.UpdateInstance(&p,r,2409)&&p.entryCalls==0);
|
||||||
|
assert(mgr.UpdateInstance(&p,r,2410)&&p.entryCalls==1&&p.map==36);
|
||||||
|
assert(mgr.UpdateInstance(&p,r,2411)&&r.Instance.Stage==Stage::Inside&&r.Instance.InstanceId==7);
|
||||||
|
int transfers=p.teleports;
|
||||||
|
assert(mgr.UpdateInstance(&p,r,3299)&&p.teleports==transfers&&p.exitCalls==0);
|
||||||
|
assert(mgr.UpdateInstance(&p,r,3300)&&r.Instance.Stage==Stage::Rest&&p.map==0&&p.x==500);
|
||||||
|
assert(p.exitCalls==1&&p.health==50&&p.damages==1&&r.Instance.InjuryApplied);
|
||||||
|
assert(mgr.UpdateInstance(&p,r,3400)&&p.damages==1);
|
||||||
|
assert(!mgr.UpdateInstance(&p,r,3600)&&mgr.finishes==1&&!r.Instance.Active);
|
||||||
|
// Persisted inside state on restart keeps the absolute return time.
|
||||||
|
r=fresh();r.Instance.Stage=Stage::Inside;r.Instance.InstanceId=7;p.map=36;p.instance=7;p.x=0;
|
||||||
|
assert(mgr.UpdateInstance(&p,r,3299)&&p.map==36);
|
||||||
|
assert(!mgr.UpdateInstance(&p,r,3700)&&p.map==0&&!r.Instance.Active);
|
||||||
|
// If native login ejected an expired copy, return to its associated exit.
|
||||||
|
r=fresh();r.Instance.Stage=Stage::Inside;r.Instance.InstanceId=7;p.map=0;p.x=999;
|
||||||
|
assert(mgr.UpdateInstance(&p,r,3400)&&p.x==500&&r.Instance.InjuryApplied);
|
||||||
|
// Replaying a saved return cannot compound damage.
|
||||||
|
r=fresh();r.Instance.Stage=Stage::Returning;r.Instance.InjuryApplied=true;p.x=500;
|
||||||
|
int damages=p.damages;assert(mgr.UpdateInstance(&p,r,3400)&&p.damages==damages);
|
||||||
|
// A failed durable entering save must not start a portal transfer.
|
||||||
|
r=fresh();r.Instance.Stage=Stage::Entering;p.map=0;p.x=0;mgr.saveOK=false;
|
||||||
|
int prevented=p.entryCalls;assert(mgr.UpdateInstance(&p,r,100)&&p.entryCalls==prevented);mgr.saveOK=true;
|
||||||
|
// A denied entrance is not a visit; no exit or damage is invented.
|
||||||
|
r=fresh();r.Instance.Stage=Stage::Entering;p.map=0;p.x=0;p.deny=true;damages=p.damages;
|
||||||
|
assert(mgr.UpdateInstance(&p,r,100)&&r.Instance.Stage==Stage::Rest&&p.damages==damages);
|
||||||
|
p.deny=false;
|
||||||
|
// Late arrival never enters, nor applies a fake injury.
|
||||||
|
r=fresh();r.State=AdventureBehaviorMgr::Phase::Travel;int calls=p.entryCalls;
|
||||||
|
assert(mgr.UpdateInstance(&p,r,3350)&&r.Instance.Stage==Stage::Rest&&p.entryCalls==calls&&p.damages==damages);
|
||||||
|
// Shared or grouped copies are not accepted for this solo performance.
|
||||||
|
r=fresh();r.Instance.Stage=Stage::Entering;p.group=true;
|
||||||
|
assert(mgr.UpdateInstance(&p,r,100)&&p.entryCalls==calls);p.group=false;
|
||||||
|
r=fresh();r.Instance.Stage=Stage::Entering;p.bind.save=&save;CharacterDatabase.shared=true;
|
||||||
|
assert(mgr.UpdateInstance(&p,r,100)&&p.entryCalls==calls);CharacterDatabase.shared=false;p.bind.save=nullptr;
|
||||||
|
// A denied exit remains pending and cannot wound a player still inside.
|
||||||
|
r=fresh();r.Instance.Stage=Stage::Returning;p.map=36;p.instance=7;p.x=0;p.deny=true;
|
||||||
|
assert(mgr.UpdateInstance(&p,r,3400)&&r.Instance.Stage==Stage::Returning&&p.damages==damages);
|
||||||
|
p.deny=false;
|
||||||
|
// With no separate native exit, use the original entrance location.
|
||||||
|
r=fresh();r.Instance.Stage=Stage::Returning;r.Instance.Exit=0;p.map=36;p.instance=7;p.x=50;
|
||||||
|
assert(mgr.UpdateInstance(&p,r,3400)&&p.map==0&&p.x==0);
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
src=Path(tmp)/'instance.cpp';exe=Path(tmp)/'instance';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 instance lifecycle: time budget, true entry, denied/shared entry, native rear exit, restart, expired copy, late arrival, HP and return replay passed.')
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""Run the actual optional WorldScript startup recovery with stored visits."""
|
||||||
|
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()
|
||||||
|
s=s[s.index('class ClientlessInstanceVisitResume :'):s.index('\nvoid AddSC_viano_bot_commands()')]
|
||||||
|
code=r'''
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
using uint32=uint32_t;
|
||||||
|
struct WorldScript{WorldScript(char const*){}virtual void OnStartup(){}};
|
||||||
|
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>();
|
||||||
|
std::shared_ptr<Rows> Query(char const* sql){if(sql[0]=='S'&&sql[1]=='H')return schema?std::make_shared<Rows>():nullptr;rows->index=0;return rows->data.empty()?nullptr:rows;}
|
||||||
|
}CharacterDatabase;
|
||||||
|
struct Manager{std::vector<std::pair<uint32,uint32>> started;void StartPlayer(uint32 account,uint32 guid,std::unique_ptr<int> controller){assert(controller&&*controller==42);started.emplace_back(account,guid);}}manager;
|
||||||
|
auto sClientlessPlayerMgr=&manager;
|
||||||
|
std::unique_ptr<int> CreateClientlessAdventureLuaController(){return std::make_unique<int>(42);}
|
||||||
|
'''+s+r'''
|
||||||
|
int main(){ClientlessInstanceVisitResume script;
|
||||||
|
CharacterDatabase.schema=false;script.OnStartup();assert(manager.started.empty());
|
||||||
|
CharacterDatabase.schema=true;script.OnStartup();assert(manager.started.empty());
|
||||||
|
CharacterDatabase.rows->data={{{6},{26}},{{7},{27}}};script.OnStartup();
|
||||||
|
assert((manager.started==std::vector<std::pair<uint32,uint32>>{{6,26},{7,27}}));
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
src=Path(tmp)/'startup.cpp';exe=Path(tmp)/'startup';src.write_text(code)
|
||||||
|
subprocess.run(['g++','-std=c++17',str(src),'-o',str(exe)],check=True)
|
||||||
|
subprocess.run([str(exe)],check=True)
|
||||||
|
print('Instance startup script: no schema/no visits stays idle; saved visitors resume with the Adventure controller and native accounts.')
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""Entrance visits use ordinary location travel, retaining the local MMAP leg."""
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess,tempfile
|
||||||
|
root=Path(__file__).resolve().parents[2]
|
||||||
|
s=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text()
|
||||||
|
a=s.index('void AdventureBehaviorMgr::Begin(');b=s.index('\nvoid AdventureBehaviorMgr::Fallback(',a)
|
||||||
|
code=r'''
|
||||||
|
#include <cassert>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
#define TC_LOG_INFO(...) ((void)0)
|
||||||
|
struct Player{struct Guid{int GetCounter(){return 26;}};Guid GetGUID(){return {};}};
|
||||||
|
struct ClientlessIntent{std::string Name;std::unordered_map<std::string,std::string> Parameters;};
|
||||||
|
struct Location{bool IsArea=false;}location;
|
||||||
|
struct Store{bool exists=true;Location* Find(std::string const&){return exists?&location:nullptr;}}store;auto sAdventureLocationMgr=&store;
|
||||||
|
struct Manager{void ClearIntent(int){}void SetIntent(int,ClientlessIntent){}}manager;auto sClientlessPlayerMgr=&manager;
|
||||||
|
struct AdventureBehaviorMgr{
|
||||||
|
enum class Phase{Idle,Travel};
|
||||||
|
struct Record{bool Dirty=true,Enabled=true,Publishing=false,Queued=false;std::string Intent,Location,WantedIntent="instance",WantedLocation="instance_visit_at_101",Detail;Phase State=Phase::Idle;std::unordered_set<std::string>Tried;};
|
||||||
|
bool mapped=true,allowed=true;int fallback=0;std::string action,parameter,target;bool hard=false;
|
||||||
|
void Begin(Player*,Record&);
|
||||||
|
bool Allowed(Player*,Location const&,std::string const&){return allowed;}
|
||||||
|
void Fallback(Player*,Record&){++fallback;}
|
||||||
|
bool ResolveInstance(Player*,Record&){return mapped;}
|
||||||
|
std::string AreaNode(Player*,Location const&){return "reachable";}
|
||||||
|
void Queue(Player*,Record&,std::string const&a,std::string const&p,std::string const&t,bool h=false){action=a;parameter=p;target=t;hard=h;}
|
||||||
|
};
|
||||||
|
'''+s[a:b]+r'''
|
||||||
|
int main(){Player p;AdventureBehaviorMgr mgr;AdventureBehaviorMgr::Record r;
|
||||||
|
mgr.Begin(&p,r);assert(mgr.action=="travel_to_location"&&mgr.parameter=="id"&&mgr.target=="instance_visit_at_101"&&mgr.hard);
|
||||||
|
assert(r.State==AdventureBehaviorMgr::Phase::Travel&&!r.Dirty);
|
||||||
|
mgr.mapped=false;mgr.Begin(&p,r);assert(mgr.fallback==1);
|
||||||
|
mgr.mapped=true;r.WantedIntent="social";mgr.Begin(&p,r);assert(mgr.action=="travel_to_location"&&!mgr.hard);
|
||||||
|
location.IsArea=true;mgr.Begin(&p,r);assert(mgr.action=="travel_to_node"&&mgr.target=="reachable"&&mgr.hard);
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
src=Path(tmp)/'begin.cpp';exe=Path(tmp)/'begin';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 entrance travel uses the existing location planner, requires mapping and retains normal social/area travel.')
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""Execute the entrance migration against native portal/graph fixtures."""
|
||||||
|
from pathlib import Path
|
||||||
|
import re,sqlite3
|
||||||
|
root=Path(__file__).resolve().parents[2]
|
||||||
|
db=sqlite3.connect(':memory:');db.create_function('CONCAT',-1,lambda *a:''.join(map(str,a)))
|
||||||
|
db.executescript('''
|
||||||
|
CREATE TABLE instance_template(map INTEGER PRIMARY KEY);
|
||||||
|
CREATE TABLE areatrigger_teleport(ID INTEGER PRIMARY KEY,Name TEXT,target_map INTEGER);
|
||||||
|
CREATE TABLE vcore_clientless_travel_node(node_key TEXT PRIMARY KEY,world_point_key TEXT,enabled INTEGER);
|
||||||
|
CREATE TABLE vcore_clientless_world_point(point_key TEXT PRIMARY KEY,category TEXT,map INTEGER,position_x REAL,position_y REAL,position_z REAL,orientation REAL,enabled INTEGER,comment TEXT);
|
||||||
|
CREATE TABLE vcore_adventure_location(point_key TEXT PRIMARY KEY,display_name TEXT,source_type TEXT,source_id INTEGER);
|
||||||
|
CREATE TABLE vcore_adventure_location_intent(category TEXT,intent TEXT,weight REAL,PRIMARY KEY(category,intent));
|
||||||
|
INSERT INTO instance_template VALUES(36);
|
||||||
|
INSERT INTO areatrigger_teleport VALUES(78,'Deadmines Entrance Target',36),(119,'Front exit',0),(121,'Rear exit',0),(527,'City portal',1),(999,'Disabled entrance',36);
|
||||||
|
INSERT INTO vcore_clientless_world_point VALUES
|
||||||
|
('s78','travel_portal',0,1,2,3,0,1,'native'),('s119','travel_portal',36,4,5,6,0,1,'native'),
|
||||||
|
('s121','travel_portal',36,7,8,9,0,1,'native'),('s527','travel_portal',0,10,11,12,0,1,'native'),
|
||||||
|
('s999','travel_portal',0,13,14,15,0,1,'native');
|
||||||
|
INSERT INTO vcore_clientless_travel_node VALUES
|
||||||
|
('portal_at_78_source','s78',1),('portal_at_119_source','s119',1),('portal_at_121_source','s121',1),
|
||||||
|
('portal_at_527_source','s527',1),('portal_at_999_source','s999',0);
|
||||||
|
''')
|
||||||
|
sql=(root/'sql/custom/world/2026_10_27_07_adventure_instances.sql').read_text()
|
||||||
|
sql=re.sub(r" COMMENT '[^']*'",'',sql)
|
||||||
|
sql=re.sub(r'UNIQUE KEY\(entry_trigger\)','UNIQUE(entry_trigger)',sql)
|
||||||
|
sql=re.sub(r'\) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;',');',sql).replace('INSERT IGNORE','INSERT OR IGNORE')
|
||||||
|
db.executescript(sql)
|
||||||
|
assert db.execute('SELECT location_key,entry_trigger,exit_trigger FROM vcore_adventure_instance').fetchall()==[('instance_visit_at_78',78,0)]
|
||||||
|
assert db.execute("SELECT category,map FROM vcore_clientless_world_point WHERE point_key='instance_visit_at_78'").fetchone()==('instance_entrance',0)
|
||||||
|
assert db.execute("SELECT category,map FROM vcore_clientless_world_point WHERE point_key='s78'").fetchone()==('travel_portal',0)
|
||||||
|
db.executescript("UPDATE vcore_adventure_instance SET exit_trigger=121,enabled=0; UPDATE vcore_adventure_location_intent SET weight=2.5; UPDATE vcore_clientless_world_point SET position_x=77 WHERE point_key='instance_visit_at_78';")
|
||||||
|
before=list(db.iterdump());db.executescript(sql);assert list(db.iterdump())==before
|
||||||
|
characters=(root/'sql/custom/characters/2026_10_27_00_adventure_instance_visit.sql').read_text()
|
||||||
|
characters=re.sub(r'\) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;',');',characters)
|
||||||
|
db.executescript(characters)
|
||||||
|
db.execute("INSERT INTO vcore_adventure_instance_visit VALUES(26,'instance_visit_at_78',78,121,36,123,3600,10,3,50,0,0)")
|
||||||
|
db.executescript(characters)
|
||||||
|
assert db.execute('SELECT block_end,instance_id,phase FROM vcore_adventure_instance_visit').fetchone()==(3600,123,3)
|
||||||
|
print('Instance migrations: outside entrances only, native graph preserved, disabled/missing transitions excluded, edits and saved visits preserved.')
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
"""Run production save/restore with a transactional character-DB adapter."""
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess,tempfile
|
||||||
|
root=Path(__file__).resolve().parents[2]
|
||||||
|
s=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text()
|
||||||
|
def function(signature):
|
||||||
|
a=s.index(signature);b=s.index('{',a);n=b+1;depth=1
|
||||||
|
while depth:
|
||||||
|
depth+=(s[n]=='{')-(s[n]=='}');n+=1
|
||||||
|
return s[a:n]
|
||||||
|
code=r'''
|
||||||
|
#include "AdventureInstancePolicy.h"
|
||||||
|
#include <cassert>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <cstdint>
|
||||||
|
using uint32=uint32_t;using uint64=uint64_t;using uint8=uint8_t;
|
||||||
|
#define TC_LOG_INFO(...) ((void)0)
|
||||||
|
#define TC_LOG_ERROR(...) ((void)0)
|
||||||
|
constexpr int UNIT_STAND_STATE_STAND=0;
|
||||||
|
std::string str(char const* s){return s;}template<class T>std::string str(T n){return std::to_string(n);}
|
||||||
|
struct Field{std::string value;std::string GetString(){return value;}uint8 GetUInt8(){return std::stoul(value);}uint32 GetUInt32(){return std::stoul(value);}uint64 GetUInt64(){return std::stoull(value);}};
|
||||||
|
struct Row{std::vector<Field> fields;Field* Fetch(){return fields.data();}};
|
||||||
|
using Result=std::shared_ptr<Row>;
|
||||||
|
struct Operation{std::string sql;std::vector<std::string> args;};
|
||||||
|
struct Transaction{std::vector<Operation> ops;template<class... A>void PAppend(char const* sql,A... args){ops.push_back({sql,{str(args)...}});}};
|
||||||
|
using Tx=std::shared_ptr<Transaction>;
|
||||||
|
struct DB{
|
||||||
|
bool fail=false,table=true;std::vector<std::string> row;std::vector<std::string> character;std::vector<Operation> last;
|
||||||
|
Tx BeginTransaction(){return std::make_shared<Transaction>();}void EscapeString(std::string&){}
|
||||||
|
struct Future{bool success;bool get(){return success;}};
|
||||||
|
struct Commit{Future m_future;};
|
||||||
|
Commit AsyncCommitTransaction(Tx& tx){last=tx->ops;if(fail)return {{false}};for(auto const& op:tx->ops){
|
||||||
|
if(op.sql=="CHARACTER_SAVE")character=op.args;
|
||||||
|
else if(op.sql.find("REPLACE INTO")==0)row=op.args;
|
||||||
|
else if(op.sql.find("DELETE FROM")==0)row.clear();
|
||||||
|
}return {{true}};}
|
||||||
|
Result Query(char const*){return table?std::make_shared<Row>():nullptr;}
|
||||||
|
Result PQuery(char const* sql,uint32){if(row.empty())return nullptr;auto r=std::make_shared<Row>();
|
||||||
|
if(std::string(sql).find("SELECT phase,")==0)for(auto i:{8,6,10})r->fields.push_back({row.at(i)});
|
||||||
|
else for(unsigned i=1;i<row.size();++i)r->fields.push_back({row[i]});
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}CharacterDatabase;
|
||||||
|
struct Player{
|
||||||
|
uint32 health=100,map=36,instance=7;bool teleport=false;int stand=0;
|
||||||
|
struct Guid{uint32 GetCounter(){return 26;}};Guid GetGUID(){return {};}
|
||||||
|
bool IsBeingTeleported(){return teleport;}uint32 GetMapId(){return map;}uint32 GetInstanceId(){return instance;}
|
||||||
|
void SaveToDB(Tx& tx){tx->PAppend("CHARACTER_SAVE",health,map,instance);}
|
||||||
|
void SetStandState(int v){stand=v;}
|
||||||
|
};
|
||||||
|
struct AdventureBehaviorMgr{
|
||||||
|
enum class Phase{Idle,OnSite};
|
||||||
|
struct Record{AdventureInstancePolicy::Visit Instance;bool Schedule=false,Dirty=false;Phase State=Phase::Idle;
|
||||||
|
std::string Intent,WantedIntent,Location,WantedLocation,Detail;time_t Deadline=0;};
|
||||||
|
bool SaveInstance(Player*,Record&);bool RestoreInstance(Player*,Record&);void FinishInstance(Player*,Record&);
|
||||||
|
};
|
||||||
|
'''
|
||||||
|
for signature in ('bool AdventureBehaviorMgr::SaveInstance(', 'bool AdventureBehaviorMgr::RestoreInstance(', 'void AdventureBehaviorMgr::FinishInstance('):code+=function(signature)+'\n'
|
||||||
|
code+=r'''
|
||||||
|
int main(){
|
||||||
|
AdventureBehaviorMgr mgr;AdventureBehaviorMgr::Record r;Player p;
|
||||||
|
assert(!mgr.RestoreInstance(&p,r));
|
||||||
|
auto& v=r.Instance;v.Active=true;v.Location="instance_visit_at_78";v.Entry=78;v.Exit=121;v.Map=36;v.InstanceId=7;
|
||||||
|
v.BlockEnd=3600;v.PrepareUntil=2410;v.Stage=AdventureInstancePolicy::Phase::Inside;v.LossPercent=37;r.Schedule=true;
|
||||||
|
assert(mgr.SaveInstance(&p,r));assert(CharacterDatabase.last.size()==2);
|
||||||
|
assert(CharacterDatabase.character[0]=="100"&&CharacterDatabase.character[2]=="7");
|
||||||
|
// Restart creates an empty behavior record, then restores the actual visit.
|
||||||
|
AdventureBehaviorMgr::Record restored;assert(mgr.RestoreInstance(&p,restored));
|
||||||
|
assert(restored.Intent=="instance"&&restored.Location==v.Location&&restored.Deadline==3600&&restored.Schedule);
|
||||||
|
assert(restored.Instance.Stage==AdventureInstancePolicy::Phase::Inside&&restored.Instance.InstanceId==7);
|
||||||
|
assert(restored.Instance.LossPercent==37&&!restored.Instance.InjuryApplied);
|
||||||
|
// Character health and the once-only marker commit together, never separately.
|
||||||
|
v.Stage=AdventureInstancePolicy::Phase::Rest;v.InjuryApplied=true;p.health=63;p.map=0;p.instance=0;
|
||||||
|
CharacterDatabase.fail=true;assert(!mgr.SaveInstance(&p,r));
|
||||||
|
assert(CharacterDatabase.character[0]=="100");assert(mgr.RestoreInstance(&p,restored)&&!restored.Instance.InjuryApplied);
|
||||||
|
CharacterDatabase.fail=false;assert(mgr.SaveInstance(&p,r));
|
||||||
|
assert(CharacterDatabase.character[0]=="63"&&CharacterDatabase.character[1]=="0");
|
||||||
|
assert(mgr.RestoreInstance(&p,restored)&&restored.Instance.InjuryApplied&&restored.Deadline==3600);
|
||||||
|
// Clear the visit atomically with final character state, and release a queued intent.
|
||||||
|
r.Dirty=true;r.State=AdventureBehaviorMgr::Phase::OnSite;mgr.FinishInstance(&p,r);
|
||||||
|
assert(!r.Instance.Active&&r.State==AdventureBehaviorMgr::Phase::Idle&&CharacterDatabase.row.empty());
|
||||||
|
assert(!mgr.RestoreInstance(&p,restored));
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
src=Path(tmp)/'save.cpp';exe=Path(tmp)/'save';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 instance persistence: phases, original deadline, instance ID, transactional HP marker, failed commit and intent release passed.')
|
||||||
@@ -55,6 +55,7 @@ public:
|
|||||||
time_t Deadline=1000;uint32 Date=20261026,Timer=0,MicroTimer=0,GestureTimer=0,RetryTimer=0;int Hour=5;
|
time_t Deadline=1000;uint32 Date=20261026,Timer=0,MicroTimer=0,GestureTimer=0,RetryTimer=0;int Hour=5;
|
||||||
struct{bool Active=false;}Social;
|
struct{bool Active=false;}Social;
|
||||||
struct{bool Active=false;}Recovery;
|
struct{bool Active=false;}Recovery;
|
||||||
|
struct{bool Active=false;}Instance;
|
||||||
struct{bool Active=false,Flying=false,Progressive=false;std::vector<Position> Route;size_t Step=0;}Leisure;
|
struct{bool Active=false,Flying=false,Progressive=false;std::vector<Position> Route;size_t Step=0;}Leisure;
|
||||||
AdventureBehaviorPolicy::Day Plan;Position Anchor;uint32 AnchorMap=0;int WorkObject=0;uint32 WorkEmote=0;bool WorkPending=false;
|
AdventureBehaviorPolicy::Day Plan;Position Anchor;uint32 AnchorMap=0;int WorkObject=0;uint32 WorkEmote=0;bool WorkPending=false;
|
||||||
};
|
};
|
||||||
@@ -67,6 +68,7 @@ public:
|
|||||||
void Fallback(Player*,Record&){++fallbacks;}
|
void Fallback(Player*,Record&){++fallbacks;}
|
||||||
void ResetMicro(Player*,Record&r){++resets;r.Micro.clear();}
|
void ResetMicro(Player*,Record&r){++resets;r.Micro.clear();}
|
||||||
void Micro(Player*,Record&){++micros;}
|
void Micro(Player*,Record&){++micros;}
|
||||||
|
bool UpdateInstance(Player*,Record&,time_t){return false;}
|
||||||
bool UpdateLeisure(Player*,Record&,uint32){++leisureUpdates;return true;}
|
bool UpdateLeisure(Player*,Record&,uint32){++leisureUpdates;return true;}
|
||||||
bool UpdateSocial(Player*,Record&,uint32){return true;}
|
bool UpdateSocial(Player*,Record&,uint32){return true;}
|
||||||
bool UpdateRecovery(Player*,Record&,uint32){return true;}
|
bool UpdateRecovery(Player*,Record&,uint32){return true;}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ def function(signature):
|
|||||||
return source[a:e]
|
return source[a:e]
|
||||||
code=r'''
|
code=r'''
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include "AdventureInstancePolicy.h"
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
@@ -86,6 +87,7 @@ struct Executor{bool RequestStopAtNode(uint32){return false;}}executor;auto sCli
|
|||||||
class AdventureBehaviorMgr{public:
|
class AdventureBehaviorMgr{public:
|
||||||
enum class Phase{Idle,OnSite,Switching};
|
enum class Phase{Idle,OnSite,Switching};
|
||||||
struct Record{
|
struct Record{
|
||||||
|
AdventureInstancePolicy::Visit Instance;
|
||||||
std::string Intent="leisure",Location="city",WantedIntent,WantedLocation,Detail;Position Anchor;uint32 AnchorMap=0,MicroTimer=240000;std::mt19937 Random{26};
|
std::string Intent="leisure",Location="city",WantedIntent,WantedLocation,Detail;Position Anchor;uint32 AnchorMap=0,MicroTimer=240000;std::mt19937 Random{26};
|
||||||
time_t Deadline=0;bool Dirty=false;std::unordered_set<std::string> Tried;Phase State=Phase::OnSite;
|
time_t Deadline=0;bool Dirty=false;std::unordered_set<std::string> Tried;Phase State=Phase::OnSite;
|
||||||
struct LeisureScene{bool Active=false,Flying=false,Moving=false,StopRequested=false,Returning=false,PlannedWalk=false,Progressive=false;std::string Kind;Position Origin;std::vector<Position> Route,WalkTrace;std::size_t Step=0,OutboundSteps=0;uint32 Elapsed=0,LegElapsed=0,PauseMs=0,CompanionWait=0,CompletedLegs=0;ClientlessCompanionScene Companion;}Leisure;
|
struct LeisureScene{bool Active=false,Flying=false,Moving=false,StopRequested=false,Returning=false,PlannedWalk=false,Progressive=false;std::string Kind;Position Origin;std::vector<Position> Route,WalkTrace;std::size_t Step=0,OutboundSteps=0;uint32 Elapsed=0,LegElapsed=0,PauseMs=0,CompanionWait=0,CompletedLegs=0;ClientlessCompanionScene Companion;}Leisure;
|
||||||
@@ -93,6 +95,7 @@ class AdventureBehaviorMgr{public:
|
|||||||
int moves=0,resets=0;bool lastRiding=false,moveAllowed=true;Position requested;
|
int moves=0,resets=0;bool lastRiding=false,moveAllowed=true;Position requested;
|
||||||
bool LocalMove(Player*,Record&,Position const& target,float,bool riding=false){++moves;lastRiding=riding;requested=target;travel.status.State=ClientlessTravelState::Moving;return moveAllowed;}
|
bool LocalMove(Player*,Record&,Position const& target,float,bool riding=false){++moves;lastRiding=riding;requested=target;travel.status.State=ClientlessTravelState::Moving;return moveAllowed;}
|
||||||
void ResetMicro(Player* p,Record&r){++resets;ResetLeisure(p,r);}
|
void ResetMicro(Player* p,Record&r){++resets;ResetLeisure(p,r);}
|
||||||
|
void FinishInstance(Player*,Record&r){r.Instance={};}
|
||||||
bool workAvailable=false;bool ProfessionWorkAvailable(Player*,Record&){return workAvailable;}
|
bool workAvailable=false;bool ProfessionWorkAvailable(Player*,Record&){return workAvailable;}
|
||||||
bool FishingDirection(Player*,float&)const;
|
bool FishingDirection(Player*,float&)const;
|
||||||
bool LeisureAvailable(Player*,Record const&,AdventureMicroAction const&)const;
|
bool LeisureAvailable(Player*,Record const&,AdventureMicroAction const&)const;
|
||||||
@@ -215,6 +218,6 @@ int main(){
|
|||||||
'''
|
'''
|
||||||
with tempfile.TemporaryDirectory(prefix='vcore-leisure-') as folder:
|
with tempfile.TemporaryDirectory(prefix='vcore-leisure-') as folder:
|
||||||
cpp=Path(folder)/'test.cpp';binary=Path(folder)/'test';cpp.write_text(code)
|
cpp=Path(folder)/'test.cpp';binary=Path(folder)/'test';cpp.write_text(code)
|
||||||
subprocess.run(['c++','-std=c++17',str(cpp),'-o',str(binary)],check=True)
|
subprocess.run(['c++','-std=c++17','-I'+str(root/'src/server/game/World'),str(cpp),'-o',str(binary)],check=True)
|
||||||
subprocess.run([str(binary)],check=True)
|
subprocess.run([str(binary)],check=True)
|
||||||
print('Leisure: substantial curved MMAP routes, mirrored return, interrupted return, rejected corridors, actual arrival, companion follow, riding, flight and fishing passed.')
|
print('Leisure: substantial curved MMAP routes, mirrored return, interrupted return, rejected corridors, actual arrival, companion follow, riding, flight and fishing passed.')
|
||||||
@@ -17,6 +17,7 @@ def function(signature):
|
|||||||
|
|
||||||
code = r'''
|
code = r'''
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -25,13 +26,17 @@ using uint32=uint32_t;
|
|||||||
#define TC_LOG_INFO(...) ((void)0)
|
#define TC_LOG_INFO(...) ((void)0)
|
||||||
constexpr int UNIT_STAND_STATE_SLEEP=3,UNIT_STAND_STATE_SIT=1;
|
constexpr int UNIT_STAND_STATE_SLEEP=3,UNIT_STAND_STATE_SIT=1;
|
||||||
struct Position {
|
struct Position {
|
||||||
float x=0,y=0,z=0;
|
float x=0,y=0,z=0,o=0;
|
||||||
|
Position()=default; Position(float a,float b,float c,float d=0):x(a),y(b),z(c),o(d){}
|
||||||
|
float GetOrientation()const{return o;}
|
||||||
|
float GetExactDist(Position const& p)const{return GetExactDist(p.x,p.y,p.z);}
|
||||||
float GetPositionZ()const{return z;}
|
float GetPositionZ()const{return z;}
|
||||||
float GetExactDist(float a,float b,float c)const{return std::sqrt((x-a)*(x-a)+(y-b)*(y-b)+(z-c)*(z-c));}
|
float GetExactDist(float a,float b,float c)const{return std::sqrt((x-a)*(x-a)+(y-b)*(y-b)+(z-c)*(z-c));}
|
||||||
};
|
};
|
||||||
struct Guid{uint32 GetCounter(){return 26;}};
|
struct Guid{uint32 GetCounter(){return 26;}};
|
||||||
struct Player:Position {
|
struct Player:Position {
|
||||||
uint32 map=0;bool water=false,falling=false,phase=true;int stand=0,poses=0;
|
uint32 map=0;bool water=false,falling=false,phase=true;int stand=0,poses=0;
|
||||||
|
bool space=true;
|
||||||
std::vector<Player*> occupants;
|
std::vector<Player*> occupants;
|
||||||
uint32 GetMapId(){return map;}Guid GetGUID(){return {};}
|
uint32 GetMapId(){return map;}Guid GetGUID(){return {};}
|
||||||
bool IsInWater(){return water;}bool IsFalling(){return falling;}
|
bool IsInWater(){return water;}bool IsFalling(){return falling;}
|
||||||
@@ -40,6 +45,13 @@ struct Player:Position {
|
|||||||
void SetFacingTo(float){}void SetStandState(int value){stand=value;++poses;}
|
void SetFacingTo(float){}void SetStandState(int value){stand=value;++poses;}
|
||||||
int GetStandState(){return stand;}
|
int GetStandState(){return stand;}
|
||||||
};
|
};
|
||||||
|
bool SleepSpace(Player* p,Position const&){return p->space;}
|
||||||
|
std::vector<Position> candidates;
|
||||||
|
std::vector<Position> SleepCandidates(Player*,Position const&,bool){return candidates;}
|
||||||
|
struct ClientlessPathDiagnostics{bool Complete=true,Suspicious=false;float FinalDistance=0;};
|
||||||
|
double Novelty(std::vector<std::string> const&,std::string const&){return 1;}
|
||||||
|
void Remember(std::vector<std::string>& h,std::string const& s){h.push_back(s);}
|
||||||
|
std::size_t Choose(std::vector<double> const& weights,int&){for(std::size_t i=0;i<weights.size();++i)if(weights[i]>0)return i;return weights.size();}
|
||||||
struct AdventureMicroAction{std::string Requirement;};
|
struct AdventureMicroAction{std::string Requirement;};
|
||||||
struct AdventureLocation {
|
struct AdventureLocation {
|
||||||
struct {std::string Id="bed",Category="rest_sleep";uint32 MapId=0;float X=0,Y=0,Z=0,O=0;}Point;
|
struct {std::string Id="bed",Category="rest_sleep";uint32 MapId=0;float X=0,Y=0,Z=0,O=0;}Point;
|
||||||
@@ -54,14 +66,19 @@ auto sAdventureLocationMgr=&locations;
|
|||||||
enum class ClientlessTravelState{Moving,Arrived,PathRejected};
|
enum class ClientlessTravelState{Moving,Arrived,PathRejected};
|
||||||
struct Travel {
|
struct Travel {
|
||||||
struct Status{ClientlessTravelState State=ClientlessTravelState::Moving;}status;
|
struct Status{ClientlessTravelState State=ClientlessTravelState::Moving;}status;
|
||||||
|
float endpointError=0;bool suspicious=false;
|
||||||
|
bool AnalyzePath(Player*,Position const&,ClientlessPathDiagnostics& d){d.FinalDistance=endpointError;d.Suspicious=suspicious;return true;}
|
||||||
bool present=true;Status const* GetStatus(uint32){return present?&status:nullptr;}
|
bool present=true;Status const* GetStatus(uint32){return present?&status:nullptr;}
|
||||||
}travel;
|
}travel;
|
||||||
auto sClientlessTravelMgr=&travel;
|
auto sClientlessTravelMgr=&travel;
|
||||||
class AdventureBehaviorMgr {
|
class AdventureBehaviorMgr {
|
||||||
public:
|
public:
|
||||||
struct RecoveryScene{bool Active=true,Approaching=true;std::string Point="bed",Posture="sleep";uint32 Remaining=3000,ApproachElapsed=0;};
|
struct RecoveryScene{bool Active=true,Approaching=true;std::string Point="bed",Posture="sleep";uint32 Remaining=3000,ApproachElapsed=0;Position Destination;bool HasDestination=false;uint32 SpaceCheckMs=0;};
|
||||||
struct Record{Position Anchor;uint32 AnchorMap=0;RecoveryScene Recovery;std::string Detail;};
|
struct Record{Position Anchor;uint32 AnchorMap=0;RecoveryScene Recovery;std::string Detail;std::vector<std::string> TargetHistory;int Random=0;};
|
||||||
bool Allowed(Player*,AdventureLocation const& p,std::string const&)const{return p.allowed;}
|
bool Allowed(Player*,AdventureLocation const& p,std::string const&)const{return p.allowed;}
|
||||||
|
std::vector<Position> moves;bool moveOK=true;
|
||||||
|
bool LocalMove(Player*,Record&,Position const& p,int){moves.push_back(p);return moveOK;}
|
||||||
|
bool StartRecovery(Player*,Record&,AdventureMicroAction const&,uint32);
|
||||||
bool RecoveryAvailable(Player*,Record const&,AdventureMicroAction const&)const;
|
bool RecoveryAvailable(Player*,Record const&,AdventureMicroAction const&)const;
|
||||||
bool UpdateRecovery(Player*,Record&,uint32);
|
bool UpdateRecovery(Player*,Record&,uint32);
|
||||||
};
|
};
|
||||||
@@ -69,6 +86,7 @@ public:
|
|||||||
# Avoid a class/member name collision in the adapter.
|
# Avoid a class/member name collision in the adapter.
|
||||||
code = code.replace('struct Locations {', 'struct LocationStore {')
|
code = code.replace('struct Locations {', 'struct LocationStore {')
|
||||||
code += function('bool AdventureBehaviorMgr::RecoveryAvailable(')
|
code += function('bool AdventureBehaviorMgr::RecoveryAvailable(')
|
||||||
|
code += function('bool AdventureBehaviorMgr::StartRecovery(')
|
||||||
code += function('bool AdventureBehaviorMgr::UpdateRecovery(')
|
code += function('bool AdventureBehaviorMgr::UpdateRecovery(')
|
||||||
code += r'''
|
code += r'''
|
||||||
int main(){
|
int main(){
|
||||||
@@ -78,8 +96,20 @@ int main(){
|
|||||||
locations.points.push_back({});
|
locations.points.push_back({});
|
||||||
assert(mgr.RecoveryAvailable(&p,r,sleep));
|
assert(mgr.RecoveryAvailable(&p,r,sleep));
|
||||||
assert(!mgr.RecoveryAvailable(&p,r,ground)&&!mgr.RecoveryAvailable(&p,r,unknown));
|
assert(!mgr.RecoveryAvailable(&p,r,ground)&&!mgr.RecoveryAvailable(&p,r,unknown));
|
||||||
|
// Reject endpoints beneath an elevated surface before starting movement.
|
||||||
|
candidates={Position(3,0,0)};travel.endpointError=0.7f;
|
||||||
|
assert(!mgr.StartRecovery(&p,r,sleep,7000)&&mgr.moves.empty());
|
||||||
|
travel.endpointError=0;assert(mgr.StartRecovery(&p,r,sleep,7000));
|
||||||
|
assert(r.Recovery.HasDestination && r.Recovery.Destination.x==3 && mgr.moves.size()==1);
|
||||||
|
assert(r.Recovery.Remaining==7000);
|
||||||
|
mgr.moves.clear();r=AdventureBehaviorMgr::Record{};
|
||||||
|
// Even if a tavern is first in the catalog, explicit sleep anchors have priority.
|
||||||
|
locations.points.insert(locations.points.begin(),AdventureLocation{});locations.points[0].Point.Id="inn";
|
||||||
|
locations.points[0].Point.Category="tavern";
|
||||||
|
assert(mgr.StartRecovery(&p,r,sleep,7000)&&r.Recovery.Point=="bed");
|
||||||
|
locations.points.erase(locations.points.begin());r=AdventureBehaviorMgr::Record{};
|
||||||
auto& point=locations.points[0];
|
auto& point=locations.points[0];
|
||||||
point.Point.Category="tavern";assert(!mgr.RecoveryAvailable(&p,r,sleep)); // no arbitrary tavern floor
|
point.Point.Category="tavern";assert(mgr.RecoveryAvailable(&p,r,sleep)); // bounded indoor floor search
|
||||||
point.Point.Category="rest_sleep";point.Point.MapId=1;assert(!mgr.RecoveryAvailable(&p,r,sleep));
|
point.Point.Category="rest_sleep";point.Point.MapId=1;assert(!mgr.RecoveryAvailable(&p,r,sleep));
|
||||||
point.Point.MapId=0;point.allowed=false;assert(!mgr.RecoveryAvailable(&p,r,sleep));
|
point.Point.MapId=0;point.allowed=false;assert(!mgr.RecoveryAvailable(&p,r,sleep));
|
||||||
point.allowed=true;point.Point.X=31;assert(!mgr.RecoveryAvailable(&p,r,sleep));
|
point.allowed=true;point.Point.X=31;assert(!mgr.RecoveryAvailable(&p,r,sleep));
|
||||||
@@ -94,6 +124,11 @@ int main(){
|
|||||||
assert(mgr.UpdateRecovery(&p,r,1000)&&p.stand==UNIT_STAND_STATE_SLEEP&&!r.Recovery.Approaching);
|
assert(mgr.UpdateRecovery(&p,r,1000)&&p.stand==UNIT_STAND_STATE_SLEEP&&!r.Recovery.Approaching);
|
||||||
assert(mgr.UpdateRecovery(&p,r,1000));assert(mgr.UpdateRecovery(&p,r,1000));
|
assert(mgr.UpdateRecovery(&p,r,1000));assert(mgr.UpdateRecovery(&p,r,1000));
|
||||||
assert(!mgr.UpdateRecovery(&p,r,1000)); // full duration starts after arrival
|
assert(!mgr.UpdateRecovery(&p,r,1000)); // full duration starts after arrival
|
||||||
|
r.Recovery={};p.space=false;assert(!mgr.UpdateRecovery(&p,r,1000)&&r.Recovery.Approaching);p.space=true;
|
||||||
|
r.Recovery.Destination=Position(4,0,0);r.Recovery.HasDestination=true;
|
||||||
|
assert(!mgr.UpdateRecovery(&p,r,1000));p.x=4;
|
||||||
|
assert(mgr.UpdateRecovery(&p,r,1000)); // chosen offset, not the old obstructed marker
|
||||||
|
p.space=false;r.Recovery.SpaceCheckMs=0;assert(!mgr.UpdateRecovery(&p,r,1000));p.space=true;p.x=0;
|
||||||
r.Recovery={};travel.status.State=ClientlessTravelState::PathRejected;
|
r.Recovery={};travel.status.State=ClientlessTravelState::PathRejected;
|
||||||
assert(!mgr.UpdateRecovery(&p,r,1000));
|
assert(!mgr.UpdateRecovery(&p,r,1000));
|
||||||
travel.status.State=ClientlessTravelState::Moving;r.Recovery.ApproachElapsed=60000;
|
travel.status.State=ClientlessTravelState::Moving;r.Recovery.ApproachElapsed=60000;
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""Run the production lie-down footprint against controlled collision geometry."""
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
root = Path(__file__).resolve().parents[2]
|
||||||
|
code = r'''
|
||||||
|
#include "AdventureRestSpace.h"
|
||||||
|
#include <cassert>
|
||||||
|
using namespace AdventureRestSpace;
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
Spot p{0,0,10,0};
|
||||||
|
auto flat=[](float,float,float){return 10.0f;};
|
||||||
|
auto open=[](float,float,float,float,float,float){return true;};
|
||||||
|
auto empty=[](float,float,float){return false;};
|
||||||
|
assert(Clear(p,2,flat,open,empty));
|
||||||
|
auto platform=[](float,float y,float){return std::fabs(y)<1?10.0f:9.3f;};
|
||||||
|
auto slope=[](float,float y,float){return 10.0f-std::fabs(y)*0.5f;};
|
||||||
|
assert(Raised(p,platform));assert(!Raised(p,slope));assert(!Raised(p,flat));
|
||||||
|
// A chair can be non-colliding: native chair occupancy must still veto it.
|
||||||
|
auto chair=[](float x,float y,float){return std::fabs(x-1.0f)<0.5f && std::fabs(y)<0.5f;};
|
||||||
|
assert(!Clear(p,2,flat,open,chair));
|
||||||
|
// Standing cylinder is clear; a wall at the sleeping body's foot is not.
|
||||||
|
auto wall=[](float,float,float,float x,float,float){return x<1.5f;};
|
||||||
|
assert(!Clear(p,2,flat,wall,empty));
|
||||||
|
auto underTable=[](float,float,float z,float,float,float c){return !(z<10.7f && c>10.7f);};
|
||||||
|
assert(!Clear(p,2,flat,underTable,empty));
|
||||||
|
auto ledge=[](float x,float,float){return x>1?8.0f:10.0f;};
|
||||||
|
assert(!Clear(p,2,ledge,open,empty));
|
||||||
|
auto stairs=[](float x,float,float){return x>0.5f?10.3f:10.0f;};
|
||||||
|
assert(!Clear(p,2,stairs,open,empty));
|
||||||
|
auto missing=[](float,float,float){return -100000.0f;};
|
||||||
|
assert(!Clear(p,2,missing,open,empty));
|
||||||
|
auto nan=[](float,float,float){return NAN;};
|
||||||
|
assert(!Clear(p,2,nan,open,empty));
|
||||||
|
// Rotate the whole body into a corridor; the unrotated pose hits its wall.
|
||||||
|
auto corridor=[](float,float,float,float x,float,float){return std::fabs(x)<0.8f;};
|
||||||
|
assert(!Clear(p,2,flat,corridor,empty));p.O=1.5707963f;
|
||||||
|
assert(Clear(p,2,flat,corridor,empty));
|
||||||
|
// A larger model requires proportionally more space.
|
||||||
|
assert(!Clear(p,4,flat,corridor,empty));
|
||||||
|
assert(BedName("Fancy Bed") && BedName("Hospital Bed") && BedName("Bedroll"));
|
||||||
|
assert(!BedName("Flowerbed") && !BedName("Wooden Chair") && !BedName("Bedside Table"));
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
src=Path(tmp)/'space.cpp';src.write_text(code)
|
||||||
|
exe=Path(tmp)/'space'
|
||||||
|
subprocess.run(['g++','-std=c++17','-Wall','-Wextra','-Werror','-I'+str(root/'src/server/game/World'),str(src),'-o',str(exe)],check=True)
|
||||||
|
subprocess.run([str(exe)],check=True)
|
||||||
|
print('Sleep footprint: furniture, body extent, overhead collision, ledges, stairs, missing heights, rotation and scale passed.')
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
"""Exercise native sleep-space and candidate adapters with map/object fixtures."""
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess,tempfile
|
||||||
|
root=Path(__file__).resolve().parents[2]
|
||||||
|
source=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text()
|
||||||
|
def function(signature):
|
||||||
|
start=source.index(signature);body=source.index('{',start);end=body+1;depth=1
|
||||||
|
while depth:
|
||||||
|
depth+=(source[end]=='{')-(source[end]=='}');end+=1
|
||||||
|
return source[start:end]
|
||||||
|
code=r'''
|
||||||
|
#include "AdventureRestSpace.h"
|
||||||
|
#include <vector>
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstdint>
|
||||||
|
using uint32=uint32_t;
|
||||||
|
#define TC_LOG_INFO(...) ((void)0)
|
||||||
|
constexpr float INVALID_HEIGHT=-100000;
|
||||||
|
constexpr int GAMEOBJECT_TYPE_CHAIR=7, LINEOFSIGHT_ALL_CHECKS=0;
|
||||||
|
namespace VMAP{enum class ModelIgnoreFlags{Nothing};}
|
||||||
|
struct Position{
|
||||||
|
float x=0,y=0,z=10,o=0;
|
||||||
|
Position()=default;Position(float a,float b,float c,float d=0):x(a),y(b),z(c),o(d){}
|
||||||
|
float GetPositionX()const{return x;}float GetPositionY()const{return y;}
|
||||||
|
float GetPositionZ()const{return z;}float GetOrientation()const{return o;}
|
||||||
|
float GetExactDist(float a,float b,float c)const{return std::sqrt((x-a)*(x-a)+(y-b)*(y-b)+(z-c)*(z-c));}
|
||||||
|
float GetExactDist2d(float a,float b)const{return std::hypot(x-a,y-b);}
|
||||||
|
};
|
||||||
|
struct GameObject:Position{
|
||||||
|
std::string name="Wooden Chair";bool phase=true,spawned=true;int type=7;
|
||||||
|
bool isSpawned(){return spawned;}auto GetName(){return name;}int GetGoType(){return type;}
|
||||||
|
float GetObjectScale(){return 1;}
|
||||||
|
};
|
||||||
|
struct Creature:Position{bool phase=true;float GetBoundaryRadius(){return 0.5f;}};
|
||||||
|
struct PositionFullTerrainStatus{bool outdoors=false;};
|
||||||
|
struct Terrain{
|
||||||
|
bool outdoors=false;
|
||||||
|
void GetFullTerrainStatusForPosition(int,int,float,float,float,PositionFullTerrainStatus& t){t.outdoors=outdoors;}
|
||||||
|
};
|
||||||
|
struct Map{
|
||||||
|
Terrain terrain;bool water=false,bed=false;float level=10;
|
||||||
|
float GetHeight(int,float x,float y,float probe,bool,float){
|
||||||
|
float h=bed && std::fabs(x-4)<2.1f && std::fabs(y)<0.95f?10.7f:level;
|
||||||
|
return probe>=h?h:level;
|
||||||
|
}
|
||||||
|
float GetWaterLevel(int,float,float){return water?12:-100000;}
|
||||||
|
bool isInLineOfSight(int,float,float,float,float,float,float,int,VMAP::ModelIgnoreFlags){return true;}
|
||||||
|
Terrain* GetTerrain(){return &terrain;}
|
||||||
|
};
|
||||||
|
struct Player:Position{
|
||||||
|
bool phase=true;Map map;std::vector<GameObject*> objects;std::vector<Player*> people;std::vector<Creature*> creatures;
|
||||||
|
struct Guid{uint32 GetCounter(){return 26;}};Guid GetGUID(){return {};}
|
||||||
|
Map* GetMap(){return ↦}int GetPhaseShift(){return 0;}int GetMapId(){return 0;}
|
||||||
|
float GetCollisionHeight(){return 2;}
|
||||||
|
template<class T>bool IsInPhase(T* p){return p->phase;}
|
||||||
|
void GetGameObjectListWithEntryInGrid(std::vector<GameObject*>& v,int,float){v=objects;}
|
||||||
|
void GetPlayerListInGrid(std::vector<Player*>& v,float){v=people;}
|
||||||
|
void GetCreatureListWithEntryInGrid(std::vector<Creature*>& v,int,float){v=creatures;}
|
||||||
|
};
|
||||||
|
'''
|
||||||
|
code+=function(' bool SleepSpace(')+'\n'+function(' std::vector<Position> SleepCandidates(')
|
||||||
|
code+=r'''
|
||||||
|
int main(){
|
||||||
|
Player p;Position anchor(0,0,10);GameObject chair;p.objects={&chair};
|
||||||
|
assert(!SleepSpace(&p,anchor));
|
||||||
|
chair.phase=false;assert(SleepSpace(&p,anchor));chair.phase=true;
|
||||||
|
chair.spawned=false;assert(SleepSpace(&p,anchor));chair.spawned=true;
|
||||||
|
chair.z=3;assert(SleepSpace(&p,anchor));chair.z=10;
|
||||||
|
auto candidates=SleepCandidates(&p,anchor,true);
|
||||||
|
assert(!candidates.empty()); // shift off the chair, rather than merely reject the marker
|
||||||
|
for(auto const& c:candidates){assert(c.GetExactDist2d(0,0)>1);assert(std::fabs(c.z-10)<0.01f);}
|
||||||
|
p.map.terrain.outdoors=true;assert(SleepCandidates(&p,anchor,true).empty());p.map.terrain.outdoors=false;
|
||||||
|
p.map.water=true;assert(SleepCandidates(&p,anchor,true).empty());p.map.water=false;
|
||||||
|
p.map.level=3;assert(SleepCandidates(&p,anchor,true).empty());p.map.level=10; // not the storey below
|
||||||
|
p.objects.clear();Player visitor;visitor.x=1;p.people={&visitor};assert(!SleepSpace(&p,anchor));p.people.clear();
|
||||||
|
Creature npc;npc.x=1;p.creatures={&npc};assert(!SleepSpace(&p,anchor));p.creatures.clear();
|
||||||
|
GameObject bed;bed.name="Hospital Bed";bed.type=5;bed.x=4;p.objects={&bed};p.map.bed=true;
|
||||||
|
candidates=SleepCandidates(&p,anchor,true);assert(!candidates.empty());
|
||||||
|
assert(candidates.front().x==4 && std::fabs(candidates.front().z-10.7f)<0.01f); // real bed before floor
|
||||||
|
p.objects.clear();candidates=SleepCandidates(&p,anchor,true);assert(!candidates.empty());
|
||||||
|
assert(std::fabs(candidates.front().z-10.7f)<0.01f); // static raised surface before floor
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
src=Path(tmp)/'targets.cpp';src.write_text(code);exe=Path(tmp)/'targets'
|
||||||
|
subprocess.run(['g++','-std=c++17','-Wall','-Wextra','-Werror','-I'+str(root/'src/server/game/World'),str(src),'-o',str(exe)],check=True)
|
||||||
|
subprocess.run([str(exe)],check=True)
|
||||||
|
print('Native sleep targets: chair exclusion, phase, despawn, floor search, indoors, water, storey, occupancy, bed/surface preference passed.')
|
||||||
Reference in new issue
Block a user