Validate profession scenes and fix native workstation and area targeting
This commit is contained in:
1 parent
722b513ee0
commit
440c53acd7
17 files changed
+425
-13
No files matched your search
@@ -1,3 +1,54 @@
|
||||
# Fortsetzung am 27.10.2026 – aktueller Stand hat Vorrang
|
||||
|
||||
Der unten erhaltene Sitzungswechsel-Checkpoint ist historisch. Der Berufs-Ausbau
|
||||
ist jetzt vollständig gebaut (`-j10`), auf dem Testserver installiert und die
|
||||
Migration `_05` ist auf der Test-World-DB angewendet. **37 Testskripte bestanden**.
|
||||
Keine Änderungen wurden gepusht. Branch bleibt `feature/clientless-player-core`.
|
||||
|
||||
Seit dem Checkpoint behoben:
|
||||
- Berufsarbeit an GOs nähert sich nach Verdrängung erneut, bevor weitere Gesten laufen.
|
||||
- Native unsichtbare Arbeitsplatzmarker verwenden VMAP-Sichtprüfung zur Arbeitsfläche
|
||||
(mindestens 1,5 Yards über dem Marker); Bodenmarker im Amboss blockierten zuvor die Szene.
|
||||
- Location-Abfrage liest numerische native Spalten ohne COALESCE-Widening. Debug verlor
|
||||
zuvor `is_area`/`faction`; Elwynn wird live jetzt korrekt als `area=1` geladen.
|
||||
|
||||
Zusätzliche Tests: echter Lua-5.4-Professionsparser mit Arthoran/generic und ungültigen
|
||||
Profilen; native Lock-/Station-/Kadaver-/Trainerklassifikation; Szene erst nach Ankunft,
|
||||
Profilentzug, Wiederannäherung; SQL-Fixtures mit erhaltenen Gewichten/Freigaben;
|
||||
erreichbare Gebietsknoten; progressive Suche und Uferbedingungen; echte Debug-Fieldgetter
|
||||
und VMAP-Sichtlinienadapter. Siehe `tests/clientless/test_adventure_profession_*.py`,
|
||||
`test_adventure_area_node.py` und erweiterte bestehende Harnesses.
|
||||
|
||||
Live bestätigt:
|
||||
- Frische Ready-Meldung, laufender Prozess und Port 8085 nach Installation.
|
||||
- Arthoran lädt zwei Hauptberufe; Profil ist bytegleich wiederhergestellt:
|
||||
`social` / `goldshire_tavern`, mining + blacksmithing. Kein temporäres Profil liegenlassen.
|
||||
- Reguläre Anreise zu `poi_guard_541` über Graph, Taxi und Bodenwege.
|
||||
- `smithing_show` wählt echtes Anvil-GO **201415**, Low **556**; Work-Emote **233**;
|
||||
`PROFESSION_WORK_COMPLETE` und `PROFESSION_VISIT_COMPLETE` nach Abschluss.
|
||||
- `profession_herbalism` live als nicht verfügbar zurückgewiesen. Die isolierte
|
||||
Profilprüfung auch bei vorhandener Pflanze ist durch den produktiven Zieltest belegt.
|
||||
- Nach `saveall` unveränderte Geld-/Inventar-Prüfsumme:
|
||||
`8d40931782d05219466ba9fd077ce7acd3e02bd53eb63f6e96115042c620d26a`.
|
||||
- Elwynn-Auftrag `profession poi_tele_338` lädt `area=1`, startet `travel_to_node`,
|
||||
erreicht mit bestehendem Free-flight-Fallback den Gebietsknoten und meldet `INTENT_ARRIVED`.
|
||||
Lokale `profession_search` absolvierte danach alle sechs progressiven MMAP-Wegpunkte
|
||||
(ca. 104 Yards, `LEISURE_COMPLETE`, legs=6).
|
||||
- Letzter geprüfter Testserverprozess: PID 28577, frische Ready-Meldung und Port 8085.
|
||||
Beim Wiederaufnehmen immer den tatsächlichen Zustand neu prüfen.
|
||||
|
||||
Offener Befund: Der erste Intentwechsel während aktiver Goldshire-Anreise geriet in
|
||||
Graph-Handover-/Recovery-Schleifen zum nächstgelegenen `portal_at_109_target`.
|
||||
Für die gezielten Tests wurde deshalb nur beim Login die Startvorgabe temporär
|
||||
geändert und danach bytegleich restauriert. Dieses Travel-Handover ist NICHT als
|
||||
behoben zu betrachten. Navigationslog enthält die Vorgänge ab 17:43.
|
||||
|
||||
Dokumentation: [profession-intent.md](profession-intent.md). Visuelle Abnahme bleibt
|
||||
beim Nutzer; Freizeit-Ausritt/Rundflug und Schlafdarstellung bleiben zurückgestellt.
|
||||
Alle ursprünglichen Regeln und fachlichen Festlegungen unten gelten weiterhin.
|
||||
|
||||
---
|
||||
|
||||
# Gesicherter Arbeitsstand – hier fortsetzen
|
||||
|
||||
Stand: 27.10.2026. Der Nutzer hat die Implementierung ausdrücklich für einen
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
TrinityCore rev. 722b513ee01b+ 2026-10-27 17:02:01 +0100 (feature/clientless-player-core branch) (Unix, Debug, Static) (worldserver-daemon) ready...
|
||||
PROFILE_PROFESSIONS guid=26 primary=2
|
||||
ClientlessLua[26]: Arthoran ready; no automatic test journey queued
|
||||
INTENT_TRAVEL guid=26 intent='profession' location='poi_tele_338' area=1
|
||||
INTENT_ARRIVED guid=26 intent='profession' location='poi_tele_338'
|
||||
PROFESSION_WORK_COMPLETE guid=26 kind='profession_tools' target=''
|
||||
LEISURE_WALK_PLAN guid=26 kind='work_search' span=103.64 outbound=6 origin=(-8888.98 -0.54 94.39) goal=(-8978.85 -52.09 91.55)
|
||||
LEISURE_WAYPOINT guid=26 kind='work_search' reached=1/6
|
||||
LEISURE_WAYPOINT guid=26 kind='work_search' reached=2/6
|
||||
LEISURE_WAYPOINT guid=26 kind='work_search' reached=3/6
|
||||
LEISURE_WAYPOINT guid=26 kind='work_search' reached=4/6
|
||||
LEISURE_WAYPOINT guid=26 kind='work_search' reached=5/6
|
||||
LEISURE_WAYPOINT guid=26 kind='work_search' reached=6/6
|
||||
LEISURE_COMPLETE guid=26 kind='work_search' legs=6
|
||||
@@ -34,7 +34,9 @@ Der bestehende Verhaltensmanager kennt aktuell die technischen Intentnamen
|
||||
`social`, `adventure`, `profession`, `rest`, `leisure` und `economy`. Der [Ruhe-Intent](rest-intent.md)
|
||||
ist ergänzt, ebenso der [Freizeit-Intent](leisure-intent.md) und der
|
||||
[Wirtschaften-Intent](economy-intent.md). Der [Abenteuer-Ausbau](adventure-intent.md)
|
||||
ergänzt Erkundung und Objektuntersuchung. Instanzbesuche bleiben konzeptionell
|
||||
ergänzt Erkundung und Objektuntersuchung. Der [Berufs-Ausbau](profession-intent.md)
|
||||
ergänzt profilgebundene Arbeitsdarstellungen, echte Ressourcen-/Arbeitsplatzprüfung
|
||||
und progressive Gebietssuche. Instanzbesuche bleiben konzeptionell
|
||||
festgehalten; ihr technischer Name und Detailablauf sind noch nicht umgesetzt. Der ausführliche bisherige
|
||||
[Mikroaktionskatalog](mikroaktions-katalog.md) bleibt als Grundlage erhalten.
|
||||
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
27.10.2026 – Berufs-Ausbau fortgesetzt, gebaut und auf Testserver installiert.
|
||||
Migration _05 angewendet; 37 Testskripte bestanden. Native Arbeitsplatz-Sichtlinie
|
||||
und numerische Location-Feldtypen korrigiert. Live: echter Amboss 201415,
|
||||
Work-Emote 233, Abschluss/Wegtreten; Kräuteraktion verweigert; Geld/Inventar
|
||||
unverändert. Elwynn als Gebiet über travel_to_node erreicht, sechs lokale
|
||||
progressive Suchwegpunkte vollständig absolviert. Visuelle Abnahme und echtes
|
||||
Ressourcenziel in freier Wildbahn offen. Separater Travel-Handover-Loop dokumentiert.
|
||||
Details: profession-intent.md und SESSION-RESUME.md. Kein Push.
|
||||
|
||||
› OK stop, er findet ja den weg schon wieder nicht. Es sieht so aus, als würde er den virtuellen entry auf der Kante sturmwind Hafen - sturmwind gate verwenden. Er reitet diese auch lang aber nach einem Stück bricht er ab. Warum? Er mü…
|
||||
|
||||
1. Der Planer bewertet den restlichen Weg zur Taverne zunächst nur anhand der Luftlinienentfernung. Ob MMAP ihn tatsächlich laufen kann, prüft er erst am Ausstieg.
|
||||
|
||||
@@ -7,8 +7,8 @@ ergänzt. Die neuen Detailvorschläge sind eine Arbeitsgrundlage zur Abstimmung.
|
||||
|
||||
Der bisherige Katalog vom 26.10.2026 beschreibt die vereinbarten Aktionen.
|
||||
Geselligkeitsaktionen sowie die Ausbaustufen für Ruhe, Freizeit, Wirtschaften
|
||||
und [Abenteuer-Erkundung](adventure-intent.md) sind implementiert. Weitere
|
||||
Berufsaktionen und echte Kampfhandlungen bleiben Ausbaustufen. Den geprüften Implementierungsstand
|
||||
und [Abenteuer-Erkundung](adventure-intent.md) sind implementiert. Der [Berufs-Ausbau](profession-intent.md) ergänzt profilgebundene, dauerhaft
|
||||
performative Arbeit an realen Zielen. Echte Kampfhandlungen bleiben eine spätere Ausbaustufe. Den geprüften Implementierungsstand
|
||||
beschreibt [autonomous-behavior.md](autonomous-behavior.md).
|
||||
|
||||
Die Aktionen werden nach den sieben Intents gewichtet und über
|
||||
@@ -107,7 +107,7 @@ den Abenteuer-Intent tragen.
|
||||
|
||||
Berufsaktionen sollten sich nach dem Charakterprofil richten. Ein angehender
|
||||
Schmied besucht andere Orte und Objekte als ein Kräuterkundiger. Die eigentliche
|
||||
Arbeit bleibt wie besprochen zunächst Darstellung.
|
||||
Arbeit bleibt dauerhaft rein performativ; Hauptberufe benötigen eine ausdrückliche Lua-Profilfreigabe.
|
||||
|
||||
| Aktion | Darstellung und Bedingungen |
|
||||
|---|---|
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Beruf (`profession`)
|
||||
|
||||
Berufsaktionen bleiben dauerhaft rein performativ. Der Bot bewegt sich mit
|
||||
TravelGraph und MMAP zu echten Ressourcen, Arbeitsplätzen oder Ansprechpartnern
|
||||
und stellt die Tätigkeit mit nativen Gesten dar. Er benutzt keine Quellen,
|
||||
erzeugt keine Beute, verarbeitet keine Materialien, erhöht keine Fertigkeiten,
|
||||
lernt nichts und heilt nicht tatsächlich.
|
||||
|
||||
Hauptberufe werden ausschließlich im Lua-Profil über `professions.primary`
|
||||
freigegeben. Arthoran besitzt `mining` und `blacksmithing`; das generische Profil
|
||||
besitzt keine Hauptberufe. Unbekannte Namen gewähren keine Berechtigung.
|
||||
Nebenberufe benötigen keine Hauptberufsfreigabe. Die Freigabe wird auch während
|
||||
einer laufenden Szene geprüft.
|
||||
|
||||
Der Tagesplan wählt den Intent unabhängig vom Ortsbestand. Danach wird ein
|
||||
geeigneter importierter Ort oder ein Gebiet ausgewählt. Erst vor Ort bestimmen
|
||||
reale Ziele und Profilrechte die möglichen Mikroaktionen. Eine Workshop- oder
|
||||
Gebietszuordnung ersetzt keinen Amboss und gewährt keine Hauptberufe.
|
||||
|
||||
Erz und Pflanzen werden anhand nativer Lock-Skills erkannt, nicht anhand ihrer
|
||||
Namen. Quellen müssen gespawnt, bereit, sichtbar und in der passenden Phase sein.
|
||||
Kürschnerei setzt einen vorhandenen toten, kürschnerbaren Tierkadaver voraus.
|
||||
Arbeitsplatzrollen stehen im editierbaren Katalog `vcore_adventure_workstation`.
|
||||
Die Migration `2026_10_27_05_adventure_profession.sql` ergänzt native Fokusrollen
|
||||
und erkennbare Arbeitsobjekte. Amboss und Esse haben getrennte Rollen; deaktivierte
|
||||
Rollen und bearbeitete Gewichte bleiben bei erneuter Migration erhalten.
|
||||
Trainer werden anhand nativer Tradeskill- und SkillLine-Daten zugeordnet.
|
||||
|
||||
Eine Szene nähert sich dem Ziel über normale Navigation, zeigt die Arbeit erst
|
||||
nach bestätigter Ankunft, betrachtet das Ergebnis und tritt kurz zurück.
|
||||
Verlust von Profilrecht, Ziel oder Arbeitsplatz beendet die Darstellung.
|
||||
Kürzlich vollständig dargestellte Sammelziele werden über die Zielhistorie
|
||||
vorübergehend gemieden. Reines Erzbetrachten setzt diese Bearbeitungssperre nicht.
|
||||
|
||||
Gebietsaufträge reisen zu einem erreichbaren Graphknoten im Zielgebiet. Die lokale
|
||||
Suche nutzt fortschreitende Bodenrouten und prüft an bestätigten Wegpunkten erneut
|
||||
auf zulässige Arbeit. Nach abgeschlossenem Fortschritt verschiebt sich der lokale
|
||||
Anker. Angeln verlangt eine ausgerüstete Angel und tatsächliches nahes Wasser;
|
||||
es wird kein Angelzauber ausgeführt und kein Fisch gefangen.
|
||||
|
||||
Automatisierte Prüfungen führen den produktiven Profilparser mit Lua 5.4,
|
||||
die native Zielklassifikation und die Szenensteuerung mit Server-Doubles aus.
|
||||
Katalogfixtures prüfen Wiederholbarkeit und Erhalt bearbeiteter Einstellungen.
|
||||
Diese Prüfungen ersetzen nicht die visuelle Abnahme im Spiel.
|
||||
|
||||
## Technischer Teststand am 27.10.2026
|
||||
|
||||
37 Clientless-Testskripte bestanden; Debug-Build mit `-j10` installiert,
|
||||
Migration `_05` auf der Test-DB angewendet. Frische Ready-Meldung, Prozess und
|
||||
Port 8085 geprüft. Arthoran erreichte den importierten Schmiedeort regulär
|
||||
über Graph, Taxi und MMAP und stellte am echten Amboss 201415 (Low 556)
|
||||
`smithing_show` dar. Work-Emote 233, Ergebnisbetrachtung und Wegtreten wurden
|
||||
im Serverlog bestätigt. Eine angeforderte Kräuterszene wurde zurückgewiesen.
|
||||
Nach `saveall` blieb die Geld-/Inventar-Prüfsumme unverändert.
|
||||
|
||||
Elwynn (`poi_tele_338`) wurde als Gebiet geladen, über `travel_to_node` samt
|
||||
bestehendem Free-flight-Fallback erreicht und mit `INTENT_ARRIVED` bestätigt.
|
||||
Die anschließende lokale `profession_search` absolvierte alle sechs progressiven
|
||||
MMAP-Wegpunkte mit etwa 104 Yards Ausdehnung (`LEISURE_COMPLETE`, legs=6). Ressourcenabbau in freier Wildbahn,
|
||||
weitere Berufe und die sichtbare Darstellung sind damit noch nicht live abgenommen.
|
||||
|
||||
Zwei im Test gefundene Fehler wurden behoben: VMAP-Sichtlinien zu unsichtbaren
|
||||
Arbeitsplatzmarkern prüfen jetzt die Arbeitsfläche oberhalb des Bodenmarkers;
|
||||
die Location-Abfrage erhält die nativen numerischen Spaltentypen, damit Debug
|
||||
Gebietsflag und Fraktion korrekt liest. Ein während aktiver Reise beobachteter
|
||||
Graph-Handover-/Recovery-Loop zu `portal_at_109_target` bleibt ein separater
|
||||
unbehobener Befund.
|
||||
@@ -29,7 +29,7 @@ bool AdventureLocationMgr::Reload()
|
||||
_dialogs.clear();
|
||||
_loaded = false;
|
||||
if (! WorldDatabase.Query("SHOW TABLES LIKE 'vcore_adventure_location'")) return false;
|
||||
if (auto rows = WorldDatabase.Query("SELECT w.id,w.point_key,w.category,w.map,w.position_x,w.position_y,w.position_z,w.orientation," "COALESCE(a.display_name,w.point_key),COALESCE(a.region_key,''),COALESCE(a.area_id,0)," "COALESCE(a.zone_id,0),COALESCE(a.is_area,0),COALESCE(a.faction,0) " "FROM vcore_clientless_world_point w LEFT JOIN vcore_adventure_location a ON a.point_key=w.point_key " "WHERE w.enabled=1 AND w.category NOT LIKE 'travel%' ORDER BY w.point_key"))
|
||||
if (auto rows = WorldDatabase.Query("SELECT w.id,w.point_key,w.category,w.map,w.position_x,w.position_y,w.position_z,w.orientation," "COALESCE(a.display_name,w.point_key),COALESCE(a.region_key,''),a.area_id," "a.zone_id,a.is_area,a.faction " "FROM vcore_clientless_world_point w LEFT JOIN vcore_adventure_location a ON a.point_key=w.point_key " "WHERE w.enabled=1 AND w.category NOT LIKE 'travel%' ORDER BY w.point_key"))
|
||||
{
|
||||
do
|
||||
{
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace
|
||||
// participants can see each other's head. Keep native VMAP geometry.
|
||||
return player->GetMap()->isInLineOfSight(player->GetPhaseShift(),
|
||||
player->GetPositionX(),player->GetPositionY(),player->GetPositionZ()+player->GetCollisionHeight(),
|
||||
target->GetPositionX(),target->GetPositionY(),target->GetPositionZ()+target->GetCollisionHeight(),
|
||||
target->GetPositionX(),target->GetPositionY(),target->GetPositionZ()+(AdventureProfessionPolicy::Recognized(kind) && target->ToGameObject()?std::max(1.5f,target->GetCollisionHeight()):target->GetCollisionHeight()),
|
||||
LINEOFSIGHT_ALL_CHECKS,VMAP::ModelIgnoreFlags::Nothing);
|
||||
}
|
||||
std::vector < Unit * > People(Player * player, std::string const & kind, AdventureSocialContext & c)
|
||||
@@ -189,7 +189,7 @@ namespace
|
||||
if(floor<=INVALID_HEIGHT+1||std::fabs(floor-target->GetPositionZ())>3)continue;
|
||||
p.m_positionZ=floor;
|
||||
if(!player->GetMap()->isInLineOfSight(player->GetPhaseShift(),p.GetPositionX(),p.GetPositionY(),floor+((AdventureEconomyPolicy::Economic(scene.Kind) || AdventureProfessionPolicy::Recognized(scene.Kind))?player->GetCollisionHeight():1.5f),
|
||||
target->GetPositionX(),target->GetPositionY(),target->GetPositionZ()+((AdventureEconomyPolicy::Economic(scene.Kind) || AdventureProfessionPolicy::Recognized(scene.Kind))?target->GetCollisionHeight():1.5f),
|
||||
target->GetPositionX(),target->GetPositionY(),target->GetPositionZ()+(AdventureProfessionPolicy::Recognized(scene.Kind) && target->ToGameObject()?std::max(1.5f,target->GetCollisionHeight()):AdventureEconomyPolicy::Economic(scene.Kind)?target->GetCollisionHeight():1.5f),
|
||||
LINEOFSIGHT_ALL_CHECKS,VMAP::ModelIgnoreFlags::Nothing))continue;
|
||||
if(!c.Move(p,(AdventureEconomyPolicy::Economic(scene.Kind) || AdventureExplorationPolicy::Inspection(scene.Kind) || AdventureProfessionPolicy::Recognized(scene.Kind))?350:55))continue;
|
||||
scene.Destination=p;scene.Approaching=true;return true;
|
||||
@@ -566,6 +566,13 @@ bool AdventureSocialActions::Update(Player * p, AdventureSocialScene & s, uint32
|
||||
{
|
||||
if (! InBounds(p, target, c)) return false;
|
||||
if(AdventureProfessionPolicy::Recognized(s.Kind) && !AdventureProfessionTargets::Valid(p,target,s.Kind,c))return false;
|
||||
if (AdventureProfessionPolicy::Recognized(s.Kind) && target->ToGameObject() && !s.Approaching &&
|
||||
(p->GetExactDist(target)>4 || !TargetVisible(p,target,s.Kind)))
|
||||
{
|
||||
if (++s.Repaths>3 || !Approach(p,s,target,c)) return false;
|
||||
// Do not advance a work cue while returning to the workplace.
|
||||
return true;
|
||||
}
|
||||
if (AdventureExplorationPolicy::Inspection(s.Kind))
|
||||
{
|
||||
auto object=target->ToGameObject();
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Area destinations select a reachable native graph node inside the area."""
|
||||
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('std::string AdventureBehaviorMgr::AreaNode(');b=s.index('\nvoid AdventureBehaviorMgr::Begin',a)
|
||||
code=r'''
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
using uint32=uint32_t;
|
||||
struct Position {float x=0;void Relocate(float a,float,float){x=a;}float GetPositionX(){return x;}float GetPositionY(){return 0;}};
|
||||
struct Player{int map=0;int GetPhaseShift(){return 0;}int GetMapId(){return map;}float GetExactDist(Position p){return p.x;}};
|
||||
struct AdventureLocation{struct{int MapId=0;float X=0,Y=0;}Point;uint32 AreaId=12;};
|
||||
struct Field{float value=0;std::string text;float GetFloat(){return value;}std::string GetString(){return text;}};
|
||||
struct Rows{std::vector<std::vector<Field>> data;unsigned row=0;operator bool(){return !data.empty();}Rows* operator->(){return this;}Field* Fetch(){return data[row].data();}bool NextRow(){return ++row<data.size();}};
|
||||
struct Database{Rows rows;Rows PQuery(char const*,int){return rows;}}WorldDatabase;
|
||||
struct Terrain{void GetZoneAndAreaId(int,int,uint32&zone,uint32&area,Position p){zone=p.x==1?99:12;area=zone;}}terrain;
|
||||
struct TerrainMgr{bool available=true;Terrain* LoadTerrain(int){return available?&terrain:nullptr;}}sTerrainMgr;
|
||||
struct ClientlessTravelRouteStep{};
|
||||
struct Graph{std::vector<std::string> tried;bool PlanAdaptiveRoute(Player*,std::string const&key,int,int,std::vector<ClientlessTravelRouteStep>&,bool& complete,std::string&,std::string&){tried.push_back(key);complete=key=="reachable";return true;}}graph;auto sClientlessTravelGraphMgr=&graph;
|
||||
struct AdventureBehaviorMgr{std::string AreaNode(Player*,AdventureLocation const&);};
|
||||
'''+s[a:b]+r'''
|
||||
int main(){
|
||||
WorldDatabase.rows.data={{{0,"outside"},{1,""},{},{}},{{0,"blocked"},{2,""},{},{}},{{0,"reachable"},{3,""},{},{}}};
|
||||
Player p;AdventureLocation l;AdventureBehaviorMgr mgr;
|
||||
assert(mgr.AreaNode(&p,l)=="reachable");assert((graph.tried==std::vector<std::string>{"blocked","reachable"}));
|
||||
sTerrainMgr.available=false;assert(mgr.AreaNode(&p,l).empty());sTerrainMgr.available=true;
|
||||
l.AreaId=42;graph.tried.clear();assert(mgr.AreaNode(&p,l).empty()&&graph.tried.empty());
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-area-node-') as folder:
|
||||
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([str(binary)],check=True)
|
||||
print('Production area destination excludes outside and unreachable graph nodes.')
|
||||
@@ -20,6 +20,7 @@ enum class HighGuid{Unit};
|
||||
struct Guid{int value=1;Guid(int v=1):value(v){}Guid(HighGuid,unsigned,unsigned low):value(low){}unsigned GetCounter(){return value;}bool IsEmpty(){return value==0;}};
|
||||
using ObjectGuid=Guid;
|
||||
struct WorldObject{
|
||||
void* ToGameObject(){return nullptr;}
|
||||
bool inWorld=true,phase=true;unsigned map=0;float x=0,z=0;
|
||||
bool IsInWorld()const{return inWorld;}unsigned GetMapId()const{return map;}float GetPositionX()const{return x;}float GetPositionY()const{return 0;}float GetPositionZ()const{return z;}float GetCollisionHeight()const{return 2;}
|
||||
};
|
||||
|
||||
@@ -93,7 +93,7 @@ class AdventureBehaviorMgr{public:
|
||||
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;}
|
||||
void ResetMicro(Player* p,Record&r){++resets;ResetLeisure(p,r);}
|
||||
bool ProfessionWorkAvailable(Player*,Record&){return false;}
|
||||
bool workAvailable=false;bool ProfessionWorkAvailable(Player*,Record&){return workAvailable;}
|
||||
bool FishingDirection(Player*,float&)const;
|
||||
bool LeisureAvailable(Player*,Record const&,AdventureMicroAction const&)const;
|
||||
bool PlanLeisureWalk(Player*,Record&);
|
||||
@@ -192,6 +192,25 @@ int main(){
|
||||
assert(mgr.StartLeisure(&p,r,{"explore"})&&r.Leisure.PlannedWalk&&r.Leisure.Route.size()>=6);
|
||||
mgr.ResetLeisure(&p,r);p.outdoors=false;assert(!mgr.LeisureAvailable(&p,r,{"explore"}));
|
||||
|
||||
// Profession area search progresses outward and stops only at confirmed arrival.
|
||||
p.outdoors=true;p.Relocate(0,0,0);r.Intent="profession";r.MicroTimer=180000;
|
||||
location.IsArea=true;location.AreaId=1519;
|
||||
assert(mgr.StartLeisure(&p,r,{"work_search"})&&r.Leisure.Progressive);
|
||||
assert(r.Leisure.Route.size()==r.Leisure.OutboundSteps);
|
||||
assert(mgr.UpdateLeisure(&p,r,1000)&&r.Leisure.Moving);
|
||||
mgr.workAvailable=true;
|
||||
assert(mgr.UpdateLeisure(&p,r,1000)&&r.Leisure.Step==0);
|
||||
p.Relocate(mgr.requested.GetPositionX(),mgr.requested.GetPositionY(),mgr.requested.GetPositionZ());
|
||||
travel.status.State=ClientlessTravelState::Arrived;
|
||||
assert(!mgr.UpdateLeisure(&p,r,1000)&&r.Leisure.Step==1&&r.Leisure.Route.size()==1);
|
||||
mgr.ResetLeisure(&p,r);mgr.workAvailable=false;
|
||||
// Profession fishing needs real shore water and a rod, without a fishing location key.
|
||||
location.Point.Category="zone";map.water=-1000;p.hasRod=true;
|
||||
assert(!mgr.LeisureAvailable(&p,r,{"fish"}));map.water=2;
|
||||
assert(mgr.LeisureAvailable(&p,r,{"fish"}));p.hasRod=false;
|
||||
assert(!mgr.LeisureAvailable(&p,r,{"fish"}));p.hasRod=true;p.water=true;
|
||||
assert(!mgr.LeisureAvailable(&p,r,{"fish"}));p.water=false;
|
||||
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-leisure-') as folder:
|
||||
|
||||
@@ -15,16 +15,16 @@ code=r'''
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
using uint16=uint16_t;using uint32=uint32_t;
|
||||
using uint8=uint8_t;using uint16=uint16_t;using uint32=uint32_t;
|
||||
#define TRINITY_DEBUG
|
||||
enum class DatabaseFieldTypes{Int16,Int32};
|
||||
enum class DatabaseFieldTypes{Int8,Int16,Int32};
|
||||
struct Field{
|
||||
struct{char const* value;bool raw=false;}data;DatabaseFieldTypes type;mutable int errors=0;
|
||||
bool IsType(DatabaseFieldTypes t)const{return type==t;}void LogWrongType(char const*)const{++errors;}
|
||||
uint16 GetUInt16()const;uint32 GetUInt32()const;
|
||||
uint8 GetUInt8()const;bool GetBool()const{return GetUInt8()==1;}uint16 GetUInt16()const;uint32 GetUInt32()const;
|
||||
};
|
||||
'''
|
||||
code+=function(field,'uint16 Field::GetUInt16() const')+'\n'+function(field,'uint32 Field::GetUInt32() const')
|
||||
code+=function(field,'uint8 Field::GetUInt8() const')+'\n'+function(field,'uint16 Field::GetUInt16() const')+'\n'+function(field,'uint32 Field::GetUInt32() const')
|
||||
code+=r'''
|
||||
struct AdventureLocation{struct{uint32 MapId=0;std::string Category;}Point;bool IsArea=false;};
|
||||
struct AdventureLocationMgr{std::unordered_map<std::string,std::unordered_map<std::string,float>> _types;float Weight(AdventureLocation const&,std::string const&)const;};
|
||||
@@ -33,8 +33,17 @@ code+=function(source,'float AdventureLocationMgr::Weight(')
|
||||
reload_map=re.search(r'l.Point.MapId = f\[3\].*?;',source).group()
|
||||
tele_map=re.search(r's.Map = f\[2\].*?;',source).group()
|
||||
npc_map=re.search(r's.Map = f\[3\].*?;',source).group()
|
||||
# Native nullable numeric columns retain their schema width. COALESCE with an
|
||||
# integer literal promotes the tiny area/faction fields and fails Debug getters.
|
||||
query=source[source.index('SELECT w.id,'):source.index('ORDER BY w.point_key')+len('ORDER BY w.point_key')]
|
||||
for column in ('area_id','zone_id','is_area','faction'):
|
||||
assert 'COALESCE(a.'+column not in query
|
||||
code+=r'''
|
||||
int main(){
|
||||
Field nativeArea{{"1"},DatabaseFieldTypes::Int8};assert(nativeArea.GetBool());
|
||||
Field widenedArea{{"1"},DatabaseFieldTypes::Int32};assert(!widenedArea.GetBool()&&widenedArea.errors==1);
|
||||
Field missingArea{{nullptr},DatabaseFieldTypes::Int8};assert(!missingArea.GetBool()&&missingArea.errors==0);
|
||||
|
||||
for(auto value:{"0","1","530","571"}){
|
||||
Field f[4]={{{value},DatabaseFieldTypes::Int16},{{value},DatabaseFieldTypes::Int16},{{value},DatabaseFieldTypes::Int16},{{value},DatabaseFieldTypes::Int16}};
|
||||
struct{uint32 Map=0;}s;AdventureLocation l;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Apply profession catalog to schema fixtures and preserve administrator edits."""
|
||||
from pathlib import Path
|
||||
import re,sqlite3
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
db=sqlite3.connect(':memory:')
|
||||
base=(root/'sql/custom/world/2026_10_26_03_adventure_behavior.sql').read_text()
|
||||
for table in ('vcore_adventure_location_intent','vcore_adventure_action','vcore_adventure_action_intent','vcore_adventure_dialog'):
|
||||
body=re.search(r'CREATE TABLE IF NOT EXISTS '+table+r' \((.*?)\) ENGINE=',base,re.S)[1]
|
||||
db.execute('CREATE TABLE '+table+' ('+body+')')
|
||||
db.executescript('''CREATE TABLE gameobject_template(entry INTEGER PRIMARY KEY,name TEXT,type INTEGER,data0 INTEGER);
|
||||
INSERT INTO gameobject_template VALUES(1,'Native anvil',8,1),(2,'Native forge',8,3),(3,'Fire',8,4),(4,'Lab',8,663),(5,'Lexicon',8,1589),(6,'Anvil',5,0),(7,'Forge',5,0),(8,'Runeforge',5,0),(9,'Workshop',5,0),(10,'Copper Vein',3,0),(11,'Loom',5,0),(12,'Special anvil',8,623),(13,'Special forge',8,543);
|
||||
INSERT INTO vcore_adventure_action_intent VALUES('explore','profession',2);
|
||||
''')
|
||||
sql=(root/'sql/custom/world/2026_10_27_05_adventure_profession.sql').read_text()
|
||||
sql=re.sub(r'\) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;',');',sql).replace('INSERT IGNORE','INSERT OR IGNORE')
|
||||
db.executescript(sql)
|
||||
roles=set(db.execute('SELECT gameobject_entry,role FROM vcore_adventure_workstation'))
|
||||
assert {(1,'anvil'),(2,'forge'),(3,'cooking'),(4,'alchemy'),(5,'inscription'),(6,'anvil'),(7,'forge'),(11,'sewing'),(12,'anvil'),(13,'forge')}<=roles
|
||||
assert not any(entry in (8,9,10) for entry,role in roles)
|
||||
assert (1,'forge') not in roles and (2,'anvil') not in roles
|
||||
assert db.execute("SELECT kind,requirement FROM vcore_adventure_action WHERE action_key='profession_search'").fetchone()==('outing','work_search')
|
||||
assert db.execute("SELECT weight FROM vcore_adventure_action_intent WHERE action_key='explore'").fetchone()==(0,)
|
||||
db.executescript("""UPDATE vcore_adventure_workstation SET enabled=0 WHERE gameobject_entry=1;
|
||||
UPDATE vcore_adventure_action_intent SET weight=9.5 WHERE action_key='profession_search';
|
||||
UPDATE vcore_adventure_location_intent SET weight=7 WHERE category='zone';
|
||||
UPDATE vcore_adventure_action_intent SET weight=4.5 WHERE action_key='explore';""")
|
||||
before=list(db.iterdump());db.executescript(sql);assert list(db.iterdump())==before
|
||||
print('Profession catalog: native workstation roles, no workshop/resource inference, idempotence and preserved edits passed.')
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Execute the production profession parser with real Lua and saved profiles."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
source = (root/'src/server/game/World/ClientlessLuaEngine.cpp').read_text()
|
||||
a = source.index(' lua_getfield(_state,rootTable,"professions");')
|
||||
b = source.index(' uint32 const guid', a)
|
||||
code = r'''
|
||||
#include <lua.hpp>
|
||||
#include "AdventureProfessionPolicy.h"
|
||||
#include <cassert>
|
||||
#define TC_LOG_WARN(...) ((void)0)
|
||||
struct Profile { AdventureProfessionPolicy::Professions PrimaryProfessions; };
|
||||
Profile parse(lua_State* _state) {
|
||||
Profile profile;int rootTable=lua_gettop(_state);
|
||||
'''+source[a:b]+r'''
|
||||
assert(lua_gettop(_state)==rootTable);return profile;
|
||||
}
|
||||
void check(Profile const& p,bool mining,bool herbs) {
|
||||
using namespace AdventureProfessionPolicy;
|
||||
assert(Allowed("work_mining",p.PrimaryProfessions)==mining);
|
||||
assert(Allowed("ore_inspect",p.PrimaryProfessions)==mining);
|
||||
assert(Allowed("work_smelting",p.PrimaryProfessions)==mining);
|
||||
assert(Allowed("work_blacksmithing",p.PrimaryProfessions)==mining);
|
||||
assert(Allowed("work_herbalism",p.PrimaryProfessions)==herbs);
|
||||
assert(Allowed("work_alchemy",p.PrimaryProfessions)==herbs);
|
||||
for(auto kind:{"work_first_aid","work_cooking","work_wood","profession_tools"})assert(Allowed(kind,p.PrimaryProfessions));
|
||||
assert(SkillAllowed(356,p.PrimaryProfessions));assert(!SkillAllowed(99999,p.PrimaryProfessions));
|
||||
}
|
||||
int main(int argc,char** argv) {
|
||||
lua_State* L=luaL_newstate();luaL_openlibs(L);
|
||||
assert(luaL_dofile(L,argv[1])==LUA_OK);check(parse(L),true,false);lua_settop(L,0);
|
||||
assert(luaL_dofile(L,argv[2])==LUA_OK);check(parse(L),false,false);lua_settop(L,0);
|
||||
for(auto text:{"return {}","return {professions={primary={}}}",
|
||||
"return {professions={primary={'Mining','unknown',17,false,{}}}}",
|
||||
"return {professions={primary='mining'}}","return {professions=false}"}) {
|
||||
assert(luaL_dostring(L,text)==LUA_OK);check(parse(L),false,false);lua_settop(L,0);
|
||||
}
|
||||
assert(luaL_dostring(L,"return {professions={primary={'herbalism','alchemy','alchemy'}}}")==LUA_OK);
|
||||
auto herbs=parse(L);check(herbs,false,true);assert(herbs.PrimaryProfessions.size()==2);
|
||||
using namespace AdventureProfessionPolicy;
|
||||
assert(Legacy("ore")=="work_mining"&&Legacy("forge")=="work_blacksmithing"&&Legacy("unknown").empty());
|
||||
assert(!Allowed(Legacy("ore"),herbs.PrimaryProfessions));
|
||||
lua_close(L);
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-profession-profile-') as folder:
|
||||
cpp=Path(folder)/'test.cpp'; binary=Path(folder)/'test';cpp.write_text(code)
|
||||
flags=subprocess.check_output(['pkg-config','--cflags','--libs','lua5.4'],text=True).split()
|
||||
subprocess.run(['c++','-std=c++17','-I',str(root/'src/server/game/World'),str(cpp),'-o',str(binary),*flags],check=True)
|
||||
profiles=root/'src/server/scripts/Custom/clientless/adventure/profiles'
|
||||
subprocess.run([str(binary),str(profiles/'arthoran.lua'),str(profiles/'generic.lua')],check=True)
|
||||
print('Production profession parser, real Lua profiles, primary restrictions and secondary permissions passed.')
|
||||
@@ -0,0 +1,13 @@
|
||||
"""Native workstation sightlines use the working surface and still reject blocked geometry."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
s=(root/'src/server/game/World/AdventureSocialActions.cpp').read_text()
|
||||
a=s.index(' bool TargetVisible(');b=s.index(' std::vector < Unit * > People',a)
|
||||
head='#include "AdventureProfessionPolicy.h"\n#include "AdventureEconomyPolicy.h"\n#include <cassert>\nconstexpr int LINEOFSIGHT_ALL_CHECKS=1;\nnamespace VMAP {enum class ModelIgnoreFlags{Nothing};}\nstruct GameObject{};\nstruct WorldObject {bool object=false;float height=0;GameObject* ToGameObject(){return object?reinterpret_cast<GameObject*>(this):nullptr;}float GetPositionX(){return 0;}float GetPositionY(){return 0;}float GetPositionZ(){return 95;}float GetCollisionHeight(){return height;}};\nstruct Map{float endpoint=0;bool visible=true;bool isInLineOfSight(int,float,float,float,float,float,float z,int,VMAP::ModelIgnoreFlags){endpoint=z;return visible;}}map;\nstruct Player:WorldObject{Map* GetMap(){return ↦}int GetPhaseShift(){return 0;}bool IsWithinLOSInMap(WorldObject*){return false;}};\n'
|
||||
main='int main(){Player p;WorldObject target;target.object=true;\nassert(TargetVisible(&p,&target,"work_blacksmithing")&&map.endpoint==96.5f);\nmap.visible=false;assert(!TargetVisible(&p,&target,"work_blacksmithing"));\nmap.visible=true;target.object=false;target.height=2;\nassert(TargetVisible(&p,&target,"profession_trainer")&&map.endpoint==97);\nassert(!TargetVisible(&p,&target,"inspect"));\n}'
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-profession-los-') as folder:
|
||||
cpp=Path(folder)/'test.cpp';binary=Path(folder)/'test';cpp.write_text(head+s[a:b]+main)
|
||||
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)
|
||||
print('Native workstation working-surface sightline and blocked geometry rejection passed.')
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Run native profession classification with deterministic world-data doubles."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
s=(root/'src/server/game/World/AdventureProfessionTargets.cpp').read_text()
|
||||
code=r'''
|
||||
#include "AdventureProfessionPolicy.h"
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
#include <cmath>
|
||||
#include <cassert>
|
||||
constexpr int CORPSE=1,CREATURE_TYPE_BEAST=1,UNIT_FIELD_FLAGS=0,UNIT_FLAG_SKINNABLE=1;
|
||||
constexpr int GAMEOBJECT_TYPE_CHEST=3,GAMEOBJECT_TYPE_SPELL_FOCUS=8,GO_READY=0,LOCK_KEY_SKILL=2,MAX_LOCK_CASE=2;
|
||||
using LockType=unsigned;unsigned SkillByLockType(unsigned type){return type==1?186:type==2?182:0;}
|
||||
struct Creature;struct GameObject;
|
||||
struct Guid{std::string ToString(){return "source";}bool IsEmpty(){return true;}};
|
||||
struct WorldObject{virtual ~WorldObject()=default;bool world=true;int map=0;float z=0;bool IsInWorld(){return world;}int GetMapId(){return map;}float GetPositionZ(){return z;}virtual Creature* ToCreature(){return nullptr;}virtual GameObject* ToGameObject(){return nullptr;}Guid GetGUID(){return {};}};
|
||||
struct Creature:WorldObject{
|
||||
bool alive=true,combat=false,skin=false,trainer=false,vendor=false;int death=CORPSE,type=CREATURE_TYPE_BEAST;
|
||||
Creature* ToCreature()override{return this;}bool IsTrainer(){return trainer;}bool IsAlive(){return alive;}int getDeathState(){return death;}int GetCreatureType(){return type;}bool HasFlag(int,int){return skin;}bool IsInCombat(){return combat;}bool IsVendor(){return vendor;}Guid GetCharmerOrOwnerGUID(){return {};}int GetEntry(){return 1;}
|
||||
};
|
||||
struct GameObject:WorldObject{
|
||||
int type=GAMEOBJECT_TYPE_CHEST,loot=GO_READY,entry=1;bool spawned=true;
|
||||
struct Info{unsigned lock=1;unsigned GetLockId(){return lock;}}info;
|
||||
GameObject* ToGameObject()override{return this;}bool isSpawned(){return spawned;}int GetGoType(){return type;}int getLootState(){return loot;}Info* GetGOInfo(){return &info;}int GetEntry(){return entry;}
|
||||
};
|
||||
struct Position{float distance=0,z=0;float GetExactDist(WorldObject*)const{return distance;}float GetPositionZ()const{return z;}};
|
||||
struct AdventureSocialContext{std::string Intent="profession";AdventureProfessionPolicy::Professions PrimaryProfessions{"mining","blacksmithing"};int MapId=0;Position Anchor;std::deque<std::string> TargetHistory;};
|
||||
std::vector<Creature*> creatures;std::vector<GameObject*> objects;
|
||||
struct Player{bool phase=true,visible=true,los=true,friendly=true;bool IsInPhase(WorldObject*){return phase;}bool CanSeeOrDetect(WorldObject*){return visible;}bool IsWithinLOSInMap(WorldObject*){return los;}bool IsFriendlyTo(Creature*){return friendly;}void GetCreatureListWithEntryInGrid(std::vector<Creature*>& v,int,int){v=creatures;}void GetGameObjectListWithEntryInGrid(std::vector<GameObject*>&v,int,int){v=objects;}};
|
||||
namespace Trainer{enum class Type{Tradeskill,Other};struct Spell{unsigned ReqSkillLine=0,SpellId=1;};struct Data{Type type=Type::Tradeskill;std::vector<Spell> spells;Type GetTrainerType(){return type;}auto const& GetSpells(){return spells;}};}
|
||||
struct ObjectMgr{Trainer::Data trainer;int GetCreatureDefaultTrainer(int){return 1;}auto GetTrainer(int){return &trainer;}}objectMgr;auto sObjectMgr=&objectMgr;
|
||||
struct Ability{unsigned SkillLine=0;}ability;
|
||||
struct SpellMgr{std::vector<std::pair<int,Ability*>> entries;auto GetSkillLineAbilityMapBounds(unsigned){return std::make_pair(entries.begin(),entries.end());}}spellMgr;auto sSpellMgr=&spellMgr;
|
||||
struct Lock{unsigned Type[2]={LOCK_KEY_SKILL,0},Index[2]={1,0};}lockEntry;
|
||||
struct LockStore{Lock* LookupEntry(unsigned id){return id?&lockEntry:nullptr;}}sLockStore;
|
||||
struct Locations{std::string station="anvil";bool enabled=true;bool IsWorkstation(int,std::string const& role){return enabled&&role==station;}}locations;auto sAdventureLocationMgr=&locations;
|
||||
struct AdventureProfessionTargets{static bool Valid(Player*,WorldObject*,std::string const&,AdventureSocialContext const&);static std::vector<WorldObject*> Find(Player*,std::string const&,AdventureSocialContext&);};
|
||||
'''+s[s.index('namespace\n{'):]+r'''
|
||||
int main(){
|
||||
Player p;GameObject go;AdventureSocialContext c;objects={&go};
|
||||
auto valid=[&](char const* kind){return AdventureProfessionTargets::Valid(&p,&go,kind,c);};
|
||||
assert(valid("work_mining"));assert(!valid("work_herbalism"));
|
||||
lockEntry.Index[0]=2;assert(!valid("work_mining"));
|
||||
c.PrimaryProfessions={"herbalism","alchemy"};assert(valid("work_herbalism"));assert(!valid("work_blacksmithing"));
|
||||
c.PrimaryProfessions={"mining","blacksmithing"};lockEntry.Index[0]=1;
|
||||
p.phase=false;assert(!valid("work_mining"));p.phase=true;
|
||||
p.visible=false;assert(!valid("work_mining"));p.visible=true;
|
||||
go.spawned=false;assert(!valid("work_mining"));go.spawned=true;
|
||||
go.loot=1;assert(!valid("work_mining"));go.loot=0;
|
||||
go.info.lock=0;assert(!valid("work_mining"));go.info.lock=1;
|
||||
c.Anchor.distance=201;assert(!valid("work_mining"));c.Anchor.distance=0;
|
||||
go.type=GAMEOBJECT_TYPE_SPELL_FOCUS;p.visible=false;
|
||||
assert(valid("work_blacksmithing"));assert(!valid("work_smelting"));
|
||||
locations.station="forge";assert(valid("work_smelting"));assert(!valid("work_blacksmithing"));
|
||||
locations.enabled=false;assert(!valid("work_smelting"));locations.enabled=true;
|
||||
go.type=GAMEOBJECT_TYPE_CHEST;p.visible=true;
|
||||
assert(AdventureProfessionTargets::Find(&p,"work_mining",c).size()==1);
|
||||
c.TargetHistory.push_back("work:source");assert(AdventureProfessionTargets::Find(&p,"work_mining",c).empty());
|
||||
Creature npc;creatures={&npc};c.PrimaryProfessions={"skinning"};
|
||||
assert(!AdventureProfessionTargets::Valid(&p,&npc,"work_skinning",c));npc.alive=false;npc.skin=true;
|
||||
assert(AdventureProfessionTargets::Valid(&p,&npc,"work_skinning",c));npc.type=2;
|
||||
assert(!AdventureProfessionTargets::Valid(&p,&npc,"work_skinning",c));npc.type=1;npc.death=2;
|
||||
assert(!AdventureProfessionTargets::Valid(&p,&npc,"work_skinning",c));
|
||||
npc.alive=true;npc.trainer=true;objectMgr.trainer.spells={{186,1}};c.PrimaryProfessions={"mining"};
|
||||
assert(AdventureProfessionTargets::Valid(&p,&npc,"profession_trainer",c));
|
||||
c.PrimaryProfessions.clear();assert(!AdventureProfessionTargets::Valid(&p,&npc,"profession_trainer",c));
|
||||
objectMgr.trainer.spells={{185,1}};assert(AdventureProfessionTargets::Valid(&p,&npc,"profession_trainer",c));
|
||||
objectMgr.trainer.spells={{0,1}};spellMgr.entries={{1,&ability}};ability.SkillLine=182;
|
||||
assert(!AdventureProfessionTargets::Valid(&p,&npc,"profession_trainer",c));c.PrimaryProfessions={"herbalism"};
|
||||
assert(AdventureProfessionTargets::Valid(&p,&npc,"profession_trainer",c));
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-profession-targets-') as folder:
|
||||
cpp=Path(folder)/'test.cpp';binary=Path(folder)/'test';cpp.write_text(code)
|
||||
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)
|
||||
print('Production profession targets: native locks, phase/spawn/loot, stations, corpses, history and trainers passed.')
|
||||
@@ -59,7 +59,7 @@ struct Player:Unit{
|
||||
bool IsBeingTeleported(){return false;}bool IsSitState(){return sit;}bool IsNonMeleeSpellCast(bool){return casting;}
|
||||
float GetExactDist(Position const&){return distance;}float GetExactDist(WorldObject*){return distance;}void SetFacingToObject(WorldObject*){++faces;}
|
||||
std::vector<float> headings;void SetFacingTo(float f){heading=f;headings.push_back(f);}int chairStand=5;uint32 GetStandState(){return sit?chairStand:0;}
|
||||
void SetSelection(ObjectGuid g){selection=g;}void SetUInt32Value(int,int){}
|
||||
std::vector<int> states;void SetSelection(ObjectGuid g){selection=g;}void SetUInt32Value(int,int state){states.push_back(state);}
|
||||
void HandleEmoteCommand(int id){emotes.push_back(id);}void SendPlaySpellVisualKit(int,int,int){}
|
||||
void SetStandState(int){sit=true;}int CastSpell(Player*,int,bool){++casts;casting=true;return SPELL_CAST_OK;}
|
||||
bool IsWithinLOSInMap(WorldObject*){return los;}bool IsWithinLOS(float,float,float){return los;}bool GetGameObjectIfCanInteractWith(ObjectGuid){return true;}Session* GetSession(){return &session;}
|
||||
@@ -191,6 +191,27 @@ int main(){
|
||||
active=true;for(int i=0;i<60&&active;++i) active=AdventureSocialActions::Update(&p,s,1000,c);
|
||||
assert(!active&&p.headings.size()==2&&std::fabs(p.headings[0]-0.45f)<0.001&&std::fabs(p.headings[1]-1.95f)<0.001);
|
||||
|
||||
// Profession gestures wait for arrival and never invoke use/cast handlers.
|
||||
c.Intent="profession";c.PrimaryProfessions={"mining","blacksmithing"};
|
||||
target=&chair;p.distance=2;p.visible=p.los=true;p.states.clear();
|
||||
s={};s.Kind="work_mining";s.TargetKey="2";s.Target.id=2;s.Approaching=true;
|
||||
s.Sequence.Start(Steps(s.Kind,20000));travel.status.State=ClientlessTravelState::Moving;
|
||||
oldUses=p.session.uses;oldCasts=p.casts;
|
||||
assert(AdventureSocialActions::Update(&p,s,1000,c)&&p.states.empty());
|
||||
travel.status.State=ClientlessTravelState::Arrived;
|
||||
active=true;for(int i=0;i<60&&active;++i) active=AdventureSocialActions::Update(&p,s,1000,c);
|
||||
assert(!active&&p.session.uses==oldUses&&p.casts==oldCasts);
|
||||
assert(std::find(p.states.begin(),p.states.end(),EMOTE_STATE_WORK_MINING)!=p.states.end());
|
||||
assert(std::find(c.TargetHistory.begin(),c.TargetHistory.end(),"work:2")!=c.TargetHistory.end());
|
||||
// Losing profile authorization stops a scene before the next cue.
|
||||
s={};s.Kind="work_mining";s.Target.id=2;s.Sequence.Start(Steps(s.Kind,20000));
|
||||
p.states.clear();c.PrimaryProfessions.clear();
|
||||
assert(!AdventureSocialActions::Update(&p,s,1000,c)&&p.states.empty());
|
||||
c.PrimaryProfessions={"mining"};
|
||||
// Displacement while working must reapproach without advancing the cue.
|
||||
p.distance=8;s.Approaching=false;travel.status.State=ClientlessTravelState::Moving;
|
||||
assert(AdventureSocialActions::Update(&p,s,1000,c)&&s.Approaching&&p.states.empty());
|
||||
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-social-test-') as folder:
|
||||
|
||||
Reference in new issue
Block a user