diff --git a/src/netplay.rs b/src/netplay.rs index 32c1824..b9afdbb 100644 --- a/src/netplay.rs +++ b/src/netplay.rs @@ -433,14 +433,19 @@ fn handle_desync(device: &mut device::Device) { return; } - // Host: create an in-memory savestate + // Host: create an in-memory savestate (includes device, saves, rdp_state, ra_state) let state_data = create_savestate_in_memory(device); let state_frame = netplay.session.current_frame(); + // Prepend the frame number as 4-byte big-endian i32 to the state data + // so the client knows which frame to reset to + let mut message_data = state_frame.to_be_bytes().to_vec(); + message_data.extend(state_data.clone()); + // Send resync_state message to all peers let message = NetplayMessage { name: "resync_state".to_string(), - data: state_data.clone(), + data: message_data, }; send_message(netplay, message); @@ -460,7 +465,19 @@ fn handle_desync(device: &mut device::Device) { /// Creates an in-memory savestate: serializes the full Device via postcard + compress. /// Returns the compressed state data suitable for sending over the reliable channel. +/// +/// The compressed buffer contains four sections: "device", "saves", "rdp_state", and +/// "ra_state" — full parity with disk savestates. The host's current frame is prepended +/// to the data as a 4-byte big-endian i32 so the client knows which frame to reset to. fn create_savestate_in_memory(device: &mut device::Device) -> Vec { + // Save RDP state + let mut rdp_state: Vec = vec![0; ui::video::state_size()]; + ui::video::save_state(rdp_state.as_mut_ptr()); + + // Save RA state + let mut ra_state: Vec = vec![0; retroachievements::state_size()]; + retroachievements::save_state(ra_state.as_mut_ptr(), ra_state.len()); + // Serialize the device state directly let device_data = postcard::to_stdvec(device) .expect("failed to serialize device state for resync"); @@ -469,10 +486,12 @@ fn create_savestate_in_memory(device: &mut device::Device) -> Vec { let saves_data = postcard::to_stdvec(&device.ui.storage.saves) .expect("failed to serialize saves for resync"); - // Compress both into a single buffer + // Compress all four sections into a single buffer ui::storage::compress_file(&[ (&device_data, "device"), (&saves_data, "saves"), + (&rdp_state, "rdp_state"), + (&ra_state, "ra_state"), ]) .expect("failed to compress savestate for resync") } @@ -797,34 +816,48 @@ pub fn process_resync_load(device: &mut device::Device) -> bool { return false; } - // Get the stored state data - let state_data = match netplay.messages.remove("resync_state_data") { + // Get the stored state data (includes 4-byte frame prefix + compressed state) + let raw_data = match netplay.messages.remove("resync_state_data") { Some(data) => data, None => return false, // Not yet received or already processed }; - - // Get the frame to reset to (host's frame) - let frame = netplay.session.current_frame(); drop(netplay); eprintln!("processing resync state load"); - // Decompress and load the state + // Extract the frame number (first 4 bytes, big-endian i32) and state data + if raw_data.len() < 4 { + eprintln!("resync state data too short, ignoring"); + // Still need to clean up and send ack + let netplay = device.netplay.as_mut().unwrap(); + netplay.resync = ResyncState::Idle; + return true; + } + let frame = i32::from_be_bytes(raw_data[0..4].try_into().unwrap()); + let state_data = &raw_data[4..]; + + // Decompress and load all state sections (device, saves, rdp_state, ra_state) let load_result = (|| { - let device_data = ui::storage::decompress_file(&state_data, "device").ok()?; - let saves_data = ui::storage::decompress_file(&state_data, "saves").ok()?; + let device_data = ui::storage::decompress_file(state_data, "device").ok()?; + let saves_data = ui::storage::decompress_file(state_data, "saves").ok()?; + let rdp_state = ui::storage::decompress_file(state_data, "rdp_state").ok()?; + let ra_state = ui::storage::decompress_file(state_data, "ra_state").ok()?; let state = postcard::from_bytes::>(&device_data).ok()?; let saves = postcard::from_bytes(&saves_data).ok()?; - Some((state, saves)) + Some((state, saves, rdp_state, ra_state)) })(); - if let Some((mut state, mut saves)) = load_result { - // Load the state into the device, similar to savestates::load_savestate + if let Some((mut state, mut saves, rdp_state, ra_state)) = load_result { + // Load the state into the device with full parity to disk savestates if device.rdram.size == state.rdram.size { + // Swap saves std::mem::swap(&mut device.ui.storage.saves, &mut saves); + + // Swap all device components (same as savestates::load_savestate) std::mem::swap(&mut device.cpu, &mut state.cpu); std::mem::swap(&mut device.pif, &mut state.pif); std::mem::swap(&mut device.cart, &mut state.cart); + std::mem::swap(&mut device.cart.rom, &mut state.cart.rom); // ROM not in savestate std::mem::swap(&mut device.memory, &mut state.memory); std::mem::swap(&mut device.rsp, &mut state.rsp); std::mem::swap(&mut device.rdp, &mut state.rdp); @@ -836,7 +869,17 @@ pub fn process_resync_load(device: &mut device::Device) -> bool { std::mem::swap(&mut device.ai, &mut state.ai); std::mem::swap(&mut device.si, &mut state.si); std::mem::swap(&mut device.ri, &mut state.ri); + std::mem::swap(&mut device.vru, &mut state.vru); + std::mem::swap(&mut device.cheats, &mut state.cheats); + std::mem::swap(&mut device.transferpaks, &mut state.transferpaks); + for (i, item) in device.transferpaks.iter_mut().enumerate() { + std::mem::swap( + &mut item.cart.rom, + &mut state.transferpaks[i].cart.rom, + ); + } + // Reinitialize memory mappings and instruction caches device::memory::init(device); device::vi::set_expected_refresh_rate(device); device::cpu::map_instructions(device); @@ -845,7 +888,21 @@ pub fn process_resync_load(device: &mut device::Device) -> bool { device::cop2::map_instructions(device); device::rsp_cpu::map_instructions(device); - eprintln!("resync state loaded successfully"); + // Restore RDP state via video subsystem + ui::video::load_state(device, rdp_state.as_ptr()); + + // Restore RA state + if !ra_state.is_empty() { + retroachievements::load_state(ra_state.as_ptr(), ra_state.len()); + } else { + retroachievements::load_state(std::ptr::null(), 0); + } + + // Reconnect PIF channels and update audio + device::pif::connect_pif_channels(device); + ui::audio::update_freq(device); + + eprintln!("resync state loaded successfully (frame={})", frame); } else { eprintln!("rdram size mismatch in resync state, ignoring"); } @@ -853,7 +910,7 @@ pub fn process_resync_load(device: &mut device::Device) -> bool { eprintln!("failed to decompress/deserialize resync state"); } - // Reset the GGRS frame counter + // Reset the GGRS frame counter to the host's frame reset_frame(&mut device.netplay.as_mut().unwrap().session, frame); // Send resync_ready ack to host @@ -1165,4 +1222,55 @@ mod tests { acked[1] = true; assert!(acked.iter().all(|&a| a)); } + + /// Test: frame number encoding/decoding in resync_state message. + /// The host prepends a 4-byte big-endian i32 frame number to the state data. + /// The client extracts it to know which frame to reset to. + #[test] + fn test_resync_frame_encoding() { + let frame: i32 = 42; + let state_data = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02]; + + // Encode: prepend frame as 4-byte big-endian + let mut message_data = frame.to_be_bytes().to_vec(); + message_data.extend(state_data.clone()); + + // Verify message can be serialized/deserialized as NetplayMessage + let msg = NetplayMessage { + name: "resync_state".to_string(), + data: message_data, + }; + let encoded = postcard::to_stdvec(&msg).unwrap(); + let decoded: NetplayMessage = postcard::from_bytes(&encoded).unwrap(); + assert_eq!(decoded.name, "resync_state"); + assert_eq!(decoded.data.len(), 4 + state_data.len()); + + // Decode: extract frame from first 4 bytes + let decoded_frame = i32::from_be_bytes(decoded.data[0..4].try_into().unwrap()); + assert_eq!(decoded_frame, frame); + + // Verify the remaining data is the state + let decoded_state = &decoded.data[4..]; + assert_eq!(decoded_state, state_data.as_slice()); + } + + /// Test: frame number 0 encoding/decoding. + #[test] + fn test_resync_frame_encoding_zero() { + let frame: i32 = 0; + let message_data = frame.to_be_bytes().to_vec(); + + let decoded_frame = i32::from_be_bytes(message_data[0..4].try_into().unwrap()); + assert_eq!(decoded_frame, 0); + } + + /// Test: negative frame number encoding/decoding (edge case). + #[test] + fn test_resync_frame_encoding_negative() { + let frame: i32 = -1; + let message_data = frame.to_be_bytes().to_vec(); + + let decoded_frame = i32::from_be_bytes(message_data[0..4].try_into().unwrap()); + assert_eq!(decoded_frame, -1); + } }