掠影生活 发布的文章

在战斗中,经常需要指挥让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指令显示中文的问题是需要修改代码解决的,关于国际化的问题以后可以再写一章,反正这部分动了不少的代码。

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

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

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

启动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也会自动骑鸟了。