diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index 0b94b83..fad12c8 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -1,3 +1,4 @@ +use std::collections::VecDeque; use std::net::SocketAddr; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; @@ -1184,6 +1185,49 @@ async fn client_loop( intended_use: zone.intended_use, }; } + + if let Some(entry) = connection.replay_entries.pop_front() { + // only care about Ipc packets + let filename = entry.file_name().unwrap().to_str().unwrap(); + if !filename.contains("Ipc") { + continue; + } + + // only care about packets from the server + if !filename.contains("(to client)") { + continue; + } + + let path = entry.to_str().unwrap(); + + tracing::info!("- Replaying {path}"); + + let source_actor_bytes = std::fs::read(format!("{path}/source_actor.bin")).unwrap(); + let target_actor_bytes = std::fs::read(format!("{path}/target_actor.bin")).unwrap(); + let source_actor = u32::from_le_bytes(source_actor_bytes[0..4].try_into().unwrap()); + let target_actor = u32::from_le_bytes(target_actor_bytes[0..4].try_into().unwrap()); + + let ipc_header_bytes = std::fs::read(format!("{path}/ipc_header.bin")).unwrap(); + let opcode = u16::from_le_bytes(ipc_header_bytes[2..4].try_into().unwrap()); + + let unk = std::fs::read(format!("{path}/data.bin")).unwrap(); + + connection.send_segment(PacketSegment { + source_actor, + target_actor, + segment_type: SegmentType::Ipc, + data: SegmentData::Ipc { + data: ServerZoneIpcSegment { + unk1: 20, + unk2: 0, + op_code: ServerZoneIpcType::Unknown(opcode), + option: 0, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::Unknown { unk }, + } + } + }).await; + } } }, Err(_) => { @@ -1303,6 +1347,7 @@ async fn main() { gracefully_logged_out: false, weather_id: 0, obsfucation_data: ObsfucationData::default(), + replay_entries: VecDeque::default(), }); } Some((mut socket, _)) = handle_rcon(&rcon_listener) => { diff --git a/src/world/chat_handler.rs b/src/world/chat_handler.rs index 29e7b66..bf374cf 100644 --- a/src/world/chat_handler.rs +++ b/src/world/chat_handler.rs @@ -111,6 +111,12 @@ impl ChatHandler { } true } + "!replay" => { + let (_, path) = chat_message.message.split_once(' ').unwrap(); + connection.replay_packets(path).await; + + true + } _ => false, } } diff --git a/src/world/connection.rs b/src/world/connection.rs index 9da51d7..0bf5db7 100644 --- a/src/world/connection.rs +++ b/src/world/connection.rs @@ -1,6 +1,7 @@ use std::{ - collections::HashMap, + collections::{HashMap, VecDeque}, net::SocketAddr, + path::PathBuf, sync::{Arc, Mutex}, time::Instant, }; @@ -134,6 +135,8 @@ pub struct ZoneConnection { pub weather_id: u16, pub obsfucation_data: ObsfucationData, + + pub replay_entries: VecDeque, } impl ZoneConnection { @@ -256,8 +259,11 @@ impl ZoneConnection { } pub async fn spawn_actor(&mut self, mut actor: Actor, mut spawn: NpcSpawn) { - // There is no reason for us to spawn our own player again. It's probably a bug!' - assert!(actor.id.0 != self.player_data.actor_id); + // skip during replay + if self.replay_entries.is_empty() { + // There is no reason for us to spawn our own player again. It's probably a bug!' + assert!(actor.id.0 != self.player_data.actor_id); + } actor.spawn_index = self.get_free_spawn_index() as u32; spawn.common.spawn_index = actor.spawn_index as u8; @@ -1484,4 +1490,41 @@ impl ZoneConnection { .await; } } + + pub async fn replay_packets(&mut self, path: &str) { + tracing::info!("Beginning replay from {path}..."); + + let mut entries = std::fs::read_dir(path) + .unwrap() + .map(|res| res.map(|e| e.path())) + .collect::, std::io::Error>>() + .unwrap(); + + entries.sort_by(|a, b| { + let a_seq = a + .file_name() + .unwrap() + .to_str() + .unwrap() + .split_once('-') + .unwrap() + .0 + .parse::() + .unwrap(); + let b_seq = b + .file_name() + .unwrap() + .to_str() + .unwrap() + .split_once('-') + .unwrap() + .0 + .parse::() + .unwrap(); + + a_seq.cmp(&b_seq) + }); + + self.replay_entries = entries.into(); + } }