@@ -42,7 +42,9 @@ use crate::camera::CameraMode;
4242use crate :: chunk_loader:: affected_chunks;
4343use crate :: input:: InputState ;
4444use crate :: mesh_jobs:: { MeshPriority , MeshRunStats } ;
45- use crate :: prediction:: { PendingAction , PendingKind , reconcile_self} ;
45+ use crate :: prediction:: {
46+ PendingAction , PendingKind , ReconcileResult , apply_pending_position_correction, reconcile_self,
47+ } ;
4648use crate :: raycast:: raycast;
4749use crate :: storage:: { OpfsStorage , WorldStorage } ;
4850use crate :: ui:: lobby:: {
@@ -56,6 +58,11 @@ const MAX_REACH: f32 = 6.0;
5658/// Ping 间隔(毫秒)。
5759const PING_INTERVAL_MS : f64 = 5000.0 ;
5860
61+ /// Pong 校时样本权重。Pong 带 RTT 信息,可信度高于裸 PlayerTick。
62+ const CLOCK_PONG_ALPHA : f64 = 0.2 ;
63+ /// PlayerTick 校时样本权重。它可能受单向排队延迟影响,只作为低权重补充。
64+ const CLOCK_TICK_ALPHA : f64 = 0.05 ;
65+
5966/// 信令服务 URL meta tag 名称。
6067const SIGNALING_META_NAME : & str = "signaling-url" ;
6168
@@ -1844,7 +1851,10 @@ fn render_game_frame(
18441851 return Ok ( ( ) ) ;
18451852 } ;
18461853 let now = now_ms ( ) ;
1847- if game. entity_id != 0 && now - game. last_ping_sent_ms >= PING_INTERVAL_MS {
1854+ if game. mode != GameMode :: Local
1855+ && game. entity_id != 0
1856+ && now - game. last_ping_sent_ms >= PING_INTERVAL_MS
1857+ {
18481858 let client_time_ms = now as u64 ;
18491859 game. pending_pings . insert ( client_time_ms, now) ;
18501860 // 上限 16 个待办,避免长期不通时无限增长
@@ -1934,30 +1944,34 @@ fn render_game_frame(
19341944 game. physics . step ( & getter, & game. camera , & neutral, dt) ;
19351945 }
19361946 }
1947+ apply_pending_position_correction (
1948+ & mut game. physics ,
1949+ & mut game. pending_position_correction ,
1950+ dt,
1951+ ) ;
19371952 game. camera . position = game. physics . eye_position ( ) ;
19381953
19391954 // 60Hz 逻辑帧
19401955 game. frame_clock . accumulate ( dt) ;
19411956 let mut steps_consumed: u32 = 0 ;
19421957 let server_tick_allowed = matches ! ( game. mode, GameMode :: Local | GameMode :: Host ) ;
1958+ let mut last_input_tick_to_send = None ;
19431959 while game. frame_clock . consume_logic_step ( ) {
19441960 if server_tick_allowed {
19451961 game. server . borrow_mut ( ) . tick ( ) ;
19461962 }
1947- // 每个逻辑步推一条 input history(Host reconcile 用;Remote 也可靠它追踪本地步数)
1963+ // 每个逻辑步分配本地输入序号。Remote 不能借 dummy server tick,
1964+ // 否则 Host 回播时无法把权威位置和同一条预测记录对齐。
1965+ game. local_input_tick = game. local_input_tick . wrapping_add ( 1 ) . max ( 1 ) ;
19481966 game. input_history
1949- . push ( game. server . borrow ( ) . tick , game. physics . feet_position ) ;
1967+ . push ( game. local_input_tick , game. physics . feet_position ) ;
1968+ last_input_tick_to_send = Some ( game. local_input_tick ) ;
19501969 steps_consumed += 1 ;
19511970 }
19521971
1953- // 每个 logic step 上报一条 PlayerInput
1972+ // 若本帧消费了一个或多个逻辑步,只上报最新位置;tick 仍对应最后一个本地输入序号。
19541973 if steps_consumed > 0 && game. entity_id != 0 {
1955- let tick = if server_tick_allowed {
1956- game. server . borrow ( ) . tick
1957- } else {
1958- // Remote:用自己的 input history 计数(从 physics 最后一次 reconcile 后的步数推导)
1959- 0 // Phase 5 简化:Remote 的 PlayerInput.tick 用 0;Host Server 不依赖 Remote 的 tick 做排序
1960- } ;
1974+ let tick = last_input_tick_to_send. unwrap_or ( game. local_input_tick ) ;
19611975 game. net . send_client_message ( ClientMessage :: PlayerInput {
19621976 tick,
19631977 position : game. physics . feet_position ,
@@ -2108,7 +2122,7 @@ fn render_game_frame(
21082122 let mut view_proj_for_np = glam:: Mat4 :: IDENTITY ;
21092123 if let Some ( g) = a. game . as_mut ( ) {
21102124 view_proj_for_np = g. camera . vp_matrix ( ) ;
2111- let render_target = now_local + g. server_clock_offset_ms as f64 - g. interp . delay_ms ;
2125+ let render_target = now_local + g. server_clock_offset_ms - g. interp . delay_ms ;
21122126 let cam_pos = g. camera . position ;
21132127 let eids: Vec < EntityId > = g. interp . ids ( ) . collect ( ) ;
21142128 for eid in eids {
@@ -2316,8 +2330,7 @@ fn render_game_frame(
23162330 let now = now_ms ( ) ;
23172331 let mut instances: Vec < voxweb_render:: passes:: player:: PlayerInstance > = Vec :: new ( ) ;
23182332 if let Some ( ref mut game) = a. game {
2319- let render_server_time =
2320- now + game. server_clock_offset_ms as f64 - game. interp . delay_ms ;
2333+ let render_server_time = now + game. server_clock_offset_ms - game. interp . delay_ms ;
23212334 let eids: Vec < voxweb_core:: protocol:: EntityId > = game. interp . ids ( ) . collect ( ) ;
23222335 for eid in eids {
23232336 if let Some ( ( pos, _yaw, _pitch) ) = game. interp . advance ( eid, render_server_time)
@@ -2503,9 +2516,16 @@ fn dispatch_actions(game: &mut Game, input: &InputState) {
25032516 let server = game. server . borrow ( ) ;
25042517 server. world . get_block ( pos)
25052518 } ;
2506- // Local 模式 client 和 server 共享同一份 world,乐观更新会干扰 server 校验
2507- // (server 读回 AIR 误判 BlockNotEmpty 拒绝)。因此跳过乐观 set_block;
2508- // BlockUpdate 返回后再重 mesh。Phase 5 Remote 端加独立 WorldView 时再加乐观路径。
2519+ let input_tick = game. local_input_tick ;
2520+ let player_position = game. physics . feet_position ;
2521+ // Remote 的 server.world 只是本地世界视图,可以安全乐观修改;
2522+ // Local/Host 与权威 server 共享同一份 world,仍等 BlockUpdate,避免提前改世界干扰校验。
2523+ if game. mode == GameMode :: Remote {
2524+ game. server . borrow_mut ( ) . world . set_block ( pos, BlockID :: AIR ) ;
2525+ for cp in affected_chunks ( pos) {
2526+ game. mesh_jobs . enqueue ( cp, MeshPriority :: High ) ;
2527+ }
2528+ }
25092529 let request_id = game. pending . next_request_id ( ) ;
25102530 game. pending . insert (
25112531 request_id,
@@ -2515,8 +2535,12 @@ fn dispatch_actions(game: &mut Game, input: &InputState) {
25152535 backup,
25162536 } ,
25172537 ) ;
2518- game. net
2519- . send_client_message ( ClientMessage :: Break { pos, request_id } ) ;
2538+ game. net . send_client_message ( ClientMessage :: Break {
2539+ pos,
2540+ request_id,
2541+ input_tick,
2542+ player_position,
2543+ } ) ;
25202544 game. last_break_at_ms = now;
25212545 }
25222546
@@ -2545,6 +2569,8 @@ fn dispatch_actions(game: &mut Game, input: &InputState) {
25452569 return ;
25462570 }
25472571 let request_id = game. pending . next_request_id ( ) ;
2572+ let input_tick = game. local_input_tick ;
2573+ let player_position = game. physics . feet_position ;
25482574 game. pending . insert (
25492575 request_id,
25502576 PendingAction {
@@ -2553,10 +2579,18 @@ fn dispatch_actions(game: &mut Game, input: &InputState) {
25532579 backup,
25542580 } ,
25552581 ) ;
2582+ if game. mode == GameMode :: Remote {
2583+ game. server . borrow_mut ( ) . world . set_block ( neighbor, block) ;
2584+ for cp in affected_chunks ( neighbor) {
2585+ game. mesh_jobs . enqueue ( cp, MeshPriority :: High ) ;
2586+ }
2587+ }
25562588 game. net . send_client_message ( ClientMessage :: Place {
25572589 pos : neighbor,
25582590 block,
25592591 request_id,
2592+ input_tick,
2593+ player_position,
25602594 } ) ;
25612595 }
25622596}
@@ -2575,6 +2609,21 @@ fn send_chat(game: &mut Game, content: String) {
25752609 . send_client_message ( ClientMessage :: Chat { content } ) ;
25762610}
25772611
2612+ /// 摄入一条 Host 时钟偏移样本。
2613+ ///
2614+ /// 高延迟网络下,直接用最新 `server_time_ms - now` 覆盖偏移会让远端玩家插值 target
2615+ /// 来回抖动。这里第一条样本直接采用,后续做指数平滑;Pong 样本权重较高,
2616+ /// PlayerTick 样本只做低权重微调。
2617+ fn ingest_server_clock_sample ( game : & mut Game , estimated_offset_ms : f64 , alpha : f64 ) {
2618+ if !game. server_clock_synced {
2619+ game. server_clock_offset_ms = estimated_offset_ms;
2620+ game. server_clock_synced = true ;
2621+ return ;
2622+ }
2623+ let a = alpha. clamp ( 0.0 , 1.0 ) ;
2624+ game. server_clock_offset_ms = game. server_clock_offset_ms * ( 1.0 - a) + estimated_offset_ms * a;
2625+ }
2626+
25782627fn apply_server_message ( game : & mut Game , msg : ServerMessage ) {
25792628 match msg {
25802629 ServerMessage :: Welcome {
@@ -2672,18 +2721,31 @@ fn apply_server_message(game: &mut Game, msg: ServerMessage) {
26722721 players,
26732722 server_time_ms,
26742723 } => {
2675- let now = ( now_ms ( ) ) as i64 ;
2676- game. server_clock_offset_ms = server_time_ms as i64 - now;
2724+ let now = now_ms ( ) ;
2725+ if game. mode != GameMode :: Local {
2726+ let one_way_ms = game. rtt_ms . map ( |rtt| rtt as f64 * 0.5 ) . unwrap_or ( 0.0 ) ;
2727+ let estimated_offset = server_time_ms as f64 + one_way_ms - now;
2728+ ingest_server_clock_sample ( game, estimated_offset, CLOCK_TICK_ALPHA ) ;
2729+ }
26772730
26782731 for snap in & players {
26792732 if snap. entity_id == game. entity_id {
26802733 // 自己的权威位置 → reconcile
2681- let _r = reconcile_self (
2734+ let result = reconcile_self (
26822735 snap. position ,
2683- server_tick ,
2736+ snap . last_input_tick ,
26842737 & mut game. physics ,
26852738 & mut game. input_history ,
26862739 ) ;
2740+ match result {
2741+ ReconcileResult :: SoftCorrection ( delta) => {
2742+ game. pending_position_correction += delta;
2743+ }
2744+ ReconcileResult :: HardCorrection ( _) => {
2745+ game. pending_position_correction = glam:: Vec3 :: ZERO ;
2746+ }
2747+ ReconcileResult :: Ok | ReconcileResult :: MissingHistory => { }
2748+ }
26872749 } else {
26882750 // 远端玩家 → 喂入插值缓冲
26892751 game. interp . ingest_tick (
@@ -2735,11 +2797,17 @@ fn apply_server_message(game: &mut Game, msg: ServerMessage) {
27352797 }
27362798 ServerMessage :: Pong {
27372799 client_time_ms,
2738- server_time_ms : _ ,
2800+ server_time_ms,
27392801 } => {
27402802 if let Some ( sent_ms) = game. pending_pings . remove ( & client_time_ms) {
2741- let rtt = ( now_ms ( ) - sent_ms) as f32 ;
2742- game. rtt_ms = Some ( rtt) ;
2803+ let now = now_ms ( ) ;
2804+ let rtt = ( now - sent_ms) as f32 ;
2805+ game. rtt_ms = Some ( match game. rtt_ms {
2806+ Some ( prev) => prev * 0.8 + rtt * 0.2 ,
2807+ None => rtt,
2808+ } ) ;
2809+ let estimated_offset = server_time_ms as f64 + ( rtt as f64 * 0.5 ) - now;
2810+ ingest_server_clock_sample ( game, estimated_offset, CLOCK_PONG_ALPHA ) ;
27432811 }
27442812 }
27452813 }
0 commit comments