diff --git a/USAGE.md b/USAGE.md index d45fd9f..79136aa 100644 --- a/USAGE.md +++ b/USAGE.md @@ -80,7 +80,8 @@ These special debug commands start with `!` and are custom to Kawari. * `!setpos `: Teleport to the specified location * `!spawnactor`: Spawn another actor for debugging -* `!spawnnpc`: Spawn an NPC for debugging +* `!spawnnpc`: Spawn a NPC for debugging +* `!spawnmonster`: Spawn a monster for debugging ### GM commands diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index 138e7eb..000c343 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -60,6 +60,7 @@ async fn main() { player_data: PlayerData::default(), spawn_index: 0, zone: None, + position: Position::default(), }; tokio::spawn(async move { @@ -516,8 +517,12 @@ async fn main() { .await; } } - ClientZoneIpcData::UpdatePositionHandler { .. } => { - tracing::info!("Recieved UpdatePositionHandler!"); + ClientZoneIpcData::UpdatePositionHandler { + position, .. + } => { + tracing::info!("Character moved to {position:#?}"); + + connection.position = *position; } ClientZoneIpcData::LogOut { .. } => { tracing::info!("Recieved log out from client!"); @@ -707,6 +712,9 @@ async fn main() { .await; } } + ClientZoneIpcData::Unk15 { .. } => { + tracing::info!("Recieved Unk15!"); + } } } SegmentType::KeepAlive { id, timestamp } => { diff --git a/src/world/chat_handler.rs b/src/world/chat_handler.rs index 531d001..91861f4 100644 --- a/src/world/chat_handler.rs +++ b/src/world/chat_handler.rs @@ -133,7 +133,7 @@ impl ChatHandler { 0, // left finger 0, // right finger ], - pos: Position::default(), + pos: connection.position, ..Default::default() }, ..Default::default() @@ -207,7 +207,48 @@ impl ChatHandler { 0, // left finger 0, // right finger ], - pos: Position::default(), + pos: connection.position, + ..Default::default() + }, + ..Default::default() + }), + }; + + connection + .send_segment(PacketSegment { + source_actor: 0x106ad804, + target_actor: connection.player_data.actor_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; + } + } + "!spawnmonster" => { + // spawn a tiny mandragora + { + 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: 91, + hp_max: 91, + mp_curr: 100, + mp_max: 100, + spawn_index: connection.get_free_spawn_index(), + bnpc_base: 13498, // TODO: changing this prevents it from spawning... + bnpc_name: 405, + spawner_id: connection.player_data.actor_id, + parent_actor_id: INVALID_OBJECT_ID, // TODO: make default? + object_kind: ObjectKind::BattleNpc, + target_id: INVALID_OBJECT_ID as u64, + level: 1, + battalion: 4, + model_chara: 297, + pos: connection.position, ..Default::default() }, ..Default::default() diff --git a/src/world/connection.rs b/src/world/connection.rs index 7ba9659..26c156e 100644 --- a/src/world/connection.rs +++ b/src/world/connection.rs @@ -32,6 +32,8 @@ pub struct ZoneConnection { pub zone: Option, pub spawn_index: u8, + + pub position: Position, } impl ZoneConnection { diff --git a/src/world/ipc/common_spawn.rs b/src/world/ipc/common_spawn.rs index 052b9b3..ef24ea9 100644 --- a/src/world/ipc/common_spawn.rs +++ b/src/world/ipc/common_spawn.rs @@ -67,8 +67,10 @@ pub struct CommonSpawn { pub u14: u32, pub u15: u32, - pub bnpc_base: u32, // See BNpcBase Excel sheet - pub bnpc_name: u32, // See BNpcName Excel sheet + /// See BNpcBase Excel sheet + pub bnpc_base: u32, + /// See BNpcName Excel sheet + pub bnpc_name: u32, pub unk3: [u8; 8], pub director_id: u32, // FIXME: i think the next three are in the wrong order pub spawner_id: u32, @@ -80,7 +82,8 @@ pub struct CommonSpawn { pub mp_curr: u16, pub mp_max: u16, pub unk: u16, - pub model_chara: u16, // See ModelChara Excel sheet + /// See ModelChara Excel sheet + pub model_chara: u16, pub rotation: u16, // assumed pub current_mount: u16, // assumed pub active_minion: u16, // assumed @@ -88,15 +91,20 @@ pub struct CommonSpawn { pub u24: u8, // assumed pub u25: u8, // assumed pub u26: u8, // assumed + /// Must be unique for each actor. pub spawn_index: u8, pub mode: CharacterMode, + /// Argument used in CharacterMode. + // TODO: move to enum pub persistent_emote: u8, pub object_kind: ObjectKind, pub subtype: u8, pub voice: u8, - pub enemy_type: u8, pub unk27: u8, + /// See Battalion Excel sheet. Used for determing whether it's friendy or an enemy. + pub battalion: u8, pub level: u8, + /// See ClassJob Excel sheet. pub class_job: u8, pub unk28: u8, pub unk29: u8, diff --git a/src/world/ipc/mod.rs b/src/world/ipc/mod.rs index 4f957fc..0ff4af6 100644 --- a/src/world/ipc/mod.rs +++ b/src/world/ipc/mod.rs @@ -237,6 +237,8 @@ pub enum ClientZoneIpcType { Unk14 = 0x87, // Sent by the client when a character performs an action ActionRequest = 0x213, + /// Sent by the client for unknown reasons, it's a bunch of numbers? + Unk15 = 0x10B, } #[binrw] @@ -365,7 +367,9 @@ pub enum ClientZoneIpcData { #[br(pre_assert(*magic == ClientZoneIpcType::UpdatePositionHandler))] UpdatePositionHandler { // TODO: full of possibly interesting information - unk: [u8; 24], + unk: [u8; 8], // not empty + #[brw(pad_after = 4)] // empty + position: Position, }, #[br(pre_assert(*magic == ClientZoneIpcType::LogOut))] LogOut { @@ -408,6 +412,8 @@ pub enum ClientZoneIpcData { }, #[br(pre_assert(*magic == ClientZoneIpcType::ActionRequest))] ActionRequest(ActionRequest), + #[br(pre_assert(*magic == ClientZoneIpcType::Unk15))] + Unk15 { unk: [u8; 632] }, } #[cfg(test)] diff --git a/src/world/ipc/npc_spawn.rs b/src/world/ipc/npc_spawn.rs index eb6f78a..14f16dd 100644 --- a/src/world/ipc/npc_spawn.rs +++ b/src/world/ipc/npc_spawn.rs @@ -16,12 +16,15 @@ mod tests { use binrw::BinRead; - use crate::world::ipc::{CharacterMode, ObjectKind}; + use crate::{ + common::INVALID_OBJECT_ID, + world::ipc::{CharacterMode, ObjectKind}, + }; use super::*; #[test] - fn read_npcspawn() { + fn read_carbuncle() { let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); d.push("resources/tests/npc_spawn.bin"); @@ -44,5 +47,35 @@ mod tests { assert_eq!(npc_spawn.common.mode, CharacterMode::Normal); assert_eq!(npc_spawn.common.object_kind, ObjectKind::BattleNpc); assert_eq!(npc_spawn.common.subtype, 2); + assert_eq!(npc_spawn.common.battalion, 0); + } + + #[test] + fn read_tiny_mandragora() { + let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + d.push("resources/tests/tiny_mandragora.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, 91); + assert_eq!(npc_spawn.common.hp_curr, 91); + assert_eq!(npc_spawn.common.mp_curr, 0); + assert_eq!(npc_spawn.common.mp_max, 0); + assert_eq!(npc_spawn.common.display_flags, 0); + assert_eq!(npc_spawn.common.pos.x, 116.99154); + assert_eq!(npc_spawn.common.pos.y, 76.64936); + assert_eq!(npc_spawn.common.pos.z, -187.02414); + assert_eq!(npc_spawn.common.model_chara, 297); + assert_eq!(npc_spawn.common.bnpc_base, 118); + assert_eq!(npc_spawn.common.bnpc_name, 405); + assert_eq!(npc_spawn.common.spawn_index, 14); + assert_eq!(npc_spawn.common.mode, CharacterMode::Normal); + assert_eq!(npc_spawn.common.object_kind, ObjectKind::BattleNpc); + assert_eq!(npc_spawn.common.subtype, 5); + assert_eq!(npc_spawn.common.battalion, 4); + assert_eq!(npc_spawn.common.parent_actor_id, INVALID_OBJECT_ID); + assert_eq!(npc_spawn.common.spawner_id, INVALID_OBJECT_ID); } }