Fix progression stalls and add normal class trainer visits
Core tests / core (push) Successful in 29s
Core tests / core (push) Successful in 29s
This commit is contained in:
@@ -232,3 +232,33 @@ By default:
|
||||
The project checkout keeps AzerothAI's local D1 state in `.wrangler/state`.
|
||||
|
||||
Do not delete these directories if you want the realm and character history to survive restarts.
|
||||
|
||||
## Class training and leveling
|
||||
|
||||
`ale/azerothai_training.lua` checks each managed resident after a level change and
|
||||
rechecks every minute. It visits a nearby class trainer when the core reports
|
||||
eligible spells, then uses the normal trainer purchase transaction. Purchases
|
||||
require interaction distance and sufficient character gold; spells and money are
|
||||
never granted. Combat, recovery, player control, and mixed parties suspend visits.
|
||||
Stalled visits release paused work and retry later. The monitor reports pending
|
||||
spells, trainer, blocked reason, and verified purchases separately.
|
||||
|
||||
This requires the native ALE methods installed by:
|
||||
|
||||
```bash
|
||||
python3 deploy/playable/patch-native-training.py /path/to/azerothcore
|
||||
```
|
||||
|
||||
Rebuild and deploy worldserver before enabling the training Lua module. The patch
|
||||
also restores the native RPG training trigger and prevents sightseeing flights
|
||||
for residents below level 60. Normal travel flight routes remain available.
|
||||
`install-lifecycle.py` copies the Lua module with the other resident services.
|
||||
Without the native API, the monitor reports that training is unavailable.
|
||||
|
||||
Training state and recoverable strategy leases live in
|
||||
`acore_auth.azerothai_training`, one row per resident. Existing trainer purchases
|
||||
made outside this workflow are not retroactively included in its purchase count.
|
||||
|
||||
Flight guards and telemetry use `HasUnitState(0x00000100)` (`UNIT_STATE_IN_FLIGHT`).
|
||||
ALE's `IsTaxi()` tests the flight-master NPC flag and must not be used to detect
|
||||
a player riding a taxi. Model decisions also yield to an active native flight.
|
||||
|
||||
@@ -116,7 +116,7 @@ for key in ['POLL_MS', 'PROTECTED', 'KNOWN_BOT_ACCOUNTS']:
|
||||
source, n = re.subn(pattern, lambda match: line.group(0), source, flags=re.M)
|
||||
assert n == 1, key
|
||||
(core / 'azerothai-lua/azerothai_bridge.lua').write_text(source)
|
||||
for name in ['azerothai_lifecycle.lua', 'azerothai_survival.lua', 'azerothai_experience.lua', 'azerothai_readiness.lua']:
|
||||
for name in ['azerothai_lifecycle.lua', 'azerothai_survival.lua', 'azerothai_experience.lua', 'azerothai_readiness.lua', 'azerothai_training.lua']:
|
||||
shutil.copy2(repo / 'ale' / name, core / 'azerothai-lua' / name)
|
||||
settings = {'AC_AI_PLAYERBOT_DISABLE_RANDOM_LEVELS': '1',
|
||||
'AC_AI_PLAYERBOT_RANDOMBOT_STARTING_LEVEL': '1',
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// AzerothAI: inspect actual trainer eligibility; never synthesize spell requirements.
|
||||
int GetClassTrainerStatus(lua_State* L, PlayerbotAI* botAI)
|
||||
{
|
||||
uint32 entry = ALE::CHECKVAL<uint32>(L, 2);
|
||||
Player* bot = GetBotOrNull(botAI);
|
||||
auto* trainer = sObjectMgr->GetTrainer(entry);
|
||||
auto const* info = sObjectMgr->GetCreatureTemplate(entry);
|
||||
uint32 pending = 0, cheapest = 0, total = 0;
|
||||
if (bot && trainer && info && trainer->GetTrainerType() == Trainer::Type::Class && trainer->IsTrainerValidForPlayer(bot))
|
||||
{
|
||||
auto const* faction = sFactionTemplateStore.LookupEntry(info->faction);
|
||||
float discount = faction ? bot->GetReputationPriceDiscount(faction) : 1.0f;
|
||||
for (auto const& spell : trainer->GetSpells())
|
||||
{
|
||||
if (!trainer->CanTeachSpell(bot, &spell)) continue;
|
||||
uint32 cost = uint32(spell.MoneyCost * discount);
|
||||
if (!pending || cost < cheapest) cheapest = cost;
|
||||
++pending; total += cost;
|
||||
}
|
||||
}
|
||||
ALE::Push(L, pending); ALE::Push(L, cheapest); ALE::Push(L, total);
|
||||
return 3;
|
||||
}
|
||||
|
||||
// Same transaction as a player's trainer purchase, at an interactable class trainer.
|
||||
int TrainAtClassTrainer(lua_State* L, PlayerbotAI* botAI)
|
||||
{
|
||||
Creature* npc = ALE::CHECKOBJ<Creature>(L, 2);
|
||||
Player* bot = GetBotOrNull(botAI);
|
||||
uint32 learned = 0;
|
||||
if (!bot || botAI->HasGameClientMaster() || !bot->IsAlive() || bot->IsInCombat() || bot->IsInFlight() ||
|
||||
!npc || bot->GetNPCIfCanInteractWith(npc->GetGUID(), UNIT_NPC_FLAG_TRAINER) != npc)
|
||||
{ ALE::Push(L, learned); return 1; }
|
||||
auto* trainer = sObjectMgr->GetTrainer(npc->GetEntry());
|
||||
if (!trainer || trainer->GetTrainerType() != Trainer::Type::Class || !trainer->IsTrainerValidForPlayer(bot))
|
||||
{ ALE::Push(L, learned); return 1; }
|
||||
for (auto const& spell : trainer->GetSpells())
|
||||
{
|
||||
if (!trainer->CanTeachSpell(bot, &spell)) continue;
|
||||
trainer->TeachSpell(npc, bot, spell.SpellId);
|
||||
if (!trainer->CanTeachSpell(bot, &spell)) ++learned;
|
||||
}
|
||||
ALE::Push(L, learned);
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Add guarded trainer transactions to ALE. Rebuild worldserver after applying."""
|
||||
from pathlib import Path
|
||||
import sys
|
||||
root=Path(sys.argv[1]);here=Path(__file__).resolve().parent
|
||||
header=root/'modules/mod-ale/src/LuaEngine/methods/Playerbots/PlayerBotAIMethods.h'
|
||||
s=header.read_text()
|
||||
if 'int GetClassTrainerStatus(' not in s:
|
||||
s=s.replace('#include "Strategy.h"','#include "Strategy.h"\n#include "Trainer.h"\n#include "ObjectMgr.h"\n#include "DBCStores.h"')
|
||||
s=s.replace(' inline BotState CheckBotState', (here/'native/trainer-methods.inc').read_text()+'\n inline BotState CheckBotState')
|
||||
header.write_text(s)
|
||||
p=root/'modules/mod-ale/src/LuaEngine/LuaFunctions.cpp';s=p.read_text()
|
||||
if '"GetClassTrainerStatus"' not in s:
|
||||
s=s.replace(' { "GetBot", &LuaPlayerBotAI::GetBot },',' { "GetBot", &LuaPlayerBotAI::GetBot },\n { "GetClassTrainerStatus", &LuaPlayerBotAI::GetClassTrainerStatus },\n { "TrainAtClassTrainer", &LuaPlayerBotAI::TrainAtClassTrainer },')
|
||||
p.write_text(s)
|
||||
print('Trainer methods installed; rebuild required')
|
||||
p=root/'modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp';s=p.read_text()
|
||||
marker='// AzerothAI: unsupervised sightseeing flights are unsuitable for leveling residents.'
|
||||
if marker not in s:
|
||||
import re
|
||||
pattern=r'(bool NewRpgBaseAction::CheckRpgStatusAvailable\(NewRpgStatus (\w+)\)\s*\{)'
|
||||
match=re.search(pattern,s);assert match,'Flight status signature changed'
|
||||
s=s[:match.end()]+'\n '+marker+'\n if ('+match.group(2)+' == RPG_TRAVEL_FLIGHT && bot->GetLevel() < 60) return false;\n'+s[match.end():]
|
||||
p.write_text(s)
|
||||
p=root/'modules/mod-playerbots/src/Ai/World/Rpg/Strategy/RpgStrategy.cpp'
|
||||
if not p.exists():p=next((root/'modules/mod-playerbots').rglob('RpgStrategy.cpp'))
|
||||
s=p.read_text()
|
||||
if '"rpg train"' not in s:
|
||||
s=s.replace(' // Sub actions',' // Training was missing from the NPC activity triggers.\n triggers.push_back(new TriggerNode("rpg train", { NextAction("rpg train", 1.205f) }));\n\n // Sub actions')
|
||||
p.write_text(s)
|
||||
print('Training trigger and low-level sightseeing-flight guard installed')
|
||||
Reference in New Issue
Block a user