Add persistent instance visits and validate sleeping spaces
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s

This commit is contained in:
Nico committed 2026-10-27 20:10:13 +01:00
1 parent 440c53acd7
commit e18a416bf4
22 files changed
+1621 -31

No files matched your search

+199
View File
@@ -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
+212
View File
@@ -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.
+3 -3
View File
@@ -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
+6 -2
View File
@@ -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
+96 -5
View File
@@ -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.
+425 -16
View File
@@ -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();
} }
+122
View File
@@ -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;}
+4 -1
View File
@@ -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.')
+39 -4
View File
@@ -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 &map;}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.')