Fix Classic bot login, native commands and unattended activity
Core tests / core (push) Successful in 26s
Core tests / core (push) Successful in 26s
This commit is contained in:
@@ -49,6 +49,18 @@ validated typed commands into the Classic character database. It never executes
|
||||
Lua or arbitrary game commands. Native receipts distinguish accepted intentions
|
||||
from observed gameplay outcomes; interrupted commands are not replayed.
|
||||
|
||||
`install-staging-services.py --root ROOT` installs isolated user services for
|
||||
port 8791, the native relay, and autonomy. Stop manually launched staging
|
||||
processes first. Services restart on failure and leave the live Wrath services
|
||||
unchanged. The worker credential file is private to the owner.
|
||||
|
||||
The rebuildable item-info cache uses InnoDB with a dynamic row format so the
|
||||
upstream builder's transaction batches writes with full database durability.
|
||||
The bootstrap records the existing native bot account range for login
|
||||
registration and enables activity even when no human player is present.
|
||||
Automatic population creation remains disabled; the native identity allowlist
|
||||
still limits managed login to the planned 50.
|
||||
|
||||
Remaining cutover gates include live quest/training/survival observations,
|
||||
event telemetry and human-player conversation parity, dashboard/operator actions,
|
||||
normal resurrection, actual party acceptance and speech, authentication/client
|
||||
|
||||
@@ -58,11 +58,13 @@ def main(root):
|
||||
existing=mysql('SELECT name FROM characters').splitlines()
|
||||
if any(name not in {b['name'] for b in roster} for name in existing):raise RuntimeError('Unexpected characters in isolated database; refusing to modify')
|
||||
mysql((root/'adapter/schema.sql').read_text())
|
||||
engine=mysql("SELECT ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA='classiccharacters' AND TABLE_NAME='ai_playerbot_item_info_cache'")
|
||||
if engine!='InnoDB':mysql('ALTER TABLE ai_playerbot_item_info_cache ENGINE=InnoDB, ROW_FORMAT=DYNAMIC;')
|
||||
(root/'run/logs').mkdir(exist_ok=True)
|
||||
values={'DataDir':'"/work/extracted"','LogsDir':'"/work/run/logs"','WorldServerPort':8086,'Console.Enable':1,'Ra.Enable':0,'SOAP.Enabled':0,'AzerothAI.Enable':0,'AzerothAI.RestrictBotLogin':1}
|
||||
for key,db in [('Login','classicrealmd'),('World','classicmangos'),('Character','classiccharacters'),('Logs','classiclogs')]:values[key+'DatabaseInfo']='"azerothai-classic-db;3306;'+credentials['user']+';'+credentials['password']+';'+db+'"'
|
||||
configure('mangosd',values)
|
||||
configure('aiplayerbot',{'AiPlayerbot.Enabled':1,'AiPlayerbot.RandomBotAutologin':0,'AiPlayerbot.RandomBotLoginAtStartup':0,'AiPlayerbot.BotAutologin':0,'AiPlayerbot.RandomBotAutoCreate':0,'AiPlayerbot.MinRandomBots':50,'AiPlayerbot.MaxRandomBots':50,'AiPlayerbot.RandomBotAccountCount':0,'AiPlayerbot.RandomBotAccountPrefix':'AZCLASSIC','AiPlayerbot.RandomBotGuildCount':0,'AiPlayerbot.RandomBotArenaTeamCount':0,'AiPlayerbot.DisableRandomLevels':1,'AiPlayerbot.RandomGearUpgradeEnabled':0,'AiPlayerbot.AutoLearnTrainerSpells':1,'AiPlayerbot.AutoTrainSpells':'yes','AiPlayerbot.RandomBotJoinBG':0,'AiPlayerbot.RandomBotJoinLfg':0,'AiPlayerbot.RandomBotMaps':'0,1'})
|
||||
configure('aiplayerbot',{'AiPlayerbot.Enabled':1,'AiPlayerbot.DisableBotOptimizations':1,'AiPlayerbot.DisableActivityPriorities':1,'AiPlayerbot.botActiveAlone':100,'AiPlayerbot.RandomBotAutologin':0,'AiPlayerbot.RandomBotLoginAtStartup':0,'AiPlayerbot.BotAutologin':0,'AiPlayerbot.RandomBotAutoCreate':0,'AiPlayerbot.MinRandomBots':50,'AiPlayerbot.MaxRandomBots':50,'AiPlayerbot.RandomBotAccountCount':0,'AiPlayerbot.RandomBotAccountPrefix':'AZCLASSIC','AiPlayerbot.RandomBotGuildCount':0,'AiPlayerbot.RandomBotArenaTeamCount':0,'AiPlayerbot.DisableRandomLevels':1,'AiPlayerbot.RandomGearUpgradeEnabled':0,'AiPlayerbot.AutoLearnTrainerSpells':1,'AiPlayerbot.AutoTrainSpells':'yes','AiPlayerbot.RandomBotJoinBG':0,'AiPlayerbot.RandomBotJoinLfg':0,'AiPlayerbot.RandomBotMaps':'0,1'})
|
||||
fifo=root/'run/console'
|
||||
if not fifo.exists():os.mkfifo(fifo,0o600)
|
||||
state=inspect('azerothai-classic-world')
|
||||
@@ -100,6 +102,10 @@ def main(root):
|
||||
bound.append(dict(bot,characterGuid=guid,accountId=account))
|
||||
if int(mysql('SELECT COUNT(*) FROM characters'))!=50:raise RuntimeError('Population differs from 50')
|
||||
(root/'classic-roster-bound.json').write_text(json.dumps(bound,indent=2)+'\n')
|
||||
# Native login authorization reloads this account range on restart.
|
||||
account_limit=int(mysql("SELECT COALESCE(MAX(CAST(SUBSTRING(username,10) AS UNSIGNED))+1,0) FROM classicrealmd.account WHERE username REGEXP '^AZCLASSIC[0-9]+$'"))
|
||||
config_path=root/'run/etc/aiplayerbot.conf'
|
||||
config_path.write_text(re.sub(r'^AiPlayerbot.RandomBotAccountCount\s*=.*$',f'AiPlayerbot.RandomBotAccountCount = {account_limit}',config_path.read_text(),flags=re.M))
|
||||
status('Classic world ready; 50 level-one characters verified offline; connector and gameplay validation remain',characters=50,alliance=25,horde=25)
|
||||
except Exception as error:
|
||||
status('bootstrap blocked',error=str(error).replace(credentials['root'],'[redacted]').replace(credentials['password'],'[redacted]'))
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- The upstream item-info builder already batches its writes in a transaction.
|
||||
-- MyISAM ignores that transaction and syncs each statement to the binary log.
|
||||
-- Use InnoDB so every restart retains full durability without per-row fsync.
|
||||
-- Apply to the isolated Classic character database before starting mangosd.
|
||||
ALTER TABLE ai_playerbot_item_info_cache ENGINE=InnoDB, ROW_FORMAT=DYNAMIC;
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Install isolated, restartable Classic dashboard/relay/autonomy user services.
|
||||
|
||||
Prepare staging state first and stop any manually started staging processes.
|
||||
This does not enable game login, start authentication, or change live services.
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
|
||||
|
||||
def main(root):
|
||||
app = root / 'app-staging'
|
||||
node = pathlib.Path.home() / '.local/bin/node'
|
||||
for path in [node, app / '.dev.vars', root / 'adapter/relay.py']:
|
||||
if not path.is_file():
|
||||
raise RuntimeError('Missing prerequisite: ' + str(path))
|
||||
if any(c in str(root) for c in [' ', '\n', '"', "'", '%']):
|
||||
raise RuntimeError('Use a simple absolute staging path')
|
||||
values = {}
|
||||
for line in (app / '.dev.vars').read_text().splitlines():
|
||||
if '=' in line and not line.lstrip().startswith('#'):
|
||||
key, value = line.split('=', 1)
|
||||
values[key.strip()] = value.strip().strip('"\'')
|
||||
token = values['AUTONOMY_TOKEN']
|
||||
if not token or any(c in token for c in ['\n', '\r', '"', '\\']):
|
||||
raise RuntimeError('Invalid autonomy token format')
|
||||
environment = root / 'staging-worker.env'
|
||||
fd = os.open(environment, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
with os.fdopen(fd, 'w') as out:
|
||||
out.write('AUTONOMY_TOKEN="' + token + '"\nAZEROTHAI_URL=http://127.0.0.1:8791\n')
|
||||
environment.chmod(0o600)
|
||||
units = pathlib.Path.home() / '.config/systemd/user'
|
||||
units.mkdir(parents=True, exist_ok=True)
|
||||
commands = {
|
||||
'runtime': (f'{node} {app}/scripts/start-local-runtime.mjs', 'Environment=RUNTIME_PORT=8791 RUNTIME_HOST=127.0.0.1\n'),
|
||||
'relay': (f'/usr/bin/sg docker -c "/usr/bin/python3 {root}/adapter/relay.py --root {root} --url http://127.0.0.1:8791 --env-file {app}/.dev.vars"', ''),
|
||||
'autonomy': (f'{node} {app}/scripts/autonomy-worker.mjs', f'EnvironmentFile={environment}\n'),
|
||||
}
|
||||
names = []
|
||||
for kind, (command, extra) in commands.items():
|
||||
name = f'azerothai-classic-{kind}.service'
|
||||
names.append(name)
|
||||
after = 'network.target' if kind == 'runtime' else 'azerothai-classic-runtime.service'
|
||||
(units / name).write_text(f'''[Unit]
|
||||
Description=AzerothAI Classic staging {kind}
|
||||
After={after}
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory={app}
|
||||
Environment=PATH={node.parent}:/usr/local/bin:/usr/bin:/bin
|
||||
{extra}ExecStart={command}
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
TimeoutStopSec=20
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
''')
|
||||
subprocess.run(['systemctl', '--user', 'daemon-reload'], check=True)
|
||||
subprocess.run(['systemctl', '--user', 'enable', '--now', *names], check=True)
|
||||
subprocess.run(['systemctl', '--user', 'is-active', *names], check=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--root', type=pathlib.Path, required=True)
|
||||
main(parser.parse_args().root.resolve())
|
||||
@@ -61,8 +61,8 @@ void commands() {
|
||||
if (!CharacterDatabase.DirectPExecute("UPDATE azerothai_commands SET status='processing' WHERE id='%s' AND status='pending'",sql(id).c_str())) continue;
|
||||
Player* p=sObjectMgr.GetPlayer(ObjectGuid(HIGHGUID_PLAYER,f[1].GetUInt32()));
|
||||
Player* peer=sObjectMgr.GetPlayer(ObjectGuid(HIGHGUID_PLAYER,f[6].GetUInt32()));
|
||||
if (!managed(p) || p->GetSession()->GetAccountId()!=f[2].GetUInt32() || p->GetName()!=f[3].GetString() || std::time(nullptr)-f[7].GetUInt32()>90 || std::time(nullptr)>f[10].GetUInt32()) { receipt(id,false,"Stale command or managed identity unavailable"); continue; }
|
||||
if (f[6].GetUInt32() && (!managed(peer) || peer->GetSession()->GetAccountId()!=f[8].GetUInt32() || peer->GetName()!=f[9].GetString())) {receipt(id,false,"Peer identity changed or protected");continue;}
|
||||
if (!managed(p) || p->GetSession()->GetAccountId()!=f[2].GetUInt32() || std::string(p->GetName())!=f[3].GetString() || std::time(nullptr)-f[7].GetUInt32()>90 || std::time(nullptr)>f[10].GetUInt32()) { receipt(id,false,"Stale command or managed identity unavailable"); continue; }
|
||||
if (f[6].GetUInt32() && (!managed(peer) || peer->GetSession()->GetAccountId()!=f[8].GetUInt32() || std::string(peer->GetName())!=f[9].GetString())) {receipt(id,false,"Peer identity changed or protected");continue;}
|
||||
if (kind=="party") {
|
||||
if (!nearby(p,peer,35) || !safeGroup(p) || !safeGroup(peer) || peer->GetGroup() || peer->GetGroupInvite() || p->GetGroupInvite() || (p->GetGroup() && (p->GetGroup()->GetMembersCount()>=5 || p->GetGroup()->GetLeaderGuid()!=p->GetObjectGuid()))) { receipt(id,false,"Party participants unavailable or protected"); continue; }
|
||||
WorldPacket invite(CMSG_GROUP_INVITE,32); invite << std::string(peer->GetName()); p->GetSession()->HandleGroupInviteOpcode(invite);
|
||||
|
||||
Reference in New Issue
Block a user