Stabilize grouped instance intent transitions
GCC PCH / build (push) Canceled after 0s
clang PCH / build (push) Canceled after 0s

This commit is contained in:
Nico committed 2026-10-31 22:13:39 +01:00
1 parent d1ef007cef
commit 7b4994400b
6 files changed
+118 -4

No files matched your search

+30
View File
@@ -3653,3 +3653,33 @@ Console-Beispiele ohne führenden Punkt:
Der Nutzer möchte vor allem konkrete, überprüfte Ergebnisse und wenig
Bestätigungsfragen. Die Hauptberufs-Rückfrage ist bereits BEANTWORTET; nicht
noch einmal stellen. Testservermanagement und lokale Commits sind autorisiert.
## Checkpoint 2026-10-31 – Gruppen-/Instanz-Resilienz
Auf Nutzerwunsch wurden die ersten drei Befunde des Gruppentests bearbeitet:
- `AdventureBehaviorMgr::Fallback` verwirft bei einem fehlgeschlagenen
Anreiseversuch nicht mehr den aktiven Instanzbesuch. Derselbe Eingang wird
nach 30 Sekunden erneut versucht; dadurch kann ein Stundenblock nicht mehr
nacheinander verschiedene Instanzen auswählen.
- Ein Mitglied einer botgeführten Gruppe wählt bei einem lokal nicht
erreichbaren, vom Anführer geerbten Ziel kein eigenes Ersatzziel mehr. Es
wartet auf Gruppenreise, Browseraufnahme oder Neuplanung des Anführers. Dies
beseitigt die bei Fizzira (GUID 43) beobachtete Endlosschleife, bei der
`AdoptGroup` das gerade gewählte Fallbackziel sofort wieder überschrieb.
- Die 10-Minuten-AFK-/Intentwechsel-Austrittslogik ist nun während der gesamten
Gruppenbildung, Sammelphase, vorgezogenen Abreise und des sicheren
Intent-Handoffs gesperrt, nicht nur solange `GroupPlan.Forming` wahr ist.
Regressionstests:
`tests/clientless/test_adventure_interactions.py` und
`tests/clientless/test_adventure_intent_fallbacks.py` bestanden. Vollbuild mit
`cmake --build /home/wow_admin/wow_cata_server/build/debug -j10` und Installation
waren erfolgreich. Der Server startete frisch bis `(worldserver-daemon) ready`,
PID 56787, und lauscht auf 8085/8087. Loader und Testbots waren beim Start
deaktiviert und wurden danach wieder auf die vorherigen Werte gestellt; die
Population loggt regulär ein. Fizzira erreichte Online; im frischen Log trat
ein einzelner normaler Fallback vom Gruppenbildungsziel `poi_tele_1377` auf,
aber keine wiederholte A/B-Schleife und kein Instanzbesuchswechsel. Die vom
Nutzer ausdrücklich zurückgestellten Befunde 4/5
(Gruppenkonsolidierung und Raid-Browser/Intent-Auswertung) wurden nicht geändert.
+41 -1
View File
@@ -531,7 +531,38 @@ void AdventureBehaviorMgr::Begin(Player * player, Record & r)
void AdventureBehaviorMgr::Fallback(Player * player, Record & r)
{
if (r.Instance.Active && r.Instance.Stage==AdventureInstancePolicy::Phase::Travel) FinishInstance(player,r);
if (r.Instance.Active && r.Instance.Stage==AdventureInstancePolicy::Phase::Travel)
{
// An instance/raid block owns one persistent visit. A failed approach
// may be retried, but must never delete that visit and roll another
// dungeon during the same block. UpdateInstance will turn a visit that
// can no longer arrive in time into its normal final rest phase.
r.State=Phase::Idle;
r.Dirty=true;
r.Queued=false;
r.RetryTimer=30000;
r.Detail="instance entrance travel failed; retrying the same visit";
TC_LOG_WARN("server.adventurebot","INSTANCE_TRAVEL_RETRY guid=%u location='%s' retryMs=%u",
player->GetGUID().GetCounter(),r.Instance.Location.c_str(),r.RetryTimer);
return;
}
if (auto group=player->GetGroup(); group && group->GetLeaderGUID()!=player->GetGUID() &&
sAdventureGroupMgr->BotLed(player) && r.GroupOverride)
{
// The leader owns a bot-led expedition target. A member-local fallback
// would be overwritten by AdoptGroup on the next tick, producing an
// endless A->B->A loop with Tried reset to one. Hold the inherited
// target and let group travel, browser admission or leader replanning
// decide the next transition.
r.State=Phase::Idle;
r.Dirty=false;
r.Queued=false;
r.RetryTimer=30000;
r.Detail="group destination unavailable locally; waiting for leader";
TC_LOG_WARN("server.adventurebot","GROUP_MEMBER_FALLBACK_HOLD guid=%u leader=%u location='%s' retryMs=%u",
player->GetGUID().GetCounter(),group->GetLeaderGUID().GetCounter(),r.Location.c_str(),r.RetryTimer);
return;
}
r.Tried.insert(r.Location);
AdventureLocation const * nearest = nullptr;
double best = std::numeric_limits < double >::infinity();
@@ -2143,6 +2174,15 @@ std::string AdventureBehaviorMgr::InteractionSchedule(Player* player)
auto const& slot=r.Plan[Local(GameTime::GetGameTime()).tm_hour];
return slot.Intent+":"+slot.Location;
}
bool AdventureBehaviorMgr::ProtectGroupLeave(Player* player) const
{
auto it=_records.find(player->GetGUID().GetCounter());
if(it==_records.end())return false;
auto const& r=it->second;auto now=GameTime::GetGameTime();
return r.Intent=="groupformation" || r.WantedIntent=="groupformation" ||
r.Gathering || r.Departure.Active(now) || r.State==Phase::Switching ||
r.State==Phase::Handover;
}
void AdventureBehaviorMgr::InterruptForInteraction(Player* player)
{
@@ -42,6 +42,7 @@ public:
std::string CombatIntent(Player* player) const;
std::string InteractionIntent(Player* player) const;
std::string InteractionSchedule(Player* player);
bool ProtectGroupLeave(Player* player) const;
private:
bool EditPlan(ChatHandler* handler, std::string const& args);
enum class Phase {Idle, Travel, OnSite, Switching, Handover, WaitingGroup};
@@ -175,10 +175,11 @@ bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
if(leader->isAFK()){if(!r.AfkSince)r.AfkSince=now;}else r.AfkSince=0;
}
bool forming=sAdventureGroupMgr->Forming(p);
if(forming){r.LastActivity=now;r.AfkSince=0;r.Schedule=sAdventureBehaviorMgr->InteractionSchedule(p);}
bool protectedFormation=forming || sAdventureBehaviorMgr->ProtectGroupLeave(p);
if(protectedFormation){r.LastActivity=now;r.AfkSince=0;r.Schedule=sAdventureBehaviorMgr->InteractionSchedule(p);}
bool protectedTravel=sAdventureGroupTravelMgr->Protected(p) || p->isUsingLfg() || p->InBattlegroundQueue() || p->InBattleground();
if(protectedTravel){r.LastActivity=now;r.AfkSince=0;}
if(!forming && !protectedTravel && AdventureInteractionPolicy::CanLeave(p->IsAlive(),p->IsInCombat(),p->GetMap()->IsDungeon(),p->GetTransport() || p->IsInFlight() || p->IsFlying()))
if(!protectedFormation && !protectedTravel && AdventureInteractionPolicy::CanLeave(p->IsAlive(),p->IsInCombat(),p->GetMap()->IsDungeon(),p->GetTransport() || p->IsInFlight() || p->IsFlying()))
{
if(r.Schedule!=sAdventureBehaviorMgr->InteractionSchedule(p))
{
@@ -0,0 +1,38 @@
"""Guard the two fallback invariants that previously caused live intent loops."""
from pathlib import Path
root = Path(__file__).resolve().parents[2]
source = (root / "src/server/game/World/AdventureBehaviorMgr.cpp").read_text()
start = source.index("void AdventureBehaviorMgr::Fallback(")
end = source.index("void AdventureBehaviorMgr::ResetMicro(", start)
fallback = source[start:end]
instance_guard = fallback.index("r.Instance.Stage==AdventureInstancePolicy::Phase::Travel")
ordinary_reselection = fallback.index("r.Tried.insert(r.Location)")
assert instance_guard < ordinary_reselection
assert "FinishInstance(player,r)" not in fallback[instance_guard:ordinary_reselection]
assert "INSTANCE_TRAVEL_RETRY" in fallback[instance_guard:ordinary_reselection]
assert "r.Instance.Location" in fallback[instance_guard:ordinary_reselection]
follower_guard = fallback.index("sAdventureGroupMgr->BotLed(player)")
assert follower_guard < ordinary_reselection
follower_hold = fallback[follower_guard:ordinary_reselection]
assert "r.GroupOverride" in follower_hold
assert "r.Dirty=false" in follower_hold
assert "GROUP_MEMBER_FALLBACK_HOLD" in follower_hold
protect_start = source.index("bool AdventureBehaviorMgr::ProtectGroupLeave(")
protect_end = source.index("void AdventureBehaviorMgr::InterruptForInteraction(", protect_start)
protection = source[protect_start:protect_end]
for invariant in (
'r.Intent=="groupformation"',
'r.WantedIntent=="groupformation"',
"r.Gathering",
"r.Departure.Active(now)",
"r.State==Phase::Switching",
"r.State==Phase::Handover",
):
assert invariant in protection
print("Instance visit retention, inherited group-target hold and formation leave protection passed.")
@@ -58,7 +58,7 @@ unsigned urand(unsigned a,unsigned){return a;}float roll=0;float frand(float,flo
namespace ObjectAccessor {std::unordered_map<ObjectGuid,Player*> players;Player*FindPlayer(ObjectGuid id){auto it=players.find(id);return it==players.end()?nullptr:it->second;}}
struct Profile {std::string Locale="deDE";std::unordered_map<std::string,float> Personality;};
struct Lua {std::unordered_map<unsigned,Profile> profiles;Profile const*GetAdventureProfile(unsigned id){auto it=profiles.find(id);return it==profiles.end()?nullptr:&it->second;}}lua;auto sClientlessLuaEngine=&lua;
struct Behavior {int interrupts=0;std::string intent="social",schedule="social:inn";std::string InteractionIntent(Player*){return intent;}std::string InteractionSchedule(Player*){return schedule;}void ResumePersonalPlan(Player*){}void InterruptForInteraction(Player*){++interrupts;}}behavior;auto sAdventureBehaviorMgr=&behavior;
struct Behavior {int interrupts=0;bool protectGroupLeave=false;std::string intent="social",schedule="social:inn";std::string InteractionIntent(Player*){return intent;}std::string InteractionSchedule(Player*){return schedule;}bool ProtectGroupLeave(Player*)const{return protectGroupLeave;}void ResumePersonalPlan(Player*){}void InterruptForInteraction(Player*){++interrupts;}}behavior;auto sAdventureBehaviorMgr=&behavior;
struct Combat {bool busy=false;int assists=0;bool Busy(Player*){return busy;}bool Assist(Player*,Unit*){++assists;return true;}}combat;auto sAdventureCombatMgr=&combat;
struct ClientlessAction{std::string Name;std::unordered_map<std::string,std::string> Parameters;};
struct Core {time_t activity=0;int queued=0;time_t LastActivity(ObjectGuid){return activity;}bool QueueAction(unsigned,ClientlessAction){++queued;return true;}}core;auto sClientlessPlayerMgr=&core;
@@ -129,6 +129,10 @@ int main(){
bot.lfg=false;bot.bgqueue=true;tick(6000);assert(bot.group);
bot.bgqueue=false;bot.battleground=true;tick(7000);assert(bot.group);
bot.battleground=false;tick(7001);assert(!bot.group);bot.group=&group;
// Formation, gathering and departure handover suppress both schedule and AFK exits.
mgr._records.clear();behavior.protectGroupLeave=true;behavior.schedule="groupformation:meeting";leader.afk=true;
tick(8000);behavior.schedule="instance:dungeon";tick(9000);assert(bot.group);
behavior.protectGroupLeave=false;leader.afk=false;behavior.schedule="adventure:road";tick(9001);assert(!bot.group);bot.group=&group;
// Explicit diagnostic leave uses ordinary safe-state guards.
bot.combat=true;assert(!mgr.RequestLeave(&bot)&&bot.group);bot.combat=false;
assert(mgr.RequestLeave(&bot)&&!bot.group);