Improve AdventureBot travel recovery and destination eligibility
GCC PCH / build (push) Waiting to run
clang PCH / build (push) Canceled after 0s

This commit is contained in:
Nico committed 2026-10-29 11:10:27 +01:00
1 parent 321a28640c
commit 49712aadfe
50 files changed
+984 -44

No files matched your search

+69
View File
@@ -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
}
@@ -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.
@@ -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.
+54
View File
@@ -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.
+17 -14
View File
@@ -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;
+1 -1
View File
@@ -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())
+3 -1
View File
@@ -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);
+89 -2
View File
@@ -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);
+22 -1
View File
@@ -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);
+20 -1
View File
@@ -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;
+36 -1
View File
@@ -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
+25 -1
View File
@@ -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')
+16 -2
View File
@@ -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 &map;}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 &map;}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 &map;}
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 &map;}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')
+14 -4
View File
@@ -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";
}
'''
+2 -1
View File
@@ -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 &map;}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 &map;}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')
+20 -1
View File
@@ -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";
}
'''