mirror of
https://github.com/redstrate/Kawari.git
synced 2025-04-24 08:07:45 +00:00
Add support for spawning NPCs, and a debug command to do it
Since the structs are so similar, I created a CommonSpawn struct to hold most of the interesting fields.
This commit is contained in:
parent
039f4d7f95
commit
bd67eb0127
8 changed files with 325 additions and 159 deletions
BIN
resources/tests/npc_spawn.bin
Normal file
BIN
resources/tests/npc_spawn.bin
Normal file
Binary file not shown.
|
@ -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,6 +282,7 @@ async fn main() {
|
|||
timestamp: timestamp_secs(),
|
||||
data: ServerZoneIpcData::PlayerSpawn(PlayerSpawn {
|
||||
content_id: CONTENT_ID,
|
||||
common: CommonSpawn {
|
||||
current_world_id: WORLD_ID,
|
||||
home_world_id: WORLD_ID,
|
||||
title: 1,
|
||||
|
@ -291,7 +292,7 @@ async fn main() {
|
|||
hp_max: 100,
|
||||
mp_curr: 100,
|
||||
mp_max: 100,
|
||||
model_type: 1,
|
||||
object_kind: ObjectKind::Player,
|
||||
gm_rank: 3,
|
||||
look: CUSTOMIZE_DATA,
|
||||
fc_tag: "LOCAL".to_string(),
|
||||
|
@ -311,6 +312,8 @@ async fn main() {
|
|||
pos: exit_position
|
||||
.unwrap_or(Position::default()),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,6 +48,7 @@ impl ChatHandler {
|
|||
data: ServerZoneIpcData::PlayerSpawn(PlayerSpawn {
|
||||
some_unique_id: 1,
|
||||
content_id: 1,
|
||||
common: CommonSpawn {
|
||||
current_world_id: WORLD_ID,
|
||||
home_world_id: WORLD_ID,
|
||||
title: 1,
|
||||
|
@ -54,7 +58,7 @@ impl ChatHandler {
|
|||
hp_max: 100,
|
||||
mp_curr: 100,
|
||||
mp_max: 100,
|
||||
model_type: 1,
|
||||
object_kind: ObjectKind::Player,
|
||||
gm_rank: 3,
|
||||
spawn_index: connection.get_free_spawn_index(),
|
||||
look: CUSTOMIZE_DATA,
|
||||
|
@ -73,6 +77,59 @@ impl ChatHandler {
|
|||
],
|
||||
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()
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
93
src/world/ipc/common_spawn.rs
Normal file
93
src/world/ipc/common_spawn.rs
Normal file
|
@ -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,
|
||||
}
|
|
@ -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 {
|
||||
|
|
50
src/world/ipc/npc_spawn.rs
Normal file
50
src/world/ipc/npc_spawn.rs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue