Playerbot: auto-learn riding skill + mount; owner-mount party sync

- Riding (skill 762) granted automatically at modern thresholds (L10
  Journeyman 150, L20 Expert 225, L30 Master 375). One tier-appropriate
  mount spell learned per tier, data-driven from the Mount DB2.
- Runs on bot login, level-up and alt finalize.
- Owner-mount sync: Unit::Mount/Unit::Dismount hooks push
  MountIntent/DismountIntent to owned bots so the party travels together.
- Add Unit.cpp mount/dismount hunks to the core-hooks patch.
This commit is contained in:
devbox
2026-08-22 21:58:39 +10:00
parent 75c0cc46a6
commit 86a60c86a0
5 changed files with 172 additions and 5 deletions
@@ -13,6 +13,8 @@ namespace Playerbot::Hooks {
void OnPlayerLogin(Player* p) { V2::Module::instance().OnPlayerLogin(p); } void OnPlayerLogin(Player* p) { V2::Module::instance().OnPlayerLogin(p); }
void OnPlayerLogout(Player* p) { V2::Module::instance().OnPlayerLogout(p); } void OnPlayerLogout(Player* p) { V2::Module::instance().OnPlayerLogout(p); }
void OnLevelUp(Player* p, uint8 new_level) { V2::Module::instance().OnLevelUp(p, new_level); } void OnLevelUp(Player* p, uint8 new_level) { V2::Module::instance().OnLevelUp(p, new_level); }
void OnPlayerMounted(Player* p) { V2::Module::instance().OnMounted(p); }
void OnPlayerDismounted(Player* p) { V2::Module::instance().OnDismounted(p); }
void OnDeath(Unit* victim, Unit* killer) { V2::Module::instance().OnDeath(victim, killer); } void OnDeath(Unit* victim, Unit* killer) { V2::Module::instance().OnDeath(victim, killer); }
void OnResurrect(Player* p) { V2::Module::instance().OnResurrect(p); } void OnResurrect(Player* p) { V2::Module::instance().OnResurrect(p); }
void OnSpecChanged(Player* p, uint8 new_spec) { V2::Module::instance().OnSpecChanged(p, new_spec); } void OnSpecChanged(Player* p, uint8 new_spec) { V2::Module::instance().OnSpecChanged(p, new_spec); }
@@ -26,6 +26,8 @@ namespace Playerbot::Hooks {
void OnPlayerLogin(Player* p); void OnPlayerLogin(Player* p);
void OnPlayerLogout(Player* p); void OnPlayerLogout(Player* p);
void OnLevelUp(Player* p, uint8 new_level); void OnLevelUp(Player* p, uint8 new_level);
void OnPlayerMounted(Player* p);
void OnPlayerDismounted(Player* p);
void OnDeath(Unit* victim, Unit* killer); void OnDeath(Unit* victim, Unit* killer);
void OnResurrect(Player* p); void OnResurrect(Player* p);
void OnSpecChanged(Player* p, uint8 new_spec); void OnSpecChanged(Player* p, uint8 new_spec);
@@ -199,6 +201,8 @@ void OnPathOutcome(uint8 outcome);
inline void OnPlayerLogin(Player*) {} inline void OnPlayerLogin(Player*) {}
inline void OnPlayerLogout(Player*) {} inline void OnPlayerLogout(Player*) {}
inline void OnLevelUp(Player*, uint8) {} inline void OnLevelUp(Player*, uint8) {}
inline void OnPlayerMounted(Player*) {}
inline void OnPlayerDismounted(Player*) {}
inline void OnDeath(Unit*, Unit*) {} inline void OnDeath(Unit*, Unit*) {}
inline void OnResurrect(Player*) {} inline void OnResurrect(Player*) {}
inline void OnSpecChanged(Player*, uint8) {} inline void OnSpecChanged(Player*, uint8) {}
+134 -5
View File
@@ -2506,6 +2506,12 @@ void Module::SampleFleetVitals(std::chrono::milliseconds total, uint32 now_ms,
// Every hook is wired but inert until the corresponding subsystem lands. // Every hook is wired but inert until the corresponding subsystem lands.
// Each one will dispatch to a Fleet or Bot-layer component per FEATURE_MATRIX.md. // Each one will dispatch to a Fleet or Bot-layer component per FEATURE_MATRIX.md.
// Riding/mount helpers are defined below (before OnLevelUp); forward-declare
// here because OnPlayerLogin and the alt finalize path call them earlier.
static uint16 RidingSkillForLevel(uint8 level);
static uint32 MountSpellForRidingSkill(uint16 ridingSkill);
static void EnsureRidingSkillAndMount(Player* bot);
void Module::OnPlayerLogin(Player* p) void Module::OnPlayerLogin(Player* p)
{ {
if (!initialized_ || !p) return; if (!initialized_ || !p) return;
@@ -2597,6 +2603,10 @@ void Module::OnPlayerLogin(Player* p)
// for sessionless bots.) // for sessionless bots.)
p->SetPlayerLocalFlag(PLAYER_LOCAL_FLAG_OVERRIDE_TRANSPORT_SERVER_TIME); p->SetPlayerLocalFlag(PLAYER_LOCAL_FLAG_OVERRIDE_TRANSPORT_SERVER_TIME);
// Riding + mount: bots that enter the world already >= L20 need their
// riding skill and a mount spell (level-ups are covered by OnLevelUp).
EnsureRidingSkillAndMount(p);
// Cross-map travel on-ramp (2026-06-16): ensure the bot KNOWS its faction's // Cross-map travel on-ramp (2026-06-16): ensure the bot KNOWS its faction's
// flight paths. The travel graph prunes every taxi edge to a flight master // flight paths. The travel graph prunes every taxi edge to a flight master
// NOT in the bot's taximask (UnifiedTravelGraph EdgeUsable → IsTaximaskNodeKnown), // NOT in the bot's taximask (UnifiedTravelGraph EdgeUsable → IsTaximaskNodeKnown),
@@ -2808,12 +2818,130 @@ void Module::OnPlayerLogout(Player* p)
TC_LOG_INFO("playerbot.v2", "[PlayerbotV2] Bot AI detached: {} (id {})", p->GetName(), id); TC_LOG_INFO("playerbot.v2", "[PlayerbotV2] Bot AI detached: {} (id {})", p->GetName(), id);
} }
void Module::OnLevelUp(Player* /*p*/, uint8 /*new_level*/) // ----------------------------------------------------------------------------
// RIDING + MOUNT — bots learn riding skill automatically at the modern
// (9.0+/11.0+, client 12.x) thresholds and are taught ONE tier-appropriate
// mount spell (data-driven from the Mount DB2). Riding skill 762 is granted
// without a trainer visit in this client generation:
// L10 -> Journeyman (150) ground +100% (and Skyriding)
// L20 -> Expert (225) Steady Flight +220%
// L30 -> Master (375) Steady Flight +420%
// The bot AI's best_mount_spell scoring picks the fastest known mount, so a
// single per-tier mount is all that's needed. Called on bot login, on level
// up, and from the alt finalize success path (covers bots already past a
// threshold when they (re)enter the world).
// ----------------------------------------------------------------------------
static uint16 RidingSkillForLevel(uint8 level)
{ {
if (!initialized_) return; if (level >= 30) return 375;
// Intentionally a no-op. Snapshot's `level` field updates next tick; if (level >= 20) return 225;
// newly-trained spells appear in `known_spells` as the bot learns them. if (level >= 10) return 150;
// Rules already gate on `knows_spell` so unlocks light up automatically. return 0;
}
// Pick the mount spell for the bot's riding tier: the mount whose LOWEST
// riding requirement is the highest value <= the bot's skill (so each tier
// upgrades). Mounts gated behind a PlayerCondition (race/rep/quest) are
// skipped, as are flying-capability-only mounts below Expert (225) riding.
static uint32 MountSpellForRidingSkill(uint16 ridingSkill)
{
uint32 best = 0;
uint16 bestReq = 0;
for (uint32 id = 0; id < sMountStore.GetNumRows(); ++id)
{
MountEntry const* m = sMountStore.LookupEntry(id);
if (!m || m->SourceSpellID <= 0 || m->PlayerConditionID != 0)
continue;
auto const* caps = sDB2Manager.GetMountCapabilities(m->MountTypeID);
if (!caps || caps->empty())
continue;
uint16 minReq = UINT16_MAX;
bool flight = false;
for (MountTypeXCapabilityEntry const* cx : *caps)
{
MountCapabilityEntry const* cap = sMountCapabilityStore.LookupEntry(cx->MountCapabilityID);
if (!cap) continue;
minReq = std::min(minReq, cap->ReqRidingSkill);
if (cap->ReqSpellAuraID != 0)
flight = true; // capability needs the "can fly" aura
}
if (minReq == UINT16_MAX || minReq > ridingSkill)
continue;
if (ridingSkill < 225 && flight)
continue; // not flying-capable yet — skip flying-only mounts
if (best == 0 || minReq > bestReq)
{
best = m->SourceSpellID;
bestReq = minReq;
}
}
return best;
}
static void EnsureRidingSkillAndMount(Player* bot)
{
if (!bot || !bot->IsInWorld()) return;
uint16 const want = RidingSkillForLevel(bot->GetLevel());
if (want == 0) return;
if (bot->GetSkillValue(SKILL_RIDING) < want)
bot->SetSkill(SKILL_RIDING, 0, want, want);
uint32 const mount = MountSpellForRidingSkill(want);
if (mount && !bot->HasSpell(mount))
bot->LearnSpell(mount, false);
}
void Module::OnLevelUp(Player* p, uint8 /*new_level*/)
{
if (!initialized_ || !p) return;
EnsureRidingSkillAndMount(p);
}
void Module::OnMounted(Player* owner)
{
if (!initialized_ || !owner) return;
WorldSession* sess = owner->GetSession();
if (!sess || sess->IsBot()) return;
uint32 const account_id = sess->GetAccountId();
if (account_id == 0) return;
auto bots = Services::Altbots().AltsOfAccount(account_id);
for (BotId id : bots)
{
Player* bot = ObjectAccessor::FindConnectedPlayer(
ObjectGuid::Create<HighGuid::Player>(id));
if (!bot || !bot->IsInWorld()) continue;
if (bot->IsInCombat() || bot->IsMounted()) continue;
if (!Services::HasIntents(id)) continue;
Intent it{};
it.bot_id = id;
it.body = MountIntent{0}; // 0 = best mount for context
Services::Intents(id).push(std::move(it));
}
}
void Module::OnDismounted(Player* owner)
{
if (!initialized_ || !owner) return;
WorldSession* sess = owner->GetSession();
if (!sess || sess->IsBot()) return;
uint32 const account_id = sess->GetAccountId();
if (account_id == 0) return;
auto bots = Services::Altbots().AltsOfAccount(account_id);
for (BotId id : bots)
{
Player* bot = ObjectAccessor::FindConnectedPlayer(
ObjectGuid::Create<HighGuid::Player>(id));
if (!bot || !bot->IsInWorld()) continue;
if (!bot->IsMounted()) continue;
if (!Services::HasIntents(id)) continue;
Intent it{};
it.bot_id = id;
it.body = DismountIntent{};
Services::Intents(id).push(std::move(it));
}
} }
void Module::OnDeath(Unit* victim, Unit* /*killer*/) void Module::OnDeath(Unit* victim, Unit* /*killer*/)
@@ -3841,6 +3969,7 @@ void Module::DrainAltFinalizes(uint32 /*now_ms*/)
bot->InitTalentForLevel(); bot->InitTalentForLevel();
bot->SetXP(0); bot->SetXP(0);
} }
EnsureRidingSkillAndMount(bot);
float const ang = owner->GetOrientation() + float(M_PI) float const ang = owner->GetOrientation() + float(M_PI)
+ (float(id % 7) - 3.f) * 0.25f; + (float(id % 7) - 3.f) * 0.25f;
+2
View File
@@ -48,6 +48,8 @@ public:
void OnPlayerLogin(Player* p); void OnPlayerLogin(Player* p);
void OnPlayerLogout(Player* p); void OnPlayerLogout(Player* p);
void OnLevelUp(Player* p, uint8 new_level); void OnLevelUp(Player* p, uint8 new_level);
void OnMounted(Player* owner);
void OnDismounted(Player* owner);
void OnDeath(Unit* victim, Unit* killer); void OnDeath(Unit* victim, Unit* killer);
void OnResurrect(Player* p); void OnResurrect(Player* p);
void OnSpecChanged(Player* p, uint8 new_spec); void OnSpecChanged(Player* p, uint8 new_spec);
@@ -1396,6 +1396,36 @@ index 1f376653f2..971fb43af8 100644
Unit* unit = healer; Unit* unit = healer;
if (healer && healer->GetTypeId() == TYPEID_UNIT && healer->IsTotem()) if (healer && healer->GetTypeId() == TYPEID_UNIT && healer->IsTotem())
unit = healer->GetOwner(); unit = healer->GetOwner();
@@ -8430,6 +8431,15 @@ void Unit::Mount(uint32 mount, uint32 VehicleId, uint32 creatureEntry)
SetUnitFlag(UNIT_FLAG_MOUNT);
+#if TRINITY_PLAYERBOT_V2
+ // Owner-mount sync (PlayerbotV2): when a human owner mounts a real
+ // mount (mount display id != 0 - vehicle mounts pass 0 here), tell all
+ // owned bots to mount too so the party travels at the same speed.
+ if (Player* mp = ToPlayer(); mp && mount && mp->GetSession() &&
+ !mp->GetSession()->IsBot())
+ Playerbot::Hooks::OnPlayerMounted(mp);
+#endif
+
CalculateHoverHeight();
if (Player* player = ToPlayer())
@@ -8460,6 +8470,13 @@ void Unit::Dismount()
if (!IsMounted())
return;
+#if TRINITY_PLAYERBOT_V2
+ // Owner-dismount sync (PlayerbotV2): tell owned bots to dismount when
+ // the human owner dismounts (combat, cast, water, manual, ...).
+ if (Player* mp = ToPlayer(); mp && mp->GetSession() && !mp->GetSession()->IsBot())
+ Playerbot::Hooks::OnPlayerDismounted(mp);
+#endif
+
SetMountDisplayId(0);
RemoveUnitFlag(UNIT_FLAG_MOUNT);
@@ -11459,6 +11478,13 @@ void Unit::SetMeleeAnimKitId(uint16 animKitId) @@ -11459,6 +11478,13 @@ void Unit::SetMeleeAnimKitId(uint16 animKitId)
if (attacker && !attacker->IsInMap(victim)) if (attacker && !attacker->IsInMap(victim))
attacker = nullptr; attacker = nullptr;