注:本文只是记录个人开发过程,可能有部分操作很奇怪或者不对,请见谅!
这不是教程!这不是教程!这不是教程!
前阵子无聊,在群里讨论开发什么插件时,硝酸银大佬讲述了一些他本来想做但是因为时间关系没做的功能,详细了解后,逐渐又又又有了新插件的想法,于是就有了这一系列文章,来讲述我的开发历程~
(本篇文章实在我写完这些功能后一个月才写的)
一、开发环境
这里采用目前较为流行的LiteLoaderBDS加载器,提供了大量API使开发变得容易
当然VS也是必不可少的一部分
二、开发思路
首先提出的功能是一人睡觉全体过夜,这个功能说简单其实很简单,当一个人睡觉时,直接/time set 100就行了。但是这方式是不是过于简单粗暴了,而且据其他开发者反馈,会出现神秘BUG。于是打开了IDA,重新寻找起了方法。
先在IDA搜索关键词"sleeping",仔细查看后 ServerLevel::updateSleepingPlayerList
可能是相关内容。按F5查看伪代码,如图(1-1),查看虚表后,更加直观,如注释部分。

在实际代码测试后,发现这个点位并不能控制睡觉的部分,无法通过修改他来实现一键跳过夜晚。但是*(_BYTE *)(level + 10408)
引起了我的主要,测试后发现Bool如果为true代表所有玩家已经上床睡觉。(如果强制改成true,服务器会crash)
思路中断,于是寻找群中大佬帮助,得知ServerLevel::tick
内含有睡觉判断的整个部分。如下图(1-2)

查询虚表补齐函数名后,可以发现每tick都在判断是否所有玩家睡觉,以及集体睡觉后变化成白天的过程。(这里遇到了很多坑,比如一开始没注意到isAllPlayerSleeping,导致出现了一些奇怪的思路,比如Hookhook那个findPlayer,判断lambda地址,换掉lambda,代码如下

由于无法dlsym,这里的地址需要用base addr算,这里不多说了)
在多种方法无法实现的情况下,于是想了想,直接还原这个变化过程不就行了?
三、实现过程
首先需要找到一个玩家一睡觉就会触发的函数,之前走过了无数个坑,所以一下就找到了ServerLevel::updateSleepingPlayerList
,于是Hook它进行一顿操作。
由于这个点位并未告诉我们哪个玩家睡觉了,为了以防万一,判断了所有玩家中是否有玩家睡觉了。
THook(void, "?updateSleepingPlayerList@ServerLevel@@UEAAXXZ", ServerLevel* self) {
original(self);
Level::forEachPlayer([](Player& sp)->bool {
if (sp.isSleeping()) {
}
});
}
随后根据IDA给出的伪代码,逐步还原实现,代码以注释,帮助理解。
THook(void, "?updateSleepingPlayerList@ServerLevel@@UEAAXXZ", ServerLevel* self) {
original(self);
Level::forEachPlayer([](Player& sp)->bool {
if (sp.isSleeping()) {//是否有玩家睡觉
auto level = Global<Level>;
auto& gameRule = level->getGameRules();//获取全局规则
if (gameRule.getBool(GameRuleId(1), 0)) {//获取时间是否昼夜交替
level->setTime((unsigned int)(24000 * ((level->getTime() + 24000) / 24000)));//设置服务器时间至白天
auto pkt = SetTimePacket(level->getTime());//创建数据包,使客户端更新
level->getPacketSender()->send(pkt);//发生创建的数据包
Level::forEachPlayer([](Player& pl)->bool {//lambda_a1ff52de66d256430b242cdbc6303a4b
if (pl.isSleeping()) {
pl.stopSleepInBed(0, 0);//起床
if (!(unsigned int)Global<Level>->getLevelData().getGameDifficulty())//获取难度,如果为0,为和平
{
auto att = pl.getMutableAttribute(SharedAttributes::HEALTH);
att->resetToMaxValue();
*((int*)&pl + 172) = 20;//设置血量等操作
pl._sendDirtyActorData();//刷新状态
}
}
return true;
});
*(bool*)(level + 10408) = 0;//所有玩家是否睡觉改为False
level->forEachDimension([](Dimension& dim)->bool {//获取所有维度
dim.getWeather().stop();//天气恢复晴天
return true;
});
}
}
});
}
进游戏测试,发现刚睡上去就白天,完全不符合实际,继续阅读IDA分析后发现,居然有个类似与延迟执行的东西,大概为5s。

那我们可以利用Lliteloader所提供的ScheduleAPI,我们这里延迟80tick=4s
Schedule::delay([]() {
}, 80);
然后重写阅读一遍代码,寻找是否有BUG存在。
事实真找出来了一个小问题,由于延迟了4s,导致这4s内有玩家也睡觉,会导致再次触发,导致多次重置时间,那就整个全局,做个小判断吧。随后继续测试,效果完美。
完整代码如下
bool isPlayerSleeping = false;
THook(void, "?updateSleepingPlayerList@ServerLevel@@UEAAXXZ", ServerLevel* self) {
original(self);
Level::forEachPlayer([](Player& sp)->bool {
if (sp.isSleeping()) {
isPlayerSleeping = true;
Schedule::delay([]() {
auto level = Global<Level>;
auto& gameRule = level->getGameRules();
if (gameRule.getBool(GameRuleId(1), 0)) {
level->setTime((unsigned int)(24000 * ((level->getTime() + 24000) / 24000)));
auto pkt = SetTimePacket(level->getTime());
level->getPacketSender()->send(pkt);
Level::forEachPlayer([](Player& pl)->bool {
if (pl.isSleeping()) {
pl.stopSleepInBed(0, 0);
if (!(unsigned int)Global<Level>->getLevelData().getGameDifficulty())
{
auto att = pl.getMutableAttribute(SharedAttributes::HEALTH);
att->resetToMaxValue();
*((int*)&pl + 172) = 20;
pl._sendDirtyActorData();
}
}
return true;
});
*(bool*)(level + 10408) = 0;
level->forEachDimension([](Dimension& dim)->bool {
dim.getWeather().stop();
return true;
});
}
isPlayerSleeping = false;
}, 80);
}
});
}
四、总结
写完这个功能,大致能了解BDS是如何操作睡觉过夜的。(以下为个人理解,如果有错误请指出
- 判断是否所有玩家已经睡觉,如果为true,继续向下执行
- 判断玩家isSleepingLongEnough,如果为true,继续向下执行
- 检查服务器是否昼夜更替
- 设置服务器时间
- 发包同步到所有客户端
- 设置是否所有玩家已经睡觉为false
- 如果为和平模式,则恢复玩家生命值到最大值
- 设置天气为晴天(睡觉过夜100%恢复晴天)
文章评论