From 60fcac80c225c2e49ca87601c7ddaad540d4d39e Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Fri, 18 Jul 2025 16:53:40 -0400 Subject: [PATCH] Implement more of the Content Finder Now it can put you in the zone properly (just Satasha, really.) The "checking member status" window doesn't go away yet. --- Cargo.toml | 2 +- resources/opcodes.json | 18 ++- src/bin/kawari-world.rs | 248 +++++++++++++++++++++++++++++++--------- src/common/gamedata.rs | 16 +++ src/ipc/zone/mod.rs | 17 ++- src/world/connection.rs | 3 + 6 files changed, 237 insertions(+), 67 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 988692f..ef1f182 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,7 +83,7 @@ physis = { git = "https://github.com/redstrate/physis", default-features = false bitflags = { version = "2.9", default-features = false } # excel sheet data -icarus = { git = "https://github.com/redstrate/Icarus", branch = "ver/2025.06.28.0000.0000", features = ["Warp", "Tribe", "ClassJob", "World", "TerritoryType", "Race", "Aetheryte", "EquipSlotCategory", "Action", "WeatherRate", "PlaceName", "GilShopItem"], default-features = false } +icarus = { git = "https://github.com/redstrate/Icarus", branch = "ver/2025.06.28.0000.0000", features = ["Warp", "Tribe", "ClassJob", "World", "TerritoryType", "Race", "Aetheryte", "EquipSlotCategory", "Action", "WeatherRate", "PlaceName", "GilShopItem", "InstanceContent", "ContentFinderCondition"], default-features = false } # navimesh visualization bevy = { version = "0.16", features = ["std", diff --git a/resources/opcodes.json b/resources/opcodes.json index 0a93ad7..00400e0 100644 --- a/resources/opcodes.json +++ b/resources/opcodes.json @@ -251,14 +251,14 @@ "size": 16 }, { - "name": "ContentFinderFound", + "name": "ContentFinderUpdate", "opcode": 619, "size": 40 }, { - "name": "ContentFinderFound2", - "opcode": 619, - "size": 812 + "name": "ContentFinderFound", + "opcode": 730, + "size": 40 }, { "name": "ObjectSpawn", @@ -314,6 +314,11 @@ "name": "ItemObtainedLogMessage", "opcode": 456, "size": 22 + }, + { + "name": "ContentFinderCommencing", + "opcode": 485, + "size": 24 } ], "ClientZoneIpcType": [ @@ -471,6 +476,11 @@ "name": "ContentFinderRegister", "opcode": 991, "size": 40 + }, + { + "name": "ContentFinderAction", + "opcode": 409, + "size": 8 } ], "ServerLobbyIpcType": [ diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index aa342e2..01f4941 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -1147,64 +1147,197 @@ async fn client_loop( } ClientZoneIpcData::ContentFinderRegister { content_ids, .. } => { tracing::info!("Searching for {content_ids:?}"); - let ipc = ServerZoneIpcSegment { - op_code: ServerZoneIpcType::ContentFinderFound, - timestamp: timestamp_secs(), - data: ServerZoneIpcData::ContentFinderFound { - state1: 2, - classjob_id: 1, - unk1: [ - 5, - 2, - 5, - 2, - 5, - 2, - 96, - 4, - 5, - 64, - 2, - 5, - 2, - 5, - 2, - 2, - 2, - 2, - 4, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 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; + connection.queued_content = Some(content_ids[0]); + + // update + { + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::ContentFinderUpdate, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::ContentFinderUpdate { + state1: 1, + classjob_id: connection.player_data.classjob_id, // TODO: store what they registered with, because it can change + unk1: [ + 0, + 0, + 0, + 0, + 0, + 0, + 96, + 4, + 2, + 64, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + ], + content_ids: [ + 4, + 0, + 0, + 0, + 0, + ], + unk2: [ + 0, + 0, + 0, + 0, + 0, + 0, + 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; + } + + // found + { + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::ContentFinderFound, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::ContentFinderFound { + unk1: [ + 3, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 96, + 4, + 2, + 64, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + ], + }, + ..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::ContentFinderAction { unk1 } => { + dbg!(unk1); + + + // commencing + { + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::ContentFinderCommencing, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::ContentFinderCommencing { + unk1: [ + 4, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 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; + } + + // TODO: content finder should be moved to global state + // For now, just send them to do the zone if they do anything + let zone_id; + { + let mut game_data = game_data.lock().unwrap(); + zone_id = game_data.find_zone_for_content(connection.queued_content.unwrap()); + } + + if let Some(zone_id) = zone_id { + connection.change_zone(zone_id).await; + } else { + tracing::warn!("Failed to find zone id for content?!"); + } + + connection.queued_content = None; } ClientZoneIpcData::Unknown { .. } => { tracing::warn!("Unknown packet {:?} recieved, this should be handled!", data.op_code); @@ -1376,6 +1509,7 @@ async fn main() { gracefully_logged_out: false, weather_id: 0, obsfucation_data: ObsfucationData::default(), + queued_content: None, }); } Some((mut socket, _)) = handle_rcon(&rcon_listener) => { diff --git a/src/common/gamedata.rs b/src/common/gamedata.rs index f9af5b5..8181ff7 100644 --- a/src/common/gamedata.rs +++ b/src/common/gamedata.rs @@ -3,8 +3,10 @@ use std::path::PathBuf; use icarus::Action::ActionSheet; use icarus::Aetheryte::AetheryteSheet; use icarus::ClassJob::ClassJobSheet; +use icarus::ContentFinderCondition::ContentFinderConditionSheet; use icarus::EquipSlotCategory::EquipSlotCategorySheet; use icarus::GilShopItem::GilShopItemSheet; +use icarus::InstanceContent::InstanceContentSheet; use icarus::PlaceName::PlaceNameSheet; use icarus::TerritoryType::TerritoryTypeSheet; use icarus::WeatherRate::WeatherRateSheet; @@ -437,6 +439,20 @@ impl GameData { self.get_item_info(ItemInfoQuery::ById(*item_id as u32)) } + + /// Gets the zone id for the given InstanceContent. + pub fn find_zone_for_content(&mut self, content_id: u16) -> Option { + let instance_content_sheet = + InstanceContentSheet::read_from(&mut self.resource, Language::None).unwrap(); + let instance_content_row = instance_content_sheet.get_row(content_id as u32)?; + + let content_finder_row_id = instance_content_row.ContentFinderCondition().into_u16()?; + let content_finder_sheet = + ContentFinderConditionSheet::read_from(&mut self.resource, Language::English).unwrap(); + let content_finder_row = content_finder_sheet.get_row(*content_finder_row_id as u32)?; + + content_finder_row.TerritoryType().into_u16().copied() + } } // Simple enum for GameData::get_territory_name diff --git a/src/ipc/zone/mod.rs b/src/ipc/zone/mod.rs index 7b42d76..5f8af9d 100644 --- a/src/ipc/zone/mod.rs +++ b/src/ipc/zone/mod.rs @@ -459,8 +459,8 @@ pub enum ServerZoneIpcData { /// Unknown, seems to always be 0x00000200. unk2: u32, }, - #[br(pre_assert(*magic == ServerZoneIpcType::ContentFinderFound))] - ContentFinderFound { + #[br(pre_assert(*magic == ServerZoneIpcType::ContentFinderUpdate))] + ContentFinderUpdate { /// 0 = Nothing happens /// 1 = Reserving server /// 2 = again? ^ @@ -469,10 +469,12 @@ pub enum ServerZoneIpcData { /// nothing appears to happen above 5 state1: u8, classjob_id: u8, - unk1: [u8; 38], + unk1: [u8; 18], + content_ids: [u16; 5], + unk2: [u8; 10], }, - #[br(pre_assert(*magic == ServerZoneIpcType::ContentFinderFound2))] - ContentFinderFound2 { unk1: [u8; 8] }, + #[br(pre_assert(*magic == ServerZoneIpcType::ContentFinderFound))] + ContentFinderFound { unk1: [u8; 40] }, #[br(pre_assert(*magic == ServerZoneIpcType::ObjectSpawn))] ObjectSpawn(ObjectSpawn), #[br(pre_assert(*magic == ServerZoneIpcType::ActorGauge))] @@ -554,6 +556,9 @@ pub enum ServerZoneIpcData { }, #[br(pre_assert(*magic == ServerZoneIpcType::EffectResult))] EffectResult(EffectResult), + /// Sent to give you the green checkmark before entering a CF zone. + #[br(pre_assert(*magic == ServerZoneIpcType::ContentFinderCommencing))] + ContentFinderCommencing { unk1: [u8; 24] }, Unknown { #[br(count = size - 32)] unk: Vec, @@ -750,6 +755,8 @@ pub enum ClientZoneIpcData { #[brw(pad_after = 4)] // seems to empty content_ids: [u16; 5], }, + #[br(pre_assert(*magic == ClientZoneIpcType::ContentFinderAction))] + ContentFinderAction { unk1: [u8; 8] }, Unknown { #[br(count = size - 32)] unk: Vec, diff --git a/src/world/connection.rs b/src/world/connection.rs index f6aa21d..cb4fcd6 100644 --- a/src/world/connection.rs +++ b/src/world/connection.rs @@ -139,6 +139,9 @@ pub struct ZoneConnection { pub weather_id: u16, pub obsfucation_data: ObsfucationData, + + // TODO: support more than one content in the queue + pub queued_content: Option, } impl ZoneConnection {