Improve AdventureBot travel recovery and destination eligibility
This commit is contained in:
1 parent
321a28640c
commit
49712aadfe
50 files changed
+984
-44
No files matched your search
@@ -1,3 +1,72 @@
|
||||
# Umsetzung 2026-10-29: Navigation und Verhaltensgrenzen
|
||||
|
||||
Die unten festgehaltene Liste ist implementiert; lokaler Commit vom Nutzer angefordert.
|
||||
Nächster besprochener Schwerpunkt: sichere Fahrzeugnavigation an Bord.
|
||||
Build8 erfolgreich, installiert; finaler Neustart erfolgreich, ready und TCP8085,
|
||||
PID264359, tmux vcore-worldserver, Konsole /tmp/vcore-recovery-final-20261029.log.
|
||||
23 gezielte Regressionstests erfolgreich; spätere Flugstart-Blacklist- und native
|
||||
rekursive Skill-Aktivierungstests ebenfalls erfolgreich. Änderungen werden lokal committed; kein Push.
|
||||
Frühe Startversuche fanden einen bestehenden MAX(id)/GetUInt64-Typfehler bei
|
||||
Instanz-ID-Initialisierung: Abfrage explizit nach UNSIGNED gecastet, Daten erhalten.
|
||||
Live auf vorherigem Zwischenbuild: Terrainprobe Orgrimmar jetzt valid1;
|
||||
Aldren32/Talzul97 Reiten225/Lizenz1; Naxx level60/78 abgelehnt,85 erlaubt.
|
||||
Finaler Build behebt zusätzlich rekursive Skillpersistierung und respektiert
|
||||
Blacklist für Flugstart-Anschlüsse. Endgültige DB-Persistenzprüfung erfolgreich: character_skills762 für32 und97
|
||||
beide225/225 nach nativem saveall. Loader läuft mit50/5 jeFraktion weiter.
|
||||
Siehe navigation-recovery.md und checkpoints/2026-10-29-navigation-recovery/.
|
||||
Lunna nicht reproduziert, gemäß Zeitlimit vertagt. Keine Verteilung an Bord:
|
||||
verlässliche lokale Fahrzeugnavigation fehlt; gemeinsamer sicherer Standplatz bleibt.
|
||||
|
||||
---
|
||||
|
||||
# Nächster Entwicklungsschritt: vereinbarte Anforderungen (noch KEIN Code)
|
||||
|
||||
Letzter Commit321a28640c Gebietskatalog/NodeACL/Planerreiseintegration, vorher0173a434f4.
|
||||
Nutzer verlangte erstCommit, dann Liste vorCode. AktuellePräzisierungen vomNutzer:
|
||||
|
||||
1 Flugreisen regulärmitGraphkosten vomStandortzumNode,zwischenNodesundalsallgemeiner
|
||||
Fallback,auchlokalerAbreiseknotenbeifernemZielaufandererMap. TerrainFallback aus
|
||||
untenstehenderDiagnosekorrigieren,Phase169Standardnichtändern,KollisionundBounds
|
||||
nichtabschalten. NichtblindfehlendePhasenkachelnmitBasisdateienkopieren.
|
||||
2 LunnaDock-Flugmeister-PendelnRuttheran: nurkurznachdirekterUrsachesuchen,
|
||||
sonstüberspringen; bereitsvorGebietskatalogbeobachtet.
|
||||
3 BergungbeidauerhaftfestgefahrenerNavigation NICHTvoreilig. Ruhesteinanimation
|
||||
verwenden; ZielnichtindividuellerHeimatortsondernFraktionshauptstadtSturmwindbzw
|
||||
Orgrimmar. Zeiten/SchwellenfürfestgefahrennochmitvernünftigerkonservativerWahl
|
||||
implementieren; keinNutzerwunschfürsofortigeRettungbeieinzelnenPathRejected.
|
||||
4 Kettentod: nachWiederbelebunginnerhalb60swiedersterben,5malhintereinander.
|
||||
DannbeimnächstenToddenbestehendenGeistheiler/FriedhofFallbackfürunerreichbare
|
||||
Leichenversuchen. WeiterOHNEWiederbelebungsschwäche/Haltbarkeitsstrafe; kein
|
||||
normalerNPCGeistheilerhandel. Nichtwillkürlichbereitsnach1-2Todenretten.
|
||||
5 LetzterTagesplanblockGruppenbildung -> durchRuheersetzen, daFolgeintentfehlt.
|
||||
6 EigeneDungeon/RaidZielwahlMindestlevelprüfen. BotlädtSpielernur±5seinesLevels
|
||||
undnurwennZielpersonMindestlevelvonDungeon/Raid/Abenteuergebieterfüllt.
|
||||
SpielerlädtBot: Botmehrals5LevelÜBERSpieler lehntIMMERab, nachUNTENkeineLevel-
|
||||
Sperre. AlleanderenbereitsvereinbartenPersönlichkeits-/Intentregelnbleibengelten.
|
||||
7 Transport: zuerstNodeerreichen, dannnaheMMAP-sichereWarteplätzeverteilen;
|
||||
beiFahrzeugankunftdentatsächlichgegangenenWegzurückzumNode, normalereinsteigen.
|
||||
8 AufVehikelVerteilungNURwennsicherzuverlässignavigierbarundkeinRunterfallen.
|
||||
VorAusstiegRückwegzumherkömmlichengemeinsamenStandplatz, dannbisherigerAusstieg.
|
||||
Gruppenstaffelung/FormationerstnachvollständigabgeschlossenemAusstiegwieder.
|
||||
9 BeianderenAktivitätenAbstandzuNPC/Botssuchen,beiPlatzmangeleng/überlappenderlaubt.
|
||||
|
||||
ZusätzlichAldren/Talzuljetztgeprüft:32AldrenDK60,97TalzulDruide66,beideonline.
|
||||
BeidekennenHordeFlightmounts32243/44/45und90267; Aldrenkennt34090,Talzulnicht.
|
||||
Beidenfehltcharacter_skills762auchNACHbestätigtemnativemsaveall(Allplayerssaved).
|
||||
Skillgesamtzahlen30bzw26,nichtvoll. GeladeneProfilebeidecanGroundMount1 und
|
||||
aktuelleProfilbewegungaufFlugausgelegt. RuntimeGetSkillValuenichtdirektgelesen,
|
||||
fehlendepersistierteReitfertigkeitistaberreproduzierbar. NativeSetSkillhatpotenziell
|
||||
fehlerhafteSlot0Sentinelprüfung(PLAYER_MAX_SKILLS-Schleife,if(!skillSlot)),OHNE
|
||||
konkretenNachweisfürdiese2Bots. KeineLogsTriedtoaddskill762gefunden. Imkommenden
|
||||
SchrittBereitstellung+Speicherung+Laufzeitdiagnosekontrollieren, nichtvoreiligallein
|
||||
DBnullalsvollständigenBeweisfürjedenLaufzeitstatusdarstellen.
|
||||
|
||||
FürdieseTurnpräzisierungennochkeineC++/Lua/DBÄnderung,vomnormalensaveallabgesehen.
|
||||
SourcebisaufdieseAnforderungsnotizsauber;keineBinaries/Config/Intentsverändert.
|
||||
ServerPID247021,tmuxvcore-worldserver,Console/tmp/vcore-area-start-verified-20261029.log.
|
||||
|
||||
---
|
||||
|
||||
# WICHTIGSTER NEUER BEFUND: Freiflug scheitert an fehlendem Terrain-Fallback
|
||||
|
||||
Native `clientless flight probe 128 1751.49 -4389.67` (09:20CETca) ergab
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"test_adventure_recovery_policy": 0,
|
||||
"test_adventure_destination_level": 0,
|
||||
"test_native_skill_slot": 0,
|
||||
"test_flight_terrain_fallback": 0,
|
||||
"test_free_flight_planner": 0,
|
||||
"test_graph_probe_cache": 0,
|
||||
"test_flight_policy": 0,
|
||||
"test_flight_handoff": 0,
|
||||
"test_group_flight_follow": 0,
|
||||
"test_flight_area_query": 0,
|
||||
"test_transport": 0,
|
||||
"test_adventure_groups": 0,
|
||||
"test_adventure_native_lfg": 0,
|
||||
"test_adventure_interactions": 0,
|
||||
"test_adventure_group_travel": 0,
|
||||
"test_adventure_combat_reactions": 0,
|
||||
"test_adventure_day_persistence": 0,
|
||||
"test_adventure_intent_state": 0,
|
||||
"test_adventure_social": 0,
|
||||
"test_adventure_economy": 0,
|
||||
"test_adventure_profession_sightline": 0,
|
||||
"test_adventure_exploration": 0,
|
||||
"test_adventure_navigation_rescue": 0
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
Production combat focus, damage ordering, risk filters, corpse radius/delay, flight/group override, blocked escape, danger avoidance and malus-free persisted fallback passed.
|
||||
+1
@@ -0,0 +1 @@
|
||||
Real hearth recovery: grace, animation, faction capitals, cancellation, activity gaps and long taxi grace passed
|
||||
@@ -0,0 +1 @@
|
||||
ground planner regressions and dynamic flight/node/location/virtual-exit integration passed
|
||||
@@ -0,0 +1,2 @@
|
||||
Native riding/profession allocation accepts slot zero and last slot, rejects a full skill array
|
||||
Native recursive skill activation preserves INSERT for new placeholders and UPDATE for saved skills
|
||||
@@ -0,0 +1 @@
|
||||
Transport dock, frame, recorder, cross-map copy and ride regressions passed
|
||||
@@ -0,0 +1,24 @@
|
||||
# Validation 2026-10-29
|
||||
|
||||
- Build8 completed; 23 targeted regression programs passed. Flight-root rejection
|
||||
and recursive skill activation regressions were added after the first full run
|
||||
and passed separately.
|
||||
- First/second startup attempts exposed an existing saved-instance ID allocation
|
||||
assertion; debugger traced RegisterInstanceId(2). Corrected aggregate type;
|
||||
third startup reached ready and listened on TCP8085.
|
||||
- Native flight status: Aldren32 and Talzul97 both enabled=1, riding=225,
|
||||
flightLicense=1.
|
||||
- Native terrain probe at 1751.49/-4389.67 on map1: valid=1, ground132.703,
|
||||
ceiling493.191 (previously missing child719 tile blocked it).
|
||||
- Native Naxxramas eligibility: Aldren60 and Tahuima78 content/own/invitation=0;
|
||||
Braneven85 content/own/invitation=1.
|
||||
- Saved plan audit: 2640 slots, no hour23 groupformation.
|
||||
- Final Build8 installed after a second clean shutdown. Fresh startup log
|
||||
/tmp/vcore-recovery-final-20261029.log reaches ready; PID264359 remains alive,
|
||||
TCP8085 listening. Native saveall followed by DB query confirms Aldren32 and
|
||||
Talzul97 riding225/max225 persisted. Loader continues normal50/5 per faction.
|
||||
- Collision/departure restrictions still reject unsafe/indoor flight origins;
|
||||
cold graph route evaluation can stall for multiple seconds. No claim of fully
|
||||
verified flight gameplay or elimination of existing navigation limitations.
|
||||
- No visual gameplay acceptance claimed. Lunna not reproduced, onboard offsets
|
||||
deferred for lack of validated vehicle navigation.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Navigation recovery and eligibility
|
||||
|
||||
Implemented on feature/clientless-player-core, 2026-10-29.
|
||||
|
||||
Free flight can connect the current position to a graph departure node with
|
||||
normal travel costs, as well as bridge nodes. Recovery can reach an intermediate
|
||||
node on the current map when the final destination is on another map. Missing
|
||||
child terrain tiles use the native parent terrain fallback; existing child tiles
|
||||
with missing collision remain blocked. No phase or collision checks are disabled.
|
||||
|
||||
Navigation rescue requires at least ten minutes without substantial progress and
|
||||
five failures. Taxi/transport/hover waiting receives thirty minutes of grace.
|
||||
Unrelated activity gaps reset the watch. Rescue plays ten seconds of the native
|
||||
hearthstone precast animation, then teleports to Stormwind or Orgrimmar. Movement,
|
||||
combat or death interrupts preparation. The real hearthstone spell is not cast,
|
||||
because its personal homebind destination would be incorrect. Cooldown is thirty
|
||||
minutes. Recovery history is runtime state and resets on logout.
|
||||
|
||||
Five consecutive deaths within sixty seconds of resurrection arm the NEXT death
|
||||
for the existing graveyard resurrection fallback, without sickness or an added
|
||||
repair penalty. Native resurrection notifications also capture rapid redeaths
|
||||
between controller ticks. Existing group wipe/revival handling is retained.
|
||||
|
||||
Dungeon and raid destinations use native LFG/access minimum levels, including
|
||||
normal difficulty downscaling. Unknown minima are rejected. Outgoing invitations
|
||||
require levels within five and the destination minimum, including adventure areas.
|
||||
Incoming player invitations always reject bots more than five levels above the
|
||||
player, with no lower bot-level cutoff. Personality decisions remain in force.
|
||||
The final hourly groupformation slot becomes rest and is persisted.
|
||||
|
||||
Dock waiting uses safe nearby local paths and returns along the actual recorded
|
||||
walk to the original node before boarding. Group formations wait until complete
|
||||
arrival. Onboard distribution is deferred: recorded transport transitions do not
|
||||
provide validated local vehicle navigation. NPC/activity spacing prefers free
|
||||
positions but permits overlap when safe space is unavailable.
|
||||
|
||||
Riding provisioning teaches native riding spells and reconciles the skill.
|
||||
The native Player::SetSkill empty-slot sentinel now correctly accepts slot zero.
|
||||
Read-only flight status reports riding and flight eligibility.
|
||||
|
||||
Lunna's Rut'theran oscillation was not reproduced in the bounded log inspection;
|
||||
further investigation is deferred as requested. In-game acceptance remains
|
||||
necessary for visual hearth effects, spacing and transport behaviour.
|
||||
|
||||
Startup validation exposed an existing instance-ID storage bug: MAX(id) retains
|
||||
the INT field type, so GetUInt64 returned zero in debug builds and registration
|
||||
of saved instance 2 asserted. The aggregate now explicitly casts to UNSIGNED
|
||||
(BIGINT), preserving the existing 64-bit allocation arithmetic and saved data.
|
||||
|
||||
Live checks also exposed recursive rewarded-spell skill activation: an initialized
|
||||
zero-rank placeholder could become CHANGED before its first INSERT. SetSkill now
|
||||
marks persistence state before learning rewarded spells; the regression exercises
|
||||
actual recursive activation. Failed flight-departure roots now honor the existing
|
||||
executor blacklist when replanning.
|
||||
@@ -1,3 +1,4 @@
|
||||
#include "ClientlessPlayerMgr.h"
|
||||
/*
|
||||
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
@@ -4117,6 +4118,7 @@ void Player::ResurrectPlayer(float restore_percent, bool applySickness)
|
||||
SetFlag(UNIT_DYNAMIC_FLAGS, UNIT_DYNFLAG_REFER_A_FRIEND);
|
||||
|
||||
setDeathState(ALIVE);
|
||||
if(GetSession() && GetSession()->IsClientless())sClientlessPlayerMgr->Resurrection(GetGUID().GetCounter(),GameTime::GetGameTime());
|
||||
|
||||
SetWaterWalking(false);
|
||||
if (!HasUnitState(UNIT_STATE_STUNNED))
|
||||
@@ -5497,18 +5499,7 @@ void Player::SetSkill(uint16 id, uint16 step, uint16 newVal, uint16 maxVal)
|
||||
SetSkillRank(itr->second.pos, newVal);
|
||||
SetSkillMaxRank(itr->second.pos, maxVal);
|
||||
|
||||
LearnSkillRewardedSpells(id, newVal);
|
||||
// if skill value is going up, update enchantments after setting the new value
|
||||
if (newVal > currVal)
|
||||
{
|
||||
UpdateSkillEnchantments(id, currVal, newVal);
|
||||
if (id == SKILL_RIDING)
|
||||
UpdateMountCapability();
|
||||
}
|
||||
|
||||
UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_REACH_SKILL_LEVEL, id);
|
||||
UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LEARN_SKILL_LEVEL, id);
|
||||
|
||||
// Reward spells may recursively update this skill; preserve NEW before recursion.
|
||||
// update skill state
|
||||
if (itr->second.uState == SKILL_UNCHANGED || itr->second.uState == SKILL_DELETED)
|
||||
{
|
||||
@@ -5532,6 +5523,18 @@ void Player::SetSkill(uint16 id, uint16 step, uint16 newVal, uint16 maxVal)
|
||||
itr->second.uState = SKILL_CHANGED;
|
||||
}
|
||||
|
||||
LearnSkillRewardedSpells(id, newVal);
|
||||
// if skill value is going up, update enchantments after setting the new value
|
||||
if (newVal > currVal)
|
||||
{
|
||||
UpdateSkillEnchantments(id, currVal, newVal);
|
||||
if (id == SKILL_RIDING)
|
||||
UpdateMountCapability();
|
||||
}
|
||||
|
||||
UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_REACH_SKILL_LEVEL, id);
|
||||
UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LEARN_SKILL_LEVEL, id);
|
||||
|
||||
// archaeology handling
|
||||
if (id == SKILL_ARCHAEOLOGY)
|
||||
{
|
||||
@@ -5576,7 +5579,7 @@ void Player::SetSkill(uint16 id, uint16 step, uint16 newVal, uint16 maxVal)
|
||||
else
|
||||
{
|
||||
// We are about to learn a skill that has been added outside of normal circumstances (Game Master command, scripts etc.)
|
||||
uint8 skillSlot = 0;
|
||||
uint16 skillSlot = PLAYER_MAX_SKILLS;
|
||||
|
||||
// Find a free skill slot
|
||||
for (uint32 i = 0; i < PLAYER_MAX_SKILLS; ++i)
|
||||
@@ -5588,7 +5591,7 @@ void Player::SetSkill(uint16 id, uint16 step, uint16 newVal, uint16 maxVal)
|
||||
}
|
||||
}
|
||||
|
||||
if (!skillSlot)
|
||||
if (skillSlot == PLAYER_MAX_SKILLS)
|
||||
{
|
||||
TC_LOG_ERROR("misc", "Tried to add skill %u but player %s (%s) cannot have additional skills", id, GetName().c_str(), GetGUID().ToString().c_str());
|
||||
return;
|
||||
|
||||
@@ -249,7 +249,7 @@ void WorldSession::HandleMoveTeleportAck(WorldPackets::Movement::MoveTeleportAck
|
||||
|
||||
void WorldSession::CompleteClientlessTeleport()
|
||||
{
|
||||
if (!IsClientless() || !GetPlayer())
|
||||
if (!IsClientless() || !GetPlayer() || GetPlayer()->IsHasDelayedTeleport())
|
||||
return;
|
||||
bool const pending = GetPlayer()->IsBeingTeleported();
|
||||
if (GetPlayer()->IsBeingTeleportedFar())
|
||||
|
||||
@@ -431,7 +431,9 @@ void MapManager::InitInstanceIds()
|
||||
{
|
||||
_nextInstanceId = 1;
|
||||
|
||||
if (QueryResult result = CharacterDatabase.Query("SELECT IFNULL(MAX(id), 0) FROM instance"))
|
||||
// MAX preserves the INT column type; explicitly widen it for GetUInt64.
|
||||
// Debug builds reject a mismatched getter and otherwise allocate only two IDs.
|
||||
if (QueryResult result = CharacterDatabase.Query("SELECT CAST(IFNULL(MAX(id), 0) AS UNSIGNED) FROM instance"))
|
||||
_freeInstanceIds->resize((*result)[0].GetUInt64() + 2, true); // make space for one extra to be able to access [_nextInstanceId] index in case all slots are taken
|
||||
else
|
||||
_freeInstanceIds->resize(_nextInstanceId + 1, true);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#include "ClientlessPositioning.h"
|
||||
#include "MotionMaster.h"
|
||||
#include "AdventureAreaMgr.h"
|
||||
#include "ClientlessTravelGraphMgr.h"
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
@@ -8,6 +10,7 @@
|
||||
#include "AdventureLocationMgr.h"
|
||||
#include "AdventureEconomyPolicy.h"
|
||||
#include "AdventureRestSpace.h"
|
||||
#include "AdventureRecoveryPolicy.h"
|
||||
#include "ClientlessLuaEngine.h"
|
||||
#include "ClientlessPlayerMgr.h"
|
||||
#include "ClientlessActionExecutorMgr.h"
|
||||
@@ -249,6 +252,7 @@ bool AdventureBehaviorMgr::Allowed(Player * player, AdventureLocation const & lo
|
||||
unsigned capital=AdventureGroupPolicy::CapitalFaction(location.ZoneId?location.ZoneId:location.AreaId);
|
||||
Position position; position.Relocate(location.Point.X,location.Point.Y,location.Point.Z);
|
||||
return map && map->MapType==MAP_COMMON && (!capital || capital==side) &&
|
||||
sAdventureLocationMgr->LevelEligible(player,location,intent) &&
|
||||
sAdventureLocationMgr->Weight(location,intent)>0 && (!location.Faction || location.Faction==side) &&
|
||||
(!location.AreaId || sAdventureAreaMgr->AllowsArea(player,location.AreaId)) &&
|
||||
(!location.ZoneId || sAdventureAreaMgr->AllowsArea(player,location.ZoneId)) &&
|
||||
@@ -317,10 +321,12 @@ void AdventureBehaviorMgr::Generate(Player * player, Record & record, uint32 dat
|
||||
for(unsigned h=0;h<24;++h)
|
||||
{
|
||||
auto& slot=record.Plan[h];auto location=sAdventureLocationMgr->Find(slot.Location);
|
||||
if(location && Allowed(player,*location,slot.Intent)) continue;
|
||||
bool lastFormation=h==23 && slot.Intent=="groupformation";
|
||||
if(lastFormation)slot.Intent="rest";
|
||||
if(!lastFormation && location && Allowed(player,*location,slot.Intent)) continue;
|
||||
slot.Location=PickLocation(player,record,slot.Intent);auto key=slot.Location;
|
||||
WorldDatabase.EscapeString(key);
|
||||
tx->PAppend("UPDATE vcore_adventure_day SET location_key='%s' WHERE character_guid=%u AND local_date=%u AND hour_slot=%u",key.c_str(),guid,date,h);
|
||||
tx->PAppend("UPDATE vcore_adventure_day SET intent='%s',location_key='%s' WHERE character_guid=%u AND local_date=%u AND hour_slot=%u",slot.Intent.c_str(),key.c_str(),guid,date,h);
|
||||
++repaired;
|
||||
}
|
||||
if(repaired)WorldDatabase.CommitTransaction(tx);
|
||||
@@ -349,6 +355,7 @@ void AdventureBehaviorMgr::Generate(Player * player, Record & record, uint32 dat
|
||||
});
|
||||
AdventureGroupPolicy::Prepare(record.Plan,record.Random,Affinity(profile->Personality,"sociability",0.5f),
|
||||
[&](){return PickLocation(player,record,"groupformation");});
|
||||
if(record.Plan[23].Intent=="groupformation")record.Plan[23]={"rest",PickLocation(player,record,"rest")};
|
||||
auto tx = WorldDatabase.BeginTransaction();
|
||||
tx->PAppend("DELETE FROM vcore_adventure_day WHERE character_guid=%u",guid);
|
||||
for (unsigned h = 0; h < 24; ++ h)
|
||||
@@ -1096,6 +1103,16 @@ bool AdventureBehaviorMgr::UpdateSocial(Player* player,Record& r,uint32 diff)
|
||||
|
||||
void AdventureBehaviorMgr::Micro(Player * player, Record & r)
|
||||
{
|
||||
auto spacingTravel=sClientlessTravelMgr->GetStatus(player->GetGUID().GetCounter());
|
||||
if(r.NextSpacing>GameTime::GetGameTime() && spacingTravel && spacingTravel->State==ClientlessTravelState::Moving)return;
|
||||
if(GameTime::GetGameTime()>=r.NextSpacing && player->GetStandState()==UNIT_STAND_STATE_STAND &&
|
||||
!player->GetTransport() && !player->IsFlying() && ClientlessPositioning::Clearance(player,*player)<1.1f)
|
||||
{
|
||||
r.NextSpacing=GameTime::GetGameTime()+30;
|
||||
Position spot;std::vector<Position> path;
|
||||
if(ClientlessPositioning::Nearby(player,*player,3.5f,spot,path) && sClientlessTravelMgr->StartTravel(player,spot))return;
|
||||
}
|
||||
|
||||
auto profile = sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter());
|
||||
if (! profile) return;
|
||||
auto l = sAdventureLocationMgr->Find(r.Location);
|
||||
@@ -1274,12 +1291,69 @@ void AdventureBehaviorMgr::Micro(Player * player, Record & r)
|
||||
TC_LOG_INFO("server.adventurebot", "MICRO_START guid=%u intent='%s' location='%s' action='%s' durationMs=%u", player->GetGUID().GetCounter(), r.Intent.c_str(), r.Location.c_str(), a.Key.c_str(), r.MicroTimer);
|
||||
}
|
||||
|
||||
bool AdventureBehaviorMgr::UpdateNavigationRescue(Player* player,Record& r)
|
||||
{
|
||||
auto now=GameTime::GetGameTime();uint32 guid=player->GetGUID().GetCounter();
|
||||
if(!r.RescueAt && r.LastNavigationCheck && now-r.LastNavigationCheck>5)
|
||||
{r.StalledSince=0;r.RescueFailures=0;r.LastTravelFailed=false;}
|
||||
r.LastNavigationCheck=now;
|
||||
if(!player->IsAlive() || player->IsInCombat() || player->IsBeingTeleported() || player->GetMap()->IsDungeon())
|
||||
{
|
||||
if(r.RescueAt)player->SetUInt32Value(UNIT_NPC_EMOTESTATE,0);
|
||||
r.RescueAt=0;r.StalledSince=0;return false;
|
||||
}
|
||||
if(r.RescueAt)
|
||||
{
|
||||
if(player->GetExactDist(r.RescueOrigin)>1.0f)
|
||||
{r.RescueAt=0;player->SetUInt32Value(UNIT_NPC_EMOTESTATE,0);return false;}
|
||||
if(now<r.RescueAt)return true;
|
||||
r.RescueAt=0;player->SetUInt32Value(UNIT_NPC_EMOTESTATE,0);
|
||||
auto city=sObjectMgr->GetGameTeleExactName(player->GetTeamId()==TEAM_ALLIANCE?"Stormwind":"Orgrimmar");
|
||||
if(!city)return false;
|
||||
Position target;target.Relocate(city->position_x,city->position_y,city->position_z,city->orientation);
|
||||
if(!sAdventureAreaMgr->AllowsPosition(player,city->mapId,target))return false;
|
||||
if(r.Instance.Active){FinishInstance(player,r);if(r.Instance.Active)return false;}
|
||||
if(player->IsInFlight()){player->GetMotionMaster()->MovementExpired();player->CleanupAfterTaxiFlight();}
|
||||
if(!player->TeleportTo(city->mapId,target.GetPositionX(),target.GetPositionY(),target.GetPositionZ(),target.GetOrientation()))return false;
|
||||
player->GetSession()->CompleteClientlessTeleport();
|
||||
r.LastRescue=now;r.StalledSince=0;r.RescueFailures=0;r.LastTravelFailed=false;r.Queued=false;r.Dirty=true;r.State=Phase::Idle;
|
||||
player->SaveToDB();
|
||||
TC_LOG_INFO("server.adventurebot","NAV_HEARTH_COMPLETE guid=%u city='%s'",guid,city->name.c_str());
|
||||
return true;
|
||||
}
|
||||
auto action=sClientlessActionExecutorMgr->GetStatus(guid);
|
||||
bool failed=action && action->State==ClientlessActionExecutionState::Failed;
|
||||
bool travelling=r.State==Phase::Travel || (action && action->State==ClientlessActionExecutionState::Moving) || failed;
|
||||
if(!travelling || r.State==Phase::OnSite)
|
||||
{r.StalledSince=0;r.RescueFailures=0;r.LastTravelFailed=false;return false;}
|
||||
if(!r.StalledSince || r.ProgressMap!=player->GetMapId() || player->GetExactDist(r.ProgressPosition)>=30)
|
||||
{r.StalledSince=now;r.ProgressMap=player->GetMapId();r.ProgressPosition.Relocate(player);r.RescueFailures=0;}
|
||||
if(failed && !r.LastTravelFailed)++r.RescueFailures;
|
||||
r.LastTravelFailed=failed;
|
||||
bool aboard=player->GetTransport() || player->IsInFlight() || player->IsFlying();
|
||||
// Normal traffic, waits and hover/flight pauses get a much longer grace.
|
||||
if(aboard && now<r.StalledSince+1800)return false;
|
||||
if(aboard)r.RescueFailures=std::max(r.RescueFailures,5u);
|
||||
if(player->IsFalling() || player->HasUnitState(UNIT_STATE_CASTING) ||
|
||||
!AdventureRecoveryPolicy::Rescue(now,r.StalledSince,r.LastRescue,r.RescueFailures))return false;
|
||||
ResetMicro(player,r);sClientlessPlayerMgr->ClearQueuedActions(guid);sClientlessActionExecutorMgr->Clear(guid);
|
||||
sClientlessTravelMgr->CancelTravel(guid);player->StopMoving();player->SetStandState(UNIT_STAND_STATE_STAND);
|
||||
r.RescueOrigin.Relocate(player);r.RescueAt=now+10;r.Detail="using hearthstone after persistent navigation failure";
|
||||
// Native Hearthstone visual220 uses precast kit36. Animate without casting
|
||||
// its bind-location teleport effect: this recovery always goes to the capital.
|
||||
player->SetUInt32Value(UNIT_NPC_EMOTESTATE,EMOTE_STATE_SPELL_PRECAST);
|
||||
player->SendPlaySpellVisualKit(36,0,10000);
|
||||
TC_LOG_INFO("server.adventurebot","NAV_HEARTH_START guid=%u stalled=%llu failures=%u",guid,static_cast<unsigned long long>(now-r.StalledSince),r.RescueFailures);
|
||||
return true;
|
||||
}
|
||||
|
||||
void AdventureBehaviorMgr::Update(Player * player, uint32 diff)
|
||||
{
|
||||
uint32 guid = player->GetGUID().GetCounter();
|
||||
auto entry = _records.find(guid);
|
||||
if (entry == _records.end() || ! entry->second.Enabled || ! player->IsInWorld()) return;
|
||||
auto & r = entry->second;
|
||||
if(UpdateNavigationRescue(player,r))return;
|
||||
if(r.AreaRevision!=sAdventureAreaMgr->Revision())
|
||||
{
|
||||
if(r.Schedule) {Generate(player,r,DateOf(Local(GameTime::GetGameTime())));r.Hour=-1;}
|
||||
@@ -1496,6 +1570,17 @@ bool AdventureBehaviorMgr::Command(ChatHandler * handler, char const * args)
|
||||
handler->SendSysMessage("Use an online clientless character GUID.");
|
||||
return false;
|
||||
}
|
||||
if(op=="eligibility")
|
||||
{
|
||||
std::string intent,key;in>>intent>>key;
|
||||
auto location=sAdventureLocationMgr->Find(key);
|
||||
if(!location){handler->SendSysMessage("Unknown adventure location.");return false;}
|
||||
handler->PSendSysMessage("Eligibility GUID %u: intent='%s' location='%s' instanceMap=%u level=%u content=%u own=%u invitation=%u",guid,
|
||||
intent.c_str(),key.c_str(),location->InstanceMap,player->getLevel(),
|
||||
sAdventureLocationMgr->LevelEligible(player,*location,intent)?1:0,Allowed(player,*location,intent)?1:0,
|
||||
sAdventureLocationMgr->LevelEligible(player,*location,intent,true)?1:0);
|
||||
return true;
|
||||
}
|
||||
if(op=="area")
|
||||
{
|
||||
if(!sAdventureAreaMgr->EnsureLoaded())return false;
|
||||
@@ -1879,6 +1964,8 @@ void AdventureBehaviorMgr::InterruptForCombat(Player* player)
|
||||
auto it=_records.find(player->GetGUID().GetCounter());
|
||||
if(it==_records.end()) return;
|
||||
auto& r=it->second;
|
||||
r.StalledSince=0;r.RescueFailures=0;r.LastTravelFailed=false;
|
||||
if(r.RescueAt){r.RescueAt=0;player->SetUInt32Value(UNIT_NPC_EMOTESTATE,0);}
|
||||
ResetMicro(player,r);
|
||||
sClientlessPlayerMgr->ClearQueuedActions(player->GetGUID().GetCounter());
|
||||
sClientlessActionExecutorMgr->Clear(player->GetGUID().GetCounter());
|
||||
|
||||
@@ -60,6 +60,10 @@ private:
|
||||
time_t PersonalDeadline=0;
|
||||
time_t Deadline=0;
|
||||
uint32 AreaRevision=0;
|
||||
time_t StalledSince=0,LastRescue=0,RescueAt=0,NextSpacing=0,LastNavigationCheck=0;
|
||||
uint32 RescueFailures=0,ProgressMap=0;
|
||||
bool LastTravelFailed=false;
|
||||
Position ProgressPosition,RescueOrigin;
|
||||
uint32 Date=0, Timer=0, MicroTimer=0, GestureTimer=0, RetryTimer=0;
|
||||
int Hour=-1;
|
||||
AdventureBehaviorPolicy::Day Plan;
|
||||
@@ -87,6 +91,7 @@ private:
|
||||
bool WorkPending=false;
|
||||
};
|
||||
bool ResolveInstance(Player* player, Record& record);
|
||||
bool UpdateNavigationRescue(Player* player,Record& record);
|
||||
bool SaveInstance(Player* player, Record& record);
|
||||
bool RestoreInstance(Player* player, Record& record);
|
||||
bool UpdateInstance(Player* player, Record& record, time_t now);
|
||||
|
||||
@@ -549,10 +549,18 @@ bool AdventureCombatMgr::Flee(Player* p,Record& r)
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void AdventureCombatMgr::Revived(Player* player,time_t when)
|
||||
{
|
||||
auto& r=_records[player->GetGUID().GetCounter()];
|
||||
r.DeathChain.Revived=when;r.Dead=false;r.ForceGraveyard=false;r.CemeteryPending=false;
|
||||
r.Suspended=false;r.NextCorpseSearch=0;r.FailedCorpsePoints.clear();r.Cooldown=30000;r.Timer=0;
|
||||
}
|
||||
|
||||
bool AdventureCombatMgr::Death(Player* p,Record& r)
|
||||
{
|
||||
if(!r.Dead)
|
||||
{
|
||||
r.ForceGraveyard=r.DeathChain.Died(GameTime::GetGameTime());
|
||||
if(r.AI)Finish(p,r);
|
||||
r.Fleeing=false;r.ThreatPositions.clear();
|
||||
sAdventureBehaviorMgr->InterruptForCombat(p);r.Damagers.clear();r.Dead=true;r.Suspended=true;
|
||||
@@ -577,9 +585,17 @@ bool AdventureCombatMgr::Death(Player* p,Record& r)
|
||||
{
|
||||
r.CemeteryPending=false;
|
||||
p->ResurrectPlayer(0.5f,false);p->SpawnCorpseBones();
|
||||
r.ForceGraveyard=false;
|
||||
TC_LOG_INFO("server.adventurebot","DEATH_CEMETERY_REVIVE guid=%u",p->GetGUID().GetCounter());
|
||||
return true;
|
||||
}
|
||||
if(r.ForceGraveyard)
|
||||
{
|
||||
sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());
|
||||
r.CemeteryPending=true;p->RepopAtGraveyard();
|
||||
TC_LOG_INFO("server.adventurebot","DEATH_CHAIN_GRAVEYARD guid=%u",p->GetGUID().GetCounter());
|
||||
return true;
|
||||
}
|
||||
auto corpse=p->GetCorpse();if(!corpse)return true;
|
||||
auto now=GameTime::GetGameTime();
|
||||
if(corpse->IsWithinDistInMap(p,CORPSE_RECLAIM_RADIUS,true))
|
||||
@@ -761,6 +777,12 @@ bool AdventureCombatMgr::Update(Player* p,uint32 diff)
|
||||
r.Timer=500;
|
||||
if(p->IsAlive() && sAdventureEncounterMgr->Update(p,500))return true;
|
||||
if(!p->IsAlive())return Death(p,r);
|
||||
if(r.Dead)
|
||||
{
|
||||
r.DeathChain.Revived=GameTime::GetGameTime();
|
||||
r.Dead=false;r.ForceGraveyard=false;r.Suspended=false;r.NextCorpseSearch=0;r.FailedCorpsePoints.clear();r.Cooldown=30000;r.Timer=0;
|
||||
return false;
|
||||
}
|
||||
if(p->IsResurrectRequested())p->ClearResurrectRequestData();
|
||||
if(!p->IsInCombat() && !p->HasUnitState(UNIT_STATE_CASTING) && !p->IsFlying() && !p->IsFalling() && GameTime::GetGameTime()>=r.NextRole)
|
||||
{
|
||||
@@ -781,7 +803,6 @@ bool AdventureCombatMgr::Update(Player* p,uint32 diff)
|
||||
}
|
||||
if(GroupHeal(p,r))return true;
|
||||
if(GroupTarget(p,r))return true;
|
||||
if(r.Dead){r.Dead=false;r.Suspended=false;r.NextCorpseSearch=0;r.FailedCorpsePoints.clear();r.Cooldown=30000;r.Timer=0;return false;}
|
||||
// First actual damager wins, including interrupting a proactive/training
|
||||
// target. Keep all subsequent damagers ordered for after that focus dies.
|
||||
if(!r.Defending && !p->GetGroup())while(!r.Damagers.empty())
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include "Position.h"
|
||||
#include "AdventureRecoveryPolicy.h"
|
||||
#include <vector>
|
||||
class Player;
|
||||
class Unit;
|
||||
@@ -17,6 +18,7 @@ public:
|
||||
static AdventureCombatMgr* instance();
|
||||
bool Update(Player* player,uint32 diff);
|
||||
void DamageTaken(Player* player,Unit* attacker,uint32 damage);
|
||||
void Revived(Player* player,time_t when);
|
||||
void Offline(Player* player);
|
||||
bool TrainingAvailable(Player* player) const;
|
||||
bool Train(Player* player,uint32 durationMs,time_t deadline=0);
|
||||
@@ -37,6 +39,8 @@ private:
|
||||
std::vector<Position> FailedCorpsePoints,ThreatPositions;
|
||||
uint32 Timer=0,UnreachableMs=0,Cooldown=0;
|
||||
int PreparedMode=-1;
|
||||
AdventureRecoveryPolicy::DeathChain DeathChain;
|
||||
bool ForceGraveyard=false;
|
||||
};
|
||||
bool Start(Player* player,Record& record,Unit* target,bool training,bool defending,time_t until);
|
||||
bool Valid(Player* player,Unit* target,bool training) const;
|
||||
|
||||
@@ -609,7 +609,14 @@ bool AdventureGeneratorMgr::Reconcile(Player* player,std::string& error)
|
||||
if(player->getLevel()>=20 && profile->CanGroundMount)
|
||||
{
|
||||
uint32 rank=player->getLevel()>=60 && profile->CanFreeFly?3:player->getLevel()>=40?2:1;
|
||||
for(auto spell:{33388u,33391u,34090u})
|
||||
{
|
||||
if((spell==33391 && rank<2) || (spell==34090 && rank<3))continue;
|
||||
if(Learnable(player,spell) && !player->HasSpell(spell))player->LearnSpell(spell,false);
|
||||
}
|
||||
player->SetSkill(SKILL_RIDING,rank,rank==3?225:rank==2?150:75,rank==3?225:rank==2?150:75);
|
||||
if(player->GetSkillValue(SKILL_RIDING)<(rank==3?225:rank==2?150:75))
|
||||
{error="native riding skill provisioning failed";return false;}
|
||||
}
|
||||
if(player->getLevel()>=60 && Learnable(player,90267) && !player->HasSpell(90267))player->LearnSpell(90267,false);
|
||||
if(player->getLevel()>=68 && Learnable(player,54197) && !player->HasSpell(54197))player->LearnSpell(54197,false);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "AdventureCombatMgr.h"
|
||||
#include "AdventureLocationMgr.h"
|
||||
#include "AdventureAreaMgr.h"
|
||||
#include "ClientlessLuaEngine.h"
|
||||
#include "ClientlessPlayerMgr.h"
|
||||
#include "ClientlessTravelMgr.h"
|
||||
@@ -97,7 +98,7 @@ bool AdventureGroupMgr::RecruitPlayer(Player* player, std::string const& intent)
|
||||
if(other==player || other->GetSession()->IsClientless() || !Ready(other) || other->isAFK() ||
|
||||
other->GetGroup() || other->GetGroupInvite() || other->GetTeam()!=player->GetTeam() ||
|
||||
!player->IsInMap(other) || !player->IsInPhase(other) || !player->CanSeeOrDetect(other) ||
|
||||
!player->IsWithinLOSInMap(other) || _playerInvites.count(other->GetGUID()))continue;
|
||||
!player->IsWithinLOSInMap(other) || _playerInvites.count(other->GetGUID()) || !CanInvite(player,other))continue;
|
||||
_playerInvites[other->GetGUID()]=now+600;
|
||||
Invite(player,other,intent,true);return true;
|
||||
}
|
||||
@@ -111,6 +112,7 @@ void AdventureGroupMgr::Offline(Player* p)
|
||||
}
|
||||
bool AdventureGroupMgr::Accept(Player* invited,Player* inviter)
|
||||
{
|
||||
if(!CanInvite(inviter,invited))return false;
|
||||
if(!Ready(invited) || !Ready(inviter) || invited->GetGroup() || !inviter->GetSession()->IsClientless() ||
|
||||
invited->GetTeam()!=inviter->GetTeam())return false;
|
||||
auto it=_records.find(invited->GetGUID());
|
||||
@@ -122,6 +124,21 @@ bool AdventureGroupMgr::Accept(Player* invited,Player* inviter)
|
||||
auto group=inviter->GetGroup();
|
||||
return (!group || (group->GetLeaderGUID()==inviter->GetGUID() && group->GetMembersCount()<AdventureGroupPolicy::Capacity(intent))) && frand(0,1)<Sociability(invited);
|
||||
}
|
||||
|
||||
bool AdventureGroupMgr::CanInvite(Player* leader,Player* member)
|
||||
{
|
||||
if(!leader || !member || !AdventureGroupPolicy::InviteLevel(leader->getLevel(),member->getLevel()))return false;
|
||||
if(!member->GetSession()->IsClientless())if(auto group=leader->GetGroup())
|
||||
for(auto const& slot:group->GetMemberSlots())
|
||||
if(auto bot=ObjectAccessor::FindPlayer(slot.guid))
|
||||
if(bot->GetSession() && bot->GetSession()->IsClientless() && !AdventureGroupPolicy::AcceptPlayerLevel(bot->getLevel(),member->getLevel()))return false;
|
||||
auto plan=sAdventureBehaviorMgr->GroupPlan(leader);
|
||||
auto location=sAdventureLocationMgr->Find(plan.Forming?plan.NextLocation:plan.Location);
|
||||
if(!location || !sAdventureLocationMgr->LevelEligible(member,*location,Activity(plan),true) ||
|
||||
!sAdventureLocationMgr->LevelEligible(leader,*location,Activity(plan),true))return false;
|
||||
Position goal;goal.Relocate(location->Point.X,location->Point.Y,location->Point.Z);
|
||||
return sAdventureAreaMgr->AllowsPosition(member,location->Point.MapId,goal);
|
||||
}
|
||||
void AdventureGroupMgr::Update(Player* p)
|
||||
{
|
||||
auto now=GameTime::GetGameTime();auto& r=_records[p->GetGUID()];
|
||||
@@ -217,6 +234,7 @@ bool AdventureGroupMgr::FollowMember(Player* player)
|
||||
|
||||
void AdventureGroupMgr::Invite(Player* p,Player* other,std::string const& intent,bool forming)
|
||||
{
|
||||
if(!CanInvite(p,other))return;
|
||||
auto now=GameTime::GetGameTime();auto& r=_records[p->GetGUID()];auto& peer=_records[other->GetGUID()];
|
||||
unsigned pending=0;
|
||||
for(auto const& entry:_records)if(entry.second.Inviter==p->GetGUID() && entry.second.InviteUntil>now)
|
||||
@@ -239,6 +257,7 @@ void AdventureGroupMgr::Invite(Player* p,Player* other,std::string const& intent
|
||||
void AdventureGroupMgr::RequestPlayerAdmission(Player* member,Player* requester)
|
||||
{
|
||||
if(!requester || requester->GetSession()->IsClientless() || requester->GetGroup() || requester->GetGroupInvite() || !BotLed(member))return;
|
||||
if(!AdventureGroupPolicy::AcceptPlayerLevel(member->getLevel(),requester->getLevel()))return;
|
||||
auto group=member->GetGroup();auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID());
|
||||
if(!Ready(leader) || !Ready(requester) || leader->GetTeam()!=requester->GetTeam())return;
|
||||
auto now=GameTime::GetGameTime();auto& r=_records[leader->GetGUID()];
|
||||
|
||||
@@ -23,6 +23,7 @@ public:
|
||||
bool PlayerLeadership(Player* leader);
|
||||
void Offline(Player* player);
|
||||
bool Accept(Player* invited,Player* inviter);
|
||||
bool CanInvite(Player* leader,Player* member);
|
||||
bool FollowMember(Player* player);
|
||||
bool BotLed(Player* player) const;
|
||||
bool Forming(Player* player) const;
|
||||
|
||||
@@ -12,6 +12,10 @@ inline bool Eligible(std::string const& intent)
|
||||
{return intent=="instance" || intent=="raid" || intent=="adventure" || intent=="profession" || intent=="social";}
|
||||
inline unsigned Capacity(std::string const& intent)
|
||||
{return intent=="raid"?20:intent=="instance"?5:intent=="social"?10:3;}
|
||||
inline bool InviteLevel(unsigned bot,unsigned player)
|
||||
{return bot>player?bot-player<=5:player-bot<=5;}
|
||||
inline bool AcceptPlayerLevel(unsigned bot,unsigned player)
|
||||
{return bot<=player || bot-player<=5;}
|
||||
inline bool Compatible(std::string const& a,std::string const& b,unsigned level,unsigned other)
|
||||
{return Eligible(a) && a==b && (level>other?level-other<=5:other-level<=5);}
|
||||
template<class Day,class Meeting>
|
||||
|
||||
@@ -330,6 +330,8 @@ bool AdventureGroupTravelMgr::Follow(Player* player)
|
||||
return false;
|
||||
}
|
||||
auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID());if(!leader || !player->IsAlive() || player->IsInCombat() || player->IsBeingTeleported() || player->IsCharmed())return true;
|
||||
auto ride=sClientlessTransportMgr->GetStatus(guid);
|
||||
if(ride && ride->Phase!=ClientlessTransportPhase::Arrived && ride->Phase!=ClientlessTransportPhase::Failed)return true;
|
||||
auto& record=_records[guid];
|
||||
if(record.FlightOwned)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "AdventureInteractionMgr.h"
|
||||
#include "AdventureInteractionPolicy.h"
|
||||
#include "AdventureGroupMgr.h"
|
||||
#include "AdventureGroupPolicy.h"
|
||||
#include "AdventureGroupTravelMgr.h"
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "AdventureCombatMgr.h"
|
||||
@@ -128,6 +129,7 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
|
||||
{
|
||||
auto inviter=ObjectAccessor::FindPlayer(leader);
|
||||
bool accept=inviter && inviter->IsAlive() && p->IsFriendlyTo(inviter) && !p->GetMap()->IsDungeon() &&
|
||||
(inviter->GetSession()->IsClientless() || AdventureGroupPolicy::AcceptPlayerLevel(p->getLevel(),inviter->getLevel())) &&
|
||||
(inviter->GetSession()->IsClientless()?sAdventureGroupMgr->Accept(p,inviter):
|
||||
frand(0,1)<AdventureInteractionPolicy::Willingness(sAdventureBehaviorMgr->InteractionIntent(p),Sociability(p)));
|
||||
WorldPackets::Party::PartyInviteResponse response{WorldPacket(CMSG_PARTY_INVITE_RESPONSE)};response.Accept=accept;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "AdventureAreaMgr.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "Player.h"
|
||||
#include "AdventureLocationMgr.h"
|
||||
#include "AdventureBehaviorPolicy.h"
|
||||
#include "AdventureGroupPolicy.h"
|
||||
@@ -31,6 +33,26 @@ bool AdventureLocationMgr::Reload()
|
||||
_actions.clear();
|
||||
_dialogs.clear();
|
||||
_loaded = false;
|
||||
_instanceMinimum.clear();
|
||||
for(auto entry:sLFGDungeonStore)
|
||||
{
|
||||
auto map=sMapStore.LookupEntry(entry->MapID);
|
||||
if(!map || !map->IsDungeon() || !entry->MinLevel)continue;
|
||||
Difficulty difficulty=map->IsRaid()?RAID_DIFFICULTY_25MAN_NORMAL:DUNGEON_DIFFICULTY_NORMAL;
|
||||
sDBCManager.GetDownscaledMapDifficultyData(map->ID,difficulty);
|
||||
if(entry->DifficultyID!=difficulty)continue;
|
||||
auto& minimum=_instanceMinimum[uint32(entry->MapID)];
|
||||
minimum=minimum?std::min(minimum,entry->MinLevel):entry->MinLevel;
|
||||
}
|
||||
// The native access requirements are authoritative even for maps absent
|
||||
// from the dungeon finder catalog (and may tighten its recommended band).
|
||||
for(auto map:sMapStore)if(map->IsDungeon())
|
||||
{
|
||||
Difficulty difficulty=map->IsRaid()?RAID_DIFFICULTY_25MAN_NORMAL:DUNGEON_DIFFICULTY_NORMAL;
|
||||
sDBCManager.GetDownscaledMapDifficultyData(map->ID,difficulty);
|
||||
if(auto access=sObjectMgr->GetAccessRequirement(map->ID,difficulty))
|
||||
if(access->levelMin)_instanceMinimum[map->ID]=std::max(_instanceMinimum[map->ID],uint32(access->levelMin));
|
||||
}
|
||||
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,''),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"))
|
||||
{
|
||||
@@ -107,7 +129,7 @@ bool AdventureLocationMgr::Reload()
|
||||
auto f=rows->Fetch();_workstations[f[0].GetUInt32()].insert(f[1].GetString());
|
||||
} while(rows->NextRow());
|
||||
_loaded = true;
|
||||
TC_LOG_INFO("server.adventurebot", "ADVENTURE_CATALOG locations=%u actions=%u lines=%u", uint32(_locations.size()), uint32(_actions.size()), uint32(_dialogs.size()));
|
||||
TC_LOG_INFO("server.adventurebot", "ADVENTURE_CATALOG locations=%u actions=%u lines=%u instanceBands=%u", uint32(_locations.size()), uint32(_actions.size()), uint32(_dialogs.size()),uint32(_instanceMinimum.size()));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -117,6 +139,19 @@ AdventureLocation const * AdventureLocationMgr::Find(std::string const & key) co
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool AdventureLocationMgr::LevelEligible(Player const* player,AdventureLocation const& location,std::string const& intent,bool strictArea) const
|
||||
{
|
||||
if(!player)return false;
|
||||
if(intent=="instance" || intent=="raid")
|
||||
{
|
||||
auto it=_instanceMinimum.find(location.InstanceMap);
|
||||
return it!=_instanceMinimum.end() && it->second && player->getLevel()>=it->second;
|
||||
}
|
||||
if(!strictArea)return true;
|
||||
auto area=sAdventureAreaMgr->Area(location.AreaId?location.AreaId:location.ZoneId);
|
||||
return area.KnownLevel && player->getLevel()>=area.MinLevel;
|
||||
}
|
||||
|
||||
bool AdventureLocationMgr::IsWorkstation(uint32 entry,std::string const& role) const
|
||||
{
|
||||
auto it=_workstations.find(entry);return it!=_workstations.end() && it->second.count(role);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <vector>
|
||||
#include <unordered_set>
|
||||
class ChatHandler;
|
||||
class Player;
|
||||
struct AdventureLocation
|
||||
{
|
||||
ClientlessWorldPoint Point;
|
||||
@@ -40,7 +41,9 @@ public:
|
||||
AdventureLocation const* Find(std::string const& key) const;
|
||||
bool IsWorkstation(uint32 entry,std::string const& role) const;
|
||||
float Weight(AdventureLocation const& location, std::string const& intent) const;
|
||||
bool LevelEligible(Player const* player, AdventureLocation const& location, std::string const& intent, bool strictArea = false) const;
|
||||
private:
|
||||
std::unordered_map<uint32,uint32> _instanceMinimum;
|
||||
std::unordered_map<uint32,std::unordered_set<std::string>> _workstations;
|
||||
bool _loaded = false;
|
||||
std::vector<AdventureLocation> _locations;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef VCORE_ADVENTURE_RECOVERY_POLICY_H
|
||||
#define VCORE_ADVENTURE_RECOVERY_POLICY_H
|
||||
#include <cstdint>
|
||||
namespace AdventureRecoveryPolicy
|
||||
{
|
||||
struct DeathChain
|
||||
{
|
||||
std::int64_t Revived = 0;
|
||||
unsigned QuickDeaths = 0;
|
||||
bool NextGraveyard = false;
|
||||
bool Died(std::int64_t now)
|
||||
{
|
||||
bool force=NextGraveyard;
|
||||
if(force){QuickDeaths=0;NextGraveyard=false;}
|
||||
else
|
||||
{
|
||||
QuickDeaths=Revived>0 && now>=Revived && now-Revived<=60?QuickDeaths+1:0;
|
||||
if(QuickDeaths>=5)NextGraveyard=true;
|
||||
}
|
||||
Revived=0;return force;
|
||||
}
|
||||
};
|
||||
inline bool Rescue(std::int64_t now,std::int64_t stalled,std::int64_t last,unsigned failures)
|
||||
{return stalled>0 && now>=stalled+600 && failures>=5 && (!last || now>=last+1800);}
|
||||
}
|
||||
#endif
|
||||
@@ -1,3 +1,4 @@
|
||||
#include "ClientlessPositioning.h"
|
||||
#include "AdventureSocialActions.h"
|
||||
#include "AdventureLocationMgr.h"
|
||||
#include "AdventureEconomyPolicy.h"
|
||||
@@ -167,12 +168,14 @@ namespace
|
||||
bool Approach(Player* player,AdventureSocialScene& scene,WorldObject* target,AdventureSocialContext& c)
|
||||
{
|
||||
if(!InBounds(player,target,c))return false;
|
||||
Position crowded;bool hasCrowded=false,already=false;
|
||||
if(player->GetExactDist(target)<=3.8f && (scene.Kind=="work_skinning" || AtFront(player,target)) &&
|
||||
((AdventureEconomyPolicy::Economic(scene.Kind) || AdventureProfessionPolicy::Recognized(scene.Kind))?TargetVisible(player,target,scene.Kind):
|
||||
player->IsWithinLOS(target->GetPositionX(),target->GetPositionY(),target->GetPositionZ()+1.5f)))
|
||||
{
|
||||
scene.Approaching=false;
|
||||
return true;
|
||||
already=true;
|
||||
if(ClientlessPositioning::Clearance(player,*player)>=1.1f)
|
||||
{scene.Approaching=false;return true;}
|
||||
}
|
||||
// People are approached in front of their actual facing, not from the
|
||||
// bot's current bearing. Furniture may still be reached from any side.
|
||||
@@ -191,9 +194,15 @@ namespace
|
||||
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()+(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(ClientlessPositioning::Clearance(player,p)<1.1f)
|
||||
{if(!hasCrowded){crowded=p;hasCrowded=true;}continue;}
|
||||
if(!c.Move(p,(AdventureEconomyPolicy::Economic(scene.Kind) || AdventureExplorationPolicy::Inspection(scene.Kind) || AdventureProfessionPolicy::Recognized(scene.Kind))?350:55))continue;
|
||||
ClientlessPositioning::Reserve(player,p);
|
||||
scene.Destination=p;scene.Approaching=true;return true;
|
||||
}
|
||||
if(already){scene.Approaching=false;return true;}
|
||||
if(hasCrowded && c.Move(crowded,(AdventureEconomyPolicy::Economic(scene.Kind) || AdventureExplorationPolicy::Inspection(scene.Kind) || AdventureProfessionPolicy::Recognized(scene.Kind))?350:55))
|
||||
{scene.Destination=crowded;scene.Approaching=true;return true;}
|
||||
return false;
|
||||
}
|
||||
bool OnChair(Player* player,GameObject* chair,float& error)
|
||||
|
||||
@@ -1881,8 +1881,21 @@ bool ClientlessActionExecutorMgr::TryFallbackFlight(Player* player, uint32 chara
|
||||
if (record.FallbackFlightAttempted || player->IsFlying())
|
||||
return false;
|
||||
ClientlessTravelNode goal;
|
||||
if (!GetTravelGoal(record, goal) || goal.MapId != player->GetMapId())
|
||||
return false;
|
||||
if (!GetTravelGoal(record, goal))return false;
|
||||
bool intermediate=goal.MapId!=player->GetMapId();
|
||||
if(intermediate)
|
||||
{
|
||||
bool found=false;
|
||||
for(std::size_t i=record.RouteStep;i<record.Route.size();++i)
|
||||
{
|
||||
auto node=sClientlessTravelGraphMgr->FindNode(record.Route[i].NodeKey);
|
||||
if(!node || node->MapId!=player->GetMapId() || !sAdventureAreaMgr->AllowsNode(player,*node) || player->GetExactDist(node->X,node->Y,node->Z)<5)continue;
|
||||
Position point;point.Relocate(node->X,node->Y,node->Z);
|
||||
if(!std::isfinite(sClientlessFlightMgr->Estimate(player,node->MapId,*player,point,true)))continue;
|
||||
goal=*node;found=true;break;
|
||||
}
|
||||
if(!found)return false;
|
||||
}
|
||||
Position from, target;
|
||||
from.Relocate(player);
|
||||
target.Relocate(goal.X, goal.Y, goal.Z, goal.O);
|
||||
@@ -1897,12 +1910,13 @@ bool ClientlessActionExecutorMgr::TryFallbackFlight(Player* player, uint32 chara
|
||||
return false;
|
||||
}
|
||||
record.FallbackFlightAttempted = true;
|
||||
record.FallbackIntermediate = intermediate;
|
||||
record.EdgePath.clear();
|
||||
record.GroundExitActive = false;
|
||||
record.GroundResumeActive = false;
|
||||
record.RecoveryWanderActive = false;
|
||||
record.FinalLocalLegActive = false;
|
||||
record.FinalLocalLegPending = record.Status.ActionName == "travel_to_location";
|
||||
record.FinalLocalLegPending = !intermediate && record.Status.ActionName == "travel_to_location";
|
||||
record.Route.clear();
|
||||
ClientlessTravelRouteStep source, destination;
|
||||
source.NodeKey = "flight_fallback_departure";
|
||||
@@ -2345,6 +2359,11 @@ void ClientlessActionExecutorMgr::UpdateTravelToNode(
|
||||
RecoverFlightLeg(player,characterGuid,record,flight?flight->Detail:"free flight state disappeared");
|
||||
return;
|
||||
}
|
||||
if(record.FallbackIntermediate)
|
||||
{
|
||||
record.FallbackIntermediate=false;
|
||||
RecoverTravel(player,characterGuid,record,0,"flight reached intermediate departure node");return;
|
||||
}
|
||||
auto const& step=record.Route[record.RouteStep];
|
||||
if (player->GetExactDist(step.X,step.Y,step.Z)>1.5f)
|
||||
{
|
||||
|
||||
@@ -77,7 +77,7 @@ private:
|
||||
bool GroundExitActive = false;
|
||||
bool GroundResumeActive = false;
|
||||
bool GroundFlightExit = false;
|
||||
bool FallbackFlightAttempted = false;
|
||||
bool FallbackFlightAttempted = false, FallbackIntermediate = false;
|
||||
|
||||
// C8.10b: active server-side taxi edge.
|
||||
bool TransportActive = false;
|
||||
|
||||
@@ -166,6 +166,10 @@ Surface ClientlessFlightMgr::Sample(Player * player, Record & record, float x, f
|
||||
{
|
||||
Map * map = player->GetMap();
|
||||
uint32 terrain = PhasingHandler::GetTerrainMapId(player->GetPhaseShift(), map->GetId(), map->GetTerrain(), x, y);
|
||||
auto grid=Trinity::ComputeGridCoord(x,y);
|
||||
int32 gx=(MAX_NUMBER_OF_GRIDS-1)-grid.x_coord,gy=(MAX_NUMBER_OF_GRIDS-1)-grid.y_coord;
|
||||
if(terrain!=map->GetTerrain()->GetId() && !map->GetTerrain()->HasChildTerrainGridFile(terrain,gx,gy))
|
||||
terrain=map->GetTerrain()->GetId();
|
||||
std::string key = std::to_string(terrain) + ":" + std::to_string(int(std::floor(x / 533.33333f))) + ":" + std::to_string(int(std::floor(y / 533.33333f)));
|
||||
auto found = record.Tiles.find(key);
|
||||
if (found == record.Tiles.end()) found = record.Tiles.emplace(key, TerrainMgr::ExistMapAndVMap(terrain, x, y)).first;
|
||||
@@ -648,6 +652,9 @@ bool ClientlessFlightMgr::Command(ChatHandler * handler, char const * args)
|
||||
handler->SendSysMessage("Usage: .clientless flight status <guid> OR test_node <guid> <node>. test_node isolates flight execution; normal trips use the planner.");
|
||||
return false;
|
||||
}
|
||||
if(auto session=sWorld->FindClientlessSession(guid))if(auto player=session->GetPlayer())
|
||||
handler->PSendSysMessage("Flight capability GUID %u: enabled=%u riding=%u flightLicense=%u coldWeather=%u outdoors=%u",guid,Enabled(player)?1:0,
|
||||
player->GetSkillValue(SKILL_RIDING),player->HasSpell(90267)?1:0,player->HasSpell(54197)?1:0,(player->IsInWorld() && player->IsOutdoors())?1:0);
|
||||
if (! status) handler->SendSysMessage("No free-flight state for this character.");
|
||||
else handler->PSendSysMessage("Flight GUID %u: state=%s map=%u mountSpell=%u segments=%u avoidances=%u handoffs=%u travelled=%.1f detail='%s'", guid, StateName(status->State), status->MapId, status->MountSpell, status->Segments, status->Avoidances, status->Handoffs, status->Travelled, status->Detail.c_str());
|
||||
return true;
|
||||
|
||||
@@ -246,6 +246,8 @@ public:
|
||||
{sAdventureGroupMgr->RequestPlayerAdmission(player,requester);}
|
||||
void OnTextEmote(Player* player, Player* source, uint32 emote) override
|
||||
{ sAdventureInteractionMgr->Emote(player,source,emote); }
|
||||
void OnResurrection(Player* player,time_t when) override
|
||||
{sAdventureCombatMgr->Revived(player,when);}
|
||||
void OnDamageTaken(Player* player, Unit* attacker, uint32 damage) override
|
||||
{ sAdventureCombatMgr->DamageTaken(player, attacker, damage); }
|
||||
void OnPlayerLogout(Player* player) override
|
||||
|
||||
@@ -168,10 +168,11 @@ void ClientlessPlayerMgr::Update(uint32 diff)
|
||||
// Damage can arrive from different map workers. Hand GUID-only notices to
|
||||
// the world-thread controller, preserving order within each character.
|
||||
decltype(_damageNotices) damageNotices;
|
||||
decltype(_resurrectionNotices) resurrectionNotices;
|
||||
decltype(_emoteNotices) emoteNotices;
|
||||
decltype(_helpNotices) helpNotices;
|
||||
decltype(_groupJoinNotices) groupJoinNotices;
|
||||
{ std::lock_guard<std::mutex> lock(_damageMutex); damageNotices.swap(_damageNotices); emoteNotices.swap(_emoteNotices); helpNotices.swap(_helpNotices); groupJoinNotices.swap(_groupJoinNotices); }
|
||||
{ std::lock_guard<std::mutex> lock(_damageMutex); damageNotices.swap(_damageNotices); resurrectionNotices.swap(_resurrectionNotices); emoteNotices.swap(_emoteNotices); helpNotices.swap(_helpNotices); groupJoinNotices.swap(_groupJoinNotices); }
|
||||
|
||||
// CLIENTLESS_PLAYER_CORE_C8_2_TRAVEL_ROBUSTNESS
|
||||
measure(0,[&]{sClientlessTravelMgr->Update(diff);});
|
||||
@@ -201,6 +202,21 @@ void ClientlessPlayerMgr::Update(uint32 diff)
|
||||
|
||||
if (state == ClientlessPlayerState::Online && player)
|
||||
{
|
||||
// Clientless sessions must acknowledge native taxi/graveyard map
|
||||
// transfers too, not only the dedicated portal/transport executor.
|
||||
if(player->IsBeingTeleported())session->CompleteClientlessTeleport();
|
||||
player=session->GetPlayer();
|
||||
if(!player || !player->IsInWorld())
|
||||
{
|
||||
auto pending=resurrectionNotices.find(characterGuid);
|
||||
if(player && pending!=resurrectionNotices.end())
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_damageMutex);
|
||||
auto& queue=_resurrectionNotices[characterGuid];
|
||||
queue.insert(queue.begin(),pending->second.begin(),pending->second.end());
|
||||
}
|
||||
++itr;continue;
|
||||
}
|
||||
auto begin=Clock::now();
|
||||
if (!record.OnlineNotified)
|
||||
{
|
||||
@@ -214,6 +230,8 @@ void ClientlessPlayerMgr::Update(uint32 diff)
|
||||
}
|
||||
if (record.Controller)
|
||||
{
|
||||
auto revivals=resurrectionNotices.find(characterGuid);
|
||||
if(revivals!=resurrectionNotices.end())for(auto when:revivals->second)record.Controller->OnResurrection(player,when);
|
||||
auto notices=damageNotices.find(characterGuid);
|
||||
if(notices!=damageNotices.end())for(auto const& notice:notices->second)
|
||||
record.Controller->OnDamageTaken(player,ObjectAccessor::GetUnit(*player,notice.Attacker),notice.Damage);
|
||||
@@ -251,6 +269,12 @@ void ClientlessPlayerMgr::Update(uint32 diff)
|
||||
}
|
||||
}
|
||||
|
||||
void ClientlessPlayerMgr::Resurrection(uint32 characterGuid,time_t when)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_damageMutex);
|
||||
_resurrectionNotices[characterGuid].push_back(when);
|
||||
}
|
||||
|
||||
void ClientlessPlayerMgr::DamageTaken(Player* player, Unit* attacker, uint32 damage)
|
||||
{
|
||||
if(!damage || !attacker || !player->GetSession()->IsClientless())return;
|
||||
|
||||
@@ -39,6 +39,7 @@ public:
|
||||
virtual void OnPlayerLogout(Player*) { }
|
||||
// Notification after health was reduced, never the mutable pre-damage event.
|
||||
virtual void OnDamageTaken(Player*, Unit*, uint32) { }
|
||||
virtual void OnResurrection(Player*,time_t) { }
|
||||
virtual void OnGroupJoinRequest(Player*,Player*) { }
|
||||
virtual void OnTextEmote(Player*, Player*, uint32) { }
|
||||
virtual void OnSharedTargetDamage(Player*, ObjectGuid, ObjectGuid) { }
|
||||
@@ -55,6 +56,7 @@ public:
|
||||
|
||||
bool IsManaged(uint32 characterGuid) const;
|
||||
void DamageTaken(Player* player, Unit* attacker, uint32 damage);
|
||||
void Resurrection(uint32 characterGuid,time_t when);
|
||||
void GroupJoinRequest(Player* target,Player* requester);
|
||||
void TextEmote(Player* source, Player* target, uint32 emote);
|
||||
void SharedTargetDamage(Unit* victim,Unit* attacker,uint32 damage);
|
||||
@@ -95,6 +97,7 @@ private:
|
||||
std::unordered_map<ObjectGuid,time_t> _activity;
|
||||
std::mutex _damageMutex;
|
||||
std::unordered_map<uint32,std::deque<DamageNotice>> _damageNotices;
|
||||
std::unordered_map<uint32,std::deque<time_t>> _resurrectionNotices;
|
||||
std::unordered_map<uint32, ManagedPlayer> _records;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
#include "ClientlessPositioning.h"
|
||||
#include "AdventureAreaMgr.h"
|
||||
#include "Creature.h"
|
||||
#include "GameTime.h"
|
||||
#include "Map.h"
|
||||
#include "PathGenerator.h"
|
||||
#include "Player.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace
|
||||
{
|
||||
struct Reservation {uint32 Map;Position Point;time_t Until;float Radius;};
|
||||
// Clientless controllers and transport executors run on the world thread.
|
||||
std::unordered_map<uint32,Reservation> reservations;
|
||||
}
|
||||
float ClientlessPositioning::Clearance(Player* p,Position const& point)
|
||||
{
|
||||
float clearance=10;auto now=GameTime::GetGameTime();
|
||||
float range=std::min(60.0f,p->GetExactDist(point)+12);
|
||||
std::vector<Creature*> creatures;p->GetCreatureListWithEntryInGrid(creatures,0,range);
|
||||
std::vector<Player*> players;p->GetPlayerListInGrid(players,range);
|
||||
auto measure=[&](Unit* other)
|
||||
{
|
||||
if(other==p || !other->IsAlive() || !p->IsInPhase(other) || std::fabs(point.GetPositionZ()-other->GetPositionZ())>2)return;
|
||||
float radii=std::clamp(p->GetFloatValue(UNIT_FIELD_BOUNDINGRADIUS),0.3f,2.0f)+
|
||||
std::clamp(other->GetFloatValue(UNIT_FIELD_BOUNDINGRADIUS),0.3f,3.0f);
|
||||
clearance=std::min(clearance,point.GetExactDist2d(other)-radii);
|
||||
};
|
||||
for(auto other:creatures)measure(other);
|
||||
for(auto other:players)measure(other);
|
||||
for(auto it=reservations.begin();it!=reservations.end();)
|
||||
{
|
||||
if(it->second.Until<=now){it=reservations.erase(it);continue;}
|
||||
if(it->first!=p->GetGUID().GetCounter() && it->second.Map==p->GetMapId() && std::fabs(point.GetPositionZ()-it->second.Point.GetPositionZ())<2)
|
||||
clearance=std::min(clearance,point.GetExactDist2d(it->second.Point)-it->second.Radius-
|
||||
std::clamp(p->GetFloatValue(UNIT_FIELD_BOUNDINGRADIUS),0.3f,2.0f));
|
||||
++it;
|
||||
}
|
||||
return clearance;
|
||||
}
|
||||
void ClientlessPositioning::Reserve(Player* p,Position const& point)
|
||||
{reservations[p->GetGUID().GetCounter()]={p->GetMapId(),point,GameTime::GetGameTime()+30,std::clamp(p->GetFloatValue(UNIT_FIELD_BOUNDINGRADIUS),0.3f,2.0f)};}
|
||||
bool ClientlessPositioning::Nearby(Player* p,Position const& center,float radius,Position& result,std::vector<Position>& path)
|
||||
{
|
||||
if(!p || p->GetTransport() || !p->IsAlive())return false;
|
||||
for(unsigned i=0;i<24;++i)
|
||||
{
|
||||
float angle=float((p->GetGUID().GetCounter()*7+i*5)%24)*6.2831853f/24;
|
||||
float reach=std::min(radius,1.8f+float(i/8)*1.5f);
|
||||
Position point;point.Relocate(center.GetPositionX()+std::cos(angle)*reach,center.GetPositionY()+std::sin(angle)*reach,center.GetPositionZ());
|
||||
float floor=p->GetMap()->GetHeight(p->GetPhaseShift(),point.GetPositionX(),point.GetPositionY(),center.GetPositionZ()+2,true,6);
|
||||
if(floor<=INVALID_HEIGHT+1 || std::fabs(floor-center.GetPositionZ())>1.25f)continue;
|
||||
point.m_positionZ=floor;
|
||||
if(Clearance(p,point)<1.3f || !sAdventureAreaMgr->AllowsPosition(p,p->GetMapId(),point))continue;
|
||||
PathGenerator nav(p);if(!nav.CalculatePath(point.GetPositionX(),point.GetPositionY(),floor) || (nav.GetPathType()&(PATHFIND_NOPATH|PATHFIND_INCOMPLETE)))continue;
|
||||
auto const& points=nav.GetPath();if(points.size()<2)continue;
|
||||
std::vector<Position> candidate;float length=0;bool safe=true;
|
||||
for(auto const& v:points)
|
||||
{
|
||||
Position q;q.Relocate(v.x,v.y,v.z);
|
||||
if(!candidate.empty())
|
||||
{
|
||||
auto const& previous=candidate.back();float segment=previous.GetExactDist(&q);length+=segment;
|
||||
if(segment>8 || std::fabs(previous.GetPositionZ()-q.GetPositionZ())>3 ||
|
||||
!sAdventureAreaMgr->AllowsSegment(p,p->GetMapId(),previous,q)){safe=false;break;}
|
||||
unsigned steps=std::max(1u,unsigned(std::ceil(segment)));
|
||||
for(unsigned s=1;s<=steps;++s)
|
||||
{
|
||||
float t=float(s)/steps,x=previous.GetPositionX()+(q.GetPositionX()-previous.GetPositionX())*t,
|
||||
y=previous.GetPositionY()+(q.GetPositionY()-previous.GetPositionY())*t,z=previous.GetPositionZ()+(q.GetPositionZ()-previous.GetPositionZ())*t;
|
||||
float ground=p->GetMap()->GetHeight(p->GetPhaseShift(),x,y,z+2,true,6);
|
||||
if(ground<=INVALID_HEIGHT+1 || std::fabs(ground-z)>1){safe=false;break;}
|
||||
}
|
||||
}
|
||||
candidate.push_back(q);
|
||||
}
|
||||
if(!safe || length>20 || candidate.back().GetExactDist(&point)>1)continue;
|
||||
result=point;path=std::move(candidate);Reserve(p,point);return true;
|
||||
}
|
||||
return false; // No space is acceptable: retain the original shared spot.
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#ifndef VCORE_CLIENTLESS_POSITIONING_H
|
||||
#define VCORE_CLIENTLESS_POSITIONING_H
|
||||
#include "Common.h"
|
||||
#include "Position.h"
|
||||
#include <vector>
|
||||
class Player;
|
||||
namespace ClientlessPositioning
|
||||
{
|
||||
float Clearance(Player* player,Position const& point);
|
||||
void Reserve(Player* player,Position const& point);
|
||||
bool Nearby(Player* player,Position const& center,float radius,Position& point,std::vector<Position>& path);
|
||||
}
|
||||
#endif
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "AdventureAreaMgr.h"
|
||||
#include "ClientlessPositioning.h"
|
||||
#include "ClientlessTravelGraphMgr.h"
|
||||
#include "ClientlessTransportMgr.h"
|
||||
#include "Chat.h"
|
||||
@@ -453,8 +454,48 @@ void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff)
|
||||
if (ride.Elapsed > limit) { fail("transport timeout; any passenger attachment retained"); return; }
|
||||
if (ride.Status.Phase == Phase::Waiting)
|
||||
{
|
||||
if(player->GetMapId()!=ride.From.Map){fail("departure map changed while waiting");return;}
|
||||
if(ride.WaitStage==1)
|
||||
{
|
||||
if(ride.WaitTrace.empty() || player->GetExactDist(ride.WaitTrace.back())>=0.3f)ride.WaitTrace.push_back(*player);
|
||||
if(player->IsSplineEnabled())return;
|
||||
if(ride.WaitTrace.back().GetExactDist(player)>0.05f)ride.WaitTrace.push_back(*player);
|
||||
ride.WaitStage=2;
|
||||
}
|
||||
if(ride.WaitStage==3)
|
||||
{
|
||||
if(player->IsSplineEnabled())return;
|
||||
if(ride.WaitTrace.empty() || player->GetExactDist(ride.WaitTrace.front())>1.5f)
|
||||
{fail("return to shared transport node did not finish");return;}
|
||||
ride.WaitStage=0;ride.WaitTrace.clear();
|
||||
}
|
||||
if(ride.WaitStage==2)
|
||||
{
|
||||
if(!ride.Abort && !AtDock(player,object,ride.From,0))return;
|
||||
std::vector<Position> back;back.push_back(*player);
|
||||
for(auto it=ride.WaitTrace.rbegin();it!=ride.WaitTrace.rend();++it)
|
||||
if(back.back().GetExactDist(&*it)>0.11f)back.push_back(*it);
|
||||
ride.WaitStage=3;
|
||||
if(back.size()>1)player->GetMotionMaster()->MoveSmoothPath(1,back.data(),back.size(),false);
|
||||
ride.Status.Detail="returning along actual waiting path to shared transport node";
|
||||
return;
|
||||
}
|
||||
if (ride.Abort) { fail("transport cancelled on land"); return; }
|
||||
if (!AtDock(player, object, ride.From, ride.RequiredPause)) return;
|
||||
if (!AtDock(player, object, ride.From, ride.RequiredPause))
|
||||
{
|
||||
if(!ride.WaitTried && !player->GetTransport() && !player->IsInCombat())
|
||||
{
|
||||
ride.WaitTried=true;Position spot;std::vector<Position> path;
|
||||
if(ClientlessPositioning::Nearby(player,ride.From.Points.front().Pos,6,spot,path))
|
||||
{
|
||||
ride.WaitTrace.push_back(*player);ride.WaitStage=1;
|
||||
player->GetMotionMaster()->MoveSmoothPath(1,path.data(),path.size(),false);
|
||||
ride.Status.Detail="walking to nearby spaced waiting position";
|
||||
TC_LOG_INFO("server.adventurebot.navigation","TRANSPORT_WAIT_SPOT guid=%u node='%s' x=%.2f y=%.2f",player->GetGUID().GetCounter(),ride.From.Node.c_str(),spot.GetPositionX(),spot.GetPositionY());
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (Distance(*player, ride.From.Points.front().Pos) > 3.0f)
|
||||
{ fail("player moved away from departure node while waiting"); return; }
|
||||
ride.Point = 0;
|
||||
|
||||
@@ -83,6 +83,9 @@ private:
|
||||
bool Abort = false;
|
||||
uint32 RecoveryMs = 0, Recoveries = 0;
|
||||
bool RecoveryMoving = false;
|
||||
uint8 WaitStage=0; // 0 node, 1 walking out, 2 waiting, 3 returning
|
||||
bool WaitTried=false;
|
||||
std::vector<Position> WaitTrace;
|
||||
};
|
||||
bool EnsureLoaded();
|
||||
bool Sample(Player* player, Recording& recording, bool force);
|
||||
|
||||
@@ -578,7 +578,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sAdventureAreaMgr->AllowsNode(player,destinationItr->second))
|
||||
if (!locationGoal && !sAdventureAreaMgr->AllowsNode(player,destinationItr->second))
|
||||
{ error="destination node denied by area/node catalog"; return false; }
|
||||
if (locationGoal)
|
||||
{
|
||||
@@ -587,13 +587,20 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
{ error="destination location denied by area catalog"; return false; }
|
||||
}
|
||||
bool const flightEnabled = sClientlessFlightMgr->Enabled(player);
|
||||
auto flightCost = [&](ClientlessTravelNode const& from, uint32 map, float x, float y, float z)
|
||||
auto flightCost = [&](ClientlessTravelNode const& from, uint32 map, float x, float y, float z, bool access = false)
|
||||
{
|
||||
if (!flightEnabled || from.MapId != map)
|
||||
return std::numeric_limits<float>::infinity();
|
||||
Position a,b; a.Relocate(from.X,from.Y,from.Z,from.O); b.Relocate(x,y,z);
|
||||
auto begin=std::chrono::steady_clock::now();++trace.FlightCalls;
|
||||
float result=sClientlessFlightMgr->Estimate(player,map,a,b);
|
||||
float result=sClientlessFlightMgr->Estimate(player,map,a,b,access);
|
||||
if(access && std::isfinite(result))
|
||||
{
|
||||
float distance=a.GetExactDist(&b);
|
||||
if(distance<20)result=std::numeric_limits<float>::infinity();
|
||||
else result=(sClientlessFlightMgr->Preferred(player,map,a) || sClientlessFlightMgr->Preferred(player,map,b))?
|
||||
40+distance*0.4f:180+distance*1.5f;
|
||||
}
|
||||
trace.FlightUs+=std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now()-begin).count();
|
||||
return result;
|
||||
};
|
||||
@@ -684,6 +691,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
: std::numeric_limits<float>::infinity();
|
||||
};
|
||||
|
||||
std::unordered_set<std::string> flightRoots;
|
||||
uint32 startCount = 0;
|
||||
for (auto const& pair : _nodes)
|
||||
{
|
||||
@@ -712,6 +720,14 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
bool const reachable = entryNav.Calculated && entryNav.Complete &&
|
||||
entryNav.FinalDistance <= 1.5f && entryNav.MaxSegmentLength <= 8.0f &&
|
||||
entryNav.MaxZDelta <= 5.0f && entryNav.PathLength <= 500.0f;
|
||||
ClientlessTravelNode departure;
|
||||
departure.MapId=player->GetMapId();departure.X=player->GetPositionX();departure.Y=player->GetPositionY();departure.Z=player->GetPositionZ();
|
||||
float air=blockedBridgeKeys && blockedBridgeKeys->count("flight:flight_departure->"+node.Key)?
|
||||
std::numeric_limits<float>::infinity():flightCost(departure,node.MapId,node.X,node.Y,node.Z,!reachable);
|
||||
if(std::isfinite(air) && (!reachable || air<entryNav.PathLength))
|
||||
{flightRoots.insert(node.Key);startCost=air;}
|
||||
else
|
||||
{
|
||||
// VCORE_C819_DEFERRED_START_NODES: incomplete is not the same as unsafe.
|
||||
// The endpoint is handled by the executor, not counted as arrival at node.
|
||||
bool const partial = entryNav.Calculated &&
|
||||
@@ -736,6 +752,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
startCost = entryNav.PathLength;
|
||||
if (!reachable)
|
||||
startCost += 100000.0f; // Tier 2 only: deferred roots rank after reachable roots
|
||||
}
|
||||
}
|
||||
State state{ node.Key, false };
|
||||
std::string const key = stateKey(state);
|
||||
@@ -1670,12 +1687,18 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
||||
item.EntryPoint.X, item.EntryPoint.Y, item.EntryPoint.Z,
|
||||
item.EdgeDatabaseId, item.GroundStartIndex);
|
||||
}
|
||||
if(route.empty() && item.IncomingEdgeType=="start" && flightRoots.count(item.Value.Node))
|
||||
{
|
||||
ClientlessTravelRouteStep departure;departure.NodeKey="flight_departure";departure.NodeType="ground";departure.EdgeType="start";
|
||||
departure.MapId=player->GetMapId();departure.X=player->GetPositionX();departure.Y=player->GetPositionY();departure.Z=player->GetPositionZ();
|
||||
route.push_back(departure);
|
||||
}
|
||||
ClientlessTravelNode const& node = _nodes.at(item.Value.Node);
|
||||
ClientlessTravelRouteStep step;
|
||||
step.NodeKey = node.Key;
|
||||
step.NodeType = node.Type;
|
||||
step.WorldPointKey = node.WorldPointKey;
|
||||
step.EdgeType = item.IncomingEdgeType;
|
||||
step.EdgeType = item.IncomingEdgeType=="start" && flightRoots.count(item.Value.Node)?"free_flight":item.IncomingEdgeType;
|
||||
step.EdgeDatabaseId = item.EdgeDatabaseId;
|
||||
step.EdgeReverse = item.EdgeReverse;
|
||||
if (item.VirtualGroundEntry)
|
||||
|
||||
@@ -11,6 +11,7 @@ header=(root/'src/server/game/World/AdventureCombatMgr.h').read_text()
|
||||
header='\n'.join(x for x in header.splitlines() if not x.startswith('#include')).replace('private:','public:')
|
||||
code=r'''
|
||||
#include "AdventureCombatPolicy.h"
|
||||
#include "AdventureRecoveryPolicy.h"
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <deque>
|
||||
@@ -120,7 +121,7 @@ Creature* AdventureCombatMgr::Proactive(Player*)const{return nullptr;}
|
||||
bool AdventureCombatMgr::Start(Player*,Record&r,Unit*c,bool train,bool defense,time_t until){r.AI=std::make_shared<Fighter>();r.Target=c->GetGUID();r.Training=train;r.Defending=defense;r.Until=until;return true;}
|
||||
void AdventureCombatMgr::Finish(Player*,Record&r){r.AI.reset();r.Target.Clear();r.Defending=false;r.Training=false;r.Cooldown=10000;}
|
||||
'''
|
||||
for name in ('bool AdventureCombatMgr::Valid(', 'void AdventureCombatMgr::DamageTaken(', 'bool AdventureCombatMgr::Flee(', 'bool AdventureCombatMgr::Death(', 'bool AdventureCombatMgr::Update('):code+='\n'+function(name)
|
||||
for name in ('bool AdventureCombatMgr::Valid(', 'void AdventureCombatMgr::DamageTaken(', 'bool AdventureCombatMgr::Flee(', 'void AdventureCombatMgr::Revived(', 'bool AdventureCombatMgr::Death(', 'bool AdventureCombatMgr::Update('):code+='\n'+function(name)
|
||||
code+=r'''
|
||||
int main(){
|
||||
using namespace AdventureCombatPolicy;
|
||||
@@ -211,6 +212,14 @@ int main(){
|
||||
GameTime::now=1006;assert(mgr.Flee(&p,r)&&r.Fleeing);
|
||||
p.Relocate(-60,0,0);GameTime::now=1008;assert(mgr.Flee(&p,r)&&!r.Fleeing&&!r.AI);
|
||||
assert(p.revives==oldRevives&&p.graveyards==oldGraves);
|
||||
// Observe actual resurrection events, including a redeath BEFORE an alive poll.
|
||||
r={};p.teleport=false;p.grouped=false;int beforeGraves=p.graveyards;
|
||||
for(unsigned i=1;i<=5;++i){mgr.Revived(&p,2000+i*100);p.alive=false;p.ghost=false;GameTime::now=2001+i*100;mgr.Death(&p,r);assert(!r.ForceGraveyard&&p.graveyards==beforeGraves);}
|
||||
assert(r.DeathChain.NextGraveyard);
|
||||
mgr.Revived(&p,2600);p.alive=false;p.ghost=false;GameTime::now=2601;
|
||||
mgr.Death(&p,r);assert(r.ForceGraveyard);mgr.Death(&p,r);
|
||||
assert(p.graveyards==beforeGraves+1&&r.CemeteryPending);
|
||||
p.teleport=false;mgr.Death(&p,r);assert(p.alive&&!p.sickness);
|
||||
// No aggression means no flight solely from low health.
|
||||
r={};p.health=1;assert(!mgr.Flee(&p,r));
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ struct Guid{unsigned GetCounter(){return 99;}};
|
||||
struct Player{Guid GetGUID(){return {};}};
|
||||
struct Field{unsigned n=0;std::string s;unsigned GetUInt8(){return n;}unsigned GetUInt32(){return 0;}std::string GetString(){return s;}};
|
||||
struct Rows{std::vector<std::array<Field,3>> data;unsigned index=0;Field*Fetch(){return data[index].data();}bool NextRow(){return ++index<data.size();}};
|
||||
struct Tx{unsigned slots=0,deletes=0,repairs=0;void PAppend(char const*,char const*,unsigned,unsigned,unsigned){++repairs;}void PAppend(char const*,unsigned){++deletes;}void PAppend(char const*,unsigned,unsigned,unsigned,char const*,char const*){++slots;}};
|
||||
struct Tx{unsigned slots=0,deletes=0,repairs=0;void PAppend(char const*,char const*,char const*,unsigned,unsigned,unsigned){++repairs;}void PAppend(char const*,unsigned){++deletes;}void PAppend(char const*,unsigned,unsigned,unsigned,char const*,char const*){++slots;}};
|
||||
struct DB{void EscapeString(std::string&){}
|
||||
std::shared_ptr<Rows> rows;std::shared_ptr<Tx> saved;unsigned oldCleanup=0;
|
||||
auto PQuery(char const*,unsigned,unsigned){if(rows)rows->index=0;return rows;}
|
||||
@@ -56,6 +56,11 @@ int main(){
|
||||
assert(WorldDatabase.saved && WorldDatabase.saved->repairs==24 && relog.AreaRevision==1);
|
||||
for(unsigned h=0;h<24;++h){assert(relog.Plan[h].Intent==(h%2?"social":"adventure"));assert(relog.Plan[h].Location!="saved_"+std::to_string(h));}
|
||||
locations.valid=true;
|
||||
// No following intent exists at 23:00: repair preparation to rest and persist it.
|
||||
rows->data[23][1].s="groupformation";mgr.Generate(&p,relog,20261029);
|
||||
assert(relog.Plan[23].Intent=="rest"&&relog.Plan[23].Location.find("rest_")==0);
|
||||
assert(WorldDatabase.saved->repairs==1);
|
||||
rows->data[23][1].s="social";
|
||||
// Missing/incomplete current day is completely replaced, deleting older dates.
|
||||
rows->data.resize(4);mgr.Generate(&p,r,20261030);
|
||||
assert(r.Date==20261030 && mgr.picks>0 && WorldDatabase.saved->slots==24 && WorldDatabase.saved->deletes==1);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Compile the real destination eligibility method with native-level fixtures."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
s=(root/'src/server/game/World/AdventureLocationMgr.cpp').read_text()
|
||||
a=s.index('bool AdventureLocationMgr::LevelEligible(');b=s.index('\nbool AdventureLocationMgr::IsWorkstation',a)
|
||||
code=r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
struct Player{unsigned level=60;unsigned getLevel()const{return level;}};
|
||||
struct AdventureLocation{unsigned InstanceMap=0,AreaId=0,ZoneId=0;};
|
||||
struct Area{bool KnownLevel=true;unsigned MinLevel=1;};
|
||||
struct Areas{auto Area(unsigned id)->::Area{return {id!=0,id};}}areas;auto sAdventureAreaMgr=&areas;
|
||||
struct AdventureLocationMgr{
|
||||
std::unordered_map<unsigned,unsigned>_instanceMinimum{{533,80},{36,15}};
|
||||
bool LevelEligible(Player const*,AdventureLocation const&,std::string const&,bool strictArea=false)const;
|
||||
};
|
||||
'''+s[a:b]+r'''
|
||||
int main(){
|
||||
AdventureLocationMgr mgr;Player p;AdventureLocation naxx{533,0,0};
|
||||
assert(!mgr.LevelEligible(&p,naxx,"raid"));p.level=79;assert(!mgr.LevelEligible(&p,naxx,"raid"));
|
||||
p.level=80;assert(mgr.LevelEligible(&p,naxx,"raid"));
|
||||
AdventureLocation deadmines{36,0,0};p.level=14;assert(!mgr.LevelEligible(&p,deadmines,"instance"));
|
||||
p.level=15;assert(mgr.LevelEligible(&p,deadmines,"instance"));
|
||||
AdventureLocation unknown{999,0,0};assert(!mgr.LevelEligible(&p,unknown,"raid"));
|
||||
AdventureLocation zone{0,60,60};p.level=59;
|
||||
assert(mgr.LevelEligible(&p,zone,"adventure")); // own area policy retains its separate tolerance
|
||||
assert(!mgr.LevelEligible(&p,zone,"adventure",true));
|
||||
p.level=60;assert(mgr.LevelEligible(&p,zone,"adventure",true));
|
||||
assert(!mgr.LevelEligible(nullptr,zone,"adventure",true));
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory()as d:
|
||||
p=Path(d);(p/'test.cpp').write_text(code)
|
||||
subprocess.run(['c++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
|
||||
subprocess.run([str(p/'test')],check=True)
|
||||
print('Native dungeon/raid minimum levels, unknown destinations and strict adventure invitation levels passed')
|
||||
@@ -53,13 +53,16 @@ struct Combat{bool Busy(Player*){return false;}}combat;auto sAdventureCombatMgr=
|
||||
namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}}
|
||||
float frand(float,float){return 0;}unsigned urand(unsigned a,unsigned){return a;}
|
||||
'''+header+r'''
|
||||
struct Location{std::string Name="Stormwind";}location;struct Locations{Location*Find(std::string const&){return &location;}}locations;auto sAdventureLocationMgr=&locations;
|
||||
struct Position{void Relocate(float,float,float){}};
|
||||
struct Location{std::string Name="Stormwind";struct{unsigned MapId=0;float X=0,Y=0,Z=0;}Point;}location;
|
||||
struct Locations{bool levelEligible=true;Location*Find(std::string const&){return &location;}bool LevelEligible(Player*,Location const&,std::string const&,bool){return levelEligible;}}locations;auto sAdventureLocationMgr=&locations;
|
||||
struct Areas{bool allowed=true;bool AllowsPosition(Player*,unsigned,Position const&){return allowed;}}areas;auto sAdventureAreaMgr=&areas;
|
||||
struct Behavior{std::unordered_map<unsigned,AdventureGroupPlan> plans;AdventureGroupPlan GroupPlan(Player*p){return plans[p->guid.id];}}behavior;auto sAdventureBehaviorMgr=&behavior;
|
||||
int messages=0;
|
||||
void AdventureGroupMgr::Speak(Player*,std::string const&){++messages;}
|
||||
'''
|
||||
code+=fun('float Sociability(')+'\n'+fun('bool Ready(')+'\n'+fun('std::string Activity(')
|
||||
for name in ['std::string AdventureGroupMgr::RecruitmentText(', 'bool AdventureGroupMgr::RecruitPlayer(', 'bool AdventureGroupMgr::BotLed(', 'bool AdventureGroupMgr::Forming(', 'bool AdventureGroupMgr::PlannedTransition(', 'bool AdventureGroupMgr::Accept(', 'void AdventureGroupMgr::Update(', 'void AdventureGroupMgr::Invite(', 'void AdventureGroupMgr::RequestPlayerAdmission(', 'bool AdventureGroupMgr::PlayerLeadership(', 'bool AdventureGroupMgr::EnforceCapacity(']:code+='\n'+fun(name)
|
||||
for name in ['std::string AdventureGroupMgr::RecruitmentText(', 'bool AdventureGroupMgr::RecruitPlayer(', 'bool AdventureGroupMgr::BotLed(', 'bool AdventureGroupMgr::Forming(', 'bool AdventureGroupMgr::PlannedTransition(', 'bool AdventureGroupMgr::CanInvite(', 'bool AdventureGroupMgr::Accept(', 'void AdventureGroupMgr::Update(', 'void AdventureGroupMgr::Invite(', 'void AdventureGroupMgr::RequestPlayerAdmission(', 'bool AdventureGroupMgr::PlayerLeadership(', 'bool AdventureGroupMgr::EnforceCapacity(']:code+='\n'+fun(name)
|
||||
code+=r'''
|
||||
int main(){
|
||||
using namespace AdventureGroupPolicy;
|
||||
@@ -113,6 +116,17 @@ int main(){
|
||||
mgr.RequestPlayerAdmission(&a,&human);assert(invites==5&&human.invite==&party);
|
||||
human.group=&party;Member humanSlot;humanSlot.player=&human;party.members=&humanSlot;
|
||||
mgr.Update(&a);assert(party.leader==human.guid&&!mgr.BotLed(&a));
|
||||
// Content checks and level ranges apply before any native invitation.
|
||||
human.group=nullptr;human.invite=nullptr;a.level=60;human.level=54;
|
||||
assert(!mgr.CanInvite(&a,&human));human.level=55;locations.levelEligible=false;
|
||||
assert(!mgr.CanInvite(&a,&human));locations.levelEligible=true;areas.allowed=false;
|
||||
assert(!mgr.CanInvite(&a,&human));areas.allowed=true;human.level=60;
|
||||
// A player must not bypass the level limit by targeting another bot in a group.
|
||||
party.leader=a.guid;a.group=&party;a.level=80;b.level=85;b.group=&party;human.level=75;
|
||||
party.slots={{{29}},{{31}}};
|
||||
assert(!mgr.CanInvite(&a,&human));int beforeAdmission=invites;
|
||||
mgr.RequestPlayerAdmission(&b,&human);assert(invites==beforeAdmission);
|
||||
a.level=b.level=human.level=60;party.slots.clear();
|
||||
// Inform nearby humans of the actual NEXT destination before invitation.
|
||||
{mgr={};Player human;human.guid={999};human.session.clientless=false;
|
||||
ObjectAccessor::players[human.guid]=&human;a.group=nullptr;a.people={&human};
|
||||
|
||||
@@ -69,6 +69,7 @@ public:
|
||||
int begun=0,fallbacks=0,micros=0,resets=0,switches=0,queues=0,leisureUpdates=0;
|
||||
bool Allowed(Player*,AdventureLocation const&,std::string const&){return true;}
|
||||
bool AdoptGroup(Player*,Player*){return false;}
|
||||
bool UpdateNavigationRescue(Player*,Record&){return false;}
|
||||
void Update(Player*,uint32);
|
||||
void EnterPlayerInstance(Player*);
|
||||
void Generate(Player*,Record&,uint32){}
|
||||
|
||||
@@ -9,6 +9,7 @@ source=(root/'src/server/game/World/AdventureInteractionMgr.cpp').read_text()
|
||||
source='\n'.join(line for line in source.splitlines() if not line.startswith('#include'))
|
||||
code=r'''
|
||||
#include "AdventureInteractionPolicy.h"
|
||||
#include "AdventureGroupPolicy.h"
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
@@ -42,7 +43,7 @@ struct Player:Unit {
|
||||
explicit Player(unsigned id=26){guid.id=id;session.owner=this;}
|
||||
ObjectGuid GetGUID(){return guid;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool GetTransport(){return transport;}bool IsFlying(){return false;}bool IsInFlight(){return flight;}bool IsCharmed(){return charmed;}bool HasUnitState(int){return casting;}
|
||||
bool IsInMap(Player*p){return mapid==p->mapid;}bool IsInPhase(Player*p){return phase && p->phase;}bool CanSeeOrDetect(Player*p){return p->visible;}bool IsWithinLOSInMap(Player*p){return los && p->los;}bool IsFriendlyTo(Player*p){return team==p->team;}
|
||||
Map*GetMap(){return ↦}unsigned GetMapId(){return mapid;}Session*GetSession(){return &session;}Group*GetGroup(){return group;}void UninviteFromGroup(){invite=nullptr;}Group*GetGroupInvite(){return invite;}unsigned GetTeam(){return team;}ObjectGuid GetTarget(){return selection;}bool isAFK(){return afk;}
|
||||
Map*GetMap(){return ↦}unsigned GetMapId(){return mapid;}unsigned level=60;unsigned getLevel(){return level;}Session*GetSession(){return &session;}Group*GetGroup(){return group;}void UninviteFromGroup(){invite=nullptr;}Group*GetGroupInvite(){return invite;}unsigned GetTeam(){return team;}ObjectGuid GetTarget(){return selection;}bool isAFK(){return afk;}
|
||||
void SetStandState(int s){posture=s;}void SetFacingToObject(Player*){}void HandleEmoteCommand(uint32 e){++gestures;lastEmote=e;}void Say(char const*,int,Player*){++says;}
|
||||
void GetNearPoint(Player*,float&x,float&y,float&z,float,float){x=m_positionX;y=m_positionY;z=m_positionZ;}
|
||||
std::vector<Player*> people;void GetPlayerListInGrid(std::vector<Player*>&out,float){out=people;}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Run the real hearth recovery state machine with a deterministic clock."""
|
||||
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('bool AdventureBehaviorMgr::UpdateNavigationRescue(');b=s.index('\nvoid AdventureBehaviorMgr::Update(',a)
|
||||
code=r'''
|
||||
#include "AdventureRecoveryPolicy.h"
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
using uint32=unsigned;
|
||||
#define TC_LOG_INFO(...) ((void)0)
|
||||
constexpr int UNIT_NPC_EMOTESTATE=0,UNIT_STAND_STATE_STAND=0,UNIT_STATE_CASTING=1,EMOTE_STATE_SPELL_PRECAST=193,TEAM_ALLIANCE=0;
|
||||
struct Position{float x=0,y=0,z=0,o=0;void Relocate(float a,float b,float c,float d=0){x=a;y=b;z=c;o=d;}
|
||||
void Relocate(Position const*p){*this=*p;}float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}float GetOrientation()const{return o;}
|
||||
float GetExactDist(Position const&p)const{return std::sqrt((x-p.x)*(x-p.x)+(y-p.y)*(y-p.y)+(z-p.z)*(z-p.z));}};
|
||||
struct Map{bool dungeon=false;bool IsDungeon(){return dungeon;}}map;
|
||||
struct Session{void CompleteClientlessTeleport(){}}session;
|
||||
struct Motion{int expired=0;void MovementExpired(){++expired;}}motion;
|
||||
struct Player:Position{bool alive=true,combat=false,taxi=false,flying=false,aboard=false,falling=false,casting=false,alliance=false;
|
||||
unsigned mapId=1,emote=0,visual=0,teleports=0,saves=0;
|
||||
struct Guid{unsigned GetCounter(){return 26;}};Guid GetGUID(){return {};}
|
||||
bool IsAlive(){return alive;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return false;}Map*GetMap(){return ↦}
|
||||
unsigned GetMapId(){return mapId;}unsigned GetTeamId(){return alliance?0:1;}bool IsInFlight(){return taxi;}bool IsFlying(){return flying;}bool GetTransport(){return aboard;}
|
||||
bool IsFalling(){return falling;}bool HasUnitState(int){return casting;}void StopMoving(){}void SetStandState(int){}
|
||||
void SetUInt32Value(int,unsigned n){emote=n;}void SendPlaySpellVisualKit(unsigned kit,unsigned,unsigned duration){assert(kit==36&&duration==10000);++visual;}
|
||||
Motion*GetMotionMaster(){return &motion;}void CleanupAfterTaxiFlight(){taxi=false;}
|
||||
bool TeleportTo(unsigned m,float a,float b,float c,float d){mapId=m;Relocate(a,b,c,d);++teleports;return true;}
|
||||
Session*GetSession(){return &session;}void SaveToDB(){++saves;}
|
||||
};
|
||||
struct Tele{unsigned mapId;float position_x,position_y,position_z,orientation;std::string name;};
|
||||
struct Objects{Tele alliance{0,-8833,622,94,0,"Stormwind"},horde{1,1570,-4397,16,0,"Orgrimmar"};
|
||||
Tele*GetGameTeleExactName(std::string const&name){return name=="Stormwind"?&alliance:&horde;}}objects;auto sObjectMgr=&objects;
|
||||
namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}}
|
||||
struct Areas{bool AllowsPosition(Player*,unsigned,Position const&){return true;}}areas;auto sAdventureAreaMgr=&areas;
|
||||
enum class ClientlessActionExecutionState{Idle,Moving,Failed};
|
||||
struct Status{ClientlessActionExecutionState State=ClientlessActionExecutionState::Failed;};
|
||||
struct Executor{Status status;int clears=0;Status*GetStatus(unsigned){return &status;}void Clear(unsigned){++clears;status.State=ClientlessActionExecutionState::Idle;}}executor;auto sClientlessActionExecutorMgr=&executor;
|
||||
struct Core{void ClearQueuedActions(unsigned){}}core;auto sClientlessPlayerMgr=&core;
|
||||
struct Travel{void CancelTravel(unsigned){}}travel;auto sClientlessTravelMgr=&travel;
|
||||
struct AdventureBehaviorMgr{
|
||||
enum class Phase{Idle,Travel,OnSite};
|
||||
struct Record{time_t StalledSince=100,LastRescue=0,RescueAt=0,LastNavigationCheck=0;unsigned RescueFailures=5,ProgressMap=1;bool LastTravelFailed=true,Queued=true,Dirty=false;Position ProgressPosition,RescueOrigin;Phase State=Phase::Travel;std::string Detail;struct{bool Active=false;}Instance;};
|
||||
void ResetMicro(Player*,Record&){}void FinishInstance(Player*,Record&r){r.Instance.Active=false;}
|
||||
bool UpdateNavigationRescue(Player*,Record&);
|
||||
};
|
||||
'''+s[a:b]+r'''
|
||||
int main(){AdventureBehaviorMgr mgr;Player p;AdventureBehaviorMgr::Record r;
|
||||
GameTime::now=699;assert(!mgr.UpdateNavigationRescue(&p,r)&&p.teleports==0);
|
||||
GameTime::now=700;assert(mgr.UpdateNavigationRescue(&p,r)&&p.visual==1&&p.emote==193&&p.teleports==0);
|
||||
GameTime::now=709;assert(mgr.UpdateNavigationRescue(&p,r)&&p.teleports==0);
|
||||
GameTime::now=710;assert(mgr.UpdateNavigationRescue(&p,r)&&p.teleports==1&&p.mapId==1&&p.saves==1&&p.emote==0&&r.Dirty);
|
||||
assert(r.LastRescue==710);
|
||||
// Alliance capital, never the character's personal homebind.
|
||||
p={};p.alliance=true;r={};executor.status.State=ClientlessActionExecutionState::Failed;
|
||||
GameTime::now=700;assert(mgr.UpdateNavigationRescue(&p,r));GameTime::now=710;assert(mgr.UpdateNavigationRescue(&p,r)&&p.mapId==0);
|
||||
// Movement/combat interrupts the animation before it can teleport.
|
||||
p={};r={};executor.status.State=ClientlessActionExecutionState::Failed;GameTime::now=700;
|
||||
assert(mgr.UpdateNavigationRescue(&p,r));p.x=2;GameTime::now=701;assert(!mgr.UpdateNavigationRescue(&p,r)&&!r.RescueAt&&p.teleports==0);
|
||||
p={};r={};GameTime::now=700;assert(mgr.UpdateNavigationRescue(&p,r));p.combat=true;GameTime::now=710;
|
||||
assert(!mgr.UpdateNavigationRescue(&p,r)&&!r.RescueAt&&p.teleports==0);
|
||||
// Long gaps due to other activities cannot count as stalled navigation.
|
||||
p={};r={};r.LastNavigationCheck=100;GameTime::now=700;
|
||||
assert(!mgr.UpdateNavigationRescue(&p,r)&&p.visual==0&&r.StalledSince==700);
|
||||
// Native taxi/transport waits receive thirty minutes, rather than ten.
|
||||
p={};p.taxi=true;r={};GameTime::now=1899;assert(!mgr.UpdateNavigationRescue(&p,r));
|
||||
r.LastNavigationCheck=0;GameTime::now=1900;assert(mgr.UpdateNavigationRescue(&p,r));
|
||||
GameTime::now=1910;assert(mgr.UpdateNavigationRescue(&p,r)&&p.teleports==1&&!p.taxi);
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory()as d:
|
||||
p=Path(d);(p/'test.cpp').write_text(code)
|
||||
subprocess.run(['c++','-std=c++17','-I'+str(root/'src/server/game/World'),str(p/'test.cpp'),'-o',str(p/'test')],check=True)
|
||||
subprocess.run([str(p/'test')],check=True)
|
||||
print('Real hearth recovery: grace, animation, faction capitals, cancellation, activity gaps and long taxi grace passed')
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Boundary tests for the production death-chain, rescue and invitation policies."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
code=r'''
|
||||
#include "AdventureRecoveryPolicy.h"
|
||||
#include "AdventureGroupPolicy.h"
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
int main(){
|
||||
using namespace AdventureRecoveryPolicy;
|
||||
DeathChain c;
|
||||
assert(!c.Died(1) && c.QuickDeaths==0);
|
||||
for(int i=1;i<=5;++i){c.Revived=i*100;assert(!c.Died(i*100+60));assert(c.QuickDeaths==unsigned(i));}
|
||||
assert(c.NextGraveyard);c.Revived=600;
|
||||
assert(c.Died(610));assert(!c.NextGraveyard && c.QuickDeaths==0);
|
||||
c.Revived=700;assert(!c.Died(761)&&c.QuickDeaths==0);
|
||||
c.Revived=800;assert(!c.Died(810)&&c.QuickDeaths==1);
|
||||
c.Revived=900;assert(!c.Died(961)&&c.QuickDeaths==0);
|
||||
assert(!Rescue(699,100,0,9));assert(Rescue(700,100,0,5));
|
||||
assert(!Rescue(700,100,0,4));assert(!Rescue(10000,0,0,9));
|
||||
assert(!Rescue(2000,100,201,5));assert(Rescue(2001,100,201,5));
|
||||
using namespace AdventureGroupPolicy;
|
||||
assert(InviteLevel(60,55)&&InviteLevel(60,65));
|
||||
assert(!InviteLevel(60,54)&&!InviteLevel(60,66));
|
||||
assert(!AcceptPlayerLevel(85,10)&&!AcceptPlayerLevel(60,54));
|
||||
assert(AcceptPlayerLevel(60,55)&&AcceptPlayerLevel(10,85));
|
||||
std::cout<<"Five quick redeaths arm the NEXT graveyard fallback; rescue grace/cooldown and asymmetric invitation limits passed.\n";
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p=Path(d);(p/'test.cpp').write_text(code)
|
||||
subprocess.run(['c++','-std=c++17','-I'+str(root/'src/server/game/World'),str(p/'test.cpp'),'-o',str(p/'test')],check=True)
|
||||
subprocess.run([str(p/'test')],check=True)
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Exercise the production terrain-selection and flight-surface guard."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
s=(root/'src/server/game/World/ClientlessFlightMgr.cpp').read_text()
|
||||
a=s.index('Surface ClientlessFlightMgr::Sample(');b=s.index('\nbool ClientlessFlightMgr::Start(',a)
|
||||
code=r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <set>
|
||||
using uint32=unsigned;using int32=int;
|
||||
constexpr int MAX_NUMBER_OF_GRIDS=64;constexpr float MAX_HEIGHT=100000,INVALID_HEIGHT=-100000;
|
||||
struct Grid{int x_coord=35,y_coord=23;};namespace Trinity{Grid ComputeGridCoord(float,float){return {};}}
|
||||
struct Terrain{bool child=false;uint32 GetId(){return 1;}
|
||||
bool HasChildTerrainGridFile(unsigned,int,int){return child;}float GetMaxHeight(int,unsigned,float,float){return 1000;}}terrain;
|
||||
struct Map{unsigned GetId(){return 1;}Terrain*GetTerrain(){return &terrain;}void LoadGrid(float,float){}
|
||||
float GetHeight(int,float,float,float,bool,float){return 20;}float GetWaterLevel(int,float,float){return -100000;}}map;
|
||||
struct Player{Map*GetMap(){return ↦}int GetPhaseShift(){return 0;}};
|
||||
namespace PhasingHandler{unsigned swap=719;unsigned GetTerrainMapId(int,unsigned,Terrain*,float,float){return swap;}}
|
||||
struct TerrainMgr{static inline std::set<unsigned>files{1};static inline unsigned checked=0;
|
||||
static bool ExistMapAndVMap(unsigned id,float,float){checked=id;return files.count(id);}};
|
||||
struct Surface{float Ground=0,Ceiling=0;bool Valid=false;};
|
||||
struct Record{std::unordered_map<std::string,bool>Tiles;};
|
||||
struct ClientlessFlightMgr{Surface Sample(Player*,Record&,float,float)const;};
|
||||
'''+s[a:b]+r'''
|
||||
int main(){ClientlessFlightMgr mgr;Player p;Record a;
|
||||
auto surface=mgr.Sample(&p,a,1751,-4389);assert(surface.Valid&&surface.Ground==20&&TerrainMgr::checked==1);
|
||||
terrain.child=true;Record b;assert(!mgr.Sample(&p,b,1751,-4389).Valid&&TerrainMgr::checked==719);
|
||||
TerrainMgr::files.insert(719);Record c;assert(mgr.Sample(&p,c,1751,-4389).Valid&&TerrainMgr::checked==719);
|
||||
TerrainMgr::files.clear();Record d;assert(!mgr.Sample(&p,d,1751,-4389).Valid);
|
||||
terrain.child=false;TerrainMgr::files.insert(1);PhasingHandler::swap=1;
|
||||
Record e;assert(mgr.Sample(&p,e,1751,-4389).Valid&&TerrainMgr::checked==1);}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory()as d:
|
||||
p=Path(d);(p/'test.cpp').write_text(code)
|
||||
subprocess.run(['c++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
|
||||
subprocess.run([str(p/'test')],check=True)
|
||||
print('Missing phase tile uses native base terrain; an existing phase tile with missing collision stays forbidden')
|
||||
@@ -27,18 +27,19 @@ using uint32=uint32_t; using int32=int32_t;
|
||||
#define TC_LOG_DEBUG(...) do {} while(0)
|
||||
#define TC_LOG_WARN(...) do {} while(0)
|
||||
constexpr uint32 NAV_GROUND=1, PATHFIND_INCOMPLETE=2, PATHFIND_NOPATH=4;
|
||||
struct Position { float x=0,y=0,z=0;float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;} void Relocate(float a,float b,float c,float=0){x=a;y=b;z=c;} };
|
||||
struct Position { float x=0,y=0,z=0;float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;} float GetExactDist(Position const* p)const{return std::sqrt((x-p->x)*(x-p->x)+(y-p->y)*(y-p->y)+(z-p->z)*(z-p->z));} void Relocate(float a,float b,float c,float=0){x=a;y=b;z=c;} };
|
||||
struct Player { float x=0,y=0,z=0; bool IsInWorld() const{return true;} uint32 GetMapId()const{return 0;} uint32 GetInstanceId()const{return 0;} float GetPositionX()const{return x;} float GetPositionY()const{return y;} float GetPositionZ()const{return z;} };
|
||||
float dist(Position a,Position b){return std::sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)+(a.z-b.z)*(a.z-b.z));}
|
||||
struct ClientlessPathDiagnostics {bool Calculated=true, Complete=true,Suspicious=false; uint32 PathType=1,PointCount=2; float DirectDistance=0,PathLength=0,MaxSegmentLength=0,MaxZDelta=0,FinalDistance=0; std::string Reason;};
|
||||
struct Travel {void AnalyzePath(Player* p,Position b,ClientlessPathDiagnostics& n){Position a;a.Relocate(p->x,p->y,p->z);n.PathLength=n.DirectDistance=dist(a,b);}} travel;
|
||||
struct Travel {bool complete=true;void AnalyzePath(Player* p,Position b,ClientlessPathDiagnostics& n){Position a;a.Relocate(p->x,p->y,p->z);n.PathLength=n.DirectDistance=dist(a,b);n.Complete=complete;}} travel;
|
||||
auto sClientlessTravelMgr=&travel;
|
||||
struct Flight {
|
||||
bool enabled=false;float minimumX=-10000;
|
||||
void BeginPlanning(Player const* = nullptr){}
|
||||
bool Enabled(Player const*){return enabled;}
|
||||
float Estimate(Player const*,uint32 map,Position a,Position b){
|
||||
float d=dist(a,b);return enabled&&map==0&&a.x>=minimumX&&d>=60?180+d*1.5f:std::numeric_limits<float>::infinity();
|
||||
bool Preferred(Player const*,uint32,Position const&){return false;}
|
||||
float Estimate(Player const*,uint32 map,Position a,Position b,bool recovery=false){
|
||||
float d=dist(a,b);return enabled&&map==0&&a.x>=minimumX&&(recovery||d>=60)?(recovery?d:180+d*1.5f):std::numeric_limits<float>::infinity();
|
||||
}
|
||||
} flight;
|
||||
auto sClientlessFlightMgr=&flight;
|
||||
@@ -142,6 +143,15 @@ assert(cached.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why)&&done);
|
||||
access.blocked.insert("B");
|
||||
assert(!cached.PlanAdaptiveRoute(&start,"B",1,200,route,done,endKey,why));
|
||||
access.blocked.clear();
|
||||
// A failed local MMAP approach must not exclude a native-capable flight root.
|
||||
travel.complete=false;flight.enabled=true;flight.minimumX=-10000;PathGenerator::available=false;
|
||||
ClientlessTravelGraphMgr accessFlight;node(accessFlight,"A",300);node(accessFlight,"B",2000);edge(accessFlight,"A","B",77,1,false,"taxi");
|
||||
assert(accessFlight.PlanAdaptiveRoute(&start,"B",400,200,route,done,endKey,why)&&done);
|
||||
assert(route.size()==3&&route.front().NodeKey=="flight_departure"&&route[1].NodeKey=="A"&&route[1].EdgeType=="free_flight");
|
||||
std::unordered_set<std::string> deniedFlightRoots={"flight:flight_departure->A"};
|
||||
route.clear();
|
||||
assert(!accessFlight.PlanAdaptiveRoute(&start,"B",400,200,route,done,endKey,why,nullptr,&deniedFlightRoots));
|
||||
travel.complete=true;
|
||||
std::cout<<"ground planner regressions and dynamic flight/node/location/virtual-exit integration passed\n";
|
||||
}
|
||||
'''
|
||||
|
||||
@@ -20,7 +20,8 @@ Point PointOf(Position const&p){return {p.x,p.y,p.z};}
|
||||
namespace Trinity{bool IsValidMapCoord(float x,float y,float z){return std::isfinite(x)&&std::isfinite(y)&&std::isfinite(z);}}
|
||||
struct Map{float floor=0;float GetHeight(int,float,float,float,bool,float){return floor;}}map;
|
||||
struct Guid{unsigned GetCounter(){return 26;}};
|
||||
struct Player:Position{bool spline=false;unsigned launches=0,stops=0;Guid GetGUID(){return {};}bool IsSplineEnabled(){return spline;}void StopMoving(){++stops;spline=false;}void AddUnitMovementFlag(int){}float GetSpeed(int){return 20;}Map*GetMap(){return ↦}int GetPhaseShift(){return 169;}};
|
||||
struct Player:Position{bool spline=false;unsigned launches=0,stops=0;unsigned GetMapId(){return 0;}Guid GetGUID(){return {};}bool IsSplineEnabled(){return spline;}void StopMoving(){++stops;spline=false;}void AddUnitMovementFlag(int){}float GetSpeed(int){return 20;}Map*GetMap(){return ↦}int GetPhaseShift(){return 169;}};
|
||||
struct AreaAccess{bool allowed=true;bool AllowsPosition(Player*,unsigned,Position const&){return allowed;}bool AllowsSegment(Player*,unsigned,Position const&,Position const&){return allowed;}}access;auto sAdventureAreaMgr=&access;
|
||||
namespace Movement{struct MoveSplineInit{Player*p;Point target;MoveSplineInit(Player*who):p(who){}void MoveTo(float x,float y,float z,bool){target={x,y,z};}void SetFly(){}void SetWalk(bool){}void SetVelocity(float){}int Launch(){++p->launches;p->spline=true;return 1;}};}
|
||||
enum class ClientlessFlightState{Mounting,Flying,Arrived,Failed};
|
||||
struct Record{
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Exercise the actual free-slot branch used by Player::SetSkill."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
s=(root/'src/server/game/Entities/Player/Player.cpp').read_text()
|
||||
a=s.index('uint16 skillSlot = PLAYER_MAX_SKILLS;',s.index('void Player::SetSkill('))
|
||||
b=s.index(' if (skillEntry->CategoryID',a)
|
||||
code=r'''
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
using uint16=unsigned short;using uint32=unsigned;
|
||||
constexpr unsigned PLAYER_MAX_SKILLS=128,PLAYER_SKILL_LINEID_0=0;
|
||||
#define TC_LOG_ERROR(...) ((void)0)
|
||||
std::array<unsigned,128> fields;
|
||||
unsigned GetUInt16Value(unsigned f,unsigned offset){return fields[2*f+offset];}
|
||||
int selected=-1;
|
||||
void add(){
|
||||
'''+s[a:b]+r'''
|
||||
selected=skillSlot;
|
||||
}
|
||||
int main(){fields.fill(1);fields[0]=0;add();assert(selected==0);
|
||||
fields.fill(1);fields[127]=0;add();assert(selected==127);
|
||||
fields.fill(1);selected=-1;add();assert(selected==-1);}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory()as d:
|
||||
p=Path(d);(p/'test.cpp').write_text(code)
|
||||
subprocess.run(['c++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
|
||||
subprocess.run([str(p/'test')],check=True)
|
||||
print('Native riding/profession allocation accepts slot zero and last slot, rejects a full skill array')
|
||||
|
||||
# Exercise recursive rewarded-spell learning on an initialized zero-rank placeholder.
|
||||
a=s.index(' // update step',s.index('void Player::SetSkill('))
|
||||
b=s.index(' // archaeology handling',a)
|
||||
activation=r'''
|
||||
#include <map>
|
||||
#include <cassert>
|
||||
using uint16=unsigned short;using uint32=unsigned;
|
||||
enum {SKILL_UNCHANGED,SKILL_NEW,SKILL_CHANGED,SKILL_DELETED};
|
||||
enum {SKILL_CATEGORY_PROFESSION=11,SKILL_RIDING=762,PLAYER_PROFESSION_SKILL_LINE_1=3,
|
||||
ACHIEVEMENT_CRITERIA_TYPE_REACH_SKILL_LEVEL=4,ACHIEVEMENT_CRITERIA_TYPE_LEARN_SKILL_LEVEL=5};
|
||||
struct State {unsigned pos=0,uState=SKILL_UNCHANGED;};
|
||||
struct Entry{unsigned CategoryID=0;} entry;
|
||||
std::map<unsigned,State> states{{762,{}}};unsigned value=0;bool recursing=false;
|
||||
void SetSkillStep(unsigned,unsigned){}void SetSkillRank(unsigned,unsigned v){value=v;}
|
||||
void SetSkillMaxRank(unsigned,unsigned){}unsigned GetUInt32Value(unsigned){return 0;}
|
||||
void SetUInt32Value(unsigned,unsigned){}void refreshSkillBonusAuras(){}
|
||||
void UpdateSkillEnchantments(unsigned,unsigned,unsigned){}void UpdateMountCapability(){}
|
||||
void UpdateAchievementCriteria(unsigned,unsigned){}
|
||||
void activate(unsigned newVal);
|
||||
void LearnSkillRewardedSpells(unsigned,unsigned v){if(!recursing){recursing=true;activate(v);recursing=false;}}
|
||||
void activate(unsigned newVal){unsigned currVal=value,step=3,maxVal=225,id=762;
|
||||
auto itr=states.find(id);auto skillEntry=&entry;
|
||||
'''+s[a:b]+r'''
|
||||
}
|
||||
int main(){activate(225);assert(states[762].uState==SKILL_NEW);
|
||||
states[762].uState=SKILL_UNCHANGED;activate(300);assert(states[762].uState==SKILL_CHANGED);
|
||||
states[762].uState=SKILL_DELETED;value=0;activate(225);assert(states[762].uState==SKILL_CHANGED);}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p=Path(d);(p/'activation.cpp').write_text(activation)
|
||||
subprocess.run(['c++','-std=c++17',str(p/'activation.cpp'),'-o',str(p/'activation')],check=True)
|
||||
subprocess.run([str(p/'activation')],check=True)
|
||||
print('Native recursive skill activation preserves INSERT for new placeholders and UPDATE for saved skills')
|
||||
@@ -53,6 +53,7 @@ struct Position {
|
||||
static float NormalizeOrientation(float a){a=std::fmod(a,float(2*M_PI));return a<0?a+2*M_PI:a;}
|
||||
void Relocate(float a,float b,float c,float d=0){x=a;y=b;z=c;o=d;}
|
||||
void Relocate(Position const& p){*this=p;}
|
||||
float GetExactDist(Position const&p)const{return GetExactDist(&p);}
|
||||
Position const& GetPosition()const{return *this;}
|
||||
void GetPosition(float&a,float&b,float&c,float&d)const{a=x;b=y;c=z;d=o;}
|
||||
float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}float GetOrientation()const{return o;}
|
||||
@@ -89,13 +90,17 @@ public:
|
||||
int teleports=0;bool falling=false;
|
||||
bool IsFalling()const{return falling;}bool IsMounted(){return false;}void Dismount(){}void UpdateSpeed(int){}void RemoveAurasByType(int){}
|
||||
bool TeleportTo(uint32 m,float a,float b,float c,float d){++teleports;map=m;Relocate(a,b,c,d);falling=false;return true;}
|
||||
bool IsAlive()const{return true;}bool IsFlying()const{return false;}bool IsInFlight()const{return false;}
|
||||
bool IsInCombat(){return false;}bool IsAlive()const{return true;}bool IsFlying()const{return false;}bool IsInFlight()const{return false;}
|
||||
bool IsInWorld()const{return true;}bool IsInPhase(GameObject*)const{return true;}
|
||||
bool IsBeingTeleported()const{return pending;}TransportBase* GetTransport()const{return transport;}
|
||||
ObjectGuid GetGUID()const{return {26};}uint32 GetMapId()const{return map;}
|
||||
float GetSpeed(int)const{return 7;}void StopMoving(){}bool IsSplineEnabled()const{return false;}
|
||||
Motion* GetMotionMaster(){return &motion;}Session* GetSession(){return &session;}
|
||||
};
|
||||
struct Node{} node;
|
||||
struct Graph{Node* FindNode(std::string const&){return &node;}}graph;auto sClientlessTravelGraphMgr=&graph;
|
||||
struct AreaAccess{bool allowed=true;bool Applies(Player*){return true;}bool AllowsNode(Player*,Node const&){return allowed;}}areas;auto sAdventureAreaMgr=&areas;
|
||||
namespace ClientlessPositioning{bool available=false;bool Nearby(Player*p,Position const&,float,Position&spot,std::vector<Position>&path){if(!available)return false;spot=*p;spot.x+=2;path={*p,spot};return true;}}
|
||||
void TransportBase::CalculatePassengerPosition(float&x,float&y,float&z,float*o)const{CalculatePassengerPosition(x,y,z,o,go->x,go->y,go->z,go->o);}
|
||||
void TransportBase::CalculatePassengerOffset(float&x,float&y,float&z,float*o)const{CalculatePassengerOffset(x,y,z,o,go->x,go->y,go->z,go->o);}
|
||||
void TransportBase::AddPassenger(Player*p){p->transport=this;p->m_movementInfo.transport.guid=go->GetGUID();}
|
||||
@@ -221,6 +226,20 @@ int main(){
|
||||
lost.x=50;lost.teleports=0;travel.path=true;recovery.Status.Phase=Phase::Riding;
|
||||
mgr.UpdateRide(&lost,recovery,100);assert(lost.teleports==0 && travel.starts==1);
|
||||
mgr.UpdateRide(&lost,recovery,100);assert(!recovery.Status.Recovering);
|
||||
// Waiting spots return along the observed path before the existing boarding sequence.
|
||||
Player waiting;GameObject dock;dock.x=5;dock.state.Stopped=false;waiting.object=&dock;
|
||||
ClientlessPositioning::available=true;
|
||||
assert(mgr.Start(&waiting,"A","B",error));auto& spaced=mgr._rides.at(26);
|
||||
mgr.UpdateRide(&waiting,spaced,100);assert(spaced.WaitStage==1&&waiting.x==2&&!waiting.transport);
|
||||
mgr.UpdateRide(&waiting,spaced,100);assert(spaced.WaitStage==2&&spaced.WaitTrace.size()==2);
|
||||
dock.state.Stopped=true;dock.state.RemainingPause=10000;
|
||||
mgr.UpdateRide(&waiting,spaced,100);assert(spaced.WaitStage==3&&waiting.x==0&&!waiting.transport);
|
||||
run(mgr,waiting,spaced);assert(spaced.Status.Phase==Phase::Riding&&waiting.transport);
|
||||
dock.x=100;waiting.x=101;run(mgr,waiting,spaced);
|
||||
assert(spaced.Status.Phase==Phase::Arrived&&!waiting.transport&&waiting.x==104);
|
||||
ClientlessPositioning::available=false;
|
||||
Player denied;denied.object=&dock;areas.allowed=false;
|
||||
assert(!mgr.Start(&denied,"A","B",error));areas.allowed=true;
|
||||
std::cout<<"Transport dock, frame, recorder, cross-map copy and ride regressions passed\n";
|
||||
}
|
||||
'''
|
||||
|
||||
Reference in new issue
Block a user