diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index f2c3661..418091f 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::net::SocketAddr; use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; use kawari::RECEIVE_BUFFER_SIZE; use kawari::common::Position; @@ -137,295 +138,168 @@ async fn client_loop( loop { tokio::select! { biased; // client data should always be prioritized - Ok(n) = connection.socket.read(&mut buf) => { - if n > 0 { - let (segments, connection_type) = connection.parse_packet(&buf[..n]).await; - for segment in &segments { - match &segment.data { - SegmentData::Setup { ticket } => { - // for some reason they send a string representation - let actor_id = ticket.parse::().unwrap(); - - // initialize player data if it doesn't exist' - if connection.player_data.actor_id == 0 { - connection.player_data = database.find_player_data(actor_id); - } - - if connection_type == ConnectionType::Zone { - // collect actor data - connection.initialize(actor_id).await; - - connection.exit_position = Some(connection.player_data.position); - connection.exit_rotation = Some(connection.player_data.rotation); - - let mut client_handle = client_handle.clone(); - client_handle.actor_id = actor_id; - client_handle.common = connection.get_player_common_spawn(connection.exit_position, connection.exit_rotation); - - // tell the server we exist, now that we confirmed we are a legitimate connection - connection.handle.send(ToServer::NewClient(client_handle)).await; - } else if connection_type == ConnectionType::Chat { - // We have send THEM a keep alive - connection.send_chat_segment(PacketSegment { - segment_type: SegmentType::KeepAliveRequest, - data: SegmentData::KeepAliveRequest { - id: 0xE0037603u32, - timestamp: timestamp_secs(), - }, - ..Default::default() - }) - .await; - - // initialize connection - connection.send_chat_segment(PacketSegment { - segment_type: SegmentType::Initialize, - data: SegmentData::Initialize { - player_id: connection.player_data.actor_id, - timestamp: timestamp_secs(), - }, - ..Default::default() - }) - .await; - - // we need the actor id at this point! - assert!(connection.player_data.actor_id != 0); - - // send login reply - { - let ipc = ServerChatIpcSegment { - op_code: ServerChatIpcType::LoginReply, - timestamp: timestamp_secs(), - data: ServerChatIpcData::LoginReply { - timestamp: 0, - sid: 0, - }, - ..Default::default() - }; - - connection.send_chat_segment(PacketSegment { - source_actor: connection.player_data.actor_id, - target_actor: connection.player_data.actor_id, - segment_type: SegmentType::Ipc, - data: SegmentData::Ipc { data: ipc }, - }) - .await; - } - } + n = connection.socket.read(&mut buf) => { + match n { + Ok(n) => { + // if the last response was over >5 seconds, the client is probably gone + if n == 0 { + let now = Instant::now(); + if now.duration_since(connection.last_keep_alive) > Duration::from_secs(5) { + tracing::info!("Connection was killed because of timeout"); + break; } - SegmentData::Ipc { data } => { - match &data.data { - ClientZoneIpcData::InitRequest { .. } => { - tracing::info!( - "Client is now requesting zone information. Sending!" - ); + } - // IPC Init(?) - { - let ipc = ServerZoneIpcSegment { - op_code: ServerZoneIpcType::InitResponse, - timestamp: timestamp_secs(), - data: ServerZoneIpcData::InitResponse { - unk1: 0, - character_id: connection.player_data.actor_id, - unk2: 0, - }, - ..Default::default() - }; + if n > 0 { + connection.last_keep_alive = Instant::now(); - connection - .send_segment(PacketSegment { - source_actor: connection.player_data.actor_id, - target_actor: connection.player_data.actor_id, - segment_type: SegmentType::Ipc, - data: SegmentData::Ipc { data: ipc }, - }) - .await; + let (segments, connection_type) = connection.parse_packet(&buf[..n]).await; + for segment in &segments { + match &segment.data { + SegmentData::Setup { ticket } => { + // for some reason they send a string representation + let actor_id = ticket.parse::().unwrap(); + + // initialize player data if it doesn't exist' + if connection.player_data.actor_id == 0 { + connection.player_data = database.find_player_data(actor_id); } - let chara_details = - database.find_chara_make(connection.player_data.content_id); + if connection_type == ConnectionType::Zone { + // collect actor data + connection.initialize(actor_id).await; - // Send inventory - connection.send_inventory(false).await; + connection.exit_position = Some(connection.player_data.position); + connection.exit_rotation = Some(connection.player_data.rotation); - // set chara gear param - connection - .actor_control_self(ActorControlSelf { - category: ActorControlCategory::SetCharaGearParamUI { - unk1: 1, - unk2: 1, + let mut client_handle = client_handle.clone(); + client_handle.actor_id = actor_id; + client_handle.common = connection.get_player_common_spawn(connection.exit_position, connection.exit_rotation); + + // tell the server we exist, now that we confirmed we are a legitimate connection + connection.handle.send(ToServer::NewClient(client_handle)).await; + } else if connection_type == ConnectionType::Chat { + // We have send THEM a keep alive + connection.send_chat_segment(PacketSegment { + segment_type: SegmentType::KeepAliveRequest, + data: SegmentData::KeepAliveRequest { + id: 0xE0037603u32, + timestamp: timestamp_secs(), }, + ..Default::default() }) .await; - // Stats - connection.send_stats(&chara_details).await; - - // Player Setup - { - let ipc = ServerZoneIpcSegment { - op_code: ServerZoneIpcType::PlayerStatus, - timestamp: timestamp_secs(), - data: ServerZoneIpcData::PlayerStatus(PlayerStatus { - content_id: connection.player_data.content_id, - exp: [10000; 32], - levels: [100; 32], - name: chara_details.name, - char_id: connection.player_data.actor_id, - race: chara_details.chara_make.customize.race, - gender: chara_details.chara_make.customize.gender, - tribe: chara_details.chara_make.customize.subrace, - city_state: chara_details.city_state, - nameday_month: chara_details.chara_make.birth_month - as u8, - nameday_day: chara_details.chara_make.birth_day as u8, - deity: chara_details.chara_make.guardian as u8, - ..Default::default() - }), - ..Default::default() - }; - - connection - .send_segment(PacketSegment { - source_actor: connection.player_data.actor_id, - target_actor: connection.player_data.actor_id, - segment_type: SegmentType::Ipc, - data: SegmentData::Ipc { data: ipc }, - }) - .await; - } - - let zone_id = connection.player_data.zone_id; - connection.change_zone(zone_id).await; - - let lua = lua.lock().unwrap(); - lua.scope(|scope| { - let connection_data = - scope.create_userdata_ref_mut(&mut lua_player).unwrap(); - - let func: Function = lua.globals().get("onBeginLogin").unwrap(); - - func.call::<()>(connection_data).unwrap(); - - Ok(()) - }) - .unwrap(); - } - ClientZoneIpcData::FinishLoading { .. } => { - // tell the server we loaded into the zone, so it can start sending us acors - connection.handle.send(ToServer::ZoneLoaded(connection.id, connection.zone.as_ref().unwrap().id)).await; - - let common = connection.get_player_common_spawn(connection.exit_position, connection.exit_rotation); - - let chara_details = database.find_chara_make(connection.player_data.content_id); - - connection.send_inventory(false).await; - connection.send_stats(&chara_details).await; - - // send player spawn - { - let ipc = ServerZoneIpcSegment { - op_code: ServerZoneIpcType::PlayerSpawn, + // initialize connection + connection.send_chat_segment(PacketSegment { + segment_type: SegmentType::Initialize, + data: SegmentData::Initialize { + player_id: connection.player_data.actor_id, timestamp: timestamp_secs(), - data: ServerZoneIpcData::PlayerSpawn(PlayerSpawn { - account_id: connection.player_data.account_id, - content_id: connection.player_data.content_id, - current_world_id: config.world.world_id, - home_world_id: config.world.world_id, - gm_rank: GameMasterRank::Debug, - online_status: OnlineStatus::GameMasterBlue, - common: common.clone(), - ..Default::default() - }), - ..Default::default() - }; - - connection - .send_segment(PacketSegment { - source_actor: connection.player_data.actor_id, - target_actor: connection.player_data.actor_id, - segment_type: SegmentType::Ipc, - data: SegmentData::Ipc { data: ipc }, - }) - .await; - } - - // fade in? - { - let ipc = ServerZoneIpcSegment { - op_code: ServerZoneIpcType::PrepareZoning, - timestamp: timestamp_secs(), - data: ServerZoneIpcData::PrepareZoning { - unk: [0, 0, 0, 0], }, ..Default::default() - }; + }) + .await; - connection - .send_segment(PacketSegment { + // we need the actor id at this point! + assert!(connection.player_data.actor_id != 0); + + // send login reply + { + let ipc = ServerChatIpcSegment { + op_code: ServerChatIpcType::LoginReply, + timestamp: timestamp_secs(), + data: ServerChatIpcData::LoginReply { + timestamp: 0, + sid: 0, + }, + ..Default::default() + }; + + connection.send_chat_segment(PacketSegment { source_actor: connection.player_data.actor_id, target_actor: connection.player_data.actor_id, segment_type: SegmentType::Ipc, data: SegmentData::Ipc { data: ipc }, }) .await; + } } + } + SegmentData::Ipc { data } => { + match &data.data { + ClientZoneIpcData::InitRequest { .. } => { + tracing::info!( + "Client is now requesting zone information. Sending!" + ); - // wipe any exit position so it isn't accidentally reused - connection.exit_position = None; - connection.exit_rotation = None; - } - ClientZoneIpcData::ClientTrigger(trigger) => { - // store the query for scripts - if let ClientTriggerCommand::TeleportQuery { aetheryte_id } = trigger.trigger { - connection.player_data.teleport_query.aetheryte_id = aetheryte_id as u16; - } + // IPC Init(?) + { + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::InitResponse, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::InitResponse { + unk1: 0, + character_id: connection.player_data.actor_id, + unk2: 0, + }, + ..Default::default() + }; - // inform the server of our trigger, it will handle sending it to other clients - connection.handle.send(ToServer::ClientTrigger(connection.id, connection.player_data.actor_id, trigger.clone())).await; - } - ClientZoneIpcData::Unk2 { .. } => { - tracing::info!("Recieved Unk2!"); - } - ClientZoneIpcData::Unk3 { .. } => { - tracing::info!("Recieved Unk3!"); - } - ClientZoneIpcData::Unk4 { .. } => { - tracing::info!("Recieved Unk4!"); - } - ClientZoneIpcData::SetSearchInfoHandler { .. } => { - tracing::info!("Recieved SetSearchInfoHandler!"); - } - ClientZoneIpcData::Unk5 { .. } => { - tracing::info!("Recieved Unk5!"); - } - ClientZoneIpcData::SocialListRequest(request) => { - tracing::info!("Recieved social list request!"); + connection + .send_segment(PacketSegment { + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, + segment_type: SegmentType::Ipc, + data: SegmentData::Ipc { data: ipc }, + }) + .await; + } - match &request.request_type { - SocialListRequestType::Party => { - let ipc = ServerZoneIpcSegment { - op_code: ServerZoneIpcType::SocialList, - timestamp: timestamp_secs(), - data: ServerZoneIpcData::SocialList(SocialList { - request_type: request.request_type, - sequence: request.count, - entries: vec![PlayerEntry { - // TODO: fill with actual player data, it also shows up wrong in game + let chara_details = + database.find_chara_make(connection.player_data.content_id); + + // Send inventory + connection.send_inventory(false).await; + + // set chara gear param + connection + .actor_control_self(ActorControlSelf { + category: ActorControlCategory::SetCharaGearParamUI { + unk1: 1, + unk2: 1, + }, + }) + .await; + + // Stats + connection.send_stats(&chara_details).await; + + // Player Setup + { + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::PlayerStatus, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::PlayerStatus(PlayerStatus { content_id: connection.player_data.content_id, - zone_id: connection.zone.as_ref().unwrap().id, - zone_id1: 0x0100, - class_job: 36, - level: 100, - one: 1, - name: "INVALID".to_string(), + exp: [10000; 32], + levels: [100; 32], + name: chara_details.name, + char_id: connection.player_data.actor_id, + race: chara_details.chara_make.customize.race, + gender: chara_details.chara_make.customize.gender, + tribe: chara_details.chara_make.customize.subrace, + city_state: chara_details.city_state, + nameday_month: chara_details.chara_make.birth_month + as u8, + nameday_day: chara_details.chara_make.birth_day as u8, + deity: chara_details.chara_make.guardian as u8, ..Default::default() - }], - }), - ..Default::default() - }; + }), + ..Default::default() + }; - connection + connection .send_segment(PacketSegment { source_actor: connection.player_data.actor_id, target_actor: connection.player_data.actor_id, @@ -433,431 +307,579 @@ async fn client_loop( data: SegmentData::Ipc { data: ipc }, }) .await; - } - SocialListRequestType::Friends => { - let ipc = ServerZoneIpcSegment { - op_code: ServerZoneIpcType::SocialList, - timestamp: timestamp_secs(), - data: ServerZoneIpcData::SocialList(SocialList { - request_type: request.request_type, - sequence: request.count, - entries: Default::default(), - }), - ..Default::default() - }; + } - connection - .send_segment(PacketSegment { - source_actor: connection.player_data.actor_id, - target_actor: connection.player_data.actor_id, - segment_type: SegmentType::Ipc, - data: SegmentData::Ipc { data: ipc }, - }) - .await; - } - } - } - ClientZoneIpcData::UpdatePositionHandler { position, rotation } => { - connection.player_data.rotation = *rotation; - connection.player_data.position = *position; - - connection.handle.send(ToServer::ActorMoved(connection.id, connection.player_data.actor_id, *position, *rotation)).await; - } - ClientZoneIpcData::LogOut { .. } => { - tracing::info!("Recieved log out from client!"); - - connection.begin_log_out().await; - } - ClientZoneIpcData::Disconnected { .. } => { - tracing::info!("Client disconnected!"); - - connection.handle.send(ToServer::Disconnected(connection.id)).await; - } - ClientZoneIpcData::ChatMessage(chat_message) => { - connection.handle.send(ToServer::Message(connection.id, chat_message.message.clone())).await; - - let mut handled = false; - { - let parts: Vec<&str> = chat_message.message.split(' ').collect(); - let command_name = &parts[0][1..]; - - let lua = lua.lock().unwrap(); - let state = lua.app_data_ref::().unwrap(); - - if let Some(command_script) = - state.command_scripts.get(command_name) - { - handled = true; + let zone_id = connection.player_data.zone_id; + connection.change_zone(zone_id).await; + let lua = lua.lock().unwrap(); lua.scope(|scope| { - let connection_data = scope - .create_userdata_ref_mut(&mut lua_player) - .unwrap(); + let connection_data = + scope.create_userdata_ref_mut(&mut lua_player).unwrap(); - let config = get_config(); + let func: Function = lua.globals().get("onBeginLogin").unwrap(); - let file_name = format!( - "{}/{}", - &config.world.scripts_location, command_script - ); - lua.load( - std::fs::read(&file_name) - .expect("Failed to locate scripts directory!"), - ) - .set_name("@".to_string() + &file_name) - .exec() - .unwrap(); - - let func: Function = - lua.globals().get("onCommand").unwrap(); - - tracing::info!("{}", &chat_message.message[command_name.len() + 2..]); - - func.call::<()>((&chat_message.message[command_name.len() + 2..], connection_data)) - .unwrap(); + func.call::<()>(connection_data).unwrap(); Ok(()) }) .unwrap(); } - } + ClientZoneIpcData::FinishLoading { .. } => { + // tell the server we loaded into the zone, so it can start sending us acors + connection.handle.send(ToServer::ZoneLoaded(connection.id, connection.zone.as_ref().unwrap().id)).await; - if !handled { - ChatHandler::handle_chat_message( - &mut connection, - &mut lua_player, - chat_message, - ) - .await; - } - } - ClientZoneIpcData::GMCommand { command, arg0, arg1, .. } => { - tracing::info!("Got a game master command!"); + let common = connection.get_player_common_spawn(connection.exit_position, connection.exit_rotation); - match &command { - GameMasterCommandType::SetLevel => { - connection.player_data.level = *arg0 as u8; - connection.update_class_info().await; + let chara_details = database.find_chara_make(connection.player_data.content_id); + + connection.send_inventory(false).await; + connection.send_stats(&chara_details).await; + + // send player spawn + { + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::PlayerSpawn, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::PlayerSpawn(PlayerSpawn { + account_id: connection.player_data.account_id, + content_id: connection.player_data.content_id, + current_world_id: config.world.world_id, + home_world_id: config.world.world_id, + gm_rank: GameMasterRank::Debug, + online_status: OnlineStatus::GameMasterBlue, + common: common.clone(), + ..Default::default() + }), + ..Default::default() + }; + + connection + .send_segment(PacketSegment { + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, + segment_type: SegmentType::Ipc, + data: SegmentData::Ipc { data: ipc }, + }) + .await; + } + + // fade in? + { + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::PrepareZoning, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::PrepareZoning { + unk: [0, 0, 0, 0], + }, + ..Default::default() + }; + + connection + .send_segment(PacketSegment { + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, + segment_type: SegmentType::Ipc, + data: SegmentData::Ipc { data: ipc }, + }) + .await; + } + + // wipe any exit position so it isn't accidentally reused + connection.exit_position = None; + connection.exit_rotation = None; } - GameMasterCommandType::ChangeWeather => { - connection.change_weather(*arg0 as u16).await + ClientZoneIpcData::ClientTrigger(trigger) => { + // store the query for scripts + if let ClientTriggerCommand::TeleportQuery { aetheryte_id } = trigger.trigger { + connection.player_data.teleport_query.aetheryte_id = aetheryte_id as u16; + } + + // inform the server of our trigger, it will handle sending it to other clients + connection.handle.send(ToServer::ClientTrigger(connection.id, connection.player_data.actor_id, trigger.clone())).await; } - GameMasterCommandType::ChangeTerritory => { - connection.change_zone(*arg0 as u16).await + ClientZoneIpcData::Unk2 { .. } => { + tracing::info!("Recieved Unk2!"); } - GameMasterCommandType::ToggleInvisibility => { - connection - .actor_control_self(ActorControlSelf { - category: + ClientZoneIpcData::Unk3 { .. } => { + tracing::info!("Recieved Unk3!"); + } + ClientZoneIpcData::Unk4 { .. } => { + tracing::info!("Recieved Unk4!"); + } + ClientZoneIpcData::SetSearchInfoHandler { .. } => { + tracing::info!("Recieved SetSearchInfoHandler!"); + } + ClientZoneIpcData::Unk5 { .. } => { + tracing::info!("Recieved Unk5!"); + } + ClientZoneIpcData::SocialListRequest(request) => { + tracing::info!("Recieved social list request!"); + + match &request.request_type { + SocialListRequestType::Party => { + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::SocialList, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::SocialList(SocialList { + request_type: request.request_type, + sequence: request.count, + entries: vec![PlayerEntry { + // TODO: fill with actual player data, it also shows up wrong in game + content_id: connection.player_data.content_id, + zone_id: connection.zone.as_ref().unwrap().id, + zone_id1: 0x0100, + class_job: 36, + level: 100, + one: 1, + name: "INVALID".to_string(), + ..Default::default() + }], + }), + ..Default::default() + }; + + connection + .send_segment(PacketSegment { + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, + segment_type: SegmentType::Ipc, + data: SegmentData::Ipc { data: ipc }, + }) + .await; + } + SocialListRequestType::Friends => { + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::SocialList, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::SocialList(SocialList { + request_type: request.request_type, + sequence: request.count, + entries: Default::default(), + }), + ..Default::default() + }; + + connection + .send_segment(PacketSegment { + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, + segment_type: SegmentType::Ipc, + data: SegmentData::Ipc { data: ipc }, + }) + .await; + } + } + } + ClientZoneIpcData::UpdatePositionHandler { position, rotation } => { + connection.player_data.rotation = *rotation; + connection.player_data.position = *position; + + connection.handle.send(ToServer::ActorMoved(connection.id, connection.player_data.actor_id, *position, *rotation)).await; + } + ClientZoneIpcData::LogOut { .. } => { + tracing::info!("Recieved log out from client!"); + + connection.begin_log_out().await; + } + ClientZoneIpcData::Disconnected { .. } => { + tracing::info!("Client disconnected!"); + + connection.handle.send(ToServer::Disconnected(connection.id)).await; + + break; + } + ClientZoneIpcData::ChatMessage(chat_message) => { + connection.handle.send(ToServer::Message(connection.id, chat_message.message.clone())).await; + + let mut handled = false; + { + let parts: Vec<&str> = chat_message.message.split(' ').collect(); + let command_name = &parts[0][1..]; + + let lua = lua.lock().unwrap(); + let state = lua.app_data_ref::().unwrap(); + + if let Some(command_script) = + state.command_scripts.get(command_name) + { + handled = true; + + lua.scope(|scope| { + let connection_data = scope + .create_userdata_ref_mut(&mut lua_player) + .unwrap(); + + let config = get_config(); + + let file_name = format!( + "{}/{}", + &config.world.scripts_location, command_script + ); + lua.load( + std::fs::read(&file_name) + .expect("Failed to locate scripts directory!"), + ) + .set_name("@".to_string() + &file_name) + .exec() + .unwrap(); + + let func: Function = + lua.globals().get("onCommand").unwrap(); + + tracing::info!("{}", &chat_message.message[command_name.len() + 2..]); + + func.call::<()>((&chat_message.message[command_name.len() + 2..], connection_data)) + .unwrap(); + + Ok(()) + }) + .unwrap(); + } + } + + if !handled { + ChatHandler::handle_chat_message( + &mut connection, + &mut lua_player, + chat_message, + ) + .await; + } + } + ClientZoneIpcData::GMCommand { command, arg0, arg1, .. } => { + tracing::info!("Got a game master command!"); + + match &command { + GameMasterCommandType::SetLevel => { + connection.player_data.level = *arg0 as u8; + connection.update_class_info().await; + } + GameMasterCommandType::ChangeWeather => { + connection.change_weather(*arg0 as u16).await + } + GameMasterCommandType::ChangeTerritory => { + connection.change_zone(*arg0 as u16).await + } + GameMasterCommandType::ToggleInvisibility => { + connection + .actor_control_self(ActorControlSelf { + category: ActorControlCategory::ToggleInvisibility { invisible: true, }, - }) - .await - } - GameMasterCommandType::ToggleWireframe => connection - .actor_control_self(ActorControlSelf { - category: - ActorControlCategory::ToggleWireframeRendering(), - }) - .await, - GameMasterCommandType::GiveItem => { - connection.player_data.inventory.add_in_next_free_slot(Item { id: *arg0, quantity: 1 }); - connection.send_inventory(false).await; - } - GameMasterCommandType::Aetheryte => { - let on = *arg0 == 0; - let id = *arg1; - - // id == 0 means "all" - if id == 0 { - for i in 1..239 { - connection.actor_control_self(ActorControlSelf { - category: ActorControlCategory::LearnTeleport { id: i, unlocked: on } }).await; + }) + .await + } + GameMasterCommandType::ToggleWireframe => connection + .actor_control_self(ActorControlSelf { + category: + ActorControlCategory::ToggleWireframeRendering(), + }) + .await, + GameMasterCommandType::GiveItem => { + connection.player_data.inventory.add_in_next_free_slot(Item { id: *arg0, quantity: 1 }); + connection.send_inventory(false).await; + } + GameMasterCommandType::Aetheryte => { + let on = *arg0 == 0; + let id = *arg1; + + // id == 0 means "all" + if id == 0 { + for i in 1..239 { + connection.actor_control_self(ActorControlSelf { + category: ActorControlCategory::LearnTeleport { id: i, unlocked: on } }).await; + } + } else { + connection.actor_control_self(ActorControlSelf { + category: ActorControlCategory::LearnTeleport { id, unlocked: on } }).await; + } } - } else { - connection.actor_control_self(ActorControlSelf { - category: ActorControlCategory::LearnTeleport { id, unlocked: on } }).await; } } - } - } - ClientZoneIpcData::ZoneJump { - exit_box, - position, - .. - } => { - tracing::info!( - "Character entered {exit_box} with a position of {position:#?}!" - ); + ClientZoneIpcData::ZoneJump { + exit_box, + position, + .. + } => { + tracing::info!( + "Character entered {exit_box} with a position of {position:#?}!" + ); - // find the exit box id - let new_territory; - { - let (_, exit_box) = connection - .zone - .as_ref() - .unwrap() - .find_exit_box(*exit_box) - .unwrap(); - - // find the pop range on the other side - let mut game_data = game_data.lock().unwrap(); - let new_zone = Zone::load(&mut game_data.game_data, exit_box.territory_type); - let (destination_object, _) = new_zone - .find_pop_range(exit_box.destination_instance_id) - .unwrap(); - - // set the exit position - connection.exit_position = Some(Position { - x: destination_object.transform.translation[0], - y: destination_object.transform.translation[1], - z: destination_object.transform.translation[2], - }); - new_territory = exit_box.territory_type; - } - - connection.change_zone(new_territory).await; - } - ClientZoneIpcData::ActionRequest(request) => { - let mut effects_builder = None; - - // run action script - { - let lua = lua.lock().unwrap(); - let state = lua.app_data_ref::().unwrap(); - - let key = request.action_key; - if let Some(action_script) = - state.action_scripts.get(&key) - { - lua.scope(|scope| { - let connection_data = scope - .create_userdata_ref_mut(&mut lua_player) - .unwrap(); - - let config = get_config(); - - let file_name = format!( - "{}/{}", - &config.world.scripts_location, action_script - ); - lua.load( - std::fs::read(&file_name) - .expect("Failed to locate scripts directory!"), - ) - .set_name("@".to_string() + &file_name) - .exec() + // find the exit box id + let new_territory; + { + let (_, exit_box) = connection + .zone + .as_ref() + .unwrap() + .find_exit_box(*exit_box) .unwrap(); - let func: Function = - lua.globals().get("doAction").unwrap(); + // find the pop range on the other side + let mut game_data = game_data.lock().unwrap(); + let new_zone = Zone::load(&mut game_data.game_data, exit_box.territory_type); + let (destination_object, _) = new_zone + .find_pop_range(exit_box.destination_instance_id) + .unwrap(); - effects_builder = Some( - func.call::(connection_data) - .unwrap(), - ); + // set the exit position + connection.exit_position = Some(Position { + x: destination_object.transform.translation[0], + y: destination_object.transform.translation[1], + z: destination_object.transform.translation[2], + }); + new_territory = exit_box.territory_type; + } - Ok(()) - }) - .unwrap(); - } else { - tracing::warn!("Action {key} isn't scripted yet! Ignoring..."); + connection.change_zone(new_territory).await; } - } + ClientZoneIpcData::ActionRequest(request) => { + let mut effects_builder = None; - // tell them the action results - if let Some(effects_builder) = effects_builder { - let mut effects = [ActionEffect::default(); 8]; - effects[..effects_builder.effects.len()] - .copy_from_slice(&effects_builder.effects); + // run action script + { + let lua = lua.lock().unwrap(); + let state = lua.app_data_ref::().unwrap(); - if let Some(actor) = - connection.get_actor_mut(request.target.object_id) - { - for effect in &effects_builder.effects { - match effect.kind { - EffectKind::Damage { amount, .. } => { - actor.hp = actor.hp.saturating_sub(amount as u32); + let key = request.action_key; + if let Some(action_script) = + state.action_scripts.get(&key) + { + lua.scope(|scope| { + let connection_data = scope + .create_userdata_ref_mut(&mut lua_player) + .unwrap(); + + let config = get_config(); + + let file_name = format!( + "{}/{}", + &config.world.scripts_location, action_script + ); + lua.load( + std::fs::read(&file_name) + .expect("Failed to locate scripts directory!"), + ) + .set_name("@".to_string() + &file_name) + .exec() + .unwrap(); + + let func: Function = + lua.globals().get("doAction").unwrap(); + + effects_builder = Some( + func.call::(connection_data) + .unwrap(), + ); + + Ok(()) + }) + .unwrap(); + } else { + tracing::warn!("Action {key} isn't scripted yet! Ignoring..."); } - _ => todo!() - } } - let actor = *actor; - connection.update_hp_mp(actor.id, actor.hp, 10000).await; - } + // tell them the action results + if let Some(effects_builder) = effects_builder { + let mut effects = [ActionEffect::default(); 8]; + effects[..effects_builder.effects.len()] + .copy_from_slice(&effects_builder.effects); - let ipc = ServerZoneIpcSegment { - op_code: ServerZoneIpcType::ActionResult, - timestamp: timestamp_secs(), - data: ServerZoneIpcData::ActionResult(ActionResult { - main_target: request.target, - target_id_again: request.target, - action_id: request.action_key, - animation_lock_time: 0.6, - rotation: connection.player_data.rotation, - action_animation_id: request.action_key as u16, // assuming action id == animation id - flag: 1, - effect_count: effects_builder.effects.len() as u8, - effects, - unk1: 2662353, - unk2: 3758096384, - hidden_animation: 1, - ..Default::default() - }), - ..Default::default() - }; + if let Some(actor) = + connection.get_actor_mut(request.target.object_id) + { + for effect in &effects_builder.effects { + match effect.kind { + EffectKind::Damage { amount, .. } => { + actor.hp = actor.hp.saturating_sub(amount as u32); + } + _ => todo!() + } + } - connection - .send_segment(PacketSegment { - source_actor: connection.player_data.actor_id, - target_actor: connection.player_data.actor_id, - segment_type: SegmentType::Ipc, - data: SegmentData::Ipc { data: ipc }, - }) - .await; + let actor = *actor; + connection.update_hp_mp(actor.id, actor.hp, 10000).await; + } - if let Some(actor) = - connection.get_actor(request.target.object_id) - { - if actor.hp == 0 { - tracing::info!("Despawning {} because they died!", actor.id.0); - // if the actor died, despawn them - /*connection.handle - .send(ToServer::ActorDespawned(connection.id, actor.id.0)) - .await;*/ + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::ActionResult, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::ActionResult(ActionResult { + main_target: request.target, + target_id_again: request.target, + action_id: request.action_key, + animation_lock_time: 0.6, + rotation: connection.player_data.rotation, + action_animation_id: request.action_key as u16, // assuming action id == animation id + flag: 1, + effect_count: effects_builder.effects.len() as u8, + effects, + unk1: 2662353, + unk2: 3758096384, + hidden_animation: 1, + ..Default::default() + }), + ..Default::default() + }; + + connection + .send_segment(PacketSegment { + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, + segment_type: SegmentType::Ipc, + data: SegmentData::Ipc { data: ipc }, + }) + .await; + + if let Some(actor) = + connection.get_actor(request.target.object_id) + { + if actor.hp == 0 { + tracing::info!("Despawning {} because they died!", actor.id.0); + // if the actor died, despawn them + /*connection.handle + * .send(ToServer::ActorDespawned(connection.id, actor.id.0)) + * .await;*/ + } + } } } - } - } - ClientZoneIpcData::Unk16 { .. } => { - tracing::info!("Recieved Unk16!"); - } - ClientZoneIpcData::Unk17 { .. } => { - tracing::info!("Recieved Unk17!"); - } - ClientZoneIpcData::Unk18 { .. } => { - tracing::info!("Recieved Unk18!"); - } - ClientZoneIpcData::EventRelatedUnk { - unk1, - unk2, - unk3, - unk4, - } => { - tracing::info!( - "Recieved EventRelatedUnk! {unk1} {unk2} {unk3} {unk4}" - ); + ClientZoneIpcData::Unk16 { .. } => { + tracing::info!("Recieved Unk16!"); + } + ClientZoneIpcData::Unk17 { .. } => { + tracing::info!("Recieved Unk17!"); + } + ClientZoneIpcData::Unk18 { .. } => { + tracing::info!("Recieved Unk18!"); + } + ClientZoneIpcData::EventRelatedUnk { + unk1, + unk2, + unk3, + unk4, + } => { + tracing::info!( + "Recieved EventRelatedUnk! {unk1} {unk2} {unk3} {unk4}" + ); - if let Some(event) = connection.event.as_mut() { - event.scene_finished(&mut lua_player, *unk2); - } - } - ClientZoneIpcData::Unk19 { .. } => { - tracing::info!("Recieved Unk19!"); - } - ClientZoneIpcData::ItemOperation(action) => { - tracing::info!("Client is modifying inventory! {action:#?}"); + if let Some(event) = connection.event.as_mut() { + event.scene_finished(&mut lua_player, *unk2); + } + } + ClientZoneIpcData::Unk19 { .. } => { + tracing::info!("Recieved Unk19!"); + } + ClientZoneIpcData::ItemOperation(action) => { + tracing::info!("Client is modifying inventory! {action:#?}"); - connection.player_data.inventory.process_action(action); - connection.send_inventory(true).await; - } - ClientZoneIpcData::StartTalkEvent { actor_id, event_id } => { - // load event - { - let ipc = ServerZoneIpcSegment { - op_code: ServerZoneIpcType::EventStart, - timestamp: timestamp_secs(), - data: ServerZoneIpcData::EventStart(EventStart { - target_id: *actor_id, - event_id: *event_id, - event_type: 1, // talk? - ..Default::default() - }), - ..Default::default() - }; + connection.player_data.inventory.process_action(action); + connection.send_inventory(true).await; + } + ClientZoneIpcData::StartTalkEvent { actor_id, event_id } => { + // load event + { + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::EventStart, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::EventStart(EventStart { + target_id: *actor_id, + event_id: *event_id, + event_type: 1, // talk? + ..Default::default() + }), + ..Default::default() + }; - connection - .send_segment(PacketSegment { - source_actor: connection.player_data.actor_id, - target_actor: connection.player_data.actor_id, - segment_type: SegmentType::Ipc, - data: SegmentData::Ipc { data: ipc }, - }) - .await; - } + connection + .send_segment(PacketSegment { + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, + segment_type: SegmentType::Ipc, + data: SegmentData::Ipc { data: ipc }, + }) + .await; + } - let mut should_cancel = false; - { - let lua = lua.lock().unwrap(); - let state = lua.app_data_ref::().unwrap(); + let mut should_cancel = false; + { + let lua = lua.lock().unwrap(); + let state = lua.app_data_ref::().unwrap(); + + if let Some(event_script) = + state.event_scripts.get(event_id) + { + connection.event = Some(Event::new(*event_id, &event_script)); + connection + .event + .as_mut() + .unwrap() + .talk(*actor_id, &mut lua_player); + } else { + tracing::warn!("Event {event_id} isn't scripted yet! Ignoring..."); + + should_cancel = true; + } + } + + if should_cancel { + // give control back to the player so they aren't stuck + connection.event_finish(*event_id).await; + } + } + ClientZoneIpcData::EventHandlerReturn { handler_id, scene, error_code, num_results, results } => { + tracing::info!("Finishing this event... {handler_id} {scene} {error_code} {num_results} {results:#?}"); - if let Some(event_script) = - state.event_scripts.get(event_id) - { - connection.event = Some(Event::new(*event_id, &event_script)); connection - .event - .as_mut() - .unwrap() - .talk(*actor_id, &mut lua_player); - } else { - tracing::warn!("Event {event_id} isn't scripted yet! Ignoring..."); - - should_cancel = true; + .event + .as_mut() + .unwrap() + .finish(*scene, results, &mut lua_player); } } - - if should_cancel { - // give control back to the player so they aren't stuck - connection.event_finish(*event_id).await; - } } - ClientZoneIpcData::EventHandlerReturn { handler_id, scene, error_code, num_results, results } => { - tracing::info!("Finishing this event... {handler_id} {scene} {error_code} {num_results} {results:#?}"); - - connection - .event - .as_mut() - .unwrap() - .finish(*scene, results, &mut lua_player); + SegmentData::KeepAliveRequest { id, timestamp } => { + send_keep_alive::( + &mut connection.socket, + &mut connection.state, + ConnectionType::Zone, + *id, + *timestamp, + ) + .await + } + SegmentData::KeepAliveResponse { .. } => { + tracing::info!("Got keep alive response from client... cool..."); + } + SegmentData::KawariIpc { data } => handle_custom_ipc(&mut connection, data).await, + _ => { + panic!("The server is recieving a response or unknown packet!") } } } - SegmentData::KeepAliveRequest { id, timestamp } => { - send_keep_alive::( - &mut connection.socket, - &mut connection.state, - ConnectionType::Zone, - *id, - *timestamp, - ) - .await - } - SegmentData::KeepAliveResponse { .. } => { - tracing::info!("Got keep alive response from client... cool..."); - } - SegmentData::KawariIpc { data } => handle_custom_ipc(&mut connection, data).await, - _ => { - panic!("The server is recieving a response or unknown packet!") - } + + // copy from lua player state, as they modify the status effects list + // TODO: i dunno? + connection.status_effects = lua_player.status_effects.clone(); + + // Process any queued packets from scripts and whatnot + connection.process_lua_player(&mut lua_player).await; + + // check if status effects need sending + connection.process_effects_list().await; + + // update lua player + lua_player.player_data = connection.player_data.clone(); + lua_player.status_effects = connection.status_effects.clone(); } - } - - // copy from lua player state, as they modify the status effects list - // TODO: i dunno? - connection.status_effects = lua_player.status_effects.clone(); - - // Process any queued packets from scripts and whatnot - connection.process_lua_player(&mut lua_player).await; - - // check if status effects need sending - connection.process_effects_list().await; - - // update lua player - lua_player.player_data = connection.player_data.clone(); - lua_player.status_effects = connection.status_effects.clone(); + }, + Err(_) => { + tracing::info!("Connection was killed because of a network error!"); + break; + }, } } msg = internal_recv.recv() => match msg { @@ -875,6 +897,16 @@ async fn client_loop( } } } + + // forcefully log out the player if they weren't logging out but force D/C'd + if connection.player_data.actor_id != 0 && !connection.gracefully_logged_out { + tracing::info!("Forcefully logging out player..."); + connection.begin_log_out().await; + connection + .handle + .send(ToServer::Disconnected(connection.id)) + .await; + } } async fn handle_rcon(listener: &Option) -> Option<(TcpStream, SocketAddr)> { @@ -994,8 +1026,10 @@ async fn main() { database: database.clone(), lua: lua.clone(), gamedata: game_data.clone(), - exit_position: None, - exit_rotation: None, + exit_position: None, + exit_rotation: None, + last_keep_alive: Instant::now(), + gracefully_logged_out: false }); } Some((mut socket, _)) = handle_rcon(&rcon_listener) => { diff --git a/src/packet/parsing.rs b/src/packet/parsing.rs index f31abbf..1c396bf 100644 --- a/src/packet/parsing.rs +++ b/src/packet/parsing.rs @@ -207,7 +207,9 @@ pub async fn send_packet( let buffer = cursor.into_inner(); - socket.write_all(&buffer).await.unwrap(); + if let Err(e) = socket.write_all(&buffer).await { + tracing::warn!("Failed to send packet: {e}"); + } } // temporary diff --git a/src/world/connection.rs b/src/world/connection.rs index e404b4e..763be05 100644 --- a/src/world/connection.rs +++ b/src/world/connection.rs @@ -1,6 +1,7 @@ use std::{ net::SocketAddr, sync::{Arc, Mutex}, + time::Instant, }; use tokio::net::TcpStream; @@ -87,6 +88,11 @@ pub struct ZoneConnection { pub exit_position: Option, pub exit_rotation: Option, + + pub last_keep_alive: Instant, + + /// Whether the player was gracefully logged out + pub gracefully_logged_out: bool, } impl ZoneConnection { @@ -612,6 +618,8 @@ impl ZoneConnection { } pub async fn begin_log_out(&mut self) { + self.gracefully_logged_out = true; + // write the player back to the database self.database.commit_player_data(&self.player_data); diff --git a/src/world/server.rs b/src/world/server.rs index 9912899..6cd9d35 100644 --- a/src/world/server.rs +++ b/src/world/server.rs @@ -313,10 +313,25 @@ pub async fn server_main_loop(mut recv: Receiver) -> Result<(), std::i } ToServer::FatalError(err) => return Err(err), } - } - // Remove any clients that errored out - for id in to_remove { - data.clients.remove(&id); + + // Remove any clients that errored out + for remove_id in &to_remove { + // remove any actors they had + let mut actor_id = None; + for (id, (handle, _)) in &mut data.clients { + if *id == *remove_id { + actor_id = Some(handle.actor_id); + } + } + + if let Some(actor_id) = actor_id { + // remove them from the instance + let current_instance = data.find_actor_instance_mut(actor_id).unwrap(); + current_instance.actors.remove(&ObjectId(actor_id)); + } + + data.clients.remove(&remove_id); + } } Ok(()) }