weopqrst/moonroll/session does not have a README file

    Sim

    pub(open) trait Sim {
    fn save(Self) -> Bytes
    fn restore(Self, Bytes) -> Unit
    fn step_players(Self, Array[UInt64]) -> Unit
    fn hash(Self) -> UInt64
    }

    接入协议:任何可确定性模拟的系统实现这 3 个方法即可接入回滚。

    • save:把当前状态编码成字节快照,必须完整
    • restore:从快照恢复,必须与 save 严格互逆
    • step_players:推进一帧,inputs[i] 是第 i 个玩家本帧的输入位图, 长度等于 Session 的玩家数(两人局就是 [p0, p1])
    • hash:状态哈希,用于两端比对,不一致即 desync

    这几个方法都必须是纯确定性的:同样的输入序列必须得到同样的状态与哈希。 这是回滚同步的唯一硬约束,库无法替你检查,但 Session 会对每帧调用 save,所以「忘了恢复某个字段」会立即表现为哈希不一致。

    step_players 收到的数组是会话内部复用的缓冲区,实现里只读不存: 需要留用就把元素拷贝出来。

    HashRing

    pub struct HashRing {
    capacity : Int
    frames : Array[Int]
    items : Array[UInt64]
    }

    帧号 -> 状态哈希的环形缓冲。

    快照字节只用于重放,比较两端「已确认帧」的世界是否一致需要历史哈希; 只比较当前帧会把两端各自不同的预测误判成分叉。

    HashRing::get

    fn HashRing::get(self : HashRing, frame : Int) -> UInt64?

    HashRing::new

    fn HashRing::new(capacity : Int) -> HashRing

    HashRing::set

    fn HashRing::set(self : HashRing, frame : Int, hash : UInt64) -> Unit

    Session

    pub struct Session[T] {
    sim : T
    local_id : Int
    num_players : Int
    window : Int
    frame : Int
    confirmed : Int
    rollbacks : Int
    replayed : Int64
    max_depth : Int
    resyncs : Int
    rings : Array[
    InputRing
    ]
    cont : Array[Int]
    active : Array[Bool]
    heard : Array[Int]
    buf : Array[UInt64]
    snaps : SnapshotRing
    hashes : HashRing
    }

    回滚会话:持有模拟器、每个玩家一条输入环、窗口内快照,负责预测、确认与重放。

    帧号从 1 开始,0 表示初始状态。frame 是本地已模拟到的最新帧, confirmed 是所有在线玩家输入都已到达的最大帧(其之前的帧不会再回滚)。

    典型循环(每帧一次):
    1. 本地推进:advance(本地输入)
    2. 把 player_input(本地玩家, 帧) 发给其他玩家
    3. 收到某玩家输入时调用 receive_from(玩家, 帧, 输入)
    4. 有人掉线时调用 set_player_active(玩家, false),并把同样的决定同步给全房

    玩家数在构造时固定,1..MAX_PLAYERS(8)人;双人局可以用更短的 receive(帧, 输入),它会自动推断对端是另一个玩家。

    Session::advance

    fn[T : Sim] Session::advance(self : Session[T], local_input : UInt64) -> Int

    本地推进一帧,返回新的帧号。

    Session::confirmed

    fn[T] Session::confirmed(self : Session[T]) -> Int

    Session::export_state

    fn[T] Session::export_state(self : Session[T], frame : Int) -> Bytes?

    取某一帧的完整状态字节(就是 Sim::save 的产物),给对端做全量重同步。

    只在 frame 位于 1..self.frame 且还没被快照窗口覆盖时返回值; 太老的帧已经无法复现,只能取一个更近的帧。

    返回的字节是「谁的世界算数」的载体:拿到别人的 Bytes 之后交给 resync_from 就能把本端掰到同一份世界。协议上对应 Snap 帧。

    Session::frame

    fn[T] Session::frame(self : Session[T]) -> Int

    Session::hash

    fn[T : Sim] Session::hash(self : Session[T]) -> UInt64

    Session::hash_at

    fn[T] Session::hash_at(self : Session[T], frame : Int) -> UInt64?

    取某一帧模拟后的状态哈希(超出快照窗口、从未模拟过时为 None)。

    两端在同一个已确认帧上比对这个哈希,才是「世界是否一致」的判据; 比对当前帧会把两端各自不同的预测误判成分叉。

    Session::is_active

    fn[T] Session::is_active(self : Session[T], player : Int) -> Bool

    某个玩家是否在线。掉线的玩家仍然占一个输入槽位(模拟器的输入表长度 不变),但不再参与确认帧的推进。

    Session::local_player

    fn[T] Session::local_player(self : Session[T]) -> Int

    Session::max_rollback_depth

    fn[T] Session::max_rollback_depth(self : Session[T]) -> Int

    单次回滚里重放过的最大帧数(最坏情况的深度)。

    Session::new

    fn[T : Sim] Session::new(sim : T, local_id : Int) -> Session[T]

    双人局会话(等价于 with_players(sim, 2, local_id, DEFAULT_WINDOW))。

    Session::online_count

    fn[T] Session::online_count(self : Session[T]) -> Int

    当前在线玩家数(未掉线的人数)。

    Session::peer_confirmed

    fn[T] Session::peer_confirmed(self : Session[T], player : Int) -> Int

    对端 player 最近声明的确认帧(从未收到过 Confirm 时为 0)。

    发送端可以据此把「重发最近 8 帧」换成「重发对方还没确认的那一段」: 链路正常时这段只有延迟那么大,丢包时水位停住、区间自动变长, 于是被丢的输入仍然会被补上。

    Session::player_count

    fn[T] Session::player_count(self : Session[T]) -> Int

    这一局的玩家数。

    Session::player_input

    fn[T] Session::player_input(self : Session[T], player : Int, frame : Int) -> UInt64?

    取某个玩家在某一帧的已知输入(未到达、已被覆盖或玩家索引越界时为 None)。

    Session::receive

    fn[T : Sim] Session::receive(self : Session[T], frame : Int, remote_input : UInt64) -> Unit

    双人局的便捷入口:收到「对端」在 frame 帧的输入。

    3 人以上请用 receive_from——「对端」在 N 人局里没有唯一含义, 这里会直接忽略,避免调用方悄悄写错。

    Session::receive_confirm

    fn[T] Session::receive_confirm(self : Session[T], player : Int, frame : Int) -> Unit

    收到对端 player 的 Confirm 包,记下它已确认到 frame。

    这个水位只用来算「重发从哪一帧开始」:对方已经确认过的帧不用再发。 记录本身不影响确认帧的推进(那由 receive_from 里的连续指针负责), 所以迟到的、重复的、乱序的 Confirm 都是安全的。

    帧号会被钳到本地已模拟帧:对端不可能确认我们还没算出来的帧。

    Session::receive_from

    fn[T : Sim] Session::receive_from(self : Session[T], player : Int, frame : Int, remote_input : UInt64) -> Unit

    收到玩家 player 在 frame 帧的输入。

    若该帧已模拟过(frame <= self.frame)且尚未确认,就从 frame - 1 的快照恢复并重放 frame..self.frame,即发生一次回滚。

    Session::replayed

    fn[T] Session::replayed(self : Session[T]) -> Int64

    累计重放过的帧数。

    回滚次数只说明「重放了几次」,真正影响 CPU 的是重放深度; 链路延迟越大,这个数相对帧数涨得越快。

    Session::resync_from

    fn[T : Sim] Session::resync_from(self : Session[T], frame : Int, state : Bytes) -> Bool

    用对端 frame 帧的完整状态覆盖本端,并重放到当前帧。

    这是分叉之后的补救:hash_at 在对端确认帧上比对失败,说明两边世界 已经不同,此时再回滚也没用——回滚只会从本端的快照恢复,把同一个 错误再算一遍。唯一能重新对齐的办法是拿一份权威状态整段替换。

    步骤:
    1. restore 到 frame 那一帧(frame 必须落在回滚窗口内,否则那一段 的输入已经被输入环淘汰,重放等于换一个地方重新分叉);
    2. 把 frame 之后已模拟的帧逐帧重放——输入环没动,所以帧号、本地输入 的发送历史都不受影响;
    3. 把在线玩家的连续指针抬到 frame:权威状态意味着这一帧之前已经结算, 之后的回滚不会再退到「分裂之前的快照」上去。

    返回 false 表示参数不可用(帧号越界、超出窗口),会话保持不变。

    调用方要保证 state 来自跑同一个 Sim 的对等体;协议层只限制长度, 读不懂的字节交给 restore 是未定义行为。

    Session::resyncs

    fn[T] Session::resyncs(self : Session[T]) -> Int

    成功执行过的全量重同步次数。

    正常情况下这个数应该恒为 0:它每加 1,就说明这一局已经分叉过一次、 又被人为掰回来了。把它打进日志/面板,是「确定性 bug 有没有再犯」的信号。

    Session::rollbacks

    fn[T] Session::rollbacks(self : Session[T]) -> Int

    Session::set_player_active

    fn[T] Session::set_player_active(self : Session[T], player : Int, on : Bool) -> Bool

    声明玩家 player 上线 / 掉线,返回状态是否真的改变。

    「什么时候算掉线」是上层的策略(超时阈值、房主踢人),本库不替你决定; 它只提供一个同步点:所有会话必须用相同的在线集合推进,否则确认帧会 各不相同。掉线期间该玩家的输入退化为「窗口内最近一次已知值、否则 0」, 这对所有对等体都是同一套确定性规则,因此不会引入分叉。

    两种调用会被安全忽略:把本地玩家标成掉线;把最后一名在线玩家标成掉线。

    Session::with_players

    fn[T : Sim] Session::with_players(sim : T, num_players : Int, local_id : Int, window : Int) -> Session[T]

    num_players 是这一局固定的人数(1..8),local_id 是本地玩家索引, window 是可回滚的最大帧数。

    Session::with_window

    fn[T : Sim] Session::with_window(sim : T, local_id : Int, window : Int) -> Session[T]

    双人局会话,指定回滚窗口。

    SnapshotRing

    pub struct SnapshotRing {
    capacity : Int
    frames : Array[Int]
    items : Array[Bytes?]
    }

    帧号 -> 快照字节的环形缓冲。容量固定、按帧号取模寻址, 槽位记录真实帧号,被覆盖后旧帧查询返回 None。

    SnapshotRing::capacity

    fn SnapshotRing::capacity(self : SnapshotRing) -> Int

    SnapshotRing::get

    fn SnapshotRing::get(self : SnapshotRing, frame : Int) -> Bytes?

    SnapshotRing::new

    fn SnapshotRing::new(capacity : Int) -> SnapshotRing

    SnapshotRing::set

    fn SnapshotRing::set(self : SnapshotRing, frame : Int, bytes : Bytes) -> Unit

    DEFAULT_WINDOW

    let DEFAULT_WINDOW : Int

    默认回滚窗口(帧)。

    MAX_PLAYERS

    let MAX_PLAYERS : Int

    单局支持的最大玩家数,与协议解码里的上限保持一致。

    hash_state

    fn hash_state(parts : ArrayView[Int64]) -> UInt64

    把一组整数折叠成 FNV-1a 64 哈希,供 Sim::hash 复用。

    fn hash(self) -> UInt64 {
    hash_state([self.x.raw(), self.y.raw()])
    }