diff --git a/resources/opcodes.json b/resources/opcodes.json index 08ec4e1..f2f755a 100644 --- a/resources/opcodes.json +++ b/resources/opcodes.json @@ -279,6 +279,11 @@ "name": "FreeCompanyInfo", "opcode": 892, "size": 80 + }, + { + "name": "TitleList", + "opcode": 731, + "size": 112 } ], "ClientZoneIpcType": [ diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index 610ea2d..737e4f2 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -2,7 +2,6 @@ use std::net::SocketAddr; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; -use kawari::RECEIVE_BUFFER_SIZE; use kawari::common::Position; use kawari::common::{GameData, timestamp_secs}; use kawari::config::get_config; @@ -27,6 +26,7 @@ use kawari::world::{ ClientHandle, Event, FromServer, LuaPlayer, PlayerData, ServerHandle, StatusEffects, ToServer, WorldDatabase, handle_custom_ipc, server_main_loop, }; +use kawari::{RECEIVE_BUFFER_SIZE, TITLE_UNLOCK_BITMASK_SIZE}; use mlua::{Function, Lua}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -415,8 +415,33 @@ async fn client_loop( 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; + match trigger.trigger { + ClientTriggerCommand::RequestTitleList {} => { + // send full title list for now + + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::TitleList, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::TitleList { + unlock_bitmask: [0xFF; TITLE_UNLOCK_BITMASK_SIZE] + }, + ..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; + }, + _ => { + // 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 { .. } => { // no-op diff --git a/src/ipc/zone/client_trigger.rs b/src/ipc/zone/client_trigger.rs index 44632fb..1dfb60d 100644 --- a/src/ipc/zone/client_trigger.rs +++ b/src/ipc/zone/client_trigger.rs @@ -59,6 +59,8 @@ pub enum ClientTriggerCommand { unk3: u32, unk4: u32, }, + #[brw(magic = 0x12Fu16)] + RequestTitleList {}, Unknown { category: u16, // seen in haircut event diff --git a/src/ipc/zone/mod.rs b/src/ipc/zone/mod.rs index e44cc3c..720bc61 100644 --- a/src/ipc/zone/mod.rs +++ b/src/ipc/zone/mod.rs @@ -92,6 +92,7 @@ mod object_spawn; pub use object_spawn::ObjectSpawn; use crate::COMPLETED_QUEST_BITMASK_SIZE; +use crate::TITLE_UNLOCK_BITMASK_SIZE; use crate::common::ObjectTypeId; use crate::common::Position; use crate::common::read_string; @@ -462,6 +463,10 @@ pub enum ServerZoneIpcData { }, #[br(pre_assert(*magic == ServerZoneIpcType::FreeCompanyInfo))] FreeCompanyInfo { unk: [u8; 80] }, + #[br(pre_assert(*magic == ServerZoneIpcType::TitleList))] + TitleList { + unlock_bitmask: [u8; TITLE_UNLOCK_BITMASK_SIZE], + }, Unknown { #[br(count = size - 32)] unk: Vec, @@ -928,6 +933,12 @@ mod tests { message: String::default(), }, ), + ( + ServerZoneIpcType::TitleList, + ServerZoneIpcData::TitleList { + unlock_bitmask: [0; TITLE_UNLOCK_BITMASK_SIZE], + }, + ), ( ServerZoneIpcType::FreeCompanyInfo, ServerZoneIpcData::FreeCompanyInfo { unk: [0; 80] }, diff --git a/src/lib.rs b/src/lib.rs index feb0d3e..6dd1da3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,6 +76,9 @@ pub const AETHERYTE_UNLOCK_BITMASK_SIZE: usize = 30; /// The size of the completed quest bitmask. pub const COMPLETED_QUEST_BITMASK_SIZE: usize = 691; +/// The size of the unlocked title bitmask. +pub const TITLE_UNLOCK_BITMASK_SIZE: usize = 112; + /// The size of various classjob arrays. pub const CLASSJOB_ARRAY_SIZE: usize = 32;