diff --git a/resources/tests/npc_spawn.bin b/resources/tests/npc_spawn.bin new file mode 100644 index 0000000..eeb9d25 Binary files /dev/null and b/resources/tests/npc_spawn.bin differ diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index b426d86..476117b 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -1,8 +1,8 @@ use kawari::oodle::OodleNetwork; use kawari::packet::{ConnectionType, PacketSegment, PacketState, SegmentType, send_keep_alive}; use kawari::world::ipc::{ - ClientZoneIpcData, GameMasterCommandType, ServerZoneIpcData, ServerZoneIpcSegment, - ServerZoneIpcType, SocialListRequestType, + ClientZoneIpcData, CommonSpawn, GameMasterCommandType, ObjectKind, ServerZoneIpcData, + ServerZoneIpcSegment, ServerZoneIpcType, SocialListRequestType, }; use kawari::world::{ ChatHandler, Zone, ZoneConnection, @@ -282,34 +282,37 @@ async fn main() { timestamp: timestamp_secs(), data: ServerZoneIpcData::PlayerSpawn(PlayerSpawn { content_id: CONTENT_ID, - current_world_id: WORLD_ID, - home_world_id: WORLD_ID, - title: 1, - class_job: 35, - name: CHAR_NAME.to_string(), - hp_curr: 100, - hp_max: 100, - mp_curr: 100, - mp_max: 100, - model_type: 1, - gm_rank: 3, - look: CUSTOMIZE_DATA, - fc_tag: "LOCAL".to_string(), - subtype: 4, - models: [ - 0, // head - 89, // body - 89, // hands - 89, // legs - 89, // feet - 0, // ears - 0, // neck - 0, // wrists - 0, // left finger - 0, // right finger - ], - pos: exit_position - .unwrap_or(Position::default()), + common: CommonSpawn { + current_world_id: WORLD_ID, + home_world_id: WORLD_ID, + title: 1, + class_job: 35, + name: CHAR_NAME.to_string(), + hp_curr: 100, + hp_max: 100, + mp_curr: 100, + mp_max: 100, + object_kind: ObjectKind::Player, + gm_rank: 3, + look: CUSTOMIZE_DATA, + fc_tag: "LOCAL".to_string(), + subtype: 4, + models: [ + 0, // head + 89, // body + 89, // hands + 89, // legs + 89, // feet + 0, // ears + 0, // neck + 0, // wrists + 0, // left finger + 0, // right finger + ], + pos: exit_position + .unwrap_or(Position::default()), + ..Default::default() + }, ..Default::default() }), ..Default::default() diff --git a/src/lib.rs b/src/lib.rs index aae5ee9..a29f55d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,7 @@ pub const WORLD_NAME: &str = "KAWARI"; pub const ZONE_ID: u16 = 132; pub const CONTENT_ID: u64 = 11111111111111111; +pub const INVALID_OBJECT_ID: u32 = 0xE0000000; pub const CUSTOMIZE_DATA: CustomizeData = CustomizeData { race: 4, diff --git a/src/world/chat_handler.rs b/src/world/chat_handler.rs index 579f300..f64560a 100644 --- a/src/world/chat_handler.rs +++ b/src/world/chat_handler.rs @@ -1,8 +1,11 @@ use crate::{ - CHAR_NAME, CUSTOMIZE_DATA, WORLD_ID, + CHAR_NAME, CUSTOMIZE_DATA, INVALID_OBJECT_ID, WORLD_ID, common::timestamp_secs, packet::{PacketSegment, SegmentType}, - world::ipc::{PlayerSpawn, ServerZoneIpcData, ServerZoneIpcSegment, ServerZoneIpcType}, + world::ipc::{ + CommonSpawn, NpcSpawn, ObjectKind, PlayerSpawn, ServerZoneIpcData, ServerZoneIpcSegment, + ServerZoneIpcType, + }, }; use super::{ @@ -45,33 +48,87 @@ impl ChatHandler { data: ServerZoneIpcData::PlayerSpawn(PlayerSpawn { some_unique_id: 1, content_id: 1, - current_world_id: WORLD_ID, - home_world_id: WORLD_ID, - title: 1, - class_job: 35, - name: CHAR_NAME.to_string(), - hp_curr: 100, - hp_max: 100, - mp_curr: 100, - mp_max: 100, - model_type: 1, - gm_rank: 3, - spawn_index: connection.get_free_spawn_index(), - look: CUSTOMIZE_DATA, - fc_tag: "LOCAL".to_string(), - models: [ - 0, // head - 89, // body - 89, // hands - 89, // legs - 89, // feet - 0, // ears - 0, // neck - 0, // wrists - 0, // left finger - 0, // right finger - ], - pos: Position::default(), + common: CommonSpawn { + current_world_id: WORLD_ID, + home_world_id: WORLD_ID, + title: 1, + class_job: 35, + name: CHAR_NAME.to_string(), + hp_curr: 100, + hp_max: 100, + mp_curr: 100, + mp_max: 100, + object_kind: ObjectKind::Player, + gm_rank: 3, + spawn_index: connection.get_free_spawn_index(), + look: CUSTOMIZE_DATA, + fc_tag: "LOCAL".to_string(), + models: [ + 0, // head + 89, // body + 89, // hands + 89, // legs + 89, // feet + 0, // ears + 0, // neck + 0, // wrists + 0, // left finger + 0, // right finger + ], + pos: Position::default(), + ..Default::default() + }, + ..Default::default() + }), + }; + + connection + .send_segment(PacketSegment { + source_actor: 0x106ad804, + target_actor: connection.player_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; + } + } + "!spawnnpc" => { + // spawn another one of us + { + let ipc = ServerZoneIpcSegment { + unk1: 20, + unk2: 0, + op_code: ServerZoneIpcType::NpcSpawn, + server_id: 0, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::NpcSpawn(NpcSpawn { + common: CommonSpawn { + hp_curr: 100, + hp_max: 100, + mp_curr: 100, + mp_max: 100, + look: CUSTOMIZE_DATA, + spawn_index: connection.get_free_spawn_index(), + bnpc_base: 13498, + bnpc_name: 10261, + spawner_id: connection.player_id, + parent_actor_id: INVALID_OBJECT_ID, // TODO: make default? + object_kind: ObjectKind::BattleNpc, + level: 1, + models: [ + 0, // head + 89, // body + 89, // hands + 89, // legs + 89, // feet + 0, // ears + 0, // neck + 0, // wrists + 0, // left finger + 0, // right finger + ], + pos: Position::default(), + ..Default::default() + }, ..Default::default() }), }; diff --git a/src/world/ipc/common_spawn.rs b/src/world/ipc/common_spawn.rs new file mode 100644 index 0000000..f95ebd9 --- /dev/null +++ b/src/world/ipc/common_spawn.rs @@ -0,0 +1,93 @@ +use binrw::binrw; + +use crate::CHAR_NAME_MAX_LENGTH; +use crate::common::{CustomizeData, read_string, write_string}; + +use super::position::Position; +use super::{CharacterMode, ObjectKind, StatusEffect}; + +#[binrw] +#[brw(little)] +#[derive(Debug, Clone, Default)] +pub struct CommonSpawn { + pub title: u16, + pub u1b: u16, + pub current_world_id: u16, + pub home_world_id: u16, + + pub gm_rank: u8, + pub u3c: u8, + pub u4: u8, + pub online_status: u8, + + pub pose: u8, + pub u5a: u8, + pub u5b: u8, + pub u5c: u8, + + pub target_id: u64, + pub u6: u32, + pub u7: u32, + pub main_weapon_model: u64, + pub sec_weapon_model: u64, + pub craft_tool_model: u64, + + pub u14: u32, + pub u15: u32, + pub bnpc_base: u32, // See BNpcBase Excel sheet + pub bnpc_name: u32, // See BNpcName Excel sheet + pub unk3: [u8; 8], + pub director_id: u32, // FIXME: i think the next three are in the wrong order + pub spawner_id: u32, + pub parent_actor_id: u32, + pub hp_max: u32, + pub hp_curr: u32, + pub display_flags: u32, // assumed + pub fate_id: u16, // assumed + pub mp_curr: u16, + pub mp_max: u16, + pub unk: u16, + pub model_chara: u16, // See ModelChara Excel sheet + pub rotation: u16, // assumed + pub current_mount: u16, // assumed + pub active_minion: u16, // assumed + pub u23: u8, // assumed + pub u24: u8, // assumed + pub u25: u8, // assumed + pub u26: u8, // assumed + pub spawn_index: u8, + pub mode: CharacterMode, + pub persistent_emote: u8, + pub object_kind: ObjectKind, + pub subtype: u8, + pub voice: u8, + pub enemy_type: u8, + pub unk27: u8, + pub level: u8, + pub class_job: u8, + pub unk28: u8, + pub unk29: u8, + pub mount_head: u8, + pub mount_body: u8, + pub mount_feet: u8, + pub mount_color: u8, + pub scale: u8, + pub element_data: [u8; 6], + pub padding2: [u8; 1], + pub effect: [StatusEffect; 30], + pub pos: Position, + pub models: [u32; 10], + pub unknown6_58: [u8; 10], + pub padding3: [u8; 4], + #[br(count = CHAR_NAME_MAX_LENGTH)] + #[bw(pad_size_to = CHAR_NAME_MAX_LENGTH)] + #[br(map = read_string)] + #[bw(map = write_string)] + pub name: String, + pub look: CustomizeData, + #[br(count = 6)] + #[bw(pad_size_to = 6)] + #[br(map = read_string)] + #[bw(map = write_string)] + pub fc_tag: String, +} diff --git a/src/world/ipc/mod.rs b/src/world/ipc/mod.rs index 3cfc755..9c7aee8 100644 --- a/src/world/ipc/mod.rs +++ b/src/world/ipc/mod.rs @@ -34,6 +34,12 @@ pub use actor_control_self::ActorControlType; mod init_zone; pub use init_zone::InitZone; +mod npc_spawn; +pub use npc_spawn::NpcSpawn; + +mod common_spawn; +pub use common_spawn::CommonSpawn; + use crate::common::read_string; use crate::common::write_string; use crate::packet::IpcSegment; @@ -86,6 +92,7 @@ impl ReadWriteIpcSegment for ServerZoneIpcSegment { ServerZoneIpcType::Unk17 => 104, ServerZoneIpcType::SocialList => 1136, ServerZoneIpcType::PrepareZoning => 16, + ServerZoneIpcType::NpcSpawn => 648, } } } @@ -121,6 +128,30 @@ pub enum GameMasterCommandType { ChangeTerritory = 0x58, } +#[binrw] +#[brw(repr = u8)] +#[derive(Clone, PartialEq, Debug, Default)] +pub enum ObjectKind { + #[default] + None = 0, + Player = 1, + BattleNpc = 2, + EventNpc = 3, + Treasure = 4, + Aetheryte = 5, + GatheringPoint = 6, + EventObj = 7, + Mount = 8, + Companion = 9, + Retainer = 10, + AreaObject = 11, + HousingEventObject = 12, + Cutscene = 13, + MjiObject = 14, + Ornament = 15, + CardStand = 16, +} + #[binrw] #[brw(repr = u16)] #[derive(Clone, PartialEq, Debug)] @@ -172,6 +203,8 @@ pub enum ServerZoneIpcType { Unk17 = 0x2A1, // Sent by the server in response to SocialListRequest SocialList = 0x36C, + // Sent by the server to spawn an NPC + NpcSpawn = 0x100, } #[binrw] @@ -287,6 +320,7 @@ pub enum ServerZoneIpcData { unk: [u8; 104], }, SocialList(SocialList), + NpcSpawn(NpcSpawn), } #[binrw] @@ -429,6 +463,10 @@ mod tests { ServerZoneIpcType::ActorSetPos, ServerZoneIpcData::ActorSetPos(ActorSetPos::default()), ), + ( + ServerZoneIpcType::NpcSpawn, + ServerZoneIpcData::NpcSpawn(NpcSpawn::default()), + ), ]; for (opcode, data) in &ipc_types { diff --git a/src/world/ipc/npc_spawn.rs b/src/world/ipc/npc_spawn.rs new file mode 100644 index 0000000..6afb498 --- /dev/null +++ b/src/world/ipc/npc_spawn.rs @@ -0,0 +1,50 @@ +use binrw::binrw; + +use crate::CHAR_NAME_MAX_LENGTH; +use crate::common::{CustomizeData, read_string, write_string}; + +use super::position::Position; +use super::{CharacterMode, CommonSpawn, ObjectKind, StatusEffect}; + +#[binrw] +#[brw(little)] +#[derive(Debug, Clone, Default)] +pub struct NpcSpawn { + pub common: CommonSpawn, + pub padding: [u8; 10], +} + +#[cfg(test)] +mod tests { + use std::{fs::read, io::Cursor, path::PathBuf}; + + use binrw::BinRead; + + use super::*; + + #[test] + fn read_npcspawn() { + let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + d.push("resources/tests/npc_spawn.bin"); + + let buffer = read(d).unwrap(); + let mut buffer = Cursor::new(&buffer); + + let npc_spawn = NpcSpawn::read_le(&mut buffer).unwrap(); + assert_eq!(npc_spawn.common.hp_max, 1393); + assert_eq!(npc_spawn.common.hp_curr, 1393); + assert_eq!(npc_spawn.common.mp_curr, 10000); + assert_eq!(npc_spawn.common.mp_max, 10000); + assert_eq!(npc_spawn.common.display_flags, 0); + assert_eq!(npc_spawn.common.pos.x, -64.17707); + assert_eq!(npc_spawn.common.pos.y, -2.0206506); + assert_eq!(npc_spawn.common.pos.z, 15.913875); + assert_eq!(npc_spawn.common.model_chara, 411); + assert_eq!(npc_spawn.common.bnpc_base, 13498); + assert_eq!(npc_spawn.common.bnpc_name, 10261); + assert_eq!(npc_spawn.common.spawn_index, 56); + assert_eq!(npc_spawn.common.mode, CharacterMode::Normal); + assert_eq!(npc_spawn.common.object_kind, ObjectKind::BattleNpc); + assert_eq!(npc_spawn.common.subtype, 2); + } +} diff --git a/src/world/ipc/player_spawn.rs b/src/world/ipc/player_spawn.rs index 6c5c907..2f09204 100644 --- a/src/world/ipc/player_spawn.rs +++ b/src/world/ipc/player_spawn.rs @@ -5,6 +5,7 @@ use crate::common::{CustomizeData, read_string, write_string}; use super::position::Position; use super::status_effect::StatusEffect; +use super::{CommonSpawn, ObjectKind}; #[binrw] #[brw(little)] @@ -27,87 +28,8 @@ pub struct PlayerSpawn { #[brw(pad_before = 4)] // always empty? pub content_id: u64, - pub title: u16, - pub u1b: u16, - pub current_world_id: u16, - pub home_world_id: u16, + pub common: CommonSpawn, - pub gm_rank: u8, - pub u3c: u8, - pub u4: u8, - pub online_status: u8, - - pub pose: u8, - pub u5a: u8, - pub u5b: u8, - pub u5c: u8, - - pub target_id: u64, - pub u6: u32, - pub u7: u32, - pub main_weapon_model: u64, - pub sec_weapon_model: u64, - pub craft_tool_model: u64, - - pub u14: u32, - pub u15: u32, - pub b_npc_base: u32, - pub b_npc_name: u32, - pub u18: u32, - pub u19: u32, - pub director_id: u32, - pub owner_id: u32, - pub u22: u32, - pub hp_max: u32, - pub hp_curr: u32, - pub display_flags: u32, - pub fate_id: u16, - pub mp_curr: u16, - pub mp_max: u16, - pub unk: u16, - pub model_chara: u16, - pub rotation: u16, - pub current_mount: u16, - pub active_minion: u16, - pub u23: u8, - pub u24: u8, - pub u25: u8, - pub u26: u8, - pub spawn_index: u8, - pub mode: CharacterMode, - pub persistent_emote: u8, - pub model_type: u8, - pub subtype: u8, - pub voice: u8, - pub enemy_type: u8, - pub unk27: u8, - pub level: u8, - pub class_job: u8, - pub unk28: u8, - pub unk29: u8, - pub mount_head: u8, - pub mount_body: u8, - pub mount_feet: u8, - pub mount_color: u8, - pub scale: u8, - pub element_data: [u8; 6], - pub padding2: [u8; 1], - pub effect: [StatusEffect; 30], - pub pos: Position, - pub models: [u32; 10], - pub unknown6_58: [u8; 10], - pub padding3: [u8; 4], - #[br(count = CHAR_NAME_MAX_LENGTH)] - #[bw(pad_size_to = CHAR_NAME_MAX_LENGTH)] - #[br(map = read_string)] - #[bw(map = write_string)] - pub name: String, - pub look: CustomizeData, - #[br(count = 6)] - #[bw(pad_size_to = 6)] - #[br(map = read_string)] - #[bw(map = write_string)] - pub fc_tag: String, pub padding: [u8; 2], } @@ -128,25 +50,27 @@ mod tests { let mut buffer = Cursor::new(&buffer); let player_spawn = PlayerSpawn::read_le(&mut buffer).unwrap(); - assert_eq!(player_spawn.current_world_id, 0x4F); - assert_eq!(player_spawn.home_world_id, 0x4F); - assert_eq!(player_spawn.hp_curr, 159); - assert_eq!(player_spawn.hp_max, 159); - assert_eq!(player_spawn.mp_curr, 10000); - assert_eq!(player_spawn.mp_max, 10000); - assert_eq!(player_spawn.mode, CharacterMode::Normal); - assert_eq!(player_spawn.spawn_index, 0); - assert_eq!(player_spawn.level, 1); - assert_eq!(player_spawn.class_job, 1); // adventurer - assert_eq!(player_spawn.scale, 36); - assert_eq!(player_spawn.pos.x, 40.519722); - assert_eq!(player_spawn.pos.y, 4.0); - assert_eq!(player_spawn.pos.z, -150.33124); - assert_eq!(player_spawn.name, "Lavenaa Warren"); - assert_eq!(player_spawn.look.race, 1); - assert_eq!(player_spawn.look.gender, 1); - assert_eq!(player_spawn.look.bust, 100); - assert_eq!(player_spawn.fc_tag, ""); - assert_eq!(player_spawn.subtype, 4); + assert_eq!(player_spawn.common.current_world_id, 0x4F); + assert_eq!(player_spawn.common.home_world_id, 0x4F); + assert_eq!(player_spawn.common.hp_curr, 159); + assert_eq!(player_spawn.common.hp_max, 159); + assert_eq!(player_spawn.common.mp_curr, 10000); + assert_eq!(player_spawn.common.mp_max, 10000); + assert_eq!(player_spawn.common.mode, CharacterMode::Normal); + assert_eq!(player_spawn.common.spawn_index, 0); + assert_eq!(player_spawn.common.level, 1); + assert_eq!(player_spawn.common.class_job, 1); // adventurer + assert_eq!(player_spawn.common.scale, 36); + assert_eq!(player_spawn.common.pos.x, 40.519722); + assert_eq!(player_spawn.common.pos.y, 4.0); + assert_eq!(player_spawn.common.pos.z, -150.33124); + assert_eq!(player_spawn.common.name, "Lavenaa Warren"); + assert_eq!(player_spawn.common.look.race, 1); + assert_eq!(player_spawn.common.look.gender, 1); + assert_eq!(player_spawn.common.look.bust, 100); + assert_eq!(player_spawn.common.fc_tag, ""); + assert_eq!(player_spawn.common.subtype, 4); + assert_eq!(player_spawn.common.model_chara, 0); + assert_eq!(player_spawn.common.object_kind, ObjectKind::Player); } }