Stabilize grouped instance intent transitions
This commit is contained in:
1 parent
d1ef007cef
commit
7b4994400b
6 files changed
+118
-4
No files matched your search
@@ -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.
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user