丢人钟V3:完全压缩,防沉迷系统等
作者:丢人素学姐,00ll00
丢人钟系统可能是目前为止唯一可以在原版Minecraft中获取机器时间的解决方案。经历了V1与V2两个版本,它的体积大幅缩小,也不会造严重卡顿。
但作者仍然想问:我还能做得更好吗?基于这套系统我还能做些什么?
基于这样的动机,在好几位大佬的提点下,作者完成了丢人钟的第三个版本,这也会是最后一个版本。以后丢人钟只会基于bug进行帖内更新。
更新内容:
- 压缩到1个命令方块。在目前的游戏机制下这已经是理论最优值。
- 加载即用,无需安装。
- 拆分数据包,重写附加功能。
- (可选)添加了在生存模式获取告示牌时钟的方法。
- (可选)使游戏内时间与机器时间同步。
- (可选,仅服务器可用)实现了原版防沉迷系统:在管理员指定的时间段内(可设置)非白名单成员(可设置)禁止上服务器。
在以下内容中,作者会不加说明地使用V1与V2的发布帖中提到的内容,所以如果有读不懂的地方请自行翻阅旧帖 (虽然V2的帖子并不算很旧)。
核心系统:
使用方法:加载即用,无需安装。
注意事项:加载该数据包时会将maxCommandChainLength设为1000000(当然实际上峰值只是8万多,并不会到一百万)。另外,在服务器中使用时请保证enable-command-block为true。
毫无疑问,要做到只有1个CB,除了被探测LastOutput的那个以外,其余所有的命令都放入mcfunction中被高频执行(带有minecraft:tick的标签)。于是我们就来分析一下这个被循环的函数(DiurenClock\data\diuren-clock\functions\main_loop.mcfunction):
- scoreboard players set DiurenClock.flag DiurenClock.impl 0
- execute if score DiurenClock.SecFromMidnight DiurenClock.impl matches ..43199 run function diuren-clock:bisearch/bs_0_43199
- execute if score DiurenClock.SecFromMidnight DiurenClock.impl matches 43200.. run function diuren-clock:bisearch/bs_43200_86399
- execute if score DiurenClock.flag DiurenClock.impl matches 0 run function #diuren-clock:runtime_all
- scoreboard players operation DiurenClock.hour DiurenClock = DiurenClock.SecFromMidnight DiurenClock.impl
- scoreboard players operation DiurenClock.hour DiurenClock /= Const3600 DiurenClock.impl
- scoreboard players operation DiurenClock.minute DiurenClock = DiurenClock.SecFromMidnight DiurenClock.impl
- scoreboard players operation DiurenClock.minute DiurenClock /= Const60 DiurenClock.impl
- scoreboard players operation DiurenClock.minute DiurenClock %= Const60 DiurenClock.impl
- scoreboard players operation DiurenClock.second DiurenClock = DiurenClock.SecFromMidnight DiurenClock.impl
- 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!示例如下:
- 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标签)中,使用
- 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内,该药水云会被杀死。之后只需高频执行
- 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的帖子下面给出了非常有意思的回复,并提出了一个看似可行的改进方案(即使用技巧拼接字符串,反向穷举)。虽然这个方案最后被证明目前不能用,但这样的思路是我从来没有考虑,或者说即使考虑了也无从下手的。
丢人钟 V2:更薄,更流畅,更多功能
作者:丢人素学姐
基于panda4994提出的架构开发
前情回顾:
在初代丢人钟的发布帖中,作者已经阐述了在游戏内获取机器时间的基本原理:通过穷举匹配rcb的LastOutput中的时间戳;并以此造出了一台巨大的机器。
作为原型演示,初代丢人钟无可厚非。然而它所存在的缺点是显而易见的:
- 体积过于庞大
- 对时的时候会出现明显的卡顿
- 对时部分的命令写得过于混乱
针对这些问题,基于Panda4994的改进版本,作者开发出了丢人钟v2。
图片展示:
注意事项:
由于作者实在是懒,本模组只给出存档与材质包,其中datapack中命令的坐标全部写死的。所以如果想移植的话请自行forceload这几个区块。
新版本特点:
- 使用datapack进行了完全的重写,代码不再混乱
- 对时可以在1 tick中完成,几乎感觉不到卡顿
- 制作了怀表、告示牌时钟、以及钟面
主要原理讲解:
显然,想知道当前机器时间的时分秒,等价于知道当前时刻到当天0点经过了多少秒,称之为“日秒”。这其中的换算小学生都会,这里就不重复了。为了减少内部命令数量,我们在穷举时获取日秒,然后再通过计算转换为时分秒。示例如下:
- 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都会运行。
该函数如图:
- tp @e[type=armor_stand,tag=DiurenClock.Iterator] -1 0 0
- scoreboard players operation DiurenClock.Iterator.PosY DiurenClock.impl = DiurenClock.SecFromMidnight DiurenClock.impl
- scoreboard players operation DiurenClock.Iterator.PosY DiurenClock.impl /= DiurenClock.Const3600 DiurenClock.impl
- scoreboard players operation DiurenClock.Iterator.PosZ DiurenClock.impl = DiurenClock.SecFromMidnight DiurenClock.impl
- scoreboard players operation DiurenClock.Iterator.PosZ DiurenClock.impl /= DiurenClock.Const30 DiurenClock.impl
- scoreboard players operation DiurenClock.Iterator.PosZ DiurenClock.impl %= DiurenClock.Const120 DiurenClock.impl
- 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
- 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
- execute at @e[type=armor_stand,tag=DiurenClock.Iterator] run setblock ~ ~ ~-1 iron_block replace
- execute at @e[type=armor_stand,tag=DiurenClock.Iterator] run fill ~ ~ ~ ~ ~ ~1 redstone_block
- execute if score DiurenClock.SecFromMidnight DiurenClock.impl < DiurenClock.Const0 DiurenClock.impl run scoreboard players set DiurenClock.flag DiurenClock.impl 1
- execute if score DiurenClock.flag DiurenClock.impl = DiurenClock.Const1 DiurenClock.impl run say Syncing...
- execute if score DiurenClock.flag DiurenClock.impl = DiurenClock.Const1 DiurenClock.impl run fill -1 0 0 -1 23 119 redstone_block replace
- execute if score DiurenClock.flag DiurenClock.impl = DiurenClock.Const1 DiurenClock.impl run fill -1 0 0 -1 23 119 iron_block replace
- execute if score DiurenClock.flag DiurenClock.impl = DiurenClock.Const1 DiurenClock.impl run say Synced.
- scoreboard players set DiurenClock.flag DiurenClock.impl 0
- scoreboard players operation DiurenClock.hour DiurenClock = DiurenClock.SecFromMidnight DiurenClock.impl
- scoreboard players operation DiurenClock.hour DiurenClock /= DiurenClock.Const3600 DiurenClock.impl
- scoreboard players operation DiurenClock.minute DiurenClock = DiurenClock.SecFromMidnight DiurenClock.impl
- scoreboard players operation DiurenClock.minute DiurenClock /= DiurenClock.Const60 DiurenClock.impl
- scoreboard players operation DiurenClock.minute DiurenClock %= DiurenClock.Const60 DiurenClock.impl
- scoreboard players operation DiurenClock.second DiurenClock = DiurenClock.SecFromMidnight DiurenClock.impl
- scoreboard players operation DiurenClock.second DiurenClock %= DiurenClock.Const60 DiurenClock.impl
- 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,就不说了。
带指针的钟面需要加载作者自己做的材质包。由于作者毫无美化的意愿,只想早点了结这个坑,所以做得很丑,读者看个样子就行。原理上很简单:转盔甲架的头(头上戴了个被我魔改了模型的按钮。。。)。命令示例如下:
- 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
某咸鱼群的各位
注意:由于上传文件的大小与格式限制,存档经行了两次压缩。
[groupid=546]Command Block Logic[/groupid]