Compare commits
2
Commits
7b4994400b
...
be049baacc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be049baacc | ||
|
|
856832e7bd |
No files matched your search
@@ -1,3 +1,53 @@
|
||||
# Aktuell: finaler Gruppenbildungsmanager implementiert und installiert
|
||||
|
||||
Nutzerauftrag: Ab zehn Minuten vor Ende von `groupformation` sollen Botgruppen
|
||||
deterministisch zu möglichst vielen vollständigen Gruppen konsolidiert werden;
|
||||
Zielzugang/Rollen müssen passen, Stufe85 bleibt eine eigene Kategorie, menschlich
|
||||
geführte Gruppen unberührt, expliziter Testfallback erwünscht. Implementiert in
|
||||
AdventureGroupMgr/Policy: 30-Sekunden-Lauf im letzten 600-Sekunden-Fenster,
|
||||
größte kompatible Zielgruppe zuerst, rollenbewusster Transfer aus kleineren
|
||||
unvollständigen botgeführten Gruppen, vollständige Revalidierung von Fraktion,
|
||||
+/-5-Levelspanne, Level85-Isolation, Beruf, Ziel-Level/Zugang, Gebietspolicy,
|
||||
Kapazität und Rolle. LFG/Browser-, Menschen-, Transit-/Kampfgruppen sowie bereits
|
||||
vollständige Gruppen werden nicht umgebaut. Fehlerhafte native Einladung versucht
|
||||
die ursprüngliche Gruppe begrenzt wiederherzustellen. Tagespläne/Ziel des Leaders
|
||||
werden nicht geändert; Fünf-Minuten-Sammelregel und Browserfallback bleiben.
|
||||
|
||||
Testschnittstelle: `clientless groupmanager preview|run|status`; `run` umgeht nur
|
||||
das Zeitfenster, nicht Sicherheits-/Zugangsregeln. Dokumentiert in bot-groups.md.
|
||||
Neue Regression test_adventure_group_manager.py plus aktualisierter Fixture-Stub.
|
||||
Gezielt PASS: group_manager, adventure_groups, group_role_composition; diff-check
|
||||
PASS. Voller CMake-Build ausschließlich-j10 PASS, installiert. Bestehende
|
||||
uncommittete Flug-WIP wurde erhalten und mitgebaut, nicht als Managerarbeit
|
||||
beansprucht.
|
||||
|
||||
Buildbetrieb: Loader/Testaufnahme pausiert, safe logout angefordert. Busy-Loader
|
||||
nahm Config-Reload nicht rechtzeitig an und hielt durch Nachladen 16/17 Sitzungen;
|
||||
nach vollen fünf Minuten Recoveryliste/Saveall unter checkpoints/2026-11-01-
|
||||
group-manager-build/recovery-before-install.tsv. Beim danach angeforderten sauberen
|
||||
Shutdown SIGSEGV im alten, unter GDB laufenden Prozess; ausschließlich abgestürzten
|
||||
Testserver/GDB per TERM beendet, Prozessende vor Installation verifiziert. Frischer
|
||||
Prozess108590 erreichte ready, Ports8085/8087. Loader/Testaufnahme wieder1.
|
||||
|
||||
Live: Zehn vorbereitete Level85-Raidbots plus Karggan geladen; aktuelle Formation
|
||||
und nächster Raid Bastion des Zwielichts gesetzt. Preview: 11 Kandidaten, gültiger
|
||||
Transfer. Erster erzwungener Apply während ihrer Anreise: null Transfers, korrekt
|
||||
wegen Ready-Schutz. Nach Ankunft wurden Brynda165 und Ragnorli167 regelkonform aus
|
||||
der kleineren Gruppe in die7er-Gruppe verschoben. Dabei zeigte sich, dass der noch
|
||||
benötigte Tank Ashaelri176 als Leiter der verbliebenen Zweiergruppe gesperrt war.
|
||||
Fix: Botleiter darf Führung an ein anderes CPC-Botmitglied abgeben; niemals wird
|
||||
ein Mensch erzwungener Nachfolger. Rollback bewahrt den ursprünglichen Leiter.
|
||||
Erneut Tests/Build-j10 PASS, zweite Fünf-Minuten-Restliste unter
|
||||
recovery-before-final-install.tsv, sauberer Shutdown, installiert, fresh ready
|
||||
Prozess113452/Ports8085+8087. Nach Neustart Preview11/moves1, Apply verschob
|
||||
Ashaelri176; DB bestätigt Gruppe2 exakt10: Renren, Marndra, Marnda, Aruendas,
|
||||
Winienwi, Taliaarnora, Brynda, Corric, Ragnorli, Ashaelri. Karggan bleibt einzeln.
|
||||
Alle20 finalen Recoverybots wurden erneut geladen und zeigen neue normale Intent-,
|
||||
Travel-, Taxi- oder Kampfzustände statt alter Restart-Transitstates. Loader/Test
|
||||
wieder1; Status enabled1/schema1/busy0, gestaffeltes Laden läuft. Kein Commit/Push.
|
||||
|
||||
---
|
||||
|
||||
# Aktuell: Routenplaner-Last behoben und volle Population live geprüft
|
||||
|
||||
Nutzer meldete Serverlags und bat nach Prüfung bestehender Navigationsprobleme
|
||||
|
||||
@@ -56,6 +56,31 @@ planned formation-to-activity transition is exempt from intent-change departure.
|
||||
Later changes retain the existing loyalty-based departure behavior, deferred in
|
||||
combat/instances/transit. Native removal handles leadership and party dissolution.
|
||||
|
||||
### Final formation manager
|
||||
|
||||
During the final ten minutes of `groupformation`, a deterministic fallback runs
|
||||
every 30 seconds. It prefers the largest compatible bot-led group, fills only its
|
||||
currently missing roles, and may move a bot out of a smaller incomplete bot-led
|
||||
group. Human-led and browser/LFG groups are never reorganized. Every transfer is
|
||||
revalidated against faction, the inclusive five-level range, profession rules,
|
||||
the leader's chosen destination, that destination's level/access restrictions,
|
||||
area policy and native capacity. Level 85 is an isolated matching category: a
|
||||
level-85 bot is only consolidated with other level-85 bots. This is a greedy
|
||||
fallback intended to maximize complete groups without replacing normal social
|
||||
formation; it does not claim a globally optimal partition.
|
||||
|
||||
The ordinary five-minute gathering/departure rule remains authoritative. A
|
||||
complete group can therefore finish consolidation by minute 50 and gather from
|
||||
minute 55. At the block boundary, groups still incomplete use the configured
|
||||
browser behavior and do not begin an overland dungeon or raid journey. The
|
||||
manager never changes stored day-plan slots or a group's leader target.
|
||||
|
||||
GM diagnostics and explicit test fallback are available as
|
||||
`clientless groupmanager preview`, `clientless groupmanager run`, and
|
||||
`clientless groupmanager status`. `run` uses the same access, role, faction and
|
||||
transfer checks but deliberately ignores the ten-minute time gate; it does not
|
||||
override human leadership or native admission rules.
|
||||
|
||||
## Shared travel and instances
|
||||
|
||||
Followers run their AdventureBehavior controller with the leader's effective
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
# safe_logout_requested approximately 2026-11-01 01:08:20 CET
|
||||
# five_minute_threshold approximately 2026-11-01 01:13:20 CET
|
||||
# snapshot_saved 2026-11-01 01:13:32 CET, after saveall
|
||||
# aggregate_nearest_status pending=20 sessions=20 combat=2 transport=0 flight=15 loading=0
|
||||
# note Loader was confirmed enabled=0/busy=0 and did not replenish sessions. Rows are generated bots online in the final saved DB snapshot. Per-bot combat/flight flags are not exposed by the console command; aggregate flags above are the available evidence. All had a pending global safe-logout request.
|
||||
guid name faction map zone x y z
|
||||
43 Fizzira Alliance 0 4 -11788.8 -3176.69 -29.7341
|
||||
51 Tordra Alliance 530 3479 -3013.51 -13204.1 59.6283
|
||||
53 Noranda Alliance 0 45 -2059.88 -2585.36 108.528
|
||||
70 Renaisia Alliance 1 148 7854.57 73.7393 78.0483
|
||||
72 Sorana Alliance 530 3479 -3013.51 -13204.1 59.6283
|
||||
81 Ashara Horde 1 14 1389.54 -4370.19 25.8776
|
||||
83 Mahuwa Horde 0 4922 -3557.36 -4377.18 268.208
|
||||
84 Brynli Alliance 1 331 2779.05 -58.9212 112.253
|
||||
85 Gixni Horde 1 357 -4300.55 -646.328 25.2518
|
||||
88 Pibwin Alliance 530 3479 -3013.51 -13204.1 59.6283
|
||||
99 Shadra Horde 1 331 2540.73 -1526.06 225.275
|
||||
102 Corren Alliance 0 12 -9484.66 443.337 68.0268
|
||||
119 Aethilrin Horde 1 400 -4763.22 -1180.05 16.4765
|
||||
121 Braneven Horde 0 139 2074.61 -4543.65 151.667
|
||||
125 Koruhon Horde 1 17 1259.58 -3802.96 122.248
|
||||
132 Braneron Horde 0 85 2539.96 -53.7843 28.1074
|
||||
134 Renven Horde 1 14 763.235 -4611.83 -1.08043
|
||||
136 Maraannora Horde 1 14 749.172 -4635.61 -3.2926
|
||||
146 Loraria Horde 1 1637 1589.99 -4212.98 82.4014
|
||||
153 Ragnli Alliance 1 141 7990.23 1296.75 59.6283
|
||||
|
@@ -0,0 +1,21 @@
|
||||
# safe_logout_requested 2026-11-01 00:55:57 CET (approximately; console request followed config edit at 00:55:52)
|
||||
# five_minute_threshold 2026-11-01 01:00:57 CET
|
||||
# snapshot_saved 2026-11-01 01:01 CET, immediately after saveall
|
||||
# aggregate_nearest_status pending=17 sessions=17 combat=2 transport=0 flight=14 loading=0
|
||||
# note The enabled loader was busy and rejected runtime reload, so it continued transient login/safe-logout cycles. Rows below are the generated bots online in the final saved DB snapshot. Per-bot combat/flight flags were not exposed by the console command; aggregate flags above are the available diagnostic evidence. All had a pending global safe-logout request.
|
||||
guid name faction map zone x y z
|
||||
43 Fizzira Alliance 0 4 -11778.6 -3179.17 -28.9872
|
||||
47 Nimibin Alliance 1 141 7990.23 1296.75 59.6283
|
||||
49 Lunna Alliance 1 141 10041.9 1838.98 1367.35
|
||||
54 Falaeriel Horde 0 2037 3538.87 -4506.39 152.38
|
||||
88 Pibwin Alliance 1 141 7990.23 1296.75 59.6283
|
||||
104 Lillini Alliance 0 1 -5066.67 -818.772 497.329
|
||||
106 Nibelin Alliance 1 141 7990.23 1296.75 59.6283
|
||||
118 Lillina Alliance 1 141 7990.23 1296.75 59.6283
|
||||
132 Braneron Horde 0 85 2539.96 -53.7843 28.1074
|
||||
135 Ragnhild Alliance 1 141 9797.77 998 1313.46
|
||||
153 Ragnli Alliance 1 141 9552.06 992.021 1366.62
|
||||
160 Marndra Alliance 530 3519 -1471.88 3873.29 268.262
|
||||
161 Marnda Alliance 530 3483 -980.07 2999.83 87.3079
|
||||
163 Winienwi Alliance 530 3519 -1666.02 4773.16 85.4449
|
||||
164 Taliaarnora Alliance 530 3519 -1673.09 4869.15 137.132
|
||||
|
@@ -1695,6 +1695,10 @@ bool AdventureBehaviorMgr::Command(ChatHandler * handler, char const * args)
|
||||
{
|
||||
std::string args;std::getline(in,args);return EditPlan(handler,args);
|
||||
}
|
||||
if(op=="groupmanager")
|
||||
{
|
||||
std::string args;std::getline(in,args);return sAdventureGroupMgr->Command(handler,args.c_str());
|
||||
}
|
||||
if (op == "import") return sAdventureLocationMgr->Import(handler);
|
||||
if (op == "repairimport") return sAdventureLocationMgr->Import(handler,true);
|
||||
if (op == "reload")
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "GroupMgr.h"
|
||||
#include "LFGPackets.h"
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -339,6 +340,189 @@ bool AdventureGroupMgr::Merge(Player* p,AdventureGroupPlan const& plan)
|
||||
}
|
||||
return true; // A mutation occurred; caller must not reuse its old Group pointer.
|
||||
}
|
||||
|
||||
bool AdventureGroupMgr::ManagedTransfer(Player* leader,Player* member)
|
||||
{
|
||||
if(!leader || !member || leader==member || !Ready(leader) || !Ready(member) ||
|
||||
leader->GetTeam()!=member->GetTeam() || !CanInvite(leader,member) || !NeedsRole(leader,member))return false;
|
||||
auto target=leader->GetGroup(),source=member->GetGroup();
|
||||
ObjectGuid originalLeader=source?source->GetLeaderGUID():ObjectGuid::Empty;
|
||||
if(target && target->GetLeaderGUID()!=leader->GetGUID())return false;
|
||||
if(source==target)return false;
|
||||
if(source)
|
||||
{
|
||||
if(!BotLed(member) || source->isLFGGroup())return false;
|
||||
if(source->GetLeaderGUID()==member->GetGUID() && source->GetMembersCount()>1)
|
||||
{
|
||||
Player* successor=nullptr;
|
||||
for(auto const& slot:source->GetMemberSlots())if(slot.guid!=member->GetGUID())
|
||||
if(auto candidate=ObjectAccessor::FindPlayer(slot.guid))
|
||||
if(candidate->GetSession() && candidate->GetSession()->IsClientless()){successor=candidate;break;}
|
||||
if(!successor)return false; // Never hand forced leadership to a real player.
|
||||
source->ChangeLeader(successor->GetGUID());source->SendUpdate();
|
||||
}
|
||||
}
|
||||
CancelBrowser(member);
|
||||
ObjectGuid old=source?source->GetGUID():ObjectGuid::Empty;
|
||||
ObjectGuid oldLeader=originalLeader;
|
||||
bool oldRaid=source && source->isRaidGroup();
|
||||
auto oldDungeonMode=source?source->GetDungeonDifficulty():DUNGEON_DIFFICULTY_NORMAL;
|
||||
auto oldRaidMode=source?source->GetRaidDifficulty():RAID_DIFFICULTY_10MAN_NORMAL;
|
||||
std::vector<ObjectGuid> oldMembers;
|
||||
if(source)for(auto const& slot:source->GetMemberSlots())oldMembers.push_back(slot.guid);
|
||||
if(source)source->RemoveMember(member->GetGUID(),GROUP_REMOVEMETHOD_LEAVE);
|
||||
Invite(leader,member,Activity(sAdventureBehaviorMgr->GroupPlan(leader)),true);
|
||||
if(member->GetGroupInvite())
|
||||
{
|
||||
WorldPackets::Party::PartyInviteResponse response{WorldPacket(CMSG_PARTY_INVITE_RESPONSE)};
|
||||
response.Accept=true;member->GetSession()->HandlePartyInviteResponseOpcode(response);
|
||||
}
|
||||
if(member->GetGroup() && member->GetGroup()==leader->GetGroup())
|
||||
{
|
||||
_records[member->GetGUID()].MergeAt=GameTime::GetGameTime()+120;
|
||||
TC_LOG_INFO("server.adventurebot","GROUP_MANAGER_TRANSFER member=%u from=%s leader=%u target='%s'",
|
||||
member->GetGUID().GetCounter(),old.ToString().c_str(),leader->GetGUID().GetCounter(),
|
||||
sAdventureBehaviorMgr->GroupPlan(leader).NextLocation.c_str());
|
||||
return true;
|
||||
}
|
||||
// Native invitation rejection must not strand a member removed from a
|
||||
// still-existing source group. Reuse the same bounded rollback as organic
|
||||
// merging, including recreation of a disbanded two-bot source.
|
||||
if(old)
|
||||
{
|
||||
if(auto previous=sGroupMgr->GetGroupByGUID(old.GetCounter()))
|
||||
{
|
||||
previous->AddMember(member);
|
||||
if(oldLeader==member->GetGUID())previous->ChangeLeader(member->GetGUID());
|
||||
previous->SendUpdate();
|
||||
}
|
||||
else if(oldMembers.size()==2)
|
||||
{
|
||||
auto peer=ObjectAccessor::FindPlayer(oldMembers[0]==member->GetGUID()?oldMembers[1]:oldMembers[0]);
|
||||
if(peer && !peer->GetGroup() && !peer->GetGroupInvite())
|
||||
{
|
||||
auto restored=new Group();auto owner=oldLeader==member->GetGUID()?member:peer;
|
||||
if(restored->Create(owner))
|
||||
{
|
||||
sGroupMgr->AddGroup(restored);if(oldRaid)restored->ConvertToRaid();
|
||||
restored->SetDungeonDifficulty(oldDungeonMode);restored->SetRaidDifficulty(oldRaidMode);
|
||||
restored->AddMember(owner==member?peer:member);restored->SendUpdate();
|
||||
}
|
||||
else delete restored;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
unsigned AdventureGroupMgr::RunFormationManager(ChatHandler* handler,bool apply,bool forced)
|
||||
{
|
||||
auto now=GameTime::GetGameTime();unsigned candidates=0,moves=0;
|
||||
auto forming=[&](Player* p)
|
||||
{
|
||||
if(!Ready(p) || !p->GetSession()->IsClientless() || p->isUsingLfg() || p->InBattlegroundQueue() || p->GetGroupInvite())return false;
|
||||
auto plan=sAdventureBehaviorMgr->GroupPlan(p);
|
||||
if(!plan.Enabled || !plan.Forming || !AdventureGroupPolicy::Eligible(plan.NextIntent) || (!forced && !AdventureGroupPolicy::ManagerWindow(plan.Deadline,now)))return false;
|
||||
if(auto group=p->GetGroup())
|
||||
{
|
||||
if(!BotLed(p) || group->isLFGGroup())return false;
|
||||
if(Complete(p))return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
std::vector<Player*> pool;
|
||||
for(auto const& entry:_records)if(auto p=ObjectAccessor::FindPlayer(entry.first))if(forming(p))pool.push_back(p);
|
||||
candidates=pool.size();
|
||||
auto groupSize=[](Player* p){return p->GetGroup()?p->GetGroup()->GetMembersCount():1u;};
|
||||
auto leaderOf=[](Player* p)->Player*
|
||||
{
|
||||
if(auto group=p->GetGroup())return ObjectAccessor::FindPlayer(group->GetLeaderGUID());
|
||||
return p;
|
||||
};
|
||||
for(unsigned pass=0;pass<pool.size();++pass)
|
||||
{
|
||||
std::vector<Player*> leaders;
|
||||
for(auto p:pool)if(forming(p))
|
||||
{
|
||||
auto leader=leaderOf(p);if(!leader || !forming(leader))continue;
|
||||
if(std::find(leaders.begin(),leaders.end(),leader)==leaders.end())leaders.push_back(leader);
|
||||
}
|
||||
std::sort(leaders.begin(),leaders.end(),[&](Player* a,Player* b)
|
||||
{auto sa=groupSize(a),sb=groupSize(b);return sa!=sb?sa>sb:a->GetGUID()<b->GetGUID();});
|
||||
bool changed=false;
|
||||
for(auto leader:leaders)
|
||||
{
|
||||
if(!forming(leader))continue;
|
||||
auto targetPlan=sAdventureBehaviorMgr->GroupPlan(leader);auto targetGroup=leader->GetGroup();
|
||||
if((targetGroup?targetGroup->GetMembersCount():1)>=Capacity(targetPlan))continue;
|
||||
Player* best=nullptr;unsigned bestSource=999;
|
||||
for(auto member:pool)
|
||||
{
|
||||
if(member==leader || !forming(member) || member->GetGroup()==targetGroup)continue;
|
||||
auto memberPlan=sAdventureBehaviorMgr->GroupPlan(member);
|
||||
if(Activity(memberPlan)!=Activity(targetPlan) || member->GetTeam()!=leader->GetTeam() ||
|
||||
!AdventureGroupPolicy::ManagerLevelCompatible(member->getLevel(),leader->getLevel()))continue;
|
||||
bool levels=true;if(targetGroup)for(auto const& slot:targetGroup->GetMemberSlots())
|
||||
if(auto present=ObjectAccessor::FindPlayer(slot.guid))
|
||||
if(!AdventureGroupPolicy::ManagerLevelCompatible(member->getLevel(),present->getLevel())){levels=false;break;}
|
||||
if(!levels || !ProfessionCompatible(leader,member,Activity(targetPlan)) || !CanInvite(leader,member) || !NeedsRole(leader,member))continue;
|
||||
auto source=member->GetGroup();unsigned sourceSize=source?source->GetMembersCount():1;
|
||||
if(source && source->GetLeaderGUID()==member->GetGUID() && sourceSize>1)
|
||||
{
|
||||
bool botSuccessor=false;
|
||||
for(auto const& slot:source->GetMemberSlots())if(slot.guid!=member->GetGUID())
|
||||
if(auto successor=ObjectAccessor::FindPlayer(slot.guid))
|
||||
if(successor->GetSession() && successor->GetSession()->IsClientless()){botSuccessor=true;break;}
|
||||
if(!botSuccessor)continue;
|
||||
}
|
||||
if(targetGroup && sourceSize>=targetGroup->GetMembersCount())continue;
|
||||
if(sourceSize<bestSource || (sourceSize==bestSource && (!best || member->GetGUID()<best->GetGUID())))
|
||||
{best=member;bestSource=sourceSize;}
|
||||
}
|
||||
if(!best)continue;
|
||||
if(handler)handler->PSendSysMessage("Group manager %s: member=%u '%s' -> leader=%u '%s' target='%s' role=%u",
|
||||
apply?"move":"proposal",best->GetGUID().GetCounter(),best->GetName().c_str(),leader->GetGUID().GetCounter(),leader->GetName().c_str(),
|
||||
targetPlan.NextLocation.c_str(),unsigned(BrowserRole(best)));
|
||||
if(!apply){++moves;pool.erase(std::remove(pool.begin(),pool.end(),best),pool.end());changed=true;break;}
|
||||
if(ManagedTransfer(leader,best)){++moves;changed=true;break;}
|
||||
}
|
||||
if(!changed)break;
|
||||
}
|
||||
_formationManagerLast=now;_formationManagerMoves=moves;_formationManagerCandidates=candidates;
|
||||
_formationManagerDetail=apply?"applied":"preview";
|
||||
TC_LOG_INFO("server.adventurebot","GROUP_MANAGER_RUN apply=%u forced=%u candidates=%u moves=%u",apply?1:0,forced?1:0,candidates,moves);
|
||||
if(handler)handler->PSendSysMessage("Group manager %s: candidates=%u moves=%u.",apply?"applied":"preview",candidates,moves);
|
||||
return moves;
|
||||
}
|
||||
|
||||
void AdventureGroupMgr::MaybeRunFormationManager(time_t now)
|
||||
{
|
||||
if(now<_formationManagerAt)return;
|
||||
_formationManagerAt=now+30;
|
||||
bool due=false;
|
||||
for(auto const& entry:_records)if(auto player=ObjectAccessor::FindPlayer(entry.first))
|
||||
{
|
||||
auto plan=sAdventureBehaviorMgr->GroupPlan(player);
|
||||
if(plan.Enabled && plan.Forming && AdventureGroupPolicy::ManagerWindow(plan.Deadline,now))
|
||||
{due=true;break;}
|
||||
}
|
||||
if(!due)return;
|
||||
RunFormationManager(nullptr,true,false);
|
||||
}
|
||||
|
||||
bool AdventureGroupMgr::Command(ChatHandler* handler,char const* args)
|
||||
{
|
||||
std::istringstream in(args?args:"");std::string op;in>>op;
|
||||
if(op=="preview"){RunFormationManager(handler,false,true);return true;}
|
||||
if(op=="run"){RunFormationManager(handler,true,true);return true;}
|
||||
if(op=="status")
|
||||
{
|
||||
handler->PSendSysMessage("Group manager last=%llu candidates=%u moves=%u mode='%s' nextAutomatic=%llu",
|
||||
static_cast<unsigned long long>(_formationManagerLast),_formationManagerCandidates,_formationManagerMoves,_formationManagerDetail.c_str(),
|
||||
static_cast<unsigned long long>(_formationManagerAt));return true;
|
||||
}
|
||||
handler->SendSysMessage("Usage: clientless groupmanager preview|run|status");return false;
|
||||
}
|
||||
void AdventureGroupMgr::CheckFullGroup(Player* p)
|
||||
{
|
||||
auto group=p->GetGroup();if(!group)return;
|
||||
@@ -361,6 +545,7 @@ void AdventureGroupMgr::CheckFullGroup(Player* p)
|
||||
void AdventureGroupMgr::Update(Player* p)
|
||||
{
|
||||
auto now=GameTime::GetGameTime();auto& r=_records[p->GetGUID()];
|
||||
MaybeRunFormationManager(now);
|
||||
CheckFullGroup(p);
|
||||
if(PlayerLeadership(p))return;
|
||||
if(!Ready(p) || p->isUsingLfg() || p->InBattlegroundQueue())return;
|
||||
|
||||
@@ -36,6 +36,7 @@ public:
|
||||
bool Forming(Player* player) const;
|
||||
bool PlannedTransition(Player* player);
|
||||
void Report(ChatHandler* handler,Player* player) const;
|
||||
bool Command(ChatHandler* handler,char const* args);
|
||||
private:
|
||||
struct Record
|
||||
{
|
||||
@@ -50,6 +51,9 @@ private:
|
||||
unsigned PendingInvites(Player* leader,Player* except=nullptr) const;
|
||||
bool SurplusRole(Player* member);
|
||||
bool Merge(Player* player,AdventureGroupPlan const& plan);
|
||||
bool ManagedTransfer(Player* leader,Player* member);
|
||||
unsigned RunFormationManager(ChatHandler* handler,bool apply,bool forced);
|
||||
void MaybeRunFormationManager(time_t now);
|
||||
void CheckFullGroup(Player* player);
|
||||
void Invite(Player* leader,Player* member,std::string const& intent,bool forming);
|
||||
void Speak(Player* player,std::string const& message);
|
||||
@@ -57,6 +61,9 @@ private:
|
||||
bool RecruitPlayer(Player* player,std::string const& intent);
|
||||
std::unordered_map<ObjectGuid,time_t> _playerInvites;
|
||||
time_t _nextChannelMessage=0;
|
||||
time_t _formationManagerAt=0,_formationManagerLast=0;
|
||||
unsigned _formationManagerMoves=0,_formationManagerCandidates=0;
|
||||
std::string _formationManagerDetail="not run";
|
||||
std::unordered_map<ObjectGuid,Record> _records;
|
||||
};
|
||||
#define sAdventureGroupMgr AdventureGroupMgr::instance()
|
||||
|
||||
@@ -40,6 +40,13 @@ 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);}
|
||||
inline bool ManagerLevelCompatible(unsigned level,unsigned other)
|
||||
{
|
||||
if(level==85 || other==85)return level==85 && other==85;
|
||||
return level>other?level-other<=5:other-level<=5;
|
||||
}
|
||||
inline bool ManagerWindow(long long deadline,long long now)
|
||||
{return deadline>now && deadline-now<=600;}
|
||||
template<class Day,class Meeting>
|
||||
void Prepare(Day& plan,std::mt19937& random,float sociability,Meeting meeting)
|
||||
{
|
||||
|
||||
@@ -206,6 +206,12 @@ ClientlessActionExecutionStatus const* ClientlessActionExecutorMgr::GetStatus(ui
|
||||
return itr != _records.end() ? &itr->second.Status : nullptr;
|
||||
}
|
||||
|
||||
bool ClientlessActionExecutorMgr::OwnsActiveFlight(uint32 characterGuid) const
|
||||
{
|
||||
auto itr = _records.find(characterGuid);
|
||||
return itr != _records.end() && itr->second.Active && itr->second.FlightActive;
|
||||
}
|
||||
|
||||
bool ClientlessActionExecutorMgr::StartAction(Player* player, ClientlessAction const& action, Record& record)
|
||||
{
|
||||
record.StopAtNode = false;record.FormationWaiting=false;record.GroundChunk=false;
|
||||
@@ -1025,7 +1031,7 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
|
||||
}
|
||||
Position target;target.Relocate(step.X,step.Y,step.Z,step.O);
|
||||
if (!validSource || step.MapId!=player->GetMapId() ||
|
||||
!sClientlessFlightMgr->Start(player,target,error))
|
||||
!sClientlessFlightMgr->Start(player,target,error,true))
|
||||
{
|
||||
ClientlessTravelTelemetry::Note(player,"flight-start-rejected",error+" "+ClientlessTravelTelemetry::State(player));
|
||||
return RecoverFlightLeg(player,characterGuid,record,error.empty()?"free flight source not physically reached":error);
|
||||
@@ -1916,7 +1922,7 @@ bool ClientlessActionExecutorMgr::TryFallbackFlight(Player* player, uint32 chara
|
||||
if (!std::isfinite(sClientlessFlightMgr->Estimate(player, goal.MapId, from, target, true)))
|
||||
return false;
|
||||
std::string error;
|
||||
if (!sClientlessFlightMgr->Start(player, target, error))
|
||||
if (!sClientlessFlightMgr->Start(player, target, error, true))
|
||||
{
|
||||
ClientlessTravelTelemetry::Note(player,"flight-fallback-rejected",error+" "+ClientlessTravelTelemetry::State(player));
|
||||
TC_LOG_INFO("server.adventurebot.navigation", "FLIGHT_FALLBACK_UNAVAILABLE guid=%u reason='%s'",
|
||||
@@ -1998,7 +2004,7 @@ void ClientlessActionExecutorMgr::CheckGroundExit(Player* player, uint32 charact
|
||||
if (std::isfinite(sClientlessFlightMgr->Estimate(player, goal.MapId, from, target)))
|
||||
{
|
||||
std::string error;
|
||||
if (sClientlessFlightMgr->Start(player, target, error))
|
||||
if (sClientlessFlightMgr->Start(player, target, error, true))
|
||||
{
|
||||
record.GroundExitActive = true;
|
||||
record.FlightActive = true;
|
||||
|
||||
@@ -50,6 +50,7 @@ public:
|
||||
void SetGroundPathAdapter(uint32 guid,GroundPathAdapter adapter);
|
||||
bool RequestStopAtNode(uint32 characterGuid);
|
||||
ClientlessActionExecutionStatus const* GetStatus(uint32 characterGuid) const;
|
||||
bool OwnsActiveFlight(uint32 characterGuid) const;
|
||||
|
||||
static char const* StateName(ClientlessActionExecutionState state);
|
||||
|
||||
|
||||
@@ -202,8 +202,13 @@ Surface ClientlessFlightMgr::Sample(Player * player, Record & record, float x, f
|
||||
};
|
||||
}
|
||||
|
||||
bool ClientlessFlightMgr::Start(Player* player,Position const& target,std::string& error)
|
||||
{return Begin(player,target,error,false);}
|
||||
bool ClientlessFlightMgr::Start(Player* player,Position const& target,std::string& error,bool actionOwned)
|
||||
{
|
||||
bool started=Begin(player,target,error,false);
|
||||
if(started)
|
||||
if(auto found=_records.find(player->GetGUID().GetCounter());found!=_records.end())found->second.ActionOwned=actionOwned;
|
||||
return started;
|
||||
}
|
||||
bool ClientlessFlightMgr::PrepareFollowingMount(Player* player)
|
||||
{
|
||||
if(!Enabled(player) || !player->IsAlive() || player->IsInCombat() || player->IsBeingTeleported() ||
|
||||
@@ -647,6 +652,18 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff
|
||||
}
|
||||
if (! next.Valid ||(! record.Returning && record.Status.Travelled > Distance(PointOf(record.Start), goal) * 2 + 500))
|
||||
{
|
||||
// A detached/pathological flight gets one fresh airborne plan. If the
|
||||
// new corridor is still impossible, finish with a checked local
|
||||
// touchdown so the ordinary behavior controller can plan from ground.
|
||||
if(!record.Returning && record.DetachedRecoveryCount && FindReturnLanding(player,record))
|
||||
{
|
||||
record.Returning=true;
|
||||
record.AbortReason="airborne recovery could not reach the original target";
|
||||
record.Breadcrumbs.clear();
|
||||
record.Memory={};record.Memory.Departed=true;
|
||||
record.Status.Detail="airborne recovery landing";
|
||||
return Advance(player,record);
|
||||
}
|
||||
Fail(player, record, "no safe bounded flight corridor; holding position mounted");
|
||||
return false;
|
||||
}
|
||||
@@ -672,7 +689,11 @@ bool ClientlessFlightMgr::Advance(Player * player, Record & record, bool handoff
|
||||
++ record.Status.Segments;
|
||||
record.Status.Avoidances += next.Avoiding ? 1 : 0;
|
||||
if (handoff) ++record.Status.Handoffs;
|
||||
record.Status.Detail = next.Avoiding ? "obstacle avoidance" : next.Landing ? "landing approach" : next.Next.Z < current.Z - 0.25f ? "diagonal descent" : next.Next.Z > current.Z + 0.25f ? "diagonal climb" : "terrain-smoothed cruise";
|
||||
bool const vertical=Horizontal(current,next.Next)<1.0f && std::fabs(next.Next.Z-current.Z)>1.0f;
|
||||
record.Status.Detail = next.Avoiding ? "obstacle avoidance" : next.Landing && vertical ? "vertical landing fallback" :
|
||||
next.Landing ? "landing approach" : vertical && next.Next.Z>current.Z ? "vertical takeoff fallback" :
|
||||
vertical ? "vertical descent fallback" : next.Next.Z < current.Z - 0.25f ? "diagonal descent" :
|
||||
next.Next.Z > current.Z + 0.25f ? "diagonal climb" : "terrain-smoothed cruise";
|
||||
TC_LOG_INFO("server.adventurebot.navigation", "FLIGHT_SEGMENT guid=%u n=%u avoid=%u handoff=%u target=(%.2f %.2f %.2f) detail='%s'", player->GetGUID().GetCounter(), record.Status.Segments, next.Avoiding ? 1 : 0, handoff ? 1 : 0, next.Next.X, next.Next.Y, next.Next.Z, record.Status.Detail.c_str());
|
||||
return true;
|
||||
}
|
||||
@@ -724,6 +745,23 @@ void ClientlessFlightMgr::Update(uint32 diff)
|
||||
Fail(player, record, "flight permission lost or total timeout");
|
||||
continue;
|
||||
}
|
||||
bool const ownerLost=record.ActionOwned && !sClientlessActionExecutorMgr->OwnsActiveFlight(guid);
|
||||
bool const pathological=record.Status.Segments>=12 && record.Status.Avoidances*2>=record.Status.Segments;
|
||||
if(!record.Returning && !record.Following && record.DetachedRecoveryCount==0 && (ownerLost || pathological))
|
||||
{
|
||||
++record.DetachedRecoveryCount;
|
||||
if(ownerLost)record.ActionOwned=false;
|
||||
player->StopMoving();record.Moving=false;record.SegmentElapsed=0;
|
||||
record.Start.Relocate(player);record.LastPosition.Relocate(player);
|
||||
record.Memory={};record.Breadcrumbs.clear();
|
||||
record.Published=std::make_shared<ClientlessFlightPath::SharedTrail>();
|
||||
record.Status.Detail=ownerLost?"replanning detached free flight":"replanning excessive obstacle avoidance";
|
||||
ClientlessTravelTelemetry::Note(player,"flight-airborne-replan",record.Status.Detail);
|
||||
TC_LOG_WARN("server.adventurebot.navigation","FLIGHT_AIRBORNE_REPLAN guid=%u ownerLost=%u segments=%u avoidances=%u target=(%.2f %.2f %.2f)",
|
||||
guid,ownerLost?1:0,record.Status.Segments,record.Status.Avoidances,record.Status.Landing.GetPositionX(),record.Status.Landing.GetPositionY(),record.Status.Landing.GetPositionZ());
|
||||
Advance(player,record);
|
||||
continue;
|
||||
}
|
||||
if (record.CheckTimer > diff)
|
||||
{
|
||||
record.CheckTimer -= diff;
|
||||
|
||||
@@ -31,7 +31,7 @@ class TC_GAME_API ClientlessFlightMgr
|
||||
bool Preferred(Player const * player, uint32 mapId, Position const & position) const;
|
||||
bool Enabled(Player const * player) const;
|
||||
void BeginPlanning(Player const* player = nullptr);
|
||||
bool Start(Player * player, Position const & target, std::string & error);
|
||||
bool Start(Player * player, Position const & target, std::string & error, bool actionOwned = false);
|
||||
bool Follow(Player* player, Position const& target, std::string& error);
|
||||
bool LandFollowing(Player* player);
|
||||
bool PrepareFollowingMount(Player* player);
|
||||
@@ -53,6 +53,8 @@ class TC_GAME_API ClientlessFlightMgr
|
||||
bool ApproachingDeparture = false;
|
||||
Position Departure;
|
||||
uint32 TargetUpdated = 0, FormationDelay = 0;
|
||||
uint32 DetachedRecoveryCount = 0;
|
||||
bool ActionOwned = false;
|
||||
std::string AbortReason;
|
||||
std::vector < Position > Breadcrumbs;
|
||||
std::unordered_map < std::string, bool > Tiles;
|
||||
|
||||
@@ -118,6 +118,13 @@ namespace ClientlessFlightPath
|
||||
if (memory.Departed && from.Z>=goal.Z && from.Z-goal.Z<=12 &&
|
||||
Distance(from,goal)<=cfg.Step && Corridor(from,goal,goal,cfg,sample,ray,false))
|
||||
return {goal,true,false,true};
|
||||
// A normal diagonal touchdown remains the first choice. When the bot
|
||||
// is already above the landing column and there is not enough lateral
|
||||
// room for that slope, descend in the verified column instead of
|
||||
// opening another wide approach.
|
||||
if (memory.Departed && distance < 2.0f && from.Z > goal.Z + 1.5f &&
|
||||
Corridor(from,goal,goal,cfg,sample,ray,false))
|
||||
return {goal,true,false,true};
|
||||
if (distance < 2 && std::fabs(from.Z - goal.Z) < 1.5f) return
|
||||
{
|
||||
goal, Corridor(from, goal, goal, cfg, sample, ray, true), false, true
|
||||
@@ -212,6 +219,20 @@ namespace ClientlessFlightPath
|
||||
return best;
|
||||
}
|
||||
}
|
||||
// Diagonal departure candidates were exhausted. Tight settlements
|
||||
// can still have a completely open vertical column, so use it before
|
||||
// asking the caller to walk to a different takeoff point.
|
||||
if (!best.Valid && departure)
|
||||
{
|
||||
float const top = std::min(here.Ceiling - 6.0f,
|
||||
std::max(from.Z + 20.0f, here.Ground + std::min(cfg.Clearance, 35.0f)));
|
||||
Point vertical{from.X,from.Y,top};
|
||||
if (top > from.Z + 2.0f && Corridor(from,vertical,goal,cfg,sample,ray,true))
|
||||
{
|
||||
memory.Departed=true;
|
||||
return {vertical,true,false,false};
|
||||
}
|
||||
}
|
||||
if (best.Valid) memory.Departed = true;
|
||||
return best;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "ClientlessActionExecutorMgr.h"
|
||||
#include "ClientlessWorldPointMgr.h"
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "AdventureGroupMgr.h"
|
||||
#include "AdventureCombatMgr.h"
|
||||
#include "AdventureInteractionMgr.h"
|
||||
#include "AdventureGeneratorMgr.h"
|
||||
@@ -2617,6 +2618,7 @@ static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*arg
|
||||
{ "info", rbac::RBAC_PERM_COMMAND_GM, false, &HandleClientlessTaxiInfo, "" },
|
||||
} },
|
||||
{ "adventure", rbac::RBAC_PERM_COMMAND_GM, true, +[](ChatHandler* handler, char const* args) { return sAdventureBehaviorMgr->Command(handler,args); }, "" },
|
||||
{ "groupmanager", rbac::RBAC_PERM_COMMAND_GM, true, +[](ChatHandler* handler, char const* args) { return sAdventureGroupMgr->Command(handler,args); }, "" },
|
||||
{ "flight", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessFlight, "" },
|
||||
{ "portal", rbac::RBAC_PERM_COMMAND_GM, true, nullptr, "", {
|
||||
{ "object", rbac::RBAC_PERM_COMMAND_GM, true, nullptr, "", {
|
||||
|
||||
@@ -1273,6 +1273,24 @@ DeletedCharacterTicketTrace = 0
|
||||
|
||||
DungeonFinder.OptionsMask = 1
|
||||
|
||||
#
|
||||
# DungeonFinder.LowPopulation.Enable
|
||||
# DungeonFinder.LowPopulation.FallbackDelay
|
||||
# DungeonFinder.LowPopulation.MinPlayers
|
||||
# DungeonFinder.LowPopulation.SoloGroupOutsideTimeout
|
||||
# Description: Allow the dungeon finder to create a dungeon group below
|
||||
# the normal group size after the fallback delay. The group
|
||||
# remains marked as low-population while inside the dungeon.
|
||||
# Default: Enable = 0
|
||||
# FallbackDelay = 60 seconds
|
||||
# MinPlayers = 1
|
||||
# SoloGroupOutsideTimeout = 300 seconds
|
||||
|
||||
DungeonFinder.LowPopulation.Enable = 0
|
||||
DungeonFinder.LowPopulation.FallbackDelay = 60
|
||||
DungeonFinder.LowPopulation.MinPlayers = 1
|
||||
DungeonFinder.LowPopulation.SoloGroupOutsideTimeout = 300
|
||||
|
||||
#
|
||||
# DungeonFinder.CallToArmsEnable
|
||||
# Description: Enable Call to Arms bonus mechanism for random Cataclysm dungeons
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
source = (root / "src/server/game/World/AdventureGroupMgr.cpp").read_text()
|
||||
|
||||
# The integration-heavy manager remains native C++, but these assertions keep
|
||||
# its safety boundaries visible when the surrounding group code is refactored.
|
||||
for required in (
|
||||
"ManagerWindow(plan.Deadline,now)",
|
||||
"ManagerLevelCompatible(member->getLevel(),present->getLevel())",
|
||||
"!BotLed(p)",
|
||||
"Complete(p)",
|
||||
"ProfessionCompatible(leader,member,Activity(targetPlan))",
|
||||
"CanInvite(leader,member)",
|
||||
"NeedsRole(leader,member)",
|
||||
"sourceSize>=targetGroup->GetMembersCount()",
|
||||
"Never hand forced leadership to a real player",
|
||||
"GROUP_MANAGER_TRANSFER",
|
||||
'op=="preview"',
|
||||
'op=="run"',
|
||||
):
|
||||
assert required in source, required
|
||||
|
||||
code = r'''
|
||||
#include "AdventureGroupPolicy.h"
|
||||
#include <cassert>
|
||||
int main()
|
||||
{
|
||||
using namespace AdventureGroupPolicy;
|
||||
assert(ManagerLevelCompatible(60, 55));
|
||||
assert(!ManagerLevelCompatible(60, 54));
|
||||
assert(ManagerLevelCompatible(84, 79));
|
||||
assert(!ManagerLevelCompatible(84, 78));
|
||||
assert(ManagerLevelCompatible(85, 85));
|
||||
assert(!ManagerLevelCompatible(85, 84));
|
||||
assert(!ManagerLevelCompatible(80, 85));
|
||||
assert(ManagerWindow(1600, 1000));
|
||||
assert(ManagerWindow(1300, 1000));
|
||||
assert(!ManagerWindow(1601, 1000));
|
||||
assert(!ManagerWindow(1000, 1000));
|
||||
assert(!ManagerWindow(999, 1000));
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
directory = Path(directory)
|
||||
cpp = directory / "test.cpp"
|
||||
exe = directory / "test"
|
||||
cpp.write_text(code)
|
||||
subprocess.run([
|
||||
"c++", "-std=c++17", "-I" + str(root / "src/server/game/World"),
|
||||
str(cpp), "-o", str(exe)
|
||||
], check=True)
|
||||
subprocess.run([str(exe)], check=True)
|
||||
|
||||
print("Formation manager window, level-85 isolation and native safety gates passed.")
|
||||
@@ -69,6 +69,7 @@ code+=r'''
|
||||
void AdventureGroupMgr::UpdateBrowser(Player*){}
|
||||
bool AdventureGroupMgr::Merge(Player*,AdventureGroupPlan const&){return false;}
|
||||
bool AdventureGroupMgr::NeedsRole(Player*,Player*){return true;}
|
||||
void AdventureGroupMgr::MaybeRunFormationManager(time_t){}
|
||||
bool AdventureGroupMgr::Complete(Player*p){auto group=p->GetGroup();if(!group)return false;auto plan=sAdventureBehaviorMgr->GroupPlan(p);return group->GetMembersCount()==(plan.Capacity?plan.Capacity:AdventureGroupPolicy::Capacity(plan.Forming?plan.NextIntent:plan.Intent));}
|
||||
'''
|
||||
code+=fun('float Sociability(')+'\n'+fun('bool ProfessionCompatible(')+'\n'+fun('bool Ready(')+'\n'+fun('std::string Activity(')+'\n'+fun('unsigned Capacity(')
|
||||
|
||||
@@ -28,6 +28,15 @@ int main(){
|
||||
assert(touchdown.Valid&&touchdown.Landing&&Distance(touchdown.Next,{0,0,0})<0.01f);
|
||||
auto solidWall=[](Point const&,Point const&){return false;};
|
||||
assert(!Next({6,0,10},{0,0,0},Settings{},finalApproach,flat,solidWall).Valid);
|
||||
// Diagonal movement is blocked but the local air column is clear: take off
|
||||
// and touch down vertically instead of rejecting an otherwise safe flight.
|
||||
auto verticalOnly=[](Point const&a,Point const&b){return Horizontal(a,b)<0.1f;};
|
||||
Memory verticalTakeoff;
|
||||
auto ascent=Next({0,0,0},{100,0,0},Settings{},verticalTakeoff,flat,verticalOnly);
|
||||
assert(ascent.Valid&&Horizontal({0,0,0},ascent.Next)<0.1f&&ascent.Next.Z>10);
|
||||
Memory verticalLanding;verticalLanding.Departed=true;
|
||||
auto descent=Next({0,0,40},{0,0,0},Settings{},verticalLanding,flat,verticalOnly);
|
||||
assert(descent.Valid&&descent.Landing&&Distance(descent.Next,{0,0,0})<0.01f);
|
||||
auto normal=simulate({0,0,0},{800,0,0},flat,clear);
|
||||
assert(normal.Arrived&&normal.Peak>30&&normal.Peak<=60.1f);
|
||||
auto low=[](float,float){return Surface{0,42,true};};
|
||||
@@ -84,3 +93,10 @@ with tempfile.TemporaryDirectory(prefix='vcore-free-flight-') as folder:
|
||||
cpp=Path(folder)/'test.cpp';binary=Path(folder)/'test';cpp.write_text(harness)
|
||||
subprocess.run(['c++','-std=c++17','-I',str(root/'src/server/game/World'),str(cpp),'-o',str(binary)],check=True)
|
||||
subprocess.run([str(binary)],check=True)
|
||||
|
||||
flight=(root/'src/server/game/World/ClientlessFlightMgr.cpp').read_text()
|
||||
executor=(root/'src/server/game/World/ClientlessActionExecutorMgr.cpp').read_text()
|
||||
assert 'FLIGHT_AIRBORNE_REPLAN' in flight
|
||||
assert 'OwnsActiveFlight(guid)' in flight
|
||||
assert 'found->second.ActionOwned=actionOwned' in flight
|
||||
assert 'itr->second.Active && itr->second.FlightActive' in executor
|
||||
Reference in new issue
Block a user