Sans-IO deterministic rollback netcode for MoonBit: input ring buffer, prediction, rollback re-simulation, state checksums, compact byte protocol with input spans, change masks and full-state resync frames, deterministic link simulator, a moonphys wasm-gc browser demo with an online page, seven runnable examples, 1..8 player sessions, drop/rejoin handling, and fork recovery by snapshot resync.
Sans-IO deterministic rollback netcode for MoonBit.
///|
pub(open) trait Sim {
fn save(Self) -> Bytes // 编码完整状态
fn restore(Self, Bytes) -> Unit // 与 save 严格互逆
fn step_players(Self, Array[UInt64]) // 推进一帧,inputs[i] 是第 i 个玩家的输入位图
fn hash(Self) -> UInt64 // 状态哈希,不一致即 desync
}moon add weopqrst/moonroll// 1) 实现 Sim(略去 save/restore/hash)
impl @session.Sim for World with fn step_players(self, inputs) {
let mut x = self.x * 31UL
for i, bits in inputs {
x = x + (bits << (i * 4))
}
self.x = x
}
// 2) 每个对等体一个会话:with_players(世界, 人数, 本地玩家, 窗口)
let peer = @session.Session::with_players(world, 4, 0, 128)
// 3) 每帧推进;收到某位玩家的输入时自动回滚重放
ignore(peer.advance(my_input))
peer.receive_from(who, frame, that_player_input)
// 4) 所有玩家在同一个已确认帧上比对
if peer.hash_at(peer.confirmed()) != remote_hash_at_confirmed {
// desync
}let a = @fixed.Fixed::from_double(1.5)
let b = @fixed.Fixed::from_double(2.25)
let c = a * b
let d = @digest.Digest::new()
d.feed_bytes(b"state")
let checksum = d.finish()
let ring = @input.InputRing::new(64)
ring.set(frame, 0b101UL)| 帧 | 布局 |
|---|---|
| Input | [0x01][varint player][varint frame][varint count][varint bits] × count |
| Confirm | [0x02][varint frame] |
| Checksum | [0x03][varint frame][8 bytes hash] |
| Snap | [0x06][varint frame][varint len][len bytes 状态] |
| SnapAsk | [0x07][varint frame] |
let wire = @protocol.encode(@protocol.Message::Input(
@protocol.InputFrame::new_from(player, 42, [0b101UL]),
))
let msg = @protocol.decode(wire) // Some(Input(...))
// 一条链路每帧只发一个包时,把多个帧塞进同一个包
let batch = @protocol.encode_batch(msgs)
for m in @protocol.decode_batch(batch) { /* ... */ }if peer.hash_at(frame) != remote_hash {
// 这一帧就是恢复点:它不早于事故帧,之后的输入两端已经一致
@protocol.SnapRequest(@protocol.SnapRequestFrame::new(frame))
}
if let Some(state) = peer.export_state(frame) {
@protocol.Snap(@protocol.SnapFrame::new(frame, state))
}
peer.resync_from(frame, state) // 越界 / 超出快照窗口返回 false
peer.resyncs() // 正常情况下恒为 0let net = @sim_net.NetSim::new(
@sim_net.Link::new(delay, jitter, loss_pct, reorder_pct, seed),
)
net.send(frame, payload) // 可能被丢弃
for packet in net.recv(frame) { /* ... */ } // 到达的包,按到达时间排序
net.delivered() // 已交付包数
net.dropped() // 丢弃包数
net.in_flight() // 在途包数moon run examples/two_peer # 最小闭环:两个 Session + 字节协议 + 3 帧延迟
moon run examples/latency_chart # 8 条典型链路的回滚成本表
moon run examples/desync_demo # 分叉检测自证:注入输入不一致,报出确切帧号
moon run examples/four_peer # 四人对局:12 条链路全互联 + 2/4 人成本对比
moon run examples/drop_out # 掉线:宣布 / 不宣布掉线的确认帧走势对比
moon run examples/bandwidth # 带宽:五种发包策略的字节数对比
moon run examples/resync # 分叉恢复:用一份完整状态把落后者掰回来| 场景 | 延迟(帧) | 抖动 | 丢包 | 乱序 | 回滚次数 | 重放帧数 | 最大深度 |
|---|---|---|---|---|---|---|---|
| 局域网 16ms | 0 | 0 | 0% | 0% | 300 | 300 | 1 |
| 同城 33ms | 2 | 0 | 0% | 0% | 300 | 897 | 3 |
| 跨城 100ms | 6 | 0 | 0% | 0% | 300 | 2079 | 7 |
| 跨城 100ms ±50ms | 6 | 3 | 0% | 0% | 300 | 2443 | 10 |
| 跨城 100ms +5% 丢包 | 6 | 3 | 5% | 0% | 300 | 2468 | 11 |
| 跨洋 200ms ±50ms | 12 | 3 | 0% | 0% | 300 | 4180 | 16 |
| 跨洋 200ms ±50ms +25% 乱序 | 12 | 3 | 0% | 25% | 300 | 4229 | 17 |
pwsh tools/build-demo.ps1 # 产出 docs/engine.wasm
node tools/check-demo.mjs # 无头验收:1800 帧、200ms 延迟、50ms 抖动
python -m http.server --directory docs 8123 # 打开 http://localhost:8123/| 场景 | 耗时(均值) |
|---|---|
| 每帧预测 + 深度 16 回滚重放 | 0.51 µs |
| 每帧预测 + 深度 128 回滚重放 | 14.4 µs |
| 包 | 内容 | 状态 |
|---|---|---|
| weopqrst/moonroll/fixed | Q16.16 定点数(+ - * /、开方、lerp、clamp) | V0.1 |
| weopqrst/moonroll/digest | FNV-1a 64 状态哈希 | V0.1 |
| weopqrst/moonroll/input | 帧号 -> 输入位图的环形缓冲 | V0.1 |
| weopqrst/moonroll/session | Sim 协议 + 预测 / 确认 / 回滚重放 / 历史哈希 + 1..8 人会话 + 分叉重同步 | V0.2 |
| weopqrst/moonroll/protocol | 输入帧、确认帧、校验和帧、全量状态帧的字节编解码 | V0.3 |
| weopqrst/moonroll/sim_net | 延迟 / 抖动 / 丢包 / 乱序模拟器 | V0.3 |
| weopqrst/moonroll/adapters/phys | moonphys 参考适配层(PhysSim) | V0.4 |
| weopqrst/moonroll/demo | wasm-gc 演示引擎(mr_* 数值导出) | V0.4 |
| weopqrst/moonroll/examples/scene | 示例共用场景与对局运行器 | V0.5 |
| weopqrst/moonroll/examples/two_peer | 最小联机闭环 | V0.5 |
| weopqrst/moonroll/examples/latency_chart | 延迟 / 抖动 / 丢包扫描表 | V0.5 |
| weopqrst/moonroll/examples/desync_demo | 分叉检测自证 | V0.5 |
| weopqrst/moonroll/examples/four_peer | 四人对局 + 2/4 人重放成本对比 | V0.6 |
| weopqrst/moonroll/examples/drop_out | 掉线前后确认帧走势对比 | V0.7 |
| weopqrst/moonroll/examples/bandwidth | 五种发包策略的字节数对比 | V0.8 |
| weopqrst/moonroll/examples/resync | 分叉后的全量重同步 | V0.9 |
node tools/verify.mjs # check + fmt + 双后端测试 + native + 七个示例 + demo 自检Input : [0x01][varint player][varint frame][varint count][varint bits] * count
Confirm : [0x02][varint frame]
Checksum : [0x03][varint frame][8 bytes hash]
Active : [0x04][varint player][varint active]
InputSpan: [0x05][varint player][varint start][varint count][varint mask][varint bits] * popcount(mask)
Snap : [0x06][varint frame][varint len][byte] * len
SnapAsk : [0x07][varint frame]Install
Download zipSans-IO deterministic rollback netcode for MoonBit: input ring buffer, prediction, rollback re-simulation, state checksums, compact byte protocol with input spans, change masks and full-state resync frames, deterministic link simulator, a moonphys wasm-gc browser demo with an online page, seven runnable examples, 1..8 player sessions, drop/rejoin handling, and fork recovery by snapshot resync.