Support interactive object portals and document travel route recording
Bind directed portal_use edges to native GameObject spawns and spell destinations. Execute normal clicks and verify completed clientless teleports, with regression coverage. Document live portal tests, mage tower calibration, Badlands portals and transport recording workflows.
This commit is contained in:
1 parent
1164a72ea3
commit
4474714c00
18 files changed
+1374
-5
No files matched your search
@@ -0,0 +1,59 @@
|
||||
# Ödland: Anleger ↔ Klippe, Trigger 6321/6322
|
||||
|
||||
Stand 26.10.2026. Laut Nutzer soll der Anleger ein erreichbares Bot-Reiseziel
|
||||
sein; diese Verbindung ist dafür wichtig. Beide gleichkartigen Übergänge sind
|
||||
im lokalen Testgraph aktiviert. Native Trigger und Ziele bleiben unverändert.
|
||||
|
||||
## Native Daten und Import
|
||||
|
||||
Beide Trigger liegen auf Karte 0, haben Radius 1.78776 und keine DBC-
|
||||
Phasenflags. Die lokale DB enthält keine eigenen AreaTrigger-Skripte und
|
||||
keine direkten Questzuordnungen über `areatrigger_involvedrelation` für diese
|
||||
IDs; in den untersuchten Core-/Skriptdateien gibt es keine Sonderbehandlung
|
||||
der IDs. Das ist keine pauschale Aussage über jede denkbare Zugangsbedingung.
|
||||
|
||||
Reproduktion über die Testserver-Konsole:
|
||||
|
||||
```text
|
||||
clientless portal import 6321
|
||||
clientless portal import 6322
|
||||
```
|
||||
|
||||
Der Import legt vier Nodes und zwei gerichtete Kanten an und lädt den Graph
|
||||
neu. Keine Höhenkalibrierung oder zusätzliche Migration erforderlich.
|
||||
|
||||
| Node | X | Y | Z | Funktion |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| portal_at_6321_source | -6518.680 | -4604.420 | 29.2335 | Anleger, hinauf |
|
||||
| portal_at_6321_target | -6547.880 | -4443.240 | 332.079 | Ankunft an der Klippe |
|
||||
| portal_at_6322_source | -6551.280 | -4446.460 | 332.829 | Klippe, hinunter |
|
||||
| portal_at_6322_target | -6519.820 | -4608.520 | 28.4842 | Ankunft am Anleger |
|
||||
|
||||
Portal-Kanten 1750 (6321_source → 6321_target) und 1751
|
||||
(6322_source → 6322_target), Kosten jeweils 1, unidirektional.
|
||||
|
||||
## Live-Test
|
||||
|
||||
Arthoran wurde nach Sicherung seiner Ausgangsdaten ausgeloggt am unteren
|
||||
Source-Node positioniert. Danach reguläre Reiseaufträge ohne weitere
|
||||
Positionsmanipulation während der Fahrt:
|
||||
|
||||
- 12:05:54: Trigger 6321, Anleger → Klippe, `EDGE_COMPLETE`,
|
||||
`arrivalError=0.00`, `recovery=0`.
|
||||
- Oben vollständig validierte MMAP-Brücke vom Ankunftspunkt zum Rücktrigger:
|
||||
4.75 Yards Pfadlänge, tatsächliche Bewegung, ohne Recovery.
|
||||
- 12:05:55: Trigger 6322, Klippe → Anleger, `EDGE_COMPLETE`,
|
||||
`arrivalError=0.00`, `recovery=0`.
|
||||
|
||||
Der zweite erfolgreiche Umlauf (12:06:18–12:06:19) ab der natürlichen unteren
|
||||
Ankunft bestätigte außerdem den Anlauf von `portal_at_6322_target` zu
|
||||
`portal_at_6321_source`. Alle vier Portalabschlüsse hatten Ankunftsfehler 0
|
||||
und Recovery 0. Arthoran wurde anschließend ausgeloggt; Ausgangsposition, Zone
|
||||
und Lua-Skript wurden wiederhergestellt.
|
||||
Die Portalreisen verwenden den normalen AreaTrigger-Handler mit
|
||||
Clientless-Near-ACK und Zielprüfung.
|
||||
|
||||
Es wurden keine Groundanschlüsse an das übrige Ödlandnetz aufgenommen oder
|
||||
getestet. Keine Coreänderung, kein Build und kein Serverneustart erforderlich.
|
||||
Finale Ingame-Abnahme durch den Nutzer steht aus.
|
||||
Sicherungen und Logs: `/tmp/vcore-badlands-test.0lkimsmi`.
|
||||
@@ -0,0 +1,87 @@
|
||||
# Interaktive Portale: Live-Test Dunkles Portal → Sturmwind
|
||||
|
||||
Implementierung und Bedienung: [docs/interactive-portals.md](../../docs/interactive-portals.md).
|
||||
Stand 26.10.2026. Keine künstlichen Teleports, keine Änderungen an nativen
|
||||
GameObject- oder Zauberdefinitionen.
|
||||
|
||||
## Erfolgreicher Teststandort
|
||||
|
||||
| Merkmal | Wert |
|
||||
| --- | --- |
|
||||
| GameObject-Spawn | 170 |
|
||||
| Entry / Name | 195139 / Portal to Stormwind |
|
||||
| Standort | Scherbenweltseite beim Dunklen Portal |
|
||||
| Quelle | Karte 530, (-337.492, 962.619, 54.2885) |
|
||||
| Nativer Zauber | 17334, Teleporteffekt 0 mit TARGET_DEST_DB |
|
||||
| Ziel | Karte 0, (-8998.140, 861.254, 29.6206), Orientierung 2.25 |
|
||||
| Nodes | portal_go_170_source → portal_go_170_target |
|
||||
| Kante | 1755, portal_use, Kosten 1, unidirektional |
|
||||
|
||||
Gezielt importiert mit `clientless portal object import 170`. Arthoran
|
||||
(GUID 26) begann ausgeloggt acht Yards vor der nativen Objektposition; nach
|
||||
Login erhielt er einen normalen Reiseauftrag zum Zielnode. Während der Reise
|
||||
keine Positionsmanipulation:
|
||||
|
||||
- 13:28:36: MMAP-Anlauf, 8 Yards, vollständiger Pfad.
|
||||
- 13:28:38: Source erreicht; normaler CMSG_GAMEOBJ_USE-Handler für Spawn 170.
|
||||
- 13:28:39: `EDGE_COMPLETE`, `edgeType='portal_use'`, Karte 0,
|
||||
`arrivalError=0.00`, `recovery=0`, anschließend regulärer Reiseabschluss.
|
||||
|
||||
Das bestätigt serverseitig Anlauf, Objektbenutzung, nativen Portalzauber,
|
||||
Far-Teleport mit Clientless-ACK und Ankunft. Visuelle Ingame-Abnahme durch den
|
||||
Nutzer steht aus. Keine Rückrichtung erfunden, keine Groundanschlüsse
|
||||
aufgenommen und keine NPC-/Gossip-Portale als unterstützt behauptet.
|
||||
|
||||
## Untersuchte Shattrath-Verbindung
|
||||
|
||||
Erster Kandidat: Spawn 83, Entry 183325, `Shattrath Portal to Stormwind`,
|
||||
gleicher Zauber 17334, Quelle (-1894.190, 5387.940, -12.4283) auf Karte 530.
|
||||
Das native Zauberziel ist identisch zum erfolgreichen zweiten Standort.
|
||||
|
||||
Der Klick erreichte die normale native Zauberausführung, aber die Effekte
|
||||
wurden in `Spell::AddUnitTarget` nach `CheckEffectTarget` verworfen. Die
|
||||
zusätzliche Diagnose zeigte `resolved=1` für Arthoran und `no effects after
|
||||
target/LOS checks` für beide Effekte. Bei diesen Effekten ist dort die native
|
||||
Sichtlinienprüfung maßgeblich. Das trat an der Objektmitte und drei Yards davor
|
||||
auf. Der genaue geometrische Grund wurde nicht geändert oder abschließend
|
||||
vermessen. Die temporäre Zauberdiagnose wurde nach der Untersuchung entfernt.
|
||||
|
||||
Der Versuch scheiterte sowohl bei Level 20 als auch bei Level 60; niedriges
|
||||
Level erklärt diesen Befund daher nicht. Die Erhöhung auf Level 60 erfolgte
|
||||
mit dem konsolenfähigen nativen Befehl `character level Arthoran 60` im Rahmen
|
||||
der Nutzerfreigabe. `.levelup` ist in dieser Core-Kommandotabelle ingame-only.
|
||||
|
||||
Die erfolglose experimentelle Kante 1752 sowie ihre beiden Graphnodes und
|
||||
Weltpunkte sind jetzt deaktiviert. Ihre Definition bleibt zur Diagnose
|
||||
erhalten. Der veränderte Graph-Anlaufpunkt liegt bei X=-1897.190; native
|
||||
GO-Position und Zauberdaten sind unverändert. Beim Wiederholungsimport bleiben
|
||||
Anlaufposition und Deaktivierung erhalten. Der native Standort muss vor einer
|
||||
späteren Freigabe gesondert geprüft werden.
|
||||
|
||||
## Import- und Fehlertests
|
||||
|
||||
- Wiederholter Import: keine zusätzlichen Nodes/Kanten; bestehende Koordinaten
|
||||
und enabled-Flags werden nicht überschrieben.
|
||||
- Fehlgeschlagene native Ausführung: nach 10 Sekunden korrekt `Failed`, kein
|
||||
behaupteter Reiseerfolg und kein Ersatzteleport.
|
||||
- Shattrath → Quel’Danas, Spawn 47196 / Zauber 44876: Import sauber vor dem
|
||||
Anlegen von Nodes abgewiesen; Zauberkette statt direktem DB-Teleport.
|
||||
- Automatisierte Tests führen Produktionsmethoden mit Core-Doubles aus:
|
||||
direkte Zielauflösung, Objektprüfung, Klick, Near/Far/globaler ACK,
|
||||
Ablehnung, Timeout, falsches Ziel und ACK-Sequenzzähler.
|
||||
- Bestehende Portal-, Transport-, Grid-, Lua-Reload- und Taxi-Importer-Tests
|
||||
bestanden ebenfalls.
|
||||
|
||||
Build und Installation bestanden. Die tatsächlichen Portalversuche wurden
|
||||
mit installiertem Code ausgeführt. Anschließend wurde der endgültige Stand
|
||||
ohne zusätzliche temporäre Zauberdiagnose gebaut und installiert. Finale
|
||||
Build-ID: `613c4236bce2212d49788e771b66f490b617d8ca`.
|
||||
|
||||
Der letzte Start wurde anhand seiner vollständigen frischen Konsole geprüft:
|
||||
Ready-Meldung vorhanden, keine kritischen Startfehler im geprüften Output,
|
||||
Prozess 53332 läuft und Port 8085 lauscht. Keine weiteren Botaufträge beim
|
||||
finalen Start. Der ursprüngliche Loglevel gilt wieder.
|
||||
|
||||
Arthoran wurde nach dem Test ausgeloggt. Ausgangsposition, Zone und Lua sind
|
||||
wiederhergestellt; das freigegebene Level 60 bleibt erhalten. Vorherdaten,
|
||||
Diagnose und Testlogs liegen in `/tmp/vcore-interactive-portal.8vfrq7g2`.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Portal-Auswahl für AdventureBots
|
||||
|
||||
Nutzerentscheidungen vom 26.10.2026, zusätzlich zur historischen
|
||||
Bestandsaufnahme in `same-map-portals.md/json`:
|
||||
|
||||
| Übergänge | Entscheidung |
|
||||
| --- | --- |
|
||||
| 527/542, Rut’theran ↔ Darnassus | Aktiv, live getestet. |
|
||||
| 702/704, Magierturm Sturmwind | Aktiv, live getestet; Groundanschluss wird vom Nutzer später aufgenommen. |
|
||||
| 6321/6322, Ödland Anleger ↔ Klippe | Als Bot-Reiseziel gewünscht; gezielt importiert und live getestet. |
|
||||
| 4233/4267, Duskwither im Immersangwald | Außerhalb des Graphen lassen. Laut Nutzer questbezogen und kein gewünschtes Reiseziel. |
|
||||
| 5273, Dalaran-Brunnen | Vorerst außerhalb lassen. Laut Nutzer voraussichtlich Sprung in den Brunnen; alternative Zugänge zur Kanalisation vorhanden. |
|
||||
| Arena-interne Umsetzungen | Für den aktuellen Reisegraph irrelevant; nicht importieren. |
|
||||
| Eindeutige Quest-/Phasenübergänge | Derzeit nicht allgemein freigeben. |
|
||||
| Dungeon-/Raid-interne Übergänge | Zurückstellen; für spätere Gruppenbegleitung gegebenenfalls erneut bewerten. |
|
||||
|
||||
Dungeon-Eingänge sind ausdrücklich relevante Reiseziele und wurden deshalb
|
||||
bereits importiert. Das bedeutet keine Freigabe selbstständiger Bewegung
|
||||
innerhalb einer Instanz. Für allein reisende AdventureBots genügt gemäß
|
||||
Nutzerwunsch perspektivisch ein zeitlich simulierter Aufenthalt mit späterer
|
||||
Rückkehr am Ausgang. Diese Aktion ist durch diese Portalarbeit **nicht
|
||||
implementiert**. Die spätere Begleitung von Spielergruppen innerhalb von
|
||||
Dungeons ist gesondert zu behandeln; interne Teleporter werden deshalb nicht
|
||||
pauschal als dauerhaft unnötig eingeordnet. DungeonBots bleiben unabhängig.
|
||||
@@ -1,6 +1,8 @@
|
||||
# Vom Bulk-Import übersprungene gleichkartige Portalübergänge
|
||||
|
||||
Bestandsaufnahme vom 26.10.2026, ausschließlich lesender Abgleich der lokalen Testdatenbank mit den installierten AreaTrigger-/Map-DBC-Dateien.
|
||||
Historische Bestandsaufnahme vom 26.10.2026 vor dem Magierturm-Import, ausschließlich lesender Abgleich der lokalen Testdatenbank mit den installierten AreaTrigger-/Map-DBC-Dateien.
|
||||
|
||||
Nachtrag: 702/704 und 6321/6322 sind inzwischen importiert und als Portalübergänge live getestet. Damit bleiben 54 der hier ursprünglich fehlenden 58 Übergänge unimportiert; das bedeutet keine geplante Freigabe dieser restlichen Übergänge. Die aktuelle Auswahl steht in [portal-selection.md](portal-selection.md), die Ödland-Tests in [badlands-dock-portals.md](badlands-dock-portals.md). Der noch fehlende Stadtanschluss ist in [stormwind-mage-tower.md](stormwind-mage-tower.md) dokumentiert. Die folgende Tabelle und die JSON-Datei zeigen weiterhin den Vorherstand.
|
||||
|
||||
Teleportdefinitionen: 316; davon gleichkartig: 60, kartenwechselnd: 256, ohne passenden DBC-Trigger: 0.
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# Sturmwind: Magierturm-Übergänge 702/704
|
||||
|
||||
Stand 26.10.2026. Auf dem lokalen Testserver gezielt importiert: vier aktive
|
||||
Portalnodes und zwei gerichtete Portalkanten. Keine Änderungen an nativen
|
||||
AreaTriggern, Teleportzielen oder interaktiven Rückreiseportalen.
|
||||
|
||||
| Node | Karte | X | Y | Z | Funktion |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| portal_at_702_source | 0 | -9020.670 | 890.104 | 29.6207 | unterer Eingang, Anlaufpunkt |
|
||||
| portal_at_702_target | 0 | -9014.940 | 873.326 | 148.616 | obere Ankunft |
|
||||
| portal_at_704_source | 0 | -9017.370 | 877.299 | 148.616 | oberer Ausgang, Anlaufpunkt |
|
||||
| portal_at_704_target | 0 | -9016.970 | 885.436 | 29.6207 | untere Ankunft |
|
||||
|
||||
Kanten: 1748 (702_source → 702_target) und 1749
|
||||
(704_source → 704_target), jeweils Kosten 1, unidirektional.
|
||||
|
||||
## Warum der reine Import nicht reicht
|
||||
|
||||
Die nativen Trigger sind gedrehte Boxen. Ihr Mittelpunkt liegt über dem Boden:
|
||||
702 bei Z=34 mit Höhe 11, 704 bei Z=152 mit Höhe 8. Der Importer übernimmt
|
||||
diesen Mittelpunkt als Source-Node. MMAP konnte insbesondere den Mittelpunkt
|
||||
von 704 nicht erreichen; der Planer wich auf einen unvollständigen langen
|
||||
Laufweg aus. Der erste Test wurde deshalb gestoppt.
|
||||
|
||||
Nur die Graph-Anlaufhöhen wurden innerhalb der Boxen auf die jeweilige Höhe
|
||||
der gegenüberliegenden nativen Ankunft gesetzt. X/Y und die Zielnodes bleiben
|
||||
unverändert. Die tatsächliche Zugehörigkeit zum Trigger wird weiterhin mit
|
||||
`IsInAreaTriggerRadius` geprüft; anschließend laufen normaler AreaTrigger-
|
||||
Handler, Clientless-ACK und Zielprüfung. Keine künstliche Portalabkürzung.
|
||||
|
||||
Reproduktion im lokalen Testsystem:
|
||||
|
||||
1. Konsole: `clientless portal import 702`, danach `clientless portal import 704`.
|
||||
2. `sql/custom/world/2026_10_26_01_clientless_stormwind_mage_tower.sql` auf die
|
||||
lokale Test-Weltdatenbank anwenden.
|
||||
3. Ingame `.clientless travelreload` oder sauberer Testserver-Neustart.
|
||||
|
||||
**Ein erneuter Portalimport überschreibt die kalibrierten Anlaufhöhen.**
|
||||
Danach die SQL-Kalibrierung erneut anwenden und den Graph neu laden. Der
|
||||
Bulk-Import überspringt gleichkartige Übergänge weiterhin.
|
||||
|
||||
## Live-Verifikation
|
||||
|
||||
Arthoran wurde ausgeloggt und nach Sicherung seiner Ausgangsdaten am nativen
|
||||
oberen Ziel von 702 positioniert. Diese Ausgangsposition simuliert eine
|
||||
Ankunft im oberen Bereich; ein interaktives Rückreiseportal selbst wurde
|
||||
nicht benutzt. Während der folgenden Reisen keine Positionsmanipulation:
|
||||
|
||||
- 11:51:06: oberer Ankunftspunkt → normaler MMAP-Anlauf → Trigger 704 → unten.
|
||||
- 11:51:07: untere Ankunft → normaler MMAP-Anlauf → Trigger 702 → oben.
|
||||
- 11:51:08: erneuter MMAP-Anlauf → Trigger 704 → unten.
|
||||
|
||||
Alle drei Übergänge: `EDGE_COMPLETE`, `arrivalError=0.00`, `recovery=0`.
|
||||
Die Kalibrierungs-SQL wurde anschließend zweimal erfolgreich angewendet;
|
||||
vier aktive Nodes und beide aktiven gerichteten Kanten wurden ausgelesen.
|
||||
|
||||
## Noch fehlender Groundanschluss
|
||||
|
||||
Der anschließende Versuch von unten nach `stormwind_gate_inner` fand keinen
|
||||
durchgängig validierten MMAP-Weg. Nach mehreren Teilanläufen endete er
|
||||
`TRAVEL_PARTIAL`, zuletzt nach einem erneuten Übergang 702. Damit ist die
|
||||
Weiterreise in den bestehenden Stadtgraph **noch nicht erfolgreich getestet**.
|
||||
Es wurden keine erfundenen Groundkanten oder geraden Wege durch Geometrie
|
||||
angelegt. Auch eine einzelne durchgehende Reise von einer beliebigen oberen
|
||||
Rückportal-Ankunft zu einem Stadtziel ist noch nicht abgenommen.
|
||||
|
||||
Nächster Schritt: mit dem GM eine echte Laufroute ab der unteren Ankunft
|
||||
`portal_at_704_target` zu einem bestehenden erreichbaren Stadtnode aufnehmen,
|
||||
zum Beispiel `stormwind_gate_inner`:
|
||||
|
||||
```text
|
||||
.clientless route start portal_at_704_target stormwind_gate_inner
|
||||
```
|
||||
|
||||
Am unteren Zielnode beginnen, den normalen begehbaren Weg bis zum Zielnode
|
||||
laufen und dort `.clientless route stop` ausführen. Der Recorder speichert
|
||||
bidirektional und lädt den Graph neu. Danach Arthoran in einem einzigen
|
||||
Reiseauftrag vom oberen Ankunftsbereich zum Stadtnode und zurück testen.
|
||||
Für den Weg hinein ist zusätzlich der Anschluss an `portal_at_702_source`
|
||||
zu prüfen; eine Aufnahme, die versehentlich durch einen Portaltrigger führt,
|
||||
ist kein durchgehender Groundweg.
|
||||
|
||||
## Abschlusszustand
|
||||
|
||||
Keine Coreänderung, daher kein neuer Build oder Binärinstall erforderlich.
|
||||
Testserver einmal sauber neu gestartet; aktuelle Ready-Meldung, Prozess 44833
|
||||
und Listener 8085 geprüft. Arthoran nach dem Test ausgeloggt, seine vorherige
|
||||
Position und das vorherige Lua-Skript wiederhergestellt.
|
||||
Sicherungen und Testlogs: `/tmp/vcore-mage-test.ntl3z51g`.
|
||||
Die Bestandsaufnahme `same-map-portals.md/json` bleibt als historischer
|
||||
Vorherstand erhalten; mit 702/704 sind nun vier gleichkartige Übergänge aktiv,
|
||||
56 der ursprünglich 58 zusätzlich fehlenden Übergänge bleiben offen.
|
||||
@@ -0,0 +1,151 @@
|
||||
# Interaktive Objektportale im TravelGraph
|
||||
|
||||
Stand 26.10.2026. Diese Erweiterung bildet einen normalen Klick auf ein
|
||||
festes Portal-GameObject als gerichtete TravelGraph-Kante `portal_use` ab.
|
||||
Die Nodes behalten den Typ `portal`. AreaTrigger-Portale (`portal_at_…`)
|
||||
und deren bestehender Import bleiben unverändert.
|
||||
|
||||
## Unterstützter erster Interaktionstyp
|
||||
|
||||
Unterstützt werden statisch gespawnte GameObjects vom Typ 22 (`SPELLCASTER`),
|
||||
deren direkt benutzter Zauber genau einen Teleporteffekt mit einem festen
|
||||
`TARGET_DEST_DB`-Ziel besitzt. Spawn, Objekt-Entry, Zauber und Ziel werden aus
|
||||
den geladenen nativen Core-/DB-Daten ermittelt.
|
||||
|
||||
Nicht jeder klickbare Teleporter erfüllt diese Voraussetzungen. NPC-Gossip,
|
||||
Gossip-Auswahl an Objekten, GOOBER-Skripte, ausgelöste Zauberketten und dynamisch
|
||||
gewählte Ziele benötigen weitere Interaktionsvarianten. Der Importer weist
|
||||
solche Fälle zurück, anstatt ein Ziel zu erraten. Beispielsweise hat der
|
||||
Shattrath-Teleporter zur Insel von Quel’Danas einen anders aufgebauten Zauber
|
||||
und ist nicht durch diesen direkten Import abgedeckt.
|
||||
|
||||
Vashj’ir mit seinen besonderen Wasserreiseangeboten bleibt gemäß
|
||||
Nutzerentscheidung eine spätere Erweiterung. Schwimmen/Tauchen gehört zur
|
||||
lokalen Navigation; Ruhesteine und eigene Teleportzauber sind nicht Teil
|
||||
dieser TravelGraph-Erweiterung.
|
||||
|
||||
## Einrichtung und gezielter Import
|
||||
|
||||
Einmalig die neue Tabelle auf der lokalen Test-Weltdatenbank anlegen:
|
||||
|
||||
```text
|
||||
sql/custom/world/2026_10_26_02_clientless_portal_use.sql
|
||||
```
|
||||
|
||||
Die Datei legt nur `vcore_clientless_portal_use` an; sie aktiviert keine
|
||||
Verbindung. Nach Installation des neuen Builds ein konkretes Objekt auswählen.
|
||||
Im Spiel hilft `.gobject near 30` bei der Suche nach vorhandenen Objekten.
|
||||
Benötigt wird die Datenbank-Spawn-ID, nicht die Objekt-Entry-ID.
|
||||
|
||||
Im GM-Spielchat:
|
||||
|
||||
```text
|
||||
.clientless portal object import <gameobjectSpawnId>
|
||||
```
|
||||
|
||||
Oder in der Testserver-Konsole ohne Punkt:
|
||||
|
||||
```text
|
||||
clientless portal object import <gameobjectSpawnId>
|
||||
```
|
||||
|
||||
Beispiel Dunkles Portal (Scherbenweltseite) → Sturmwind:
|
||||
|
||||
```text
|
||||
clientless portal object import 170
|
||||
```
|
||||
|
||||
Der Import erzeugt:
|
||||
|
||||
- `portal_go_170_source` an der nativen Objektposition.
|
||||
- `portal_go_170_target` am nativen Zauberziel.
|
||||
- Eine gerichtete Kante `portal_use`, Kosten 1, ohne automatische Rückrichtung.
|
||||
- Eine Bindung der Kanten-ID an Spawn 170, Entry 195139 und Zauber 17334.
|
||||
|
||||
Ein vorhandener Node oder eine vorhandene Kante wird nicht überschrieben.
|
||||
Insbesondere bleiben nachbearbeitete Anlaufpositionen und `enabled=0` erhalten.
|
||||
Die Transaktion kann wiederholt werden, ohne zusätzliche Nodes/Kanten zu
|
||||
produzieren. Der Import lädt Graph und Weltpunkte anschließend neu. Eine
|
||||
ungültige oder deaktivierte Verbindung wird ausdrücklich als nicht aktiv
|
||||
angezeigt; Details stehen im Navigationlog.
|
||||
|
||||
Rückreisen benötigen ein tatsächlich vorhandenes anderes Portal und eine
|
||||
eigene Definition. Ein Portal nach Sturmwind erzeugt keine Rückfahrt nach
|
||||
Shattrath. Der Graph verwirft `portal_use`-Kanten mit `bidirectional=1`, fehlender
|
||||
oder veralteter Bindung und einem unpassenden Zielnode.
|
||||
|
||||
Der zusätzlich untersuchte Shattrath-Spawn **83** ist im Testgraph deaktiviert:
|
||||
Sein nativer Zauber verwirft die Effekte bei der Sichtlinienprüfung. Das trat
|
||||
bei Level 20 und 60 sowie an zwei Anlaufpositionen auf. Es wurde keine native
|
||||
Sichtlinienprüfung umgangen und keine GO-/Zauberdefinition verändert. Ein
|
||||
Wiederholungsimport soll diese Deaktivierung erhalten.
|
||||
|
||||
## Anlauf und Reiseauftrag
|
||||
|
||||
Der Quellnode muss per normaler lokaler Navigation erreichbar sein und nahe
|
||||
genug am tatsächlichen Objekt liegen. Bei Bedarf den Source-Node mit einem
|
||||
GM auf einen begehbaren Punkt innerhalb der Interaktionsreichweite verschieben:
|
||||
|
||||
```text
|
||||
.clientless node move portal_go_170_source
|
||||
```
|
||||
|
||||
Ein Wiederholungsimport behält diese Anpassung bei. Falls der Source-Node
|
||||
anschließend über Groundwege angeschlossen werden muss, diese separat
|
||||
aufnehmen. Die Kante selbst enthält keine erfundenen Laufwege.
|
||||
|
||||
Ein normaler Lua-Reiseauftrag verwendet den Zielnode:
|
||||
|
||||
```lua
|
||||
queue_action("travel_to_node", {
|
||||
node = "portal_go_170_target",
|
||||
hard_destination = true
|
||||
})
|
||||
```
|
||||
|
||||
Der Executor prüft das konkrete gespawnte Objekt, Phase, Sichtbarkeit,
|
||||
Sperr-/Auswahlflags und die normale Interaktionsreichweite. Er stoppt die
|
||||
Bewegung, steigt gegebenenfalls vom Bodenreittier ab und ruft den normalen
|
||||
`CMSG_GAMEOBJ_USE`-Handler auf. Die native Objekt-AI, Gruppenprüfungen und
|
||||
Zauberbedingungen entscheiden weiterhin, ob der Klick funktioniert.
|
||||
Level- oder Questanforderungen werden durch den Executor nicht umgangen.
|
||||
|
||||
Nach dem Klick wartet der Bot auf einen bestätigten Clientless-Teleport.
|
||||
Ein Session-Zähler erfasst erfolgreich abgeschlossene Near-/Far-ACKs, auch
|
||||
wenn der allgemeine Session-Lifecycle den ACK vor dem nächsten Executor-Tick
|
||||
bearbeitet hat. Erst bei neuer Bestätigung, abgeschlossenem Teleport,
|
||||
richtiger Zielkarte und höchstens 5 Yards Zielfehler gilt die Kante als fertig.
|
||||
Nach 10 Sekunden ohne bestätigte Ankunft schlägt die Aktion fehl. Kein
|
||||
wiederholtes Klicken pro Tick, kein künstlicher Ersatzteleport.
|
||||
|
||||
Der Planner prüft derzeit nicht sämtliche individuellen Zauberbedingungen
|
||||
vorab. Eine geplante Verbindung kann deshalb bei der tatsächlichen Benutzung
|
||||
abgewiesen werden. Die Ablehnung ist kein erfolgreicher Reiseabschluss.
|
||||
|
||||
## Diagnose und Tests
|
||||
|
||||
```text
|
||||
.clientless actioninfo 26
|
||||
```
|
||||
|
||||
Navigationlog:
|
||||
|
||||
- `PORTAL_USE_START`: konkretes Objekt wurde an den Klick-Handler übergeben.
|
||||
- `EDGE_COMPLETE … edgeType='portal_use'`: Ankunft einschließlich ACK bestätigt.
|
||||
- `PORTAL_USE_FAILED`: Objekt/Spielerzustand ungeeignet oder Teleport nicht bestätigt.
|
||||
- `PORTAL_USE_BINDING_INVALID` / `PORTAL_USE_EDGE_UNAVAILABLE`: Definition beim Reload verworfen.
|
||||
|
||||
Automatisierte Regressionen:
|
||||
|
||||
```text
|
||||
python3 tests/clientless/test_portal_use.py
|
||||
python3 tests/clientless/test_portal_transfer.py
|
||||
```
|
||||
|
||||
Sie führen die tatsächlichen Produktionsmethoden mit Core-Doubles aus:
|
||||
Quellobjektprüfung, normaler Klick-Handler, Near/Far, bereits global bearbeiteter
|
||||
ACK, Ablehnung, Timeout, falsche Zielkarte/-position, native Zielauflösung und
|
||||
ACK-Sequenzzähler. Sie ersetzen keine Live-Fahrt oder visuelle Ingame-Abnahme.
|
||||
|
||||
Live-Ergebnisse des ersten Tests werden in
|
||||
[interactive-portals.md](../doc/clientless/interactive-portals.md) festgehalten.
|
||||
@@ -0,0 +1,371 @@
|
||||
# Fahrzeug-Haltestellen und Reiseverbindungen aufnehmen
|
||||
|
||||
Anleitung für den Vcore-Testserver, Stand 26.10.2026. Für Schiffe, Zeppeline,
|
||||
Tiefenbahn und unterstützte Fahrstühle. Gemeint sind native Transportobjekte,
|
||||
keine NPC-Fahrzeuge, Reittiere oder Flugtaxen.
|
||||
|
||||
Die Aufnahme erfolgt mit einem eingeloggten GM im Spiel. Alle Beispiele mit
|
||||
`.clientless` gehören in den Spielchat. Diagnose- und Testbefehle sind teilweise
|
||||
auch in der Worldserver-Konsole verfügbar; dort den führenden Punkt weglassen.
|
||||
Eine neue Verbindung benötigt keinen Build und normalerweise keinen Neustart.
|
||||
|
||||
## Grundprinzip
|
||||
|
||||
Pro physischer Haltestelle gibt es **einen gemeinsamen Node für Ein- und
|
||||
Ausstieg**, auf festem Boden unmittelbar vor dem Übergang zum Fahrzeug.
|
||||
Auf dem Deck wird kein zusätzlicher TravelGraph-Node angelegt.
|
||||
|
||||
Für eine Verbindung A ↔ B werden aufgenommen:
|
||||
|
||||
1. Haltestellen-Node A und dessen kurzer Weg vom Land zum Deck-Stehplatz.
|
||||
2. Haltestellen-Node B und dessen kurzer Weg zum selben Deck-Stehplatz.
|
||||
3. Eine Fahrtkante zwischen A und B.
|
||||
4. Bei Bedarf separate Groundwege vom übrigen Reisenetz zu A und B.
|
||||
|
||||
Die eigentliche Fahrt wird nicht aufgezeichnet. Der Bot fährt als Passenger
|
||||
mit dem normalen Fahrzeug. Zum Aussteigen läuft er die jeweilige
|
||||
Land-Deck-Aufnahme rückwärts ab. Deshalb müssen diese Wege in beiden
|
||||
Laufrichtungen begehbar sein.
|
||||
|
||||
| Verkehrsmittel | Node- und Fahrtkantentyp |
|
||||
| --- | --- |
|
||||
| Schiff | `ship` |
|
||||
| Zeppelin | `zeppelin` |
|
||||
| Tiefenbahn | `tram` |
|
||||
| Fahrstuhl | `elevator` |
|
||||
|
||||
## 1. Fahrzeug und Halte bestimmen
|
||||
|
||||
Für Schiffe und Zeppeline lassen sich die konfigurierten nativen Dienste
|
||||
ortsunabhängig auflisten:
|
||||
|
||||
```text
|
||||
.clientless transport services
|
||||
.clientless transport services <spawnId>
|
||||
```
|
||||
|
||||
Die Detailausgabe nennt tatsächliche Halte, Karten und Haltezeiten. Historische
|
||||
Fahrzeugnamen können veraltete Zielorte enthalten. Die ausgegebene Dockposition
|
||||
ist der Fahrzeugursprung und **nicht** die Position für den Haltestellen-Node.
|
||||
`services` listet nicht sämtliche Fahrstühle oder Tiefenbahnwagen auf.
|
||||
|
||||
Vor Ort das gewünschte Fahrzeug untersuchen:
|
||||
|
||||
```text
|
||||
.clientless transport near 50
|
||||
.clientless transport inspect <spawnId>
|
||||
```
|
||||
|
||||
Entscheidend sind:
|
||||
|
||||
- `spawn`: das konkrete Fahrzeug bzw. der konkrete Wagen; diese ID für `record start` verwenden.
|
||||
- `entry`: Fahrzeugmodell/-vorlage; wird beispielsweise für `stand list` verwendet.
|
||||
- `stopped=1`: Fahrzeug steht laut serverseitiger Bewegung.
|
||||
- `remainingPause`: verbleibende Haltezeit in Millisekunden.
|
||||
|
||||
**Spawn-ID und Entry-ID nicht verwechseln.** Beide Haltestellen einer
|
||||
Verbindung müssen dasselbe konkrete Fahrzeug verwenden. Bei der Tiefenbahn
|
||||
also auch denselben Wagen. Bei einem nativen Kartenwechsel bleibt die
|
||||
Spawn-ID gleich.
|
||||
|
||||
Für reine Patrouillen ohne geeignete Halte keine reguläre Passagierverbindung
|
||||
anlegen. Vor Beginn mindestens einen Umlauf beobachten und beide Einstiege
|
||||
prüfen. Fahrstühle sind noch nicht als konkrete Verbindung live abgenommen;
|
||||
ihre Unterstützung muss je Objekt überprüft werden.
|
||||
|
||||
## 2. Haltestellen-Node A anlegen
|
||||
|
||||
Auf festem Boden direkt vor Rampe, Steg oder Plattform stehen. Der Node muss
|
||||
auch bei abwesendem Fahrzeug sicher erreichbar sein. Vorher vorhandene Nodes
|
||||
prüfen, damit keine doppelte Haltestelle entsteht:
|
||||
|
||||
```text
|
||||
.clientless node list_nearby 30
|
||||
```
|
||||
|
||||
Beispiel für eine **neue** Schiffsverbindung mit frei gewählten Namen:
|
||||
|
||||
```text
|
||||
.clientless node add ship_hafen_a ship
|
||||
.clientless transport node ship_hafen_a
|
||||
```
|
||||
|
||||
Die Beispielnamen `ship_hafen_a`, `ship_hafen_b` und `mein_schiff_deck` sind
|
||||
Platzhalter für deine neue Verbindung. Namen eindeutig wählen, ohne Leerzeichen,
|
||||
zum Beispiel mit Kleinbuchstaben, Ziffern und Unterstrichen. Für andere
|
||||
Verkehrsmittel Namen und Nodetyp entsprechend ändern.
|
||||
|
||||
## 3. An A den Weg Land → Deck aufnehmen
|
||||
|
||||
Auf eine frische Ankunft warten. Zu Fuß am Node stehen, noch nicht Passenger.
|
||||
Der Start erlaubt höchstens **2 Yards Abstand zum Node**. Das Fahrzeug muss
|
||||
auf derselben Karte und in derselben Phase geladen sein und stillstehen.
|
||||
|
||||
`<spawnId>` vor Ausführung durch die ermittelte Zahl ersetzen:
|
||||
|
||||
```text
|
||||
.clientless transport record start ship_hafen_a <spawnId>
|
||||
```
|
||||
|
||||
Jetzt normal an Bord gehen und einen sicheren, ebenen Deckplatz aufsuchen.
|
||||
Einen kurzen Weg ohne Sprünge, Flug, Teleport oder Hindernisse wählen. Dann
|
||||
noch während **desselben Halts** speichern:
|
||||
|
||||
```text
|
||||
.clientless transport record stop mein_schiff_deck
|
||||
```
|
||||
|
||||
`stop` speichert den Übergang und legt den benannten Deck-Stehplatz bei Bedarf
|
||||
an. Der Standname bezeichnet eine Position relativ zum Fahrzeugmodell.
|
||||
Diesen Platz an der Gegenhaltestelle wieder verwenden.
|
||||
|
||||
Optionale Befehle während der Aufnahme:
|
||||
|
||||
```text
|
||||
.clientless transport record info
|
||||
.clientless transport record mark
|
||||
.clientless transport record cancel
|
||||
```
|
||||
|
||||
`info` zeigt Punkte, Länge, Passenger-Status und Fehler. `mark` erzwingt einen
|
||||
zusätzlichen Punkt, beispielsweise an einer engen Kurve. Die automatische
|
||||
Aufnahme läuft alle 100 ms mit mindestens 0,75 Yards Punktabstand;
|
||||
der Übergang zum Passenger wird automatisch markiert.
|
||||
|
||||
Die Aufnahme wird ungültig, wenn das Fahrzeug abfährt, der Recorder das
|
||||
Fahrzeug nach dem Einstieg wieder verlässt oder wechselt, ein Positionssprung
|
||||
über 8 Yards entsteht oder der Übergang länger als 60 Yards wird.
|
||||
Fehler mit `info` ansehen, danach mit `cancel` verwerfen und beim nächsten
|
||||
vollständigen Halt neu beginnen. Ein Logout oder Serverneustart verliert
|
||||
ungespeicherte Aufnahmen; jede Haltestelle zuerst erfolgreich speichern.
|
||||
|
||||
## 4. An B denselben Deckplatz aufnehmen
|
||||
|
||||
Mit genau diesem Fahrzeug zur anderen Haltestelle fahren. Dort aussteigen
|
||||
und auf festem Boden unmittelbar vor dem Übergang Node B anlegen:
|
||||
|
||||
```text
|
||||
.clientless node add ship_hafen_b ship
|
||||
```
|
||||
|
||||
Beim nächsten geeigneten Halt von B wieder auf dasselbe Fahrzeug gehen:
|
||||
|
||||
```text
|
||||
.clientless transport record start ship_hafen_b <spawnId>
|
||||
```
|
||||
|
||||
Zum selben lokalen Deckplatz laufen und mit demselben Standnamen speichern:
|
||||
|
||||
```text
|
||||
.clientless transport record stop mein_schiff_deck
|
||||
.clientless transport stops
|
||||
```
|
||||
|
||||
Ist der Stand bereits gespeichert, muss man beim Speichern höchstens
|
||||
1,5 Yards davon entfernt stehen. Die beiden aufgezeichneten Deck-Endpunkte
|
||||
müssen außerdem innerhalb von 3 Yards liegen. Es wird kein unbekannter Weg
|
||||
quer über das Deck zwischen unterschiedlichen Standorten ergänzt.
|
||||
|
||||
Optional kann der Deckplatz schon vor der ersten Aufnahme gespeichert werden,
|
||||
indem man an Bord am gewünschten Platz steht:
|
||||
|
||||
```text
|
||||
.clientless transport stand add mein_schiff_deck
|
||||
.clientless transport stand list
|
||||
```
|
||||
|
||||
Das ist nicht erforderlich, wenn `record stop` den Stand bereits angelegt hat.
|
||||
Ein vorhandener Standname wird nicht überschrieben.
|
||||
|
||||
## 5. Fahrtkante erstellen
|
||||
|
||||
Nach beiden erfolgreich gespeicherten Aufnahmen:
|
||||
|
||||
```text
|
||||
.clientless transport link ship_hafen_a ship_hafen_b ship 100 both
|
||||
```
|
||||
|
||||
`both` erstellt eine bidirektionale Kante. Nur verwenden, wenn das Fahrzeug
|
||||
beide Richtungen tatsächlich bedient. Ohne `both` gilt ausschließlich A → B:
|
||||
|
||||
```text
|
||||
.clientless transport link ship_hafen_a ship_hafen_b ship 100
|
||||
```
|
||||
|
||||
`100` ist ein positiver **Graphkostenwert**, keine Fahrzeit in Sekunden.
|
||||
Er eignet sich als vorläufiger Wert für einen isolierten Funktionstest.
|
||||
Bei konkurrierenden Fernrouten die Kosten später passend abstimmen.
|
||||
|
||||
Der Link prüft kompatible Aufnahmen, dieselbe Spawn-ID und denselben Entry,
|
||||
unterschiedliche Halte und passende Deck-Endpunkte. `tram`/`elevator` erwarten
|
||||
GameObject-Typ 11, `ship`/`zeppelin` Typ 15. Eine bereits gleichgerichtet
|
||||
vorhandene Kante wird nicht überschrieben. Erfolgreiches Speichern und
|
||||
Verknüpfen laden die betreffenden Daten automatisch neu.
|
||||
|
||||
## 6. Groundanschlüsse getrennt aufnehmen
|
||||
|
||||
Der Transportlink verbindet die beiden Haltestellen miteinander, nicht
|
||||
zugleich mit dem übrigen Reisenetz. Für einen fehlenden Landanschluss beide
|
||||
Nodes vorher anlegen bzw. einen bestehenden Stadtnode verwenden.
|
||||
|
||||
Beispiel: am bestehenden Node `stadt_knoten_a` anfangen und bis zur
|
||||
Haltestelle laufen:
|
||||
|
||||
```text
|
||||
.clientless route start stadt_knoten_a ship_hafen_a
|
||||
```
|
||||
|
||||
Am Haltestellen-Node auf festem Boden stoppen:
|
||||
|
||||
```text
|
||||
.clientless route stop
|
||||
```
|
||||
|
||||
Der Groundrecorder speichert bidirektional und lädt den Graph neu. Möglichst
|
||||
exakt an den Nodes beginnen und enden, damit die angehängten Endstücke keine
|
||||
Hindernisse schneiden. Diese Aufnahme endet **vor** dem Einstieg ins Fahrzeug;
|
||||
Land-Deck-Übergang und Fahrt werden separat wie oben erfasst. Auf Seite B
|
||||
entsprechend verfahren. Vollständig geprüfte MMAP-Anschlüsse können eine
|
||||
Groundaufnahme ersetzen, müssen aber für die konkrete Route funktionieren.
|
||||
|
||||
## 7. Botfahrt prüfen
|
||||
|
||||
Mit einem online befindlichen, untätigen Clientless-Bot an A testen.
|
||||
Die Zahl `26` ist Arthorans Charakter-GUID und bei einem anderen Bot zu ersetzen.
|
||||
Sein Login-Lua darf nicht gleichzeitig andere Reisen starten.
|
||||
|
||||
```text
|
||||
.clientless transport travel 26 ship_hafen_b
|
||||
.clientless transport status 26
|
||||
.clientless actioninfo 26
|
||||
```
|
||||
|
||||
`travel` stellt einen normalen Reiseauftrag in die Aktionswarteschlange;
|
||||
es unterbricht keine laufende Aktion. Nach Abschluss die Gegenrichtung testen:
|
||||
|
||||
```text
|
||||
.clientless transport travel 26 ship_hafen_a
|
||||
```
|
||||
|
||||
Eine funktionierende Fahrt durchläuft `Waiting` → `Boarding` → `Riding` →
|
||||
`Leaving` → `Arrived`. Prüfen, dass der Bot wirklich Passenger wird, einen
|
||||
gegebenenfalls nötigen Kartenwechsel abschließt, am Ziel aussteigt und den
|
||||
Landnode erreicht. Anschließend auch eine Reise mit Groundanschluss testen.
|
||||
Ein gespeicherter Link allein bestätigt noch keine funktionierende Botfahrt.
|
||||
|
||||
Abbruch einer bereits laufenden Transportfahrt:
|
||||
|
||||
```text
|
||||
.clientless transport cancel 26
|
||||
```
|
||||
|
||||
An Bord bleibt der Bot zunächst Passenger und versucht einen sicheren Ausstieg
|
||||
an einer aufgezeichneten Haltestelle. Das ist kein sofortiges Abspringen und
|
||||
kein allgemeines Leeren der Aktionswarteschlange.
|
||||
|
||||
## Aufnahmen korrigieren
|
||||
|
||||
**Nur die gespeicherte Land-Deck-Aufnahme ersetzen:**
|
||||
|
||||
```text
|
||||
.clientless transport forget ship_hafen_a
|
||||
```
|
||||
|
||||
Danach an A erneut `record start` und `record stop` ausführen. `forget`
|
||||
entfernt nur diese Übergangsaufnahme. Node, Groundkanten, Fahrtkante und
|
||||
benannte Deck-Stehplätze bleiben bestehen. Unvollständige Transportverbindungen
|
||||
werden währenddessen nicht geplant. Bleiben die Endpunkte kompatibel,
|
||||
muss die bestehende Fahrtkante nicht erneut angelegt werden.
|
||||
|
||||
**Haltestellen-Node verschieben:** am neuen festen Boden stehen und ausführen:
|
||||
|
||||
```text
|
||||
.clientless node move ship_hafen_a
|
||||
```
|
||||
|
||||
Danach die betroffene Transportaufnahme mit `forget` und neuer Aufnahme
|
||||
ersetzen. Angeschlossene Groundwege ebenfalls prüfen und gegebenenfalls neu
|
||||
aufnehmen; sie passen sich nicht automatisch an die neue Position an.
|
||||
|
||||
**Falsche Fahrtkante entfernen:**
|
||||
|
||||
```text
|
||||
.clientless route remove ship_hafen_a ship_hafen_b
|
||||
```
|
||||
|
||||
Dieser Befehl entfernt **alle Kanten zwischen diesen beiden Nodes in beiden
|
||||
Richtungen**, unabhängig vom Kantentyp. Daher nur verwenden, wenn genau diese
|
||||
Verbindungen entfernt werden sollen. Haltestellen-Aufnahmen und Nodes bleiben
|
||||
erhalten. Anschließend die gewünschte Fahrtkante mit `transport link` neu anlegen.
|
||||
|
||||
**Deckplätze prüfen oder einen unbenutzten Stand entfernen:**
|
||||
|
||||
```text
|
||||
.clientless transport stand list <entryId>
|
||||
.clientless transport stand remove <entryId> <standName>
|
||||
```
|
||||
|
||||
Ein von einer Haltestellen-Aufnahme verwendeter Stand kann nicht entfernt
|
||||
werden. Eine Änderung des gemeinsamen Deckplatzes erfordert passende neue
|
||||
Aufnahmen an beiden Haltestellen.
|
||||
|
||||
## Diagnose und häufige Fehler
|
||||
|
||||
| Meldung oder Symptom | Nächster Schritt |
|
||||
| --- | --- |
|
||||
| Abstand zum Node zu groß | Auf festem Boden bis auf höchstens 2 Yards an den Node zurückgehen. |
|
||||
| Recorder bereits Passenger | Aussteigen; Aufnahme muss auf festem Boden beginnen. |
|
||||
| Spawn nicht geladen | Spawn-ID, Karte und Phase prüfen; Fahrzeug vor Ort aufsuchen. |
|
||||
| Fahrzeug bewegt sich / Aufnahme nach Abfahrt ungültig | `record cancel`, nächsten vollständigen Halt abwarten. |
|
||||
| Dock bereits aufgenommen | Bestehende Daten mit `stops` prüfen; nur für einen beabsichtigten Ersatz `forget` verwenden. |
|
||||
| Standname an anderer Deckposition vorhanden | Zum ursprünglichen Stand gehen; alternativ neuen Namen und kompatible Aufnahmen verwenden. |
|
||||
| Link abgelehnt | Zwei gespeicherte Halte, gleicher Spawn/Entry, kompatible Deck-Endpunkte und richtigen Kantentyp prüfen. |
|
||||
| Bot wartet dauerhaft | Richtigen Spawn, tatsächliche Halte, Dockposition und `status` prüfen. |
|
||||
| Fahrt klappt, Weiterreise nicht | Groundanschluss der Haltestelle separat prüfen bzw. aufnehmen. |
|
||||
|
||||
Weitere Befehle:
|
||||
|
||||
```text
|
||||
.clientless transport stops
|
||||
.clientless transport node ship_hafen_a
|
||||
.clientless transport reload
|
||||
```
|
||||
|
||||
`reload` lädt Transportdaten und TravelGraph neu. Laufende Fahrten behalten
|
||||
jedoch ihre beim Start erfassten Definitionen. Änderungen deshalb mit einem
|
||||
neuen Reiseauftrag testen.
|
||||
|
||||
Konsolenbeispiele ohne Punkt; für räumliche Diagnose ist ein online
|
||||
befindlicher Bot als Beobachter auf der betreffenden Karte nötig:
|
||||
|
||||
```text
|
||||
clientless transport services
|
||||
clientless transport services <spawnId>
|
||||
clientless transport node ship_hafen_a
|
||||
clientless transport inspect <spawnId> 26
|
||||
clientless transport near 50 26
|
||||
clientless transport stops
|
||||
clientless transport status 26
|
||||
```
|
||||
|
||||
`remainingPause=4294967295` bezeichnet einen Halt ohne bekannte zeitliche
|
||||
Begrenzung. Zustandswechsel und Aufnahmefehler stehen in
|
||||
`adventurebot-navigation.log` im Arbeitsverzeichnis des Test-worldservers.
|
||||
|
||||
## Bestehende Beispiele
|
||||
|
||||
Diese Namen und IDs gehören bereits erfassten Verbindungen; nicht als neue
|
||||
Haltestellen doppelt anlegen oder zum Üben überschreiben:
|
||||
|
||||
| Verbindung | Haltestellen | Fahrzeug |
|
||||
| --- | --- | --- |
|
||||
| Sturmwind ↔ Rut’theran, The Bravery | `ship_sw_br` ↔ `ship_ruttheran_br` | Spawn 2, Entry 176310, Stand `bravery_center` |
|
||||
| Tiefenbahn | `tram_sw` ↔ `tram_if` | Im dokumentierten Fahrtest Wagen-Spawn 18803; bestehende Aufnahme mit `stops` prüfen. |
|
||||
|
||||
Bei der Tiefenbahn gibt es mehrere Wagen mit verschiedenen Spawn-IDs; deren
|
||||
Aufnahmen nicht mischen. Die kurzen Haltefenster erfordern einen kurzen
|
||||
Einstiegsweg und zügiges Speichern.
|
||||
|
||||
Weiterführend: [technische Transportdokumentation](../doc/clientless/transport-tools.md),
|
||||
[Schiff Sturmwind–Rut’theran](../doc/clientless/ship-stormwind-ruttheran.md),
|
||||
[Transport-Grid-Lifecycle](../doc/clientless/transport-grid-lifecycle.md).
|
||||
@@ -1,3 +1,5 @@
|
||||
*.sql
|
||||
!2026_10_25_00_clientless_transport.sql
|
||||
!2026_10_26_00_clientless_disable_taxi_585.sql
|
||||
!2026_10_26_01_clientless_stormwind_mage_tower.sql
|
||||
!2026_10_26_02_clientless_portal_use.sql
|
||||
@@ -0,0 +1,18 @@
|
||||
-- Explicit local test-graph calibration; first run these worldserver commands:
|
||||
-- clientless portal import 702
|
||||
-- clientless portal import 704
|
||||
-- The importer seeds source nodes at the native AreaTrigger box centers.
|
||||
-- These centers are above the walkable floor. Keep their X/Y and move only
|
||||
-- the graph approach Z inside the native box, using the opposite arrival Z.
|
||||
-- Native areatrigger_teleport and DBC data are deliberately untouched.
|
||||
-- Reapply after reimporting 702/704, then reload the travel graph.
|
||||
START TRANSACTION;
|
||||
UPDATE vcore_clientless_world_point
|
||||
SET position_z=29.6207,
|
||||
comment='Mage tower 702 walkable approach inside native trigger; calibrated 2026-10-26'
|
||||
WHERE point_key='portal_at_702_source' AND map=0;
|
||||
UPDATE vcore_clientless_world_point
|
||||
SET position_z=148.616,
|
||||
comment='Mage tower 704 walkable approach inside native trigger; calibrated 2026-10-26'
|
||||
WHERE point_key='portal_at_704_source' AND map=0;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,10 @@
|
||||
-- Apply explicitly to the dedicated test world DB. No portals are auto-enabled.
|
||||
-- A directed portal_use edge binds to one static clickable GameObject spawn.
|
||||
-- Entry and spell identify stale bindings after native portal data changes.
|
||||
CREATE TABLE IF NOT EXISTS vcore_clientless_portal_use (
|
||||
edge_id int unsigned NOT NULL,
|
||||
gameobject_spawn_id int unsigned NOT NULL,
|
||||
gameobject_entry int unsigned NOT NULL,
|
||||
spell_id int unsigned NOT NULL,
|
||||
PRIMARY KEY (edge_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@@ -251,10 +251,13 @@ void WorldSession::CompleteClientlessTeleport()
|
||||
{
|
||||
if (!IsClientless() || !GetPlayer())
|
||||
return;
|
||||
bool const pending = GetPlayer()->IsBeingTeleported();
|
||||
if (GetPlayer()->IsBeingTeleportedFar())
|
||||
HandleMoveWorldportAck();
|
||||
else if (GetPlayer()->IsBeingTeleportedNear())
|
||||
CompleteNearTeleport(GetPlayer());
|
||||
if (pending && GetPlayer() && GetPlayer()->IsInWorld() && !GetPlayer()->IsBeingTeleported())
|
||||
++_clientlessTeleportSequence;
|
||||
}
|
||||
|
||||
void WorldSession::CompleteNearTeleport(Player* plMover)
|
||||
|
||||
@@ -744,6 +744,7 @@ class TC_GAME_API WorldSession
|
||||
void HandleMoveKnockBackAck(WorldPacket& recvPacket);
|
||||
void HandleMoveTeleportAck(WorldPackets::Movement::MoveTeleportAck& packet);
|
||||
void CompleteClientlessTeleport();
|
||||
uint64 GetClientlessTeleportSequence() const { return _clientlessTeleportSequence; }
|
||||
void HandleMoveWaterWalkAck(WorldPacket& recvPacket);
|
||||
void HandleFeatherFallAck(WorldPacket& recvData);
|
||||
void HandleMoveHoverAck(WorldPacket& recvData);
|
||||
@@ -1382,6 +1383,7 @@ class TC_GAME_API WorldSession
|
||||
Player* _player;
|
||||
WorldSessionMode _sessionMode;
|
||||
ObjectGuid _clientlessPendingPlayerGuid;
|
||||
uint64 _clientlessTeleportSequence = 0;
|
||||
std::shared_ptr<WorldSocket> m_Socket[2];
|
||||
std::string m_Address; // Current Remote Address
|
||||
// std::string m_LAddress; // Last Attempted Remote Adress - we can not set attempted ip for a non-existing session!
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
#include "Player.h"
|
||||
#include "MotionMaster.h"
|
||||
#include "PathGenerator.h" // VCORE_C819_DEFERRED_START_NODES_V2
|
||||
#include "GameObject.h"
|
||||
#include "Map.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include "Position.h"
|
||||
@@ -179,6 +181,7 @@ ClientlessActionExecutionStatus const* ClientlessActionExecutorMgr::GetStatus(ui
|
||||
bool ClientlessActionExecutorMgr::StartAction(Player* player, ClientlessAction const& action, Record& record)
|
||||
{
|
||||
record.TransportActive = false;
|
||||
record.PortalUseActive = false;
|
||||
record.Status = {};
|
||||
record.Status.ActionName = action.Name;
|
||||
if (player->GetTransport())
|
||||
@@ -890,6 +893,9 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
|
||||
step.NodeKey.c_str(), step.NodeType.c_str(), step.EdgeType.c_str(),
|
||||
step.EdgeDatabaseId, step.EdgeReverse ? 1u : 0u, record.RecoveryCount);
|
||||
|
||||
if (step.EdgeType == "portal_use")
|
||||
return StartPortalUse(player, characterGuid, record);
|
||||
|
||||
if (ClientlessTransportMgr::IsTransportEdge(step.EdgeType))
|
||||
{
|
||||
std::string error;
|
||||
@@ -1434,6 +1440,116 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClientlessActionExecutorMgr::StartPortalUse(Player* player,
|
||||
uint32 characterGuid, Record& record)
|
||||
{
|
||||
auto const& step = record.Route[record.RouteStep];
|
||||
auto const* portal = sClientlessTravelGraphMgr->GetPortalUse(step.EdgeDatabaseId);
|
||||
auto fail = [&](char const* reason)
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot.navigation", "PORTAL_USE_FAILED guid=%u edge=%u reason='%s'",
|
||||
characterGuid, step.EdgeDatabaseId, reason);
|
||||
record.PortalUseActive = false;
|
||||
record.Active = false;
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail = reason;
|
||||
return false;
|
||||
};
|
||||
if (!portal || !record.RouteStep || step.EdgeReverse ||
|
||||
player->GetMapId() != portal->Source.MapId || !player->IsAlive() ||
|
||||
player->IsBeingTeleported() || player->GetTransport() ||
|
||||
!player->GetSession() || !player->GetSession()->IsClientless())
|
||||
return fail("interactive portal binding or player state invalid");
|
||||
|
||||
GameObject* object = nullptr;
|
||||
auto const& objects = player->GetMap()->GetGameObjectBySpawnIdStore();
|
||||
auto range = objects.equal_range(portal->SpawnId);
|
||||
for (auto itr = range.first; itr != range.second; ++itr)
|
||||
{
|
||||
GameObject* candidate = itr->second;
|
||||
if (candidate && candidate->GetEntry() == portal->Entry && candidate->isSpawned() &&
|
||||
player->IsInPhase(candidate) && player->CanSeeOrDetect(candidate) &&
|
||||
!candidate->HasFlag(GAMEOBJECT_FLAGS, GO_FLAG_NOT_SELECTABLE | GO_FLAG_LOCKED) &&
|
||||
candidate->GetGoType() == GAMEOBJECT_TYPE_SPELLCASTER &&
|
||||
candidate->GetGOInfo()->spellcaster.spell == portal->SpellId &&
|
||||
player->GetGameObjectIfCanInteractWith(candidate->GetGUID()) == candidate)
|
||||
{
|
||||
object = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!object)
|
||||
return fail("interactive portal not spawned, visible or in interaction range");
|
||||
|
||||
sClientlessTravelMgr->CancelTravel(characterGuid);
|
||||
record.EdgePath.clear();
|
||||
player->StopMoving();
|
||||
sClientlessLuaEngine->DismountGroundMount(player, "interactive-portal-edge");
|
||||
record.PortalUseSpawnId = portal->SpawnId;
|
||||
record.PortalUseActive = true;
|
||||
record.PortalUseTeleportSequence = player->GetSession()->GetClientlessTeleportSequence();
|
||||
record.PortalUseElapsedMs = 0;
|
||||
record.Active = true;
|
||||
record.Status.State = ClientlessActionExecutionState::Moving;
|
||||
record.Status.TargetName = "node:" + step.NodeKey;
|
||||
record.Status.Detail = "waiting for interactive portal teleport";
|
||||
TC_LOG_INFO("server.adventurebot.navigation", "PORTAL_USE_START guid=%u edge=%u spawn=%u entry=%u spell=%u sourceMap=%u targetMap=%u",
|
||||
characterGuid, step.EdgeDatabaseId, portal->SpawnId, portal->Entry, portal->SpellId,
|
||||
portal->Source.MapId, step.MapId);
|
||||
// Same packet handler as a player's click. Native Use/AI, party checks,
|
||||
// spell cast restrictions and scripted conditions remain authoritative.
|
||||
WorldPacket usePacket(CMSG_GAMEOBJ_USE, 8);
|
||||
usePacket << object->GetGUID();
|
||||
player->GetSession()->HandleGameObjectUseOpcode(usePacket);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClientlessActionExecutorMgr::UpdatePortalUse(Player* player,
|
||||
uint32 characterGuid, Record& record)
|
||||
{
|
||||
auto const& step = record.Route[record.RouteStep];
|
||||
if (player->IsBeingTeleported())
|
||||
{
|
||||
WorldSession* session = player->GetSession();
|
||||
session->CompleteClientlessTeleport();
|
||||
player = session->GetPlayer();
|
||||
if (!player)
|
||||
{
|
||||
record.PortalUseActive = false;
|
||||
record.Active = false;
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail = "interactive portal transfer lost its player";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
float const arrivalError = std::hypot(std::hypot(player->GetPositionX() - step.X,
|
||||
player->GetPositionY() - step.Y), player->GetPositionZ() - step.Z);
|
||||
bool const arrived = player->GetSession()->GetClientlessTeleportSequence() != record.PortalUseTeleportSequence && player->IsInWorld() &&
|
||||
!player->IsBeingTeleported() && player->GetMapId() == step.MapId && arrivalError <= 5.0f;
|
||||
if (arrived)
|
||||
{
|
||||
TC_LOG_INFO("server.adventurebot.navigation", "EDGE_COMPLETE guid=%u name='%s' edgeType='portal_use' edgeDbId=%u node='%s' spawn=%u map=%u arrivalError=%.2f recovery=%u",
|
||||
characterGuid, player->GetName().c_str(), step.EdgeDatabaseId, step.NodeKey.c_str(),
|
||||
record.PortalUseSpawnId, player->GetMapId(), arrivalError, record.RecoveryCount);
|
||||
record.PortalUseActive = false;
|
||||
++record.RouteStep;
|
||||
StartRouteStep(player, characterGuid, record);
|
||||
return;
|
||||
}
|
||||
// A denied click/cast is never success. Do not retry it every tick or
|
||||
// manufacture a teleport. Leave enough time for native cast/delayed events.
|
||||
if (record.PortalUseElapsedMs < 10000)
|
||||
return;
|
||||
TC_LOG_WARN("server.adventurebot.navigation", "PORTAL_USE_FAILED guid=%u edge=%u spawn=%u map=%u expectedMap=%u arrivalError=%.2f pending=%u reason='teleport-not-confirmed'",
|
||||
characterGuid, step.EdgeDatabaseId, record.PortalUseSpawnId, player->GetMapId(),
|
||||
step.MapId, arrivalError, player->IsBeingTeleported() ? 1u : 0u);
|
||||
record.PortalUseActive = false;
|
||||
record.Active = false;
|
||||
record.Status.State = ClientlessActionExecutionState::Failed;
|
||||
record.Status.Detail = "interactive portal teleport not confirmed within 10 seconds";
|
||||
}
|
||||
|
||||
bool ClientlessActionExecutorMgr::StartRecordedEdgePath(
|
||||
Player* player,
|
||||
uint32 characterGuid,
|
||||
@@ -2232,7 +2348,7 @@ void ClientlessActionExecutorMgr::UpdateActive(
|
||||
ClientlessTravelMgr::StateName(travel->State));
|
||||
}
|
||||
|
||||
void ClientlessActionExecutorMgr::Update(uint32 /*diff*/)
|
||||
void ClientlessActionExecutorMgr::Update(uint32 diff)
|
||||
{
|
||||
for (auto const& [characterGuid, session] : sWorld->GetAllClientlessSessions())
|
||||
{
|
||||
@@ -2241,11 +2357,22 @@ void ClientlessActionExecutorMgr::Update(uint32 /*diff*/)
|
||||
continue;
|
||||
|
||||
Player* player = session->GetPlayer();
|
||||
if (!player || !player->IsInWorld())
|
||||
if (!player)
|
||||
continue;
|
||||
|
||||
Record& record = _records[characterGuid];
|
||||
|
||||
// Far teleport removes the player from the old map until the client
|
||||
// ACK. Service this state before the ordinary in-world update guard.
|
||||
if (record.Active && record.PortalUseActive)
|
||||
{
|
||||
record.PortalUseElapsedMs += std::min(diff, uint32(30000));
|
||||
UpdatePortalUse(player, characterGuid, record);
|
||||
continue;
|
||||
}
|
||||
if (!player->IsInWorld())
|
||||
continue;
|
||||
|
||||
if (record.Active)
|
||||
{
|
||||
UpdateActive(player, characterGuid, record);
|
||||
|
||||
@@ -60,6 +60,10 @@ private:
|
||||
|
||||
// C8.10b: active server-side taxi edge.
|
||||
bool TransportActive = false;
|
||||
bool PortalUseActive = false;
|
||||
uint64 PortalUseTeleportSequence = 0;
|
||||
uint32 PortalUseElapsedMs = 0;
|
||||
uint32 PortalUseSpawnId = 0;
|
||||
bool TaxiActive = false;
|
||||
uint32 TaxiPathId = 0;
|
||||
uint32 TaxiSourceNodeId = 0;
|
||||
@@ -102,6 +106,8 @@ private:
|
||||
bool StartFinalLocalLeg(Player* player, uint32 characterGuid, Record& record);
|
||||
bool RecoverFinalLocalLeg(Player* player, uint32 characterGuid, Record& record, std::string const& reason);
|
||||
bool StartRouteStep(Player* player, uint32 characterGuid, Record& record);
|
||||
bool StartPortalUse(Player* player, uint32 characterGuid, Record& record);
|
||||
void UpdatePortalUse(Player* player, uint32 characterGuid, Record& record);
|
||||
bool StartRecordedEdgePath(Player* player, uint32 characterGuid, Record& record);
|
||||
bool RecoverTravel(Player* player, uint32 characterGuid, Record& record, uint32 failedEdgeDatabaseId, std::string const& reason);
|
||||
bool StartRecoveryWander(Player* player, uint32 characterGuid, Record& record, std::string const& reason);
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
#include "Player.h"
|
||||
#include "ClientlessTravelMgr.h"
|
||||
#include "PathGenerator.h" // VCORE_C818_PREDICTIVE_GRAPH
|
||||
#include "ObjectMgr.h"
|
||||
#include "SpellMgr.h"
|
||||
#include "SpellInfo.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
@@ -27,11 +30,80 @@ bool ClientlessTravelGraphMgr::EnsureLoaded()
|
||||
return Reload();
|
||||
}
|
||||
|
||||
bool ClientlessTravelGraphMgr::ResolvePortalUse(uint32 spawnId,
|
||||
ClientlessPortalUse& portal, std::string& error)
|
||||
{
|
||||
portal = {};
|
||||
auto const* spawn = sObjectMgr->GetGameObjectData(spawnId);
|
||||
auto const* info = spawn ? sObjectMgr->GetGameObjectTemplate(spawn->id) : nullptr;
|
||||
if (!info || info->type != GAMEOBJECT_TYPE_SPELLCASTER)
|
||||
{
|
||||
error = "spawn must be a static SPELLCASTER GameObject (type 22)";
|
||||
return false;
|
||||
}
|
||||
SpellInfo const* spell = sSpellMgr->GetSpellInfo(info->spellcaster.spell);
|
||||
SpellTargetPosition const* destination = nullptr;
|
||||
if (spell)
|
||||
for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i)
|
||||
{
|
||||
auto const& effect = spell->Effects[i];
|
||||
if (effect.Effect != SPELL_EFFECT_TELEPORT_UNITS)
|
||||
continue;
|
||||
// Only explicit database destinations are unambiguous. Homebind,
|
||||
// script-selected destinations and triggered spell chains need
|
||||
// their own interaction definitions, not guessed target nodes.
|
||||
if (destination || (effect.TargetA.GetTarget() != TARGET_DEST_DB &&
|
||||
effect.TargetB.GetTarget() != TARGET_DEST_DB))
|
||||
{
|
||||
error = "portal spell has an ambiguous or non-DB destination";
|
||||
return false;
|
||||
}
|
||||
destination = sSpellMgr->GetSpellTargetPosition(spell->Id, SpellEffIndex(i));
|
||||
if (!destination)
|
||||
{
|
||||
error = "portal teleport effect has no native spell_target_position";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!destination)
|
||||
{
|
||||
error = "portal requires a direct teleport spell with a fixed DB destination";
|
||||
return false;
|
||||
}
|
||||
portal.SpawnId = spawnId;
|
||||
portal.Entry = spawn->id;
|
||||
portal.SpellId = spell->Id;
|
||||
portal.Source.Key = "portal_go_" + std::to_string(spawnId) + "_source";
|
||||
portal.Source.Type = "portal";
|
||||
portal.Source.WorldPointKey = portal.Source.Key;
|
||||
portal.Source.MapId = spawn->mapId;
|
||||
portal.Source.X = spawn->spawnPoint.GetPositionX();
|
||||
portal.Source.Y = spawn->spawnPoint.GetPositionY();
|
||||
portal.Source.Z = spawn->spawnPoint.GetPositionZ();
|
||||
portal.Source.O = spawn->spawnPoint.GetOrientation();
|
||||
portal.Target.Key = "portal_go_" + std::to_string(spawnId) + "_target";
|
||||
portal.Target.Type = "portal";
|
||||
portal.Target.WorldPointKey = portal.Target.Key;
|
||||
portal.Target.MapId = destination->target_mapId;
|
||||
portal.Target.X = destination->target_X;
|
||||
portal.Target.Y = destination->target_Y;
|
||||
portal.Target.Z = destination->target_Z;
|
||||
portal.Target.O = destination->target_Orientation;
|
||||
return true;
|
||||
}
|
||||
|
||||
ClientlessPortalUse const* ClientlessTravelGraphMgr::GetPortalUse(uint32 edgeDatabaseId) const
|
||||
{
|
||||
auto itr = _portalUses.find(edgeDatabaseId);
|
||||
return itr != _portalUses.end() ? &itr->second : nullptr;
|
||||
}
|
||||
|
||||
bool ClientlessTravelGraphMgr::Reload()
|
||||
{
|
||||
_nodes.clear();
|
||||
_edges.clear();
|
||||
_edgePoints.clear();
|
||||
_portalUses.clear();
|
||||
_edgeCount = 0;
|
||||
|
||||
QueryResult nodeResult = WorldDatabase.Query(
|
||||
@@ -66,6 +138,27 @@ bool ClientlessTravelGraphMgr::Reload()
|
||||
while (nodeResult->NextRow());
|
||||
}
|
||||
|
||||
std::unordered_map<uint32, ClientlessPortalUse> portalBindings;
|
||||
// The schema is opt-in, so existing installations retain their graph even
|
||||
// before applying the interactive-portal migration.
|
||||
if (WorldDatabase.Query("SHOW TABLES LIKE 'vcore_clientless_portal_use'"))
|
||||
if (QueryResult bindings = WorldDatabase.Query(
|
||||
"SELECT edge_id,gameobject_spawn_id,gameobject_entry,spell_id FROM vcore_clientless_portal_use"))
|
||||
do
|
||||
{
|
||||
Field* fields = bindings->Fetch();
|
||||
ClientlessPortalUse portal;
|
||||
std::string error;
|
||||
if (!ResolvePortalUse(fields[1].GetUInt32(), portal, error) ||
|
||||
portal.Entry != fields[2].GetUInt32() || portal.SpellId != fields[3].GetUInt32())
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot.navigation", "PORTAL_USE_BINDING_INVALID edge=%u reason='%s'",
|
||||
fields[0].GetUInt32(), error.c_str());
|
||||
continue;
|
||||
}
|
||||
portalBindings.emplace(fields[0].GetUInt32(), std::move(portal));
|
||||
} while (bindings->NextRow());
|
||||
|
||||
QueryResult edgeResult = WorldDatabase.Query(
|
||||
"SELECT id, from_node_key, to_node_key, edge_type, cost, bidirectional "
|
||||
"FROM vcore_clientless_travel_edge "
|
||||
@@ -101,6 +194,24 @@ bool ClientlessTravelGraphMgr::Reload()
|
||||
if (edge.Cost <= 0.0f)
|
||||
edge.Cost = 1.0f;
|
||||
|
||||
if (edge.Type == "portal_use")
|
||||
{
|
||||
auto binding = portalBindings.find(edge.DatabaseId);
|
||||
auto const* portal = binding != portalBindings.end() ? &binding->second : nullptr;
|
||||
auto const& target = _nodes.at(edge.ToNode);
|
||||
if (!portal || bidirectional || edge.FromNode != portal->Source.Key ||
|
||||
edge.ToNode != portal->Target.Key ||
|
||||
_nodes.at(edge.FromNode).MapId != portal->Source.MapId ||
|
||||
target.MapId != portal->Target.MapId ||
|
||||
std::hypot(std::hypot(target.X - portal->Target.X, target.Y - portal->Target.Y),
|
||||
target.Z - portal->Target.Z) > 1.0f)
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot.navigation", "PORTAL_USE_EDGE_UNAVAILABLE edge=%u", edge.DatabaseId);
|
||||
continue;
|
||||
}
|
||||
_portalUses.emplace(edge.DatabaseId, *portal);
|
||||
}
|
||||
|
||||
if (ClientlessTransportMgr::IsTransportEdge(edge.Type) &&
|
||||
!sClientlessTransportMgr->HasConnection(edge.FromNode, edge.ToNode))
|
||||
{
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* The graph is deliberately data driven and may be incomplete. Missing routes
|
||||
* fail cleanly. New nodes and edges can be added without rebuilding the core.
|
||||
*
|
||||
* C8.6 executes only "ground" edges. Other edge types may already be stored
|
||||
* and planned, but execution stops cleanly until their executor exists.
|
||||
* Ground, taxi, AreaTrigger portals, interactive object portals and native
|
||||
* transport edges have dedicated executors.
|
||||
*/
|
||||
#ifndef VCORE_CLIENTLESS_TRAVEL_GRAPH_MGR_H
|
||||
#define VCORE_CLIENTLESS_TRAVEL_GRAPH_MGR_H
|
||||
@@ -39,6 +39,17 @@ struct TC_GAME_API ClientlessTravelNode
|
||||
float O = 0.0f;
|
||||
};
|
||||
|
||||
// A fixed-destination, clickable SPELLCASTER GameObject. The destination is
|
||||
// derived from the native spell data; this is never a synthetic teleport.
|
||||
struct TC_GAME_API ClientlessPortalUse
|
||||
{
|
||||
uint32 SpawnId = 0;
|
||||
uint32 Entry = 0;
|
||||
uint32 SpellId = 0;
|
||||
ClientlessTravelNode Source;
|
||||
ClientlessTravelNode Target;
|
||||
};
|
||||
|
||||
struct TC_GAME_API ClientlessTravelEdge
|
||||
{
|
||||
uint32 DatabaseId = 0;
|
||||
@@ -151,6 +162,9 @@ public:
|
||||
|
||||
std::vector<ClientlessTravelEdgePoint> const* GetEdgePoints(uint32 edgeDatabaseId) const;
|
||||
|
||||
static bool ResolvePortalUse(uint32 spawnId, ClientlessPortalUse& portal, std::string& error);
|
||||
ClientlessPortalUse const* GetPortalUse(uint32 edgeDatabaseId) const;
|
||||
|
||||
private:
|
||||
std::string FindNearestStartNode(Player const* player, float maxDistance) const;
|
||||
|
||||
@@ -160,6 +174,7 @@ private:
|
||||
std::unordered_map<std::string, ClientlessTravelNode> _nodes;
|
||||
std::unordered_map<std::string, std::vector<ClientlessTravelEdge>> _edges;
|
||||
std::unordered_map<uint32, std::vector<ClientlessTravelEdgePoint>> _edgePoints;
|
||||
std::unordered_map<uint32, ClientlessPortalUse> _portalUses;
|
||||
};
|
||||
|
||||
#define sClientlessTravelGraphMgr ClientlessTravelGraphMgr::instance()
|
||||
|
||||
@@ -1331,6 +1331,74 @@ static bool HandleClientlessTaxiInfo(ChatHandler* handler, char const* args)
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool HandleClientlessPortalObjectImport(ChatHandler* handler, char const* args)
|
||||
{
|
||||
std::istringstream input(args ? args : "");
|
||||
uint32 spawnId = 0;
|
||||
std::string extra;
|
||||
if (!(input >> spawnId) || !spawnId || (input >> extra))
|
||||
{
|
||||
handler->SendSysMessage("Usage: .clientless portal object import <gameobjectSpawnId>");
|
||||
return false;
|
||||
}
|
||||
if (!WorldDatabase.Query("SHOW TABLES LIKE 'vcore_clientless_portal_use'"))
|
||||
{
|
||||
handler->SendSysMessage("Apply sql/custom/world/2026_10_26_02_clientless_portal_use.sql to the test world DB first.");
|
||||
return false;
|
||||
}
|
||||
ClientlessPortalUse portal;
|
||||
std::string error;
|
||||
if (!ClientlessTravelGraphMgr::ResolvePortalUse(spawnId, portal, error))
|
||||
{
|
||||
handler->PSendSysMessage("Portal object import rejected: %s.", error.c_str());
|
||||
return false;
|
||||
}
|
||||
// Seed only. An explicit node move or enabled=0 decision must survive a
|
||||
// repeated import; native target drift is rejected when loading the graph.
|
||||
WorldDatabaseTransaction transaction = WorldDatabase.BeginTransaction();
|
||||
for (auto const* node : { &portal.Source, &portal.Target })
|
||||
{
|
||||
transaction->PAppend(
|
||||
"INSERT IGNORE INTO vcore_clientless_world_point "
|
||||
"(point_key,category,map,position_x,position_y,position_z,orientation,enabled,comment) "
|
||||
"VALUES ('%s','travel_portal',%u,%.6f,%.6f,%.6f,%.6f,1,'Interactive portal spawn %u')",
|
||||
node->Key.c_str(), node->MapId, node->X, node->Y, node->Z, node->O, spawnId);
|
||||
transaction->PAppend(
|
||||
"INSERT IGNORE INTO vcore_clientless_travel_node "
|
||||
"(node_key,node_type,world_point_key,enabled,comment) "
|
||||
"VALUES ('%s','portal','%s',1,'Interactive portal spawn %u')",
|
||||
node->Key.c_str(), node->WorldPointKey.c_str(), spawnId);
|
||||
}
|
||||
transaction->PAppend(
|
||||
"INSERT IGNORE INTO vcore_clientless_travel_edge "
|
||||
"(from_node_key,to_node_key,edge_type,cost,bidirectional,enabled,comment) "
|
||||
"VALUES ('%s','%s','portal_use',1,0,1,'Native GameObject portal spawn %u')",
|
||||
portal.Source.Key.c_str(), portal.Target.Key.c_str(), spawnId);
|
||||
transaction->PAppend(
|
||||
"INSERT IGNORE INTO vcore_clientless_portal_use "
|
||||
"(edge_id,gameobject_spawn_id,gameobject_entry,spell_id) "
|
||||
"SELECT id,%u,%u,%u FROM vcore_clientless_travel_edge "
|
||||
"WHERE from_node_key='%s' AND to_node_key='%s' AND edge_type='portal_use'",
|
||||
spawnId, portal.Entry, portal.SpellId, portal.Source.Key.c_str(), portal.Target.Key.c_str());
|
||||
WorldDatabase.DirectCommitTransaction(transaction);
|
||||
sClientlessTravelGraphMgr->Reload();
|
||||
sClientlessWorldPointMgr->Reload();
|
||||
QueryResult registered = WorldDatabase.PQuery(
|
||||
"SELECT id FROM vcore_clientless_travel_edge WHERE from_node_key='%s' "
|
||||
"AND to_node_key='%s' AND edge_type='portal_use'", portal.Source.Key.c_str(), portal.Target.Key.c_str());
|
||||
if (!registered || !sClientlessTravelGraphMgr->GetPortalUse(registered->Fetch()[0].GetUInt32()))
|
||||
{
|
||||
handler->SendSysMessage("Portal object not active in graph. Check database errors, existing enabled flags and reload rejection logs; existing data was not overwritten.");
|
||||
return true;
|
||||
}
|
||||
handler->PSendSysMessage(
|
||||
"Portal object import complete: spawn=%u entry=%u spell=%u %s -> %s (map %u -> %u). "
|
||||
"One-way native click interaction. Existing coordinates and enabled flags preserved; inspect reload logs for rejected edges.",
|
||||
spawnId, portal.Entry, portal.SpellId, portal.Source.Key.c_str(), portal.Target.Key.c_str(),
|
||||
portal.Source.MapId, portal.Target.MapId);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool HandleClientlessPortalImport(ChatHandler* handler, char const* args)
|
||||
{
|
||||
uint32 requested = 0;
|
||||
@@ -2207,6 +2275,9 @@ static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*arg
|
||||
{ "info", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessTaxiInfo, "" },
|
||||
} },
|
||||
{ "portal", rbac::RBAC_PERM_COMMAND_GM, true, nullptr, "", {
|
||||
{ "object", rbac::RBAC_PERM_COMMAND_GM, true, nullptr, "", {
|
||||
{ "import", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessPortalObjectImport, "" },
|
||||
} },
|
||||
{ "import", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessPortalImport, "" },
|
||||
{ "info", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessPortalInfo, "" },
|
||||
} },
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
"""Compile the production object-portal start/update methods against Core doubles."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
source = (ROOT / 'src/server/game/World/ClientlessActionExecutorMgr.cpp').read_text()
|
||||
a = source.index('bool ClientlessActionExecutorMgr::StartPortalUse(')
|
||||
b = source.index('bool ClientlessActionExecutorMgr::StartRecordedEdgePath(', a)
|
||||
methods = source[a:b]
|
||||
harness = r'''
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
using uint32=uint32_t; using uint64=uint64_t;
|
||||
#define TC_LOG_INFO(...) ((void)0)
|
||||
#define TC_LOG_WARN(...) ((void)0)
|
||||
constexpr int GAMEOBJECT_FLAGS=0, GO_FLAG_NOT_SELECTABLE=16, GO_FLAG_LOCKED=2;
|
||||
constexpr int GAMEOBJECT_TYPE_SPELLCASTER=22, CMSG_GAMEOBJ_USE=1;
|
||||
struct WorldPacket { int guid=0; WorldPacket(int,int){} void operator<<(int v){guid=v;} };
|
||||
struct GameObject {
|
||||
int guid=83,entry=183325,flags=0,type=22;
|
||||
bool spawned=true;
|
||||
struct {struct {int spell=17334;} spellcaster;} info;
|
||||
int GetEntry(){return entry;} int GetGUID(){return guid;}
|
||||
bool isSpawned(){return spawned;} bool HasFlag(int,int f){return flags&f;}
|
||||
int GetGoType(){return type;} auto GetGOInfo(){return &info;}
|
||||
};
|
||||
struct Map {
|
||||
std::multimap<uint32,GameObject*> objects;
|
||||
auto const& GetGameObjectBySpawnIdStore(){return objects;}
|
||||
};
|
||||
struct Player;
|
||||
struct Session {
|
||||
Player* player;
|
||||
bool clientless=true,accept=true,ackWorks=true,far=true,wrongTarget=false;
|
||||
int clicks=0,acks=0; uint64 sequence=0;
|
||||
Player* GetPlayer(){return player;}
|
||||
bool IsClientless(){return clientless;}
|
||||
uint64 GetClientlessTeleportSequence(){return sequence;}
|
||||
void HandleGameObjectUseOpcode(WorldPacket&);
|
||||
void CompleteClientlessTeleport();
|
||||
};
|
||||
using WorldSession=Session;
|
||||
struct Player {
|
||||
int mapId=530;float x=-1894,y=5387,z=-12;
|
||||
bool alive=true,pending=false,world=true,phase=true,visible=true,inRange=true,aboard=false;
|
||||
Map map;Session session{this};GameObject go;
|
||||
Player(){map.objects.emplace(83,&go);}
|
||||
int GetMapId(){return mapId;}bool IsAlive(){return alive;}
|
||||
bool IsBeingTeleported(){return pending;}bool IsInWorld(){return world;}
|
||||
void* GetTransport(){return aboard?this:nullptr;}
|
||||
Session* GetSession(){return &session;}Map* GetMap(){return ↦}
|
||||
bool IsInPhase(GameObject*){return phase;}bool CanSeeOrDetect(GameObject*){return visible;}
|
||||
GameObject* GetGameObjectIfCanInteractWith(int guid){return inRange&&guid==go.guid?&go:nullptr;}
|
||||
float GetPositionX(){return x;}float GetPositionY(){return y;}float GetPositionZ(){return z;}
|
||||
std::string GetName(){return "Arthoran";}void StopMoving(){}
|
||||
};
|
||||
void Session::HandleGameObjectUseOpcode(WorldPacket& packet) {
|
||||
++clicks;assert(packet.guid==83);
|
||||
if(accept){player->pending=true;if(far)player->world=false;}
|
||||
}
|
||||
void Session::CompleteClientlessTeleport(){
|
||||
++acks;if(!ackWorks||!player->pending)return;
|
||||
player->pending=false;player->world=true;++sequence;
|
||||
player->mapId=wrongTarget?1:0;player->x=-8998;player->y=861;player->z=29;
|
||||
}
|
||||
struct Node {std::string Key;uint32 MapId=530;};
|
||||
struct Portal {uint32 SpawnId=83,Entry=183325,SpellId=17334;Node Source;};
|
||||
struct Graph {bool available=true;Portal portal;auto GetPortalUse(uint32){return available?&portal:nullptr;}} graph;
|
||||
struct Travel {void CancelTravel(uint32){}} travel;
|
||||
struct Lua {void DismountGroundMount(Player*,const char*){}} lua;
|
||||
auto sClientlessTravelGraphMgr=&graph;auto sClientlessTravelMgr=&travel;auto sClientlessLuaEngine=&lua;
|
||||
enum class ClientlessActionExecutionState {Moving,Failed,Completed};
|
||||
struct Step {uint32 EdgeDatabaseId=9,MapId=0;std::string NodeKey="portal_go_83_target";bool EdgeReverse=false;float X=-8998,Y=861,Z=29;};
|
||||
struct Record {
|
||||
std::vector<Step> Route{{},{}};size_t RouteStep=1;
|
||||
bool PortalUseActive=false,Active=true;uint64 PortalUseTeleportSequence=0;
|
||||
uint32 PortalUseElapsedMs=0,PortalUseSpawnId=0,RecoveryCount=0;
|
||||
std::vector<int> EdgePath;
|
||||
struct {ClientlessActionExecutionState State=ClientlessActionExecutionState::Moving;std::string TargetName,Detail;} Status;
|
||||
};
|
||||
struct ClientlessActionExecutorMgr {
|
||||
int continued=0;
|
||||
bool StartPortalUse(Player*,uint32,Record&);
|
||||
void UpdatePortalUse(Player*,uint32,Record&);
|
||||
bool StartRouteStep(Player*,uint32,Record& r){++continued;r.Active=false;r.Status.State=ClientlessActionExecutionState::Completed;return true;}
|
||||
};
|
||||
'''
|
||||
harness += methods
|
||||
harness += r'''
|
||||
int main(){
|
||||
ClientlessActionExecutorMgr mgr;
|
||||
{Player p;Record r;assert(mgr.StartPortalUse(&p,26,r));assert(p.session.clicks==1&&!p.world);
|
||||
mgr.UpdatePortalUse(&p,26,r);assert(!r.PortalUseActive&&r.RouteStep==2&&p.mapId==0&&p.session.sequence==1);}
|
||||
// The global lifecycle may have ACKed a delayed cast before our next update.
|
||||
{Player p;Record r;assert(mgr.StartPortalUse(&p,26,r));p.session.CompleteClientlessTeleport();
|
||||
mgr.UpdatePortalUse(&p,26,r);assert(r.RouteStep==2&&p.session.acks==1);}
|
||||
{Player p;p.session.far=false;Record r;assert(mgr.StartPortalUse(&p,26,r));
|
||||
mgr.UpdatePortalUse(&p,26,r);assert(r.RouteStep==2);}
|
||||
// Denied native click/cast, even when already standing at the expected target.
|
||||
{Player p;p.session.accept=false;p.mapId=530;p.x=-8998;p.y=861;p.z=29;
|
||||
Record r;r.Route[1].MapId=530;assert(mgr.StartPortalUse(&p,26,r));
|
||||
mgr.UpdatePortalUse(&p,26,r);assert(r.RouteStep==1&&r.Active);
|
||||
r.PortalUseElapsedMs=10000;mgr.UpdatePortalUse(&p,26,r);assert(!r.Active&&r.RouteStep==1);}
|
||||
{Player p;p.session.ackWorks=false;Record r;assert(mgr.StartPortalUse(&p,26,r));
|
||||
r.PortalUseElapsedMs=10000;mgr.UpdatePortalUse(&p,26,r);assert(!r.Active&&p.pending&&r.RouteStep==1);}
|
||||
{Player p;p.session.wrongTarget=true;Record r;assert(mgr.StartPortalUse(&p,26,r));
|
||||
r.PortalUseElapsedMs=10000;mgr.UpdatePortalUse(&p,26,r);assert(!r.Active&&r.RouteStep==1);}
|
||||
{Player p;Record r;assert(mgr.StartPortalUse(&p,26,r));p.session.CompleteClientlessTeleport();p.x+=6;
|
||||
r.PortalUseElapsedMs=10000;mgr.UpdatePortalUse(&p,26,r);assert(!r.Active&&r.RouteStep==1);}
|
||||
for(int kind=0;kind<12;++kind){
|
||||
Player p;Record r;graph.available=true;
|
||||
switch(kind){case 0:graph.available=false;break;case 1:r.RouteStep=0;break;
|
||||
case 2:r.Route[1].EdgeReverse=true;break;case 3:p.mapId=1;break;
|
||||
case 4:p.alive=false;break;case 5:p.pending=true;break;case 6:p.aboard=true;break;
|
||||
case 7:p.session.clientless=false;break;case 8:p.map.objects.clear();break;
|
||||
case 9:p.phase=false;break;case 10:p.inRange=false;break;case 11:p.go.flags=GO_FLAG_NOT_SELECTABLE;break;}
|
||||
assert(!mgr.StartPortalUse(&p,26,r));assert(p.session.clicks==0&&!r.Active);
|
||||
}
|
||||
graph.available=true;
|
||||
{Player p;p.go.info.spellcaster.spell=1;Record r;assert(!mgr.StartPortalUse(&p,26,r));assert(!p.session.clicks);}
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-portal-use-test-') as folder:
|
||||
cpp, binary = Path(folder)/'test.cpp', Path(folder)/'test'
|
||||
cpp.write_text(harness)
|
||||
subprocess.run(['c++', '-std=c++17', str(cpp), '-o', str(binary)], check=True)
|
||||
subprocess.run([str(binary)], check=True)
|
||||
print('Interactive portal native click, near/far/global ACK, denial, timeout and target checks passed.')
|
||||
|
||||
# Exercise native destination discovery rather than duplicating the importer.
|
||||
graph_source = (ROOT / 'src/server/game/World/ClientlessTravelGraphMgr.cpp').read_text()
|
||||
a = graph_source.index('bool ClientlessTravelGraphMgr::ResolvePortalUse(')
|
||||
b = graph_source.index('ClientlessPortalUse const* ClientlessTravelGraphMgr::GetPortalUse', a)
|
||||
definition_method = graph_source[a:b]
|
||||
definition_harness = r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
using uint8=uint8_t;using uint32=uint32_t;
|
||||
constexpr uint32 MAX_SPELL_EFFECTS=3,GAMEOBJECT_TYPE_SPELLCASTER=22,SPELL_EFFECT_TELEPORT_UNITS=5,TARGET_DEST_DB=17;
|
||||
using SpellEffIndex=uint8;
|
||||
struct Position {float GetPositionX()const{return -1894;}float GetPositionY()const{return 5387;}float GetPositionZ()const{return -12;}float GetOrientation()const{return 0;}};
|
||||
struct Spawn {uint32 id=183325,mapId=530;Position spawnPoint;} spawn;
|
||||
struct Info {uint32 type=22;struct{uint32 spell=17334;}spellcaster;} info;
|
||||
struct ObjectMgr {bool hasSpawn=true;auto GetGameObjectData(uint32){return hasSpawn?&spawn:nullptr;}auto GetGameObjectTemplate(uint32){return &info;}} objects;
|
||||
struct Target{uint32 id=0;uint32 GetTarget()const{return id;}};
|
||||
struct Effect{uint32 Effect=0;Target TargetA,TargetB;};
|
||||
struct SpellInfo{uint32 Id=17334;Effect Effects[3];} spell;
|
||||
struct SpellTargetPosition {uint32 target_mapId=0;float target_X=-8998,target_Y=861,target_Z=29,target_Orientation=2;} destination;
|
||||
struct SpellMgr {bool hasSpell=true,hasTarget=true;auto GetSpellInfo(uint32){return hasSpell?&spell:nullptr;}auto GetSpellTargetPosition(uint32,SpellEffIndex){return hasTarget?&destination:nullptr;}} spells;
|
||||
auto sObjectMgr=&objects;auto sSpellMgr=&spells;
|
||||
struct Node {std::string Key,Type,WorldPointKey;uint32 MapId=0;float X=0,Y=0,Z=0,O=0;};
|
||||
struct ClientlessPortalUse {uint32 SpawnId=0,Entry=0,SpellId=0;Node Source,Target;};
|
||||
struct ClientlessTravelGraphMgr{static bool ResolvePortalUse(uint32,ClientlessPortalUse&,std::string&);};
|
||||
'''
|
||||
definition_harness += definition_method
|
||||
definition_harness += r'''
|
||||
int main(){
|
||||
spell.Effects[0].Effect=5;spell.Effects[0].TargetB.id=17;
|
||||
ClientlessPortalUse p;std::string error;
|
||||
assert(ClientlessTravelGraphMgr::ResolvePortalUse(83,p,error));
|
||||
assert(p.SpawnId==83&&p.Entry==183325&&p.SpellId==17334&&p.Source.MapId==530&&p.Target.MapId==0);
|
||||
assert(p.Source.Key=="portal_go_83_source"&&p.Target.Key=="portal_go_83_target"&&p.Target.X==-8998);
|
||||
objects.hasSpawn=false;assert(!ClientlessTravelGraphMgr::ResolvePortalUse(83,p,error));objects.hasSpawn=true;
|
||||
info.type=10;assert(!ClientlessTravelGraphMgr::ResolvePortalUse(83,p,error));info.type=22;
|
||||
spells.hasSpell=false;assert(!ClientlessTravelGraphMgr::ResolvePortalUse(83,p,error));spells.hasSpell=true;
|
||||
spells.hasTarget=false;assert(!ClientlessTravelGraphMgr::ResolvePortalUse(83,p,error));spells.hasTarget=true;
|
||||
spell.Effects[0].TargetB.id=0;assert(!ClientlessTravelGraphMgr::ResolvePortalUse(83,p,error));spell.Effects[0].TargetB.id=17;
|
||||
spell.Effects[1].Effect=5;spell.Effects[1].TargetB.id=17;
|
||||
assert(!ClientlessTravelGraphMgr::ResolvePortalUse(83,p,error));spell.Effects[1]={};
|
||||
spell.Effects[0].Effect=77;assert(!ClientlessTravelGraphMgr::ResolvePortalUse(83,p,error));
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-portal-definition-test-') as folder:
|
||||
cpp, binary = Path(folder)/'test.cpp', Path(folder)/'test'
|
||||
cpp.write_text(definition_harness)
|
||||
subprocess.run(['c++', '-std=c++17', str(cpp), '-o', str(binary)], check=True)
|
||||
subprocess.run([str(binary)], check=True)
|
||||
print('Native portal destination discovery and unsupported/ambiguous spell rejection passed.')
|
||||
|
||||
movement_source = (ROOT / 'src/server/game/Handlers/MovementHandler.cpp').read_text()
|
||||
a = movement_source.index('void WorldSession::CompleteClientlessTeleport()')
|
||||
b = movement_source.index('void WorldSession::CompleteNearTeleport(', a)
|
||||
ack_harness = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
struct Player {bool pending=false,far=false,world=true;bool IsBeingTeleported(){return pending;}bool IsBeingTeleportedFar(){return pending&&far;}bool IsBeingTeleportedNear(){return pending&&!far;}bool IsInWorld(){return world;}};
|
||||
struct WorldSession {
|
||||
Player* player;bool clientless=true,works=true;int near=0,far=0;uint64_t _clientlessTeleportSequence=0;
|
||||
bool IsClientless(){return clientless;}Player* GetPlayer(){return player;}
|
||||
void HandleMoveWorldportAck(){++far;if(works){player->pending=false;player->world=true;}}
|
||||
void CompleteNearTeleport(Player* p){++near;if(works)p->pending=false;}
|
||||
void CompleteClientlessTeleport();
|
||||
};
|
||||
'''
|
||||
ack_harness += movement_source[a:b]
|
||||
ack_harness += r'''
|
||||
int main(){
|
||||
Player p;WorldSession s{&p};s.CompleteClientlessTeleport();assert(s._clientlessTeleportSequence==0);
|
||||
p.pending=true;s.CompleteClientlessTeleport();assert(s.near==1&&s._clientlessTeleportSequence==1);
|
||||
s.CompleteClientlessTeleport();assert(s._clientlessTeleportSequence==1);
|
||||
p.pending=p.far=true;p.world=false;s.CompleteClientlessTeleport();assert(s.far==1&&s._clientlessTeleportSequence==2);
|
||||
p.pending=true;s.works=false;s.CompleteClientlessTeleport();assert(s._clientlessTeleportSequence==2&&p.pending);
|
||||
s.clientless=false;s.works=true;s.CompleteClientlessTeleport();assert(p.pending&&s._clientlessTeleportSequence==2);
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-portal-ack-test-') as folder:
|
||||
cpp, binary = Path(folder)/'test.cpp', Path(folder)/'test'
|
||||
cpp.write_text(ack_harness)
|
||||
subprocess.run(['c++', '-std=c++17', str(cpp), '-o', str(binary)], check=True)
|
||||
subprocess.run([str(binary)], check=True)
|
||||
print('Production clientless ACK completion sequence increments only after completed near/far transfers.')
|
||||
Reference in new issue
Block a user