分类 默认分类 下的文章

1、下载3.3.5a(12340)版本的客户端并解压缩
下载地址1

2、在本站的右下角有 魔兽养老 链接,点击进去注册一个账号

3、打开客户端目录。进入目录后,打开名为"Data"的目录下的realmlist.wtf文件
清空文本并更改为: set realmlist aipromise.com

在使用PlayerBot的过程中,我常用的宏如下:
分享我的宏:
1:召唤
.bot add 角色1,角色2,角色3 --- 这个名字改成你自己创建的小号角色
/invite 角色1
/invite 角色2
/invite 角色3
/t 角色1 co +bear -- 因为我的这个角色1是个德鲁伊,我让他变成熊当T
/t 角色2 co +heal -- 因为我的这个角色2是个奶骑,我让他治疗
/t 角色3 co +frost -- 因为我的这个角色3是个法师,我让他用冰法输出 相应的 还有 +fire 表示是用火法输出
说明:因为.bot add xxx 召唤出来会有一定的延迟,因此/invite可能会不起作用,多点几下等他上线了就组进来并分配好职责了。

2、跟随
/p summon
/p follow
说明:因为我的习惯是把这些机器人组进了队伍里,因此在队伍里直接说话,这些机器人就会根据这些指令进行相应的行为。当然你也可以使用密语的方式单独给某个机器人指令。例如 /t 角色1 follow,就是密语 角色1 让他跟随。

3、暂停
/p stay

4、取消召唤
.bot remove *

网上有很多关于NPCBot指令的,有些文章是比较老的,不一定适用于最新版的。我整理了一下常用的指令。

.npcbot lookup
可以帮助你查看你要召唤的职业的类型所代表的数字
1 - 战士
2 - 圣骑士
3 - 猎人
4 - 盗贼
5 - 牧师
6 - 死亡骑士
7 - 萨满
8 - 法师
9 - 术士
11 - 德鲁伊

.npcbot lookup 11 就可以列出所有可召唤的德鲁伊的ID
比如系统设定了有100个机器人,你雇佣了1个,那别人就只能看到99个,如果别人全雇佣了,你就啥也雇佣不到了。我现在设定我这个每个人雇佣的上限是9个。也就是你自己可以组个10人团。

.npcbot spawn {ID} 召唤这个ID机器人出来,这个ID 就是 .npcbot lookup xx 列出来的那个数字。
召唤出来之后,你可以点击他和他对话,然后雇佣他。这样你就可以给他设定职责,设定天赋,设置哪些技能可用不可用。然后把你包里的装备给他穿上。如果你和他对话在对话里选择解雇他,这些你给他的装备会退回到你的包里(注意和.npcbot remove的区别,下面会说)。

.npcbot remove 选中这个NPC的头像并输入 .npcbot remove 可以临时解雇该NPC,装备不会退回到背包里,他持续装备着。以后无论是你雇佣他还是别人雇佣他,装备都还在。
.npcbot command follow (机器人向你靠拢)
.npcbot command standstill(机器人和你保持距离,好多老的文档里是 .npcbot command stay)
.npcbot revive 复活小伙伴。这个最好做一个宏,在战斗中他不幸挂了,赶紧选中他并点宏就可以复活他。当然你可以不选中任何目标的情况下点这个宏就会复活所有的挂了的NPC.
.npcbot move 让机器人移动到你的身边

上面这5个命令是经常需要使用的,尤其 remove command follow/standstill revive,建议分别做成4个宏。

.npcbot distance 设定小伙伴跟随你的距离,如果设置成0,除了你先对敌人发动攻击否则小伙伴不会攻击他们。最小:0,最大:75

在战斗中,经常需要指挥让PlayerBot跑位,如果都是玩家当然可以听YY指挥,但是PlayerBot和NPCBot可没有办法。因此有个想法,就是在战斗指挥PlayerBot或者NPCBot跑位。
先说说我的思路,刚开始的想法是Ctrl+鼠标右键来设定PlayerBot/NPCBot的目的点。发现魔兽客户端只提供了GetCursorPosition()能获取x,y值,但Cursor的坐标体系是基于屏幕分辨率的,而不是后台地图的坐标体系。这条路走不通。
突然想到了一个东西,暴风雪技能以及照明弹技能,这种技能是点了之后会用鼠标选中区域,这个区域信息在后台后传入参数SpellCastTargets,里面带着目标点的地图坐标。你说让一个74或者德鲁伊学一个这种技能总不太好吧。而且暴风雪技能需要引导,照明弹时间CD太长,盗贼的扰乱需要潜行形态。于是我想到了烟幕弹,烟幕弹是物品,CD是5秒,但物品的CD是ItemTemplate表中可以改的(改完以后需要删除WDB缓存目录并重启客户端),而且烟幕弹的本质是关联了一个烟幕弹的法术,例如白色烟幕弹物品ID为23768,其表中spellid_1为30262,这是spell_dbc中一个叫白色烟幕弹的法术。扔这个烟幕弹的本质其实就是释放了这个30262的法术,大家可以.learn 30262试试看就知道了。
好了,这下思路清晰了,继续看后台如何实现,就是看使用物品是否有相对应的事件发送到后台,以AzerothCore为例,有个AllItemScript可以方便外接的类,里面有方法:
virtual bool CanItemUse(Player /player/, Item /item/, SpellCastTargets const& /targets/) { return false; }
注册一个自己的Script进去实现这个方法,在这个方法里就能获取到SpellCastTargets,里面的GetDstPos()就能返回WorldLocation对象,就有x,y值了,具体实现就不在这儿描述了。
我的设计是:
// 23768 白色烟雾弹 所有的Bots(除了T)跑位到该位置
// 23770 蓝色烟雾弹 所有的远程跑位到该位置
// 23771 绿色烟雾弹 所有的近战跑位到该位置
// 23769 红色烟雾弹 所有的T跑位到该位置
// 以上4中烟雾弹,如果你当前选择了某个Bot,那就只是指定该Bot跑位到该位置
// 25886 紫色烟雾弹 小队成员跑位专用的烟雾弹,如果选择某个Bot,则这个bot所在的小组跑位到该位置。如果没有选目标或者选择的目标是自己活着非BOT,那就是本小队的人都跑位到该位置
我在AzerothCore中集成了PlayerBot和NpcBot,就得实现了2个AllItemScript,分别用于实现PlayerBot和NPCBot的跑位。

    -- for NPCBot
  class NpcBotsAllItemScript : AllItemScript {
  public:
    NpcBotsAllItemScript() : AllItemScript("NpcBotsItemScript") {}
    bool CanItemUse(Player* player, Item* item, SpellCastTargets const& targets) {
        if (player && item && player->GetBotMgr()) {
            player->GetBotMgr()->OnUseItem(player, item, &targets);
        }
        return false;
    }
  };
  
  -- for PlayerBot
  class PlayerbotsItemScript : AllItemScript {
  public:
    PlayerbotsItemScript() : AllItemScript("PlayerBotsItemScript") { }
  
    bool CanItemUse(Player* player, Item* item, SpellCastTargets const& targets) {
        if (player && item) {
            if (PlayerbotMgr* playerbotMgr = GET_PLAYERBOT_MGR(player))
            {
                for (PlayerBotMap::const_iterator it = playerbotMgr->GetPlayerBotsBegin(); it != playerbotMgr->GetPlayerBotsEnd(); ++it)
                {
                    if (Player* const bot = it->second)
                    {
                        if (PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot)) {
                            botAI->OnUseItem(player, item, &targets);
                        }
                    }
                }
            }
            
        }
        return false;
    }
  };


关于PlayerBot和NPCBot的一些区别
在魔兽私服游戏中,PlayerBot是召唤出自己账号名下的其他角色(或者说自己的小号),可以一起接任务,一起交任务,一起抢装备,一起升级,实际上就和多开一样的,但是他们能自动和你一起打怪接任务,很好玩。
而NPCBot是召唤出系统预定的一些NPC,这些NPC的等级会自动和你自己的角色的等级一样,这些属于临时雇佣性质,可以给他穿上装备,选择天赋,设定职责。你不需要也可以解雇他,你给他穿的装备会退回到你的背包中。然后别人也就可以雇佣它了。
还有一个很大的区别是,在副本中,很多BOSS不都是有点名嘛,例如ICC老1的骨刺,NPCBot是不会被点名的,而PlayerBot却是会被点名的。所以NPCBot和PlayerBot各有特点,看个人喜欢用哪个,从智能角度来讲,NPCBot更智能一些。我升级的时候喜欢用PlayerBot,毕竟都是自己的账号,都可以一起接任务,交任务,是自己人,而NPCBot只能叫雇佣兵。所以虽然有了NPCBot,我依旧想把PlayerBot集成到服务器中。

关于PlayerBot远程发呆不能使用技能的问题
最开始用TrinityCore源码编译服务器的时候,当时我用的中文客户端地图解压出的dbc,map,mmap,vmap等游戏数据,发现PlayerBot远程发呆不能使用技能,近战也只是平砍。开始时有些懵逼,后来网上一搜说用英文版的客户端导出游戏数据就好了。一实践确实解决这个问题。但是也发现带来了其他一些不好的体验,毕竟中文的客户端上显示一堆的英文的东西也不是很爽,比如对Playerbot使用spells指令查看他的所有可用法术,显示的全都是英文,看起来太费劲了。后来换成AzerothCore源码后依旧有同样的问题。

关于该问题的解决方案
咱的原则是先解决能用,后解决好用的问题。服务器搭好了也能玩了,就开始琢磨解决这个问题了。看了PlayerBot的源码,各种Trigger使用的都是技能的英文名称去施法的,而不是法术ID,用中文客户端导出的dbc数据中法术名称都是中文的,所以肯定就找不到相应的法术了。知道了问题就很好改了,不就是把法术的英文名称在SpellInfo数据结构中正确加载进去的问题么。
1、首先还是使用中文客户端(我用的是3.3.5a 12340版本)导出数据,服务器端使用该份数据,命名为data。
2、然后使用英文版客户端再导出数据,命名为data_en。
3、找了一个WDBXEditor的工具,这个可自行上网下载。.net写的,自己用vs2022编译运行既可以。
4、使用WDBXEditor打开data下的dbc目录下的Spell.dbc,将这份数据导出成一个csv格式的,然后再手动把这个csv文件导入到数据库spell_dbc_zh中。(题外话,数据库中core_world中有个spell_dbc表是额外扩充的一些法术,你可以看到里面只有4000多条记录,而spell_dbc_zh表中有49839条记录。DBCStores.cpp会把data/dbc/Spell.dbc数据加载进去后,再把spell_dbc中的数据合并进去)
5、使用WDBXEditor打开data_en下的dbc目录下的Spell.dbc,将这份数据导出成一个csv格式的,然后再手动把这个csv文件导入到数据库spell_dbc_en中。
6、创建一个spell_locale表:
CREATE TABLE spell_locale (
ID int unsigned NOT NULL DEFAULT '0',
locale varchar(4) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
Name varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
NameSubtext varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (ID,locale)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
2023-10-04T10:14:22.png
7、把spell_dbc_zh表的Name, NameSubtext插进去,locale设置为zhCN。NameSubtext字段是Rank等级说明,例如法师的法术:寒冰箭(等级2),Name中存储的是“寒冰箭”,NameSubtext存储的是“等级2”。
insert into spell_locale(ID, locale, Name, NameSubtext) select ID, 'zhCN', Name_Lang_deDE, NameSubtext_Lang_deDE from spell_dbc_zh;
咦,这里为啥是Name_Lang_deDE 而不是Name_Lang_zhCN字段呢。这就跟结构体的加载机制有关了,其实在DBCStores中加载xxx_dbc表数据时,表字段的名称是没有意义的,而是根据结构体的结构和表的相应字段顺序依次加载,这里就不展开说了。而zhCN在LocalConstant中的值是4,也就是Name_开头字段的第五个,正好就是Name_Lang_deDE。开始我看的时候也是一脸的懵逼啊,看完代码后才释然的。
8、再把spell_dbc_en表的Name, NameSubtext插进去,locale设置为enUS
insert into spell_locale(ID, locale, Name, NameSubtext) select ID, 'enUS', Name_enUS, NameSubtext_Lang_enUS from spell_dbc_en;
9、当然这只是有数据了,服务器端还需加载进去,这时候就得改代码啦。修改SpellMgr.cpp,在LoadSpellInfoStore()方法中把spell_locale的数据加载进去并修改SpellInfo的SpellName字段:


// 把刚才我们说的表读取并根据相应的locale进行复制
QueryResult result = WorldDatabase.Query("select ID, locale, Name, NameSubtext from spell_locale");
if (result) {
    uint32 localedSpellCount = 0;

    do {
        Field* fields = result->Fetch();

        uint32 spell = fields[0].Get<uint32>();
        LocaleConstant locale = GetLocaleByName(fields[1].Get<std::string>());
        std::string localeName = fields[2].Get<std::string>();
        std::string localeRank = fields[3].Get<std::string>();
        
        SpellInfo* spellInfo = mSpellInfoMap[spell];
        if (spellInfo) {
            if (localeName.size() > 0 && (!spellInfo->SpellName[locale] || strlen(spellInfo->SpellName[locale]) == 0)) {
                char* buf = new char[localeName.size() + 1];
                memcpy(buf, localeName.c_str(), localeName.size() + 1);
                sSpellStore.getStringPool().push_back(buf);

                spellInfo->SpellName[locale] = buf;
                localedSpellCount++;
            }
            if (localeRank.size() > 0 && (!spellInfo->Rank[locale] || strlen(spellInfo->Rank[locale]) == 0)) {
                char* buf = new char[localeRank.size() + 1];
                memcpy(buf, localeRank.c_str(), localeRank.size() + 1);
                sSpellStore.getStringPool().push_back(buf);

                spellInfo->Rank[locale] = buf;
            }
        }
    } while (result->NextRow());
    LOG_INFO("server.loading", "    >> Loaded {} spell locale data in {} ms", localedSpellCount, GetMSTimeDiffToNow(oldMSTime));
}

2023-10-04T10:10:27.png
OK,编译,启动服务,PlayerBot不发呆了,各种技能嗖嗖的发呀。

前面提到一嘴的关于spells指令显示中文的问题是需要修改代码解决的,关于国际化的问题以后可以再写一章,反正这部分动了不少的代码。

启动worldserver后,发现服务器worldserver进程的CPU占用率到了100%,也会导致我打开终端都显得有点小卡顿的样子。百度了一下,一无所获,只好用bing搜索了,发现了一篇老外的关于这个问题的解释:
2023-09-25T13:03:30.png
I found that modifying this configuration item can reduce CPU usage, but it is not clear which program functions will affect the execution performance.

MinWorldUpdateTime is approximately the minimum time slot unit during game execution. For an online game, it seems that the current default recommended 1ms is too harsh, so I switched to 10ms.

MapUpdateInterval should be based on the update interval of map change data. I remember before 2021, this parameter seemed to have been 100ms, but for some reason, the recommended default value has now been changed to 10ms.

These two configuration parameters have a significant impact on the resource usage during program operation, so adjustments have also shown an effect.

Anyway, if the same source code version and configuration result in a significantly lower CPU usage of the compiled worldserver program under WINDOWS compared to LINUX, I think we still haven't found the source of this problem.

from azerothcore-wotlk.

所以,他觉得这是最新版本的默认参数调整了导致的,解决办法就是把worldserver.conf中的
MinWorldUpdateTime = 1 调整为 10
MapUpdateInterval = 1 调整为 100

改完重启,OK了!

自己在自己个人的Windows10下修改的源码并可以编译调试,然后在部署的Linux服务器上再次编译时还是出现了一点问题,mod-playerbots编译无法通过,说明提供这源码的哥们没用clang编译器。原因是enum类型在.h头文件中声明使用clang编译器是会报错的,但是在windows使用vs2022是没有问题的。

今天又把官网自带的那个幻化模块加进去了,当前服务器是AzerothCore 最新版 + Eluna + Playerbots + NpcBots + 幻化。
集成Playerbot模块花费了不少时间,改动了不少了的代码,甚至自己加了很多国际化的代码(这样就可以汉化了)。
但是感觉最新版的mod-playerbots 的跟随很是别扭。但至少修复了 如果你是德鲁伊,变成鸟的时候,你的PlayerBots也会自动骑鸟了。

游戏中可以添加自己账号下的其他角色为机器人,跟随自己一起做任务,打副本。
这里说一下PlayerBot和NPCBot的区别:

PlayerBot 是召唤出自己账号名下的其他角色(或者说自己的小号),可以一起接任务,一起交任务,一起抢装备,一起升级,实际上就和多开一样的。
NPCBot 是召唤出系统预定的一些 NPC,这些NPC的等级会自动和你自己的角色的等级一样,这些属于临时雇佣性质,可以给他穿上装备,选择天赋,设定职责。你不需要也可以解雇他,你给他穿的装备会退回到你的背包中。然后别人也就可以雇佣它了。

这里先只介绍PlayerBot。

添加机器人。可以一次添加一个或者多个,如果是多个用英文逗号分隔:
.bot add name1,name2 等等 name1, name2 是你账号下的其他角色,你可以先返回登录页面创建一些角色,然后带领大伙儿一起做任务升级。我就挺喜欢带上个治疗跟着我一起升级。

删除机器人
.bot remove name1,name2

机器人出现后,你就可以控制他们了,比如组队,你可以密语他输入:/invite,就会自动进组。这些命令就是大家经常用的宏。
机器人加入进来后,就可以用普通的聊天来控制他们了。
以下是常用指令:

help
列出所有操控命令,以及使用帮助。Help列出的命令和帮助已经汉化,我们这里不一一说明,各位举一反三的进行使用;
此命令有下级命令 例如:列出来的命令talent有下级命令,可以输入help talent查看talent的相关命令

attack
此指令将攻击玩家所选择的目标,如果选择的是友方目标则不会攻击

stay
机器人将停留在原地

grind
主动攻击机器人看到的任何怪,这个用来刷怪还不错,你站在那儿不动,你的机器人看到怪就帮你打。

follow
让机器人跟随玩家。(在使用 .revive复活机器人或使用stay命令后使用)

summon
把机器人迅速召唤到身边

spells
列出机器人的所有可用技能连接,也可以 spells 水 这样对名称中有“水”字样的技能进行过滤,避免一下出来很多的技能看的让人眼花缭乱。

cast <技能id | 技能链接>
释放指定的技能id或链接的技能 必须是在spells指令里列出的技能。例如你可以让你的法师做一点水,然后把水交易给你。

---------------------------------------任务相关---------------------------------------
任务相关操作技巧:

  1. 可以通过quest命令来获得机器人的任务链接;
  2. 任务完成后如遇可选奖励物品,机器人会密语玩家,只需回密机器人r 【所需的物品链接】即可;
  3. 当需要收集游戏物体时:例如邮箱,宝箱之类的,可能需要玩家指定机器人操作时,搭配上面的命令进行使用;
  4. 任务相关的,机器人基本都会自动接任务,自动交任务,前提必须达到接受任务的条件即可。

quests
列出当前任务

quest add [任务链接]
给机器人添加指定任务

quest drop [任务链接]
放弃指定任务

quest report
生成任务报告,包括:任务所需的东西

------------ 物品 ----------------
e [item] 装备物品
ue [item] 取消装备
u [item] 使用物品
u [item] [target] 使用项目的目标(如使用宝石项)
destroy [item] 摧毁物品
[item] 如果打开了交易对话窗口,在密语中输入物品的链接,再回车这个物品就会加入到交易窗口中