本帖最后由 Vinogradov 于 2019-8-4 16:15 编辑


丢人钟V3:完全压缩,防沉迷系统等

作者:丢人素学姐,00ll00

丢人钟系统可能是目前为止唯一可以在原版Minecraft中获取机器时间的解决方案。经历了V1V2两个版本,它的体积大幅缩小,也不会造严重卡顿。

但作者仍然想问:我还能做得更好吗?基于这套系统我还能做些什么?

基于这样的动机,在好几位大佬的提点下,作者完成了丢人钟的第三个版本,这也会是最后一个版本。以后丢人钟只会基于bug进行帖内更新。


更新内容:

  • 压缩到1个命令方块。在目前的游戏机制下这已经是理论最优值。
  • 加载即用,无需安装。
  • 拆分数据包,重写附加功能。
  • (可选)添加了在生存模式获取告示牌时钟的方法。
  • (可选)使游戏内时间与机器时间同步。
  • (可选,仅服务器可用)实现了原版防沉迷系统:在管理员指定的时间段内(可设置)非白名单成员(可设置)禁止上服务器。

在以下内容中,作者会不加说明地使用
V1V2的发布帖中提到的内容,所以如果有读不懂的地方请自行翻阅旧帖 (虽然V2的帖子并不算很旧)。

核心系统:

使用方法:加载即用,无需安装。

注意事项:加载该数据包时会将maxCommandChainLength设为1000000(当然实际上峰值只是8万多,并不会到一百万)。另外,在服务器中使用时请保证enable-command-block为true。

毫无疑问,要做到只有1个CB,除了被探测LastOutput的那个以外,其余所有的命令都放入mcfunction中被高频执行(带有minecraft:tick的标签)。于是我们就来分析一下这个被循环的函数(DiurenClock\data\diuren-clock\functions\main_loop.mcfunction):

  1. scoreboard players set DiurenClock.flag DiurenClock.impl 0

  2. execute if score DiurenClock.SecFromMidnight DiurenClock.impl matches ..43199 run function diuren-clock:bisearch/bs_0_43199
  3. execute if score DiurenClock.SecFromMidnight DiurenClock.impl matches 43200.. run function diuren-clock:bisearch/bs_43200_86399

  4. execute if score DiurenClock.flag DiurenClock.impl matches 0 run function #diuren-clock:runtime_all

  5. scoreboard players operation DiurenClock.hour DiurenClock = DiurenClock.SecFromMidnight DiurenClock.impl
  6. scoreboard players operation DiurenClock.hour DiurenClock /= Const3600 DiurenClock.impl
  7. scoreboard players operation DiurenClock.minute DiurenClock = DiurenClock.SecFromMidnight DiurenClock.impl
  8. scoreboard players operation DiurenClock.minute DiurenClock /= Const60 DiurenClock.impl
  9. scoreboard players operation DiurenClock.minute DiurenClock %= Const60 DiurenClock.impl
  10. scoreboard players operation DiurenClock.second DiurenClock = DiurenClock.SecFromMidnight DiurenClock.impl
  11. scoreboard players operation DiurenClock.second DiurenClock %= Const60 DiurenClock.impl
复制代码

  • 将flag设为0【第1行】
  • 通过二分法(实际上作者使用了二分和三分的混合。为什么要这么做?因为作者很无聊。),由当前的DiurenClock.SecFromMidnight(即日秒)决定具体要运行的是哪个穷举的分组(每个分组中穷举30秒。这个数字是可以,且应该被缩小的。但作者懒得改了,反正也不会造成卡顿,而且拆分更细的话反而可能会引起载入的卡顿)并执行该分组。若执行成功的话将flag设为1【第3行-第4行】
  • 若flag仍然为0,则一次性执行所有穷举,强制同步。【第6行】

第8行-第14行不过是简单的计算,不再分析。

在上述第2步中,有一个技巧是如何只使用一条命令,在匹配成功的分支中同时设置日秒的值并将flag设为1?利用execute store success!示例如下:

  1. execute if block ~ 0 ~ repeating_command_block{LastOutput: "{"extra":[{"color":"red","extra":[{"color":"gray","clickEvent":{"action":"suggest_command","value":"/ayaka"},"extra":[{"text":"/"},{"underlined":true,"color":"red","text":"ayaka"},{"italic":true,"color":"red","translate":"command.context.here"}],"text":""}],"text":""}],"text":"[00:09:00] "}"} run execute store success score DiurenClock.flag DiurenClock.impl run scoreboard players set DiurenClock.SecFromMidnight DiurenClock.impl 540
复制代码

至于这样的过程的合理性,留作习题。。。额。。。不对。。。显然,略。

这样下来,在常规tick,每tick只有几十个命令在执行,且多为记分板命令,不会造成卡顿。


免安装

注意到带有minecraft:tick或者minecraft:load标签的函数在自动执行时,执行地点为世界出生点,所以只需要在数据包的安装函数(带minecraft:load标签)中,使用

  1. setblock ~ 0 ~ minecraft:repeating_command_block[facing=up]{auto:1b,Command:"/ayaka"}
复制代码

,并在穷举时把坐标改成0 ~ 0 即可。如果玩家中途更改了出生点,只需/reload或者重进游戏即可,非常方便。

附加功能:

怀表(数据包名:DiurenClock-Holding,命名空间:diuren-clock.holding)

使用方法:加载即用,无需安装。

这和V2中没有很大的变化。只是将一处记分板实现改为了tag,稍稍化简了命令。

告示牌时钟 (数据包名:DiurenClock-Sign,命名空间:diuren-clock.sign)

使用方法:将一个钻石块和一个钟丢在地上合成蝙蝠蛋,然后对着告示牌(任何种类都可)Shift+右键即可。

告示牌显示和V1与V2中的技术完全一样,不另加分析。

首先,这里的合成是标准的ground crafting(我看外国论坛上是这么叫的,就是把原料扔到地上然后合成产物。为什么不用recipes?不能带nbt啊。)。

对着告示牌右键时,蝙蝠蛋会在此位置生成一个带特定tag的药水云。execute at 这个药水云,summon一个带Sign tag的marker。在这之后,同1tick内,该药水云会被杀死。之后只需高频执行

  1. execute at @e[tag=Sign] run xxx
复制代码

即可用一条命令对所有启动的告示牌时钟经行显示更新。

注意:当告示牌方块变为其他方块时,该marker会被(刻意地)清除。想要重新获得告示牌时钟需重复上述步骤。


同步游戏时间为机器时间(数据包名:DiurenClock-GameTimeSync,命名空间:diuren-clock.game_time_sync)

使用方法:加载即用,无需安装。

注意事项:加载该数据包时会将doDaylightCycle设为false。

先把日秒换算成游戏内tick数,然后再使用time add二分地将这个值设为当前游戏时间。

防沉迷系统(数据包名:DiurenClock-GoToBed,命名空间:diuren-clock.go_to_bed)

使用方法:将服务器的function-permission-level改为4,并在DiurenClock-GoToBed\data\diuren-clock.go_to_bed\functions\white_list.mcfunction中添加自己的名字(使用方法见文件内示例)。在同一文件夹的setup.mcfunction中将StartSec的值修改为开始管制的时间点的日秒数(hour*3600+minute+60+second),将EndSec的值修改为结束管制的时间点的日秒数。默认的值为StartSec=82800,EndSec=25200;即晚上11点到次日早上7点,非白名单人员禁止入服。这些设置在数据包加载后,在游戏中也可手工修改。
注意事项:该数据包只会在服务器上生效,且仅支持1.14.4。
这个功能实现起来并不复杂,无非是高频检测目前日秒是否在管制时间段内,若是则把不带白名单tag的玩家kick出去。在管制时间到来之前的5分钟,1分钟,10秒,9秒,。。。,1秒时会对非白名单玩家弹出提醒。

其它

本来想做个闹钟啥的,后来觉得太麻烦就咕了。


改进

我想,如果Minecraft内无法直接操作字符串的话,现在的架构应该很完美了。虽然作者在V2的帖子里也说过这样的话,但现在应该真的做到底了。

唯一可能改进的地方之前也提到了,就是现在实际上没有必要像V2一样一次性穷举30个可能性。为了保证检测的流畅性,每次检测两三个连续的时间点即可。具体地说,设目前日秒为s,那么V3中单次会检测s所在的半分钟(即xx:xx:00-xx:xx:29或者xx:xx:30-xx:xx::59)内的30秒及这接在这半分钟后的两秒。多两秒是为了在相邻的两个半分钟切换时不会造成变化的不均匀,也有更强的鲁棒性,即在服务器发生比较严重的卡顿时也不容易需要强行同步。但这个数字30是来源于V2中压缩CB大阵体积的需求。事实上,我们只需1+2=3条命令(1是从现在的30改的,后面的2还是起增强稳定性的作用)即可在V3中维持同样的鲁棒性,且每tick也少了20+条命令的执行。

然而这样带来的问题就是我们需要86400个runtime函数(如上所述,每个文件里是三条穷举命令),比起原来86400/30个runtime函数,会加剧加载时的卡顿。另一方面,现在这多余的20+条命令并不会对服务器的流畅运行造成明显卡顿,所以就决定维持现状了。

另外,二分和三分的混合是由于作者的强迫症:86400=2^6*3*5*30。所以实际上是先6次二分,再一次三分,再一个文件中穷举5个runtime函数。 这是不科学的,但鉴于它并不会带来多少性能损失,所以也不改了。


写在最后

从最初想到读LastOutput,到现在丢人钟的坑彻底填完,也有两年多了。在这期间,我得到了很多大佬的帮助和鼓励,这其中包括玄素,如花,土球,SPG,洞洞幺幺洞洞,panda4994,等。我对他们表示衷心的感谢。现在我的三次元事务很多,想开的长期坑也不知何时可以真正地开始做。但至少,我仍然对此保留兴趣与热忱。

感谢

  • 洞洞幺幺洞洞  与他的讨论使我受益良多。他参与了部分测试,并帮我找出了一些我找不出的问题。
  • 如花似玉 V3使用的二分法就是来自他的指导意见。
  • SPGoding 他在V2的帖子下面给出了非常有意思的回复,并提出了一个看似可行的改进方案(即使用技巧拼接字符串,反向穷举)。虽然这个方案最后被证明目前不能用,但这样的思路是我从来没有考虑,或者说即使考虑了也无从下手的。


DiurenClock.7z.zip (252.38 KB, 下载次数: 29) DiurenClock-Sign.zip (3.85 KB, 下载次数: 15) DiurenClock-GoToBed.zip (3.93 KB, 下载次数: 9) DiurenClock-GameTimeSync.zip (3.28 KB, 下载次数: 15) DiurenClock-Holding.zip (4.09 KB, 下载次数: 12)




丢人钟 V2:更薄,更流畅,更多功能

作者:丢人素学姐
基于panda4994提出的架构开发

前情回顾:

初代丢人钟的发布帖中,作者已经阐述了在游戏内获取机器时间的基本原理:通过穷举匹配rcb的LastOutput中的时间戳;并以此造出了一台巨大的机器。

作为原型演示,初代丢人钟无可厚非。然而它所存在的缺点是显而易见的:

  • 体积过于庞大
  • 对时的时候会出现明显的卡顿
  • 对时部分的命令写得过于混乱

针对这些问题,基于Panda4994的改进版本,作者开发出了丢人钟v2。

图片展示:



注意事项:

由于作者实在是懒,本模组只给出存档与材质包,其中datapack中命令的坐标全部写死的。所以如果想移植的话请自行forceload这几个区块。

新版本特点:

  • 使用datapack进行了完全的重写,代码不再混乱
  • 对时可以在1 tick中完成,几乎感觉不到卡顿
  • 制作了怀表、告示牌时钟、以及钟面

主要原理讲解:

显然,想知道当前机器时间的时分秒,等价于知道当前时刻到当天0点经过了多少秒,称之为“日秒”。这其中的换算小学生都会,这里就不重复了。为了减少内部命令数量,我们在穷举时获取日秒,然后再通过计算转换为时分秒。示例如下:

  1. execute if block -2 0 0 repeating_command_block{LastOutput: "{"extra":[{"color":"red","extra":[{"color":"gray","clickEvent":{"action":"suggest_command","value":"/ayaka"},"extra":[{"text":"/"},{"underlined":true,"color":"red","text":"ayaka"},{"italic":true,"color":"red","translate":"command.context.here"}],"text":""}],"text":""}],"text":"[00:03:26] "}"} run scoreboard players set DiurenClock.SecFromMidnight DiurenClock.impl 206
复制代码

我们将日秒存在记分板DiurenClock.impl的DiurenClock.SecFromMidnight假名下。

我们接下去着重讲解runtime.mcfunction中的命令。这个函数带有minecraft:tick标签,即它每tick都会运行。

该函数如图:

  1. tp @e[type=armor_stand,tag=DiurenClock.Iterator] -1 0 0


  2. scoreboard players operation DiurenClock.Iterator.PosY DiurenClock.impl = DiurenClock.SecFromMidnight DiurenClock.impl
  3. scoreboard players operation DiurenClock.Iterator.PosY DiurenClock.impl /= DiurenClock.Const3600 DiurenClock.impl
  4. scoreboard players operation DiurenClock.Iterator.PosZ DiurenClock.impl = DiurenClock.SecFromMidnight DiurenClock.impl
  5. scoreboard players operation DiurenClock.Iterator.PosZ DiurenClock.impl /= DiurenClock.Const30 DiurenClock.impl
  6. scoreboard players operation DiurenClock.Iterator.PosZ DiurenClock.impl %= DiurenClock.Const120 DiurenClock.impl


  7. execute store result entity @e[type=armor_stand,tag=DiurenClock.Iterator,limit=1] Pos[1] double 1 run scoreboard players get DiurenClock.Iterator.PosY DiurenClock.impl
  8. execute store result entity @e[type=armor_stand,tag=DiurenClock.Iterator,limit=1] Pos[2] double 1 run scoreboard players get DiurenClock.Iterator.PosZ DiurenClock.impl


  9. execute at @e[type=armor_stand,tag=DiurenClock.Iterator] run setblock ~ ~ ~-1 iron_block replace
  10. execute at @e[type=armor_stand,tag=DiurenClock.Iterator] run fill ~ ~ ~ ~ ~ ~1 redstone_block


  11. execute if score DiurenClock.SecFromMidnight DiurenClock.impl < DiurenClock.Const0 DiurenClock.impl run scoreboard players set DiurenClock.flag DiurenClock.impl 1
  12. execute if score DiurenClock.flag DiurenClock.impl = DiurenClock.Const1 DiurenClock.impl run say Syncing...
  13. execute if score DiurenClock.flag DiurenClock.impl = DiurenClock.Const1 DiurenClock.impl run fill -1 0 0 -1 23 119 redstone_block replace
  14. execute if score DiurenClock.flag DiurenClock.impl = DiurenClock.Const1 DiurenClock.impl run fill -1 0 0 -1 23 119 iron_block replace
  15. execute if score DiurenClock.flag DiurenClock.impl = DiurenClock.Const1 DiurenClock.impl run say Synced.
  16. scoreboard players set DiurenClock.flag DiurenClock.impl 0


  17. scoreboard players operation DiurenClock.hour DiurenClock = DiurenClock.SecFromMidnight DiurenClock.impl
  18. scoreboard players operation DiurenClock.hour DiurenClock /= DiurenClock.Const3600 DiurenClock.impl
  19. scoreboard players operation DiurenClock.minute DiurenClock = DiurenClock.SecFromMidnight DiurenClock.impl
  20. scoreboard players operation DiurenClock.minute DiurenClock /= DiurenClock.Const60 DiurenClock.impl
  21. scoreboard players operation DiurenClock.minute DiurenClock %= DiurenClock.Const60 DiurenClock.impl
  22. scoreboard players operation DiurenClock.second DiurenClock = DiurenClock.SecFromMidnight DiurenClock.impl
  23. scoreboard players operation DiurenClock.second DiurenClock %= DiurenClock.Const60 DiurenClock.impl

  24. scoreboard players set DiurenClock.SecFromMidnight DiurenClock.impl -40
复制代码

首先,我们有一个marker,带有DiurenClock.Iterator的tag。

  • 将该marker tp到rcb阵列外。【第1行】
  • 通过DiurenClock.SecFromMidnight的现有值,计算出能匹配到该值的穷举命令所在的分组,即所在的rcb的方块坐标。【第4行-第8行】
  • 将该marker tp到前述的方块坐标。(我们有execute store啦!可以直接把记分板里的值存到marker的Pos[1]和Pos[2] nbt中。)【第11行-第12行】
  • 通过marker,将该rcb 的前一个rcb关闭,即将用于激活前一rcb的红石块(如果存在)替换为铁块;同时激活该rcb及下一个rcb。(注意:这里rcb排成了长方形阵列,所以到每行末尾时,需要额外的一个cb将红石块“转移”到“下一行”的开头。当然这只是实现上的trick,不是核心问题。)【第15行-第16行】
  • 如果DiurenClock.SecFromMidnight的值小于0,则同时激活所有rcb,然后再同时关闭所有rcb。(注意,因为懒得再写一个mcfunction,这里实际模拟了一个if结构。)【第19行-第24行】
  • 通过DiurenClock.SecFromMidnight的值计算时分秒。【第27行-第33行】
  • 将DiurenClock.SecFromMidnight的值设为-40。【第35行】

为什么这样可以?分为两种情况分析。

  • 若当前tick的是“常规”tick,即当前的时间点在被激活的两个rcb(注意前述第4步,这里激活两个是为了防止在组间切换的时候恰好失去同步)的匹配范围内,在第7步执行完后,DiurenClock.SecFromMidnight会被当前的两个rcb中执行的函数所刷新,而不会是小于0的值。所以在下一tick中前述第5步不会被执行。而通过其余各步,可以稳定地更新被激活地rcb,以保持整个系统地正常运行。
  • 若当前tick不是“常规”tick,即当前的时间点不在被激活的两个rcb的匹配范围内,那么在runtime.mcfunction执行完后,即前述第7步执行完后,DiurenClock.SecFromMidnight不会被刷新。那么在下一tick时,前述第5步的判定条件就会通过,系统会通过把所有rcb都执行一次的方式强行将DiurenClock.SecFromMidnight的值纠正。(注意:在每tick中rcb的执行总是晚于带minecraft:tick的mcfunction的执行,因此值的纠正发生在第7步之后。)在此之后就回到了常规tick的情况。



附加功能:

怀表和告示牌数字时钟都是在v1中都有的功能,这里只不过写进了mcfunction,就不说了。

带指针的钟面需要加载作者自己做的材质包。由于作者毫无美化的意愿,只想早点了结这个坑,所以做得很丑,读者看个样子就行。原理上很简单:转盔甲架的头(头上戴了个被我魔改了模型的按钮。。。)。命令示例如下:

  1. execute store result entity @e[tag=s,limit=1] Pose.Head[0] float 6 run scoreboard players get DiurenClock.second DiurenClock
复制代码

(这里的这个倍数6是个小trick,意义在于60*6=360,什么意思读者自己体会吧)

另外由于Pose.Head[0]等于0的时候,看上去头实际指在了9点钟方向,所以制作模型时要进行适当旋转。

改进空间:

好像没了。。。我想不到怎么把这些压缩到单个cb。似乎如果全放到mcfunction的话又会回到最一开始那个超级卡顿的形式了。

感谢:

panda4994  丢人钟v2的主体架构是panda看了丢人钟v1后提出的。
ruhuasiyu
CBL的各位,特别是SPGoding
某咸鱼群的各位



注意:由于上传文件的大小与格式限制,存档经行了两次压缩。

丢人钟-1.14.4.7z.zip (497.21 KB, 下载次数: 9)

材质包.zip (5.32 KB, 下载次数: 6)








[groupid=546]Command Block Logic[/groupid]