个人重生点与睡眠状态机

睡袋之所以能「让某个玩家在某个睡袋上出生」,靠的是 Forge 的 IExtendedEntityProperties 扩展属性 + 每 tick 的服务端钩子。本条目覆盖 ExtendedPlayer 与 EventHandler 两个类。

基本信息

属性 值
类型 特殊机制(无指令、无配置项)
承载类 ru.npo6ka.sleepingbag.ExtendedPlayer(IExtendedEntityProperties)
驱动类 ru.npo6ka.sleepingbag.EventHandler
扩展属性名 ExtendedPlayer(EXT_PROP_NAME 常量声明了它,但 register 里用的是字面量 "ExtendedPlayer")
挂载时机 EntityEvent.EntityConstructing 中对每个 EntityPlayer 调用
触发条件 玩家放置睡袋 / 玩家睡醒
指令 无
配置文件 无(源码中没有任何 Config 读取)

存储字段

ExtendedPlayer 有 5 个字段,全部只在内存里:

字段 类型 含义 写入点
lastCoord ChunkCoordinates 放置睡袋时玩家自己站的位置(floor(posX), (int) posY, floor(posZ)) 放置睡袋时
bedCoord ChunkCoordinates 睡袋脚部方块的坐标 放置睡袋时
lastSpawnCoord ChunkCoordinates 放置睡袋时玩家原本的原版出生点(entity.getBedLocation(dimensionId)) 放置睡袋时
isSleeping int 睡眠状态机(0 / 1 / 2) 放置睡袋 + 服务端 tick
isWakeUp boolean 唤醒标志,触发一次重生点重设 睡醒(setBedOccupied)

睡眠状态机(isSleeping 三态)

isSleeping 不是一个布尔值而是三态计数器,由 EventHandler.onTick(仅 TickEvent.ServerTickEvent 且 phase == END)推进:

状态 含义 下一 tick 行为
0 未在睡(默认) 无动作
1 睡袋已铺好,等待唤醒 置为 2
2 真正调用睡袋方块 对 bedCoord 处方块调用 onBlockActivated(...),然后置回 0

可观测结果:从放下睡袋到真正躺进去有 2 个服务端 tick(≈100 ms)的延迟。 放置时 storeOriginalPosition 写入 1,第一个 tick 升到 2,第二个 tick 才真正触发放睡动画。

被调用的参数固定为 (side = 1, hitX = 0.5f, hitY = 0.25f, hitZ = 0.5f),正好落在 0.25 高的睡袋顶面中央。

唤醒与重生点重设

BlockSleepingBag.setBedOccupied 每次被调用(无论 occupied 传 true 还是 false,方法体根本不读这个参数)都会 setWakeUpFlag(true)。服务端 tick 随后执行:

player.setSpawnChunk(ep.getlastSpawnCoord(), false, player.worldObj.provider.dimensionId);
ep.setWakeUpFlag(false);

即把玩家当前的原版重生点重新写回放置睡袋之前记录的那个坐标 lastSpawnCoord。配合 getBedSpawnPosition 覆写(返回 lastCoord),形成完整闭环:

  • 战点判定 / 判怪点走 getBedSpawnPosition → 睡袋旁边的 lastCoord
  • 实际重生走原版 setSpawnChunk → 睡袋铺之前记下的 lastSpawnCoord
  • 两者分离,所以睡袋不会把全服战点搬到野外,也不会因为离睡袋太远而在半夜被判定成「离床太远」

客户端:躺姿下移

EventHandler.onRenderPlayer(RenderPlayerEvent.Pre,@SideOnly(Side.CLIENT)):

if (player.isPlayerSleeping()) GL11.glTranslated(0.0, -0.3, 0.0);

睡觉中的玩家模型整体下移 0.3 格,用于匹配 0.25 高的睡袋碰撞箱,避免人物悬空或陷进方块。

事件注册

CommonProxy.init() 把同一个 EventHandler 实例注册到两条总线:

总线 响应的事件
MinecraftForge.EVENT_BUS RenderPlayerEvent.Pre(仅客户端)、EntityEvent.EntityConstructing
FMLCommonHandler.instance().bus() TickEvent.ServerTickEvent

Core 的 preInit / init / postInit 全部只是转发给 CommonProxy,postInit 是空实现。

已知限制

  • 不持久化:saveNBTData / loadNBTData / init 三个接口方法全是空方法体,NBTUtil 也只是一个空的 final 类。ItemSleepingBag.TAG_POSITION = "player_pos_sleeping_bag" 声明了但没有任何引用。所以睡袋坐标只活在内存里,玩家登出 / 服务器重启后需要重新睡一次才能生效
  • 只遍历主世界:onTick 用 getMinecraftServerInstance().getEntityWorld() 取世界,只处理主世界的 world.playerEntities。在其他维度(如 GTNH 的口袋维度)睡觉的玩家不会被这套 tick 逻辑处理
  • 无出生点会 NPE:lastSpawnCoord 来自 entity.getBedLocation(dimensionId),玩家从未设置过原版出生点时返回 null,setSpawnChunk(null, ...) 会抛异常
  • 死亡/重生事件未挂钩:EventHandler 只监听 RenderPlayerEvent、EntityConstructing、ServerTickEvent,没有 LivingDeathEvent / PlayerRespawnEvent;「睡袋消失后重生点如何回退」完全依赖上面 lastSpawnCoord 的重写逻辑
  • Utils 类未被使用:RAND / toString / debug 三个成员在整个源码中没有任何调用点

相关条目