Add deterministic AdventureBot group formation manager
clang PCH / build (push) Waiting to run
GCC PCH / build (push) Canceled after 0s

This commit is contained in:
Nico committed 2026-11-01 01:20:37 +01:00
1 parent 856832e7bd
commit be049baacc
11 files changed
+386

No files matched your search

+50
View File
@@ -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
+25
View File
@@ -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
@@ -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
1 # safe_logout_requested approximately 2026-11-01 01:08:20 CET
2 # five_minute_threshold approximately 2026-11-01 01:13:20 CET
3 # snapshot_saved 2026-11-01 01:13:32 CET, after saveall
4 # aggregate_nearest_status pending=20 sessions=20 combat=2 transport=0 flight=15 loading=0
5 # 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.
6 guid name faction map zone x y z
7 43 Fizzira Alliance 0 4 -11788.8 -3176.69 -29.7341
8 51 Tordra Alliance 530 3479 -3013.51 -13204.1 59.6283
9 53 Noranda Alliance 0 45 -2059.88 -2585.36 108.528
10 70 Renaisia Alliance 1 148 7854.57 73.7393 78.0483
11 72 Sorana Alliance 530 3479 -3013.51 -13204.1 59.6283
12 81 Ashara Horde 1 14 1389.54 -4370.19 25.8776
13 83 Mahuwa Horde 0 4922 -3557.36 -4377.18 268.208
14 84 Brynli Alliance 1 331 2779.05 -58.9212 112.253
15 85 Gixni Horde 1 357 -4300.55 -646.328 25.2518
16 88 Pibwin Alliance 530 3479 -3013.51 -13204.1 59.6283
17 99 Shadra Horde 1 331 2540.73 -1526.06 225.275
18 102 Corren Alliance 0 12 -9484.66 443.337 68.0268
19 119 Aethilrin Horde 1 400 -4763.22 -1180.05 16.4765
20 121 Braneven Horde 0 139 2074.61 -4543.65 151.667
21 125 Koruhon Horde 1 17 1259.58 -3802.96 122.248
22 132 Braneron Horde 0 85 2539.96 -53.7843 28.1074
23 134 Renven Horde 1 14 763.235 -4611.83 -1.08043
24 136 Maraannora Horde 1 14 749.172 -4635.61 -3.2926
25 146 Loraria Horde 1 1637 1589.99 -4212.98 82.4014
26 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
1 # safe_logout_requested 2026-11-01 00:55:57 CET (approximately; console request followed config edit at 00:55:52)
2 # five_minute_threshold 2026-11-01 01:00:57 CET
3 # snapshot_saved 2026-11-01 01:01 CET, immediately after saveall
4 # aggregate_nearest_status pending=17 sessions=17 combat=2 transport=0 flight=14 loading=0
5 # 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.
6 guid name faction map zone x y z
7 43 Fizzira Alliance 0 4 -11778.6 -3179.17 -28.9872
8 47 Nimibin Alliance 1 141 7990.23 1296.75 59.6283
9 49 Lunna Alliance 1 141 10041.9 1838.98 1367.35
10 54 Falaeriel Horde 0 2037 3538.87 -4506.39 152.38
11 88 Pibwin Alliance 1 141 7990.23 1296.75 59.6283
12 104 Lillini Alliance 0 1 -5066.67 -818.772 497.329
13 106 Nibelin Alliance 1 141 7990.23 1296.75 59.6283
14 118 Lillina Alliance 1 141 7990.23 1296.75 59.6283
15 132 Braneron Horde 0 85 2539.96 -53.7843 28.1074
16 135 Ragnhild Alliance 1 141 9797.77 998 1313.46
17 153 Ragnli Alliance 1 141 9552.06 992.021 1366.62
18 160 Marndra Alliance 530 3519 -1471.88 3873.29 268.262
19 161 Marnda Alliance 530 3483 -980.07 2999.83 87.3079
20 163 Winienwi Alliance 530 3519 -1666.02 4773.16 85.4449
21 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")
+185
View File
@@ -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)
{
@@ -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, "", {
@@ -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(')