1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-04-19 22:36:49 +00:00

Add !spawnclone command, rename !spawnactor

Curiously the spawned clone shows up as a player, despite not using the
NPCSpawn packet. This might be a suitable workaround for the
PlayerSpawn packet being buggy from our side.
This commit is contained in:
Joshua Goins 2025-04-01 19:15:08 -04:00
parent b1653f0808
commit 4f8f0d1fe2
4 changed files with 124 additions and 124 deletions

View file

@ -85,13 +85,14 @@ This feature is still a work-in-progress, and not all data is imported yet.
These special debug commands start with `!` and are custom to Kawari.
* `!setpos <x> <y> <z>`: Teleport to the specified location
* `!spawnactor`: Spawn another actor for debugging
* `!spawnplayer`: Spawn another player for debugging, not known to work at the moment
* `!spawnnpc`: Spawn a NPC for debugging
* `!spawnmonster`: Spawn a monster for debugging
* `!playscene <id>`: Plays an event. Only some events are supported for now:
* Territory `181`, Event `1245185` plays the Limsa opening sequence
* Territory `182`, Event `1245187` plays the Ul'dah opening sequence
* Territory `183`, Event `1245186` plays the Gridania opening sequence
* `!spawnclone`: Spawn a clone of yourself
### GM commands

View file

@ -14,9 +14,8 @@ use kawari::packet::{
send_packet,
};
use kawari::world::ipc::{
ActionEffect, ActionResult, ClientZoneIpcData, CommonSpawn, DisplayFlag, EffectKind,
GameMasterCommandType, GameMasterRank, ObjectKind, OnlineStatus, PlayerSubKind,
ServerZoneIpcData, ServerZoneIpcSegment, SocialListRequestType,
ActionEffect, ActionResult, ClientZoneIpcData, EffectKind, GameMasterCommandType,
GameMasterRank, OnlineStatus, ServerZoneIpcData, ServerZoneIpcSegment, SocialListRequestType,
};
use kawari::world::{
Actor, ClientHandle, ClientId, EffectsBuilder, FromServer, Inventory, LuaPlayer, PlayerData,
@ -375,17 +374,9 @@ async fn client_loop(
.unwrap();
}
ClientZoneIpcData::FinishLoading { .. } => {
let chara_details =
database.find_chara_make(connection.player_data.content_id);
// send player spawn
{
let ipc;
{
let mut game_data = game_data.lock().unwrap();
let equipped = &connection.player_data.inventory.equipped;
ipc = ServerZoneIpcSegment {
let ipc = ServerZoneIpcSegment {
op_code: ServerZoneIpcType::PlayerSpawn,
timestamp: timestamp_secs(),
data: ServerZoneIpcData::PlayerSpawn(PlayerSpawn {
@ -395,64 +386,11 @@ async fn client_loop(
home_world_id: config.world.world_id,
gm_rank: GameMasterRank::Debug,
online_status: OnlineStatus::GameMasterBlue,
common: CommonSpawn {
class_job: connection.player_data.classjob_id,
name: chara_details.name,
hp_curr: connection.player_data.curr_hp,
hp_max: connection.player_data.max_hp,
mp_curr: connection.player_data.curr_mp,
mp_max: connection.player_data.max_mp,
object_kind: ObjectKind::Player(
PlayerSubKind::Player,
),
look: chara_details.chara_make.customize,
fc_tag: "LOCAL".to_string(),
display_flags: DisplayFlag::UNK,
models: [
game_data
.get_primary_model_id(equipped.head.id)
as u32,
game_data
.get_primary_model_id(equipped.body.id)
as u32,
game_data
.get_primary_model_id(equipped.hands.id)
as u32,
game_data
.get_primary_model_id(equipped.legs.id)
as u32,
game_data
.get_primary_model_id(equipped.feet.id)
as u32,
game_data
.get_primary_model_id(equipped.ears.id)
as u32,
game_data
.get_primary_model_id(equipped.neck.id)
as u32,
game_data.get_primary_model_id(
equipped.wrists.id,
)
as u32,
game_data.get_primary_model_id(
equipped.left_ring.id,
)
as u32,
game_data.get_primary_model_id(
equipped.right_ring.id,
)
as u32,
],
pos: exit_position
.unwrap_or(Position::default()),
rotation: exit_rotation.unwrap_or(0.0),
..Default::default()
},
common: connection.get_player_common_spawn(exit_position, exit_rotation),
..Default::default()
}),
..Default::default()
};
}
connection
.send_segment(PacketSegment {
@ -538,7 +476,6 @@ async fn client_loop(
level: 100,
one: 1,
name: "INVALID".to_string(),
fc_tag: "LOCAL".to_string(),
..Default::default()
}],
}),

View file

@ -69,9 +69,7 @@ impl ChatHandler {
})
.await;
}
"!spawnactor" => {
tracing::info!("Spawning actor...");
"!spawnplayer" => {
let config = get_config();
// send player spawn
@ -98,7 +96,6 @@ impl ChatHandler {
object_kind: ObjectKind::Player(PlayerSubKind::Player),
spawn_index: connection.get_free_spawn_index(),
look: CUSTOMIZE_DATA,
fc_tag: "LOCAL".to_string(),
display_flags: DisplayFlag::INVISIBLE
| DisplayFlag::HIDE_HEAD
| DisplayFlag::UNK,
@ -156,57 +153,54 @@ impl ChatHandler {
}
}
"!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,
object_kind: ObjectKind::BattleNpc(BattleNpcSubKind::Enemy),
target_id: ObjectTypeId {
object_id: ObjectId(connection.player_data.actor_id),
object_type: 0,
}, // target the player
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: connection.player_data.position,
..Default::default()
},
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,
object_kind: ObjectKind::BattleNpc(BattleNpcSubKind::Enemy),
target_id: ObjectTypeId {
object_id: ObjectId(connection.player_data.actor_id),
object_type: 0,
}, // target the player
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: connection.player_data.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;
}
connection
.send_segment(PacketSegment {
source_actor: 0x106ad804,
target_actor: connection.player_data.actor_id,
segment_type: SegmentType::Ipc { data: ipc },
})
.await;
}
"!spawnmonster" => {
let actor = Actor {
@ -325,6 +319,35 @@ impl ChatHandler {
.unwrap()
.enter_territory(lua_player, connection.zone.as_ref().unwrap());
}
"!spawnclone" => {
// spawn another one of us
let player = connection.player_data;
let mut common = connection
.get_player_common_spawn(Some(player.position), Some(player.rotation));
common.spawn_index = connection.get_free_spawn_index();
let ipc = ServerZoneIpcSegment {
unk1: 20,
unk2: 0,
op_code: ServerZoneIpcType::NpcSpawn,
server_id: 0,
timestamp: timestamp_secs(),
data: ServerZoneIpcData::NpcSpawn(NpcSpawn {
common,
..Default::default()
}),
};
connection
.send_segment(PacketSegment {
source_actor: 0x106ad804,
target_actor: connection.player_data.actor_id,
segment_type: SegmentType::Ipc { data: ipc },
})
.await;
}
_ => tracing::info!("Unrecognized debug command!"),
}
}

View file

@ -22,9 +22,9 @@ use super::{
chat_handler::CUSTOMIZE_DATA,
ipc::{
ActorControlSelf, ActorMove, ActorSetPos, BattleNpcSubKind, ClientZoneIpcSegment,
CommonSpawn, ContainerInfo, ContainerType, Equip, InitZone, ItemInfo, NpcSpawn, ObjectKind,
ServerZoneIpcData, ServerZoneIpcSegment, StatusEffect, StatusEffectList, UpdateClassInfo,
WeatherChange,
CommonSpawn, ContainerInfo, ContainerType, DisplayFlag, Equip, InitZone, ItemInfo,
NpcSpawn, ObjectKind, PlayerSubKind, ServerZoneIpcData, ServerZoneIpcSegment, StatusEffect,
StatusEffectList, UpdateClassInfo, WeatherChange,
},
};
@ -714,4 +714,43 @@ impl ZoneConnection {
})
.await;
}
pub fn get_player_common_spawn(
&mut self,
exit_position: Option<Position>,
exit_rotation: Option<f32>,
) -> CommonSpawn {
let mut game_data = self.gamedata.lock().unwrap();
let chara_details = self.database.find_chara_make(self.player_data.content_id);
let equipped = &self.player_data.inventory.equipped;
CommonSpawn {
class_job: self.player_data.classjob_id,
name: chara_details.name,
hp_curr: self.player_data.curr_hp,
hp_max: self.player_data.max_hp,
mp_curr: self.player_data.curr_mp,
mp_max: self.player_data.max_mp,
level: self.player_data.level,
object_kind: ObjectKind::Player(PlayerSubKind::Player),
look: chara_details.chara_make.customize,
display_flags: DisplayFlag::UNK,
models: [
game_data.get_primary_model_id(equipped.head.id) as u32,
game_data.get_primary_model_id(equipped.body.id) as u32,
game_data.get_primary_model_id(equipped.hands.id) as u32,
game_data.get_primary_model_id(equipped.legs.id) as u32,
game_data.get_primary_model_id(equipped.feet.id) as u32,
game_data.get_primary_model_id(equipped.ears.id) as u32,
game_data.get_primary_model_id(equipped.neck.id) as u32,
game_data.get_primary_model_id(equipped.wrists.id) as u32,
game_data.get_primary_model_id(equipped.left_ring.id) as u32,
game_data.get_primary_model_id(equipped.right_ring.id) as u32,
],
pos: exit_position.unwrap_or(Position::default()),
rotation: exit_rotation.unwrap_or(0.0),
..Default::default()
}
}
}