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. These special debug commands start with `!` and are custom to Kawari.
* `!setpos <x> <y> <z>`: Teleport to the specified location * `!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 * `!spawnnpc`: Spawn a NPC for debugging
* `!spawnmonster`: Spawn a monster for debugging * `!spawnmonster`: Spawn a monster for debugging
* `!playscene <id>`: Plays an event. Only some events are supported for now: * `!playscene <id>`: Plays an event. Only some events are supported for now:
* Territory `181`, Event `1245185` plays the Limsa opening sequence * Territory `181`, Event `1245185` plays the Limsa opening sequence
* Territory `182`, Event `1245187` plays the Ul'dah opening sequence * Territory `182`, Event `1245187` plays the Ul'dah opening sequence
* Territory `183`, Event `1245186` plays the Gridania opening sequence * Territory `183`, Event `1245186` plays the Gridania opening sequence
* `!spawnclone`: Spawn a clone of yourself
### GM commands ### GM commands

View file

@ -14,9 +14,8 @@ use kawari::packet::{
send_packet, send_packet,
}; };
use kawari::world::ipc::{ use kawari::world::ipc::{
ActionEffect, ActionResult, ClientZoneIpcData, CommonSpawn, DisplayFlag, EffectKind, ActionEffect, ActionResult, ClientZoneIpcData, EffectKind, GameMasterCommandType,
GameMasterCommandType, GameMasterRank, ObjectKind, OnlineStatus, PlayerSubKind, GameMasterRank, OnlineStatus, ServerZoneIpcData, ServerZoneIpcSegment, SocialListRequestType,
ServerZoneIpcData, ServerZoneIpcSegment, SocialListRequestType,
}; };
use kawari::world::{ use kawari::world::{
Actor, ClientHandle, ClientId, EffectsBuilder, FromServer, Inventory, LuaPlayer, PlayerData, Actor, ClientHandle, ClientId, EffectsBuilder, FromServer, Inventory, LuaPlayer, PlayerData,
@ -375,17 +374,9 @@ async fn client_loop(
.unwrap(); .unwrap();
} }
ClientZoneIpcData::FinishLoading { .. } => { ClientZoneIpcData::FinishLoading { .. } => {
let chara_details =
database.find_chara_make(connection.player_data.content_id);
// send player spawn // send player spawn
{ {
let ipc; let ipc = ServerZoneIpcSegment {
{
let mut game_data = game_data.lock().unwrap();
let equipped = &connection.player_data.inventory.equipped;
ipc = ServerZoneIpcSegment {
op_code: ServerZoneIpcType::PlayerSpawn, op_code: ServerZoneIpcType::PlayerSpawn,
timestamp: timestamp_secs(), timestamp: timestamp_secs(),
data: ServerZoneIpcData::PlayerSpawn(PlayerSpawn { data: ServerZoneIpcData::PlayerSpawn(PlayerSpawn {
@ -395,64 +386,11 @@ async fn client_loop(
home_world_id: config.world.world_id, home_world_id: config.world.world_id,
gm_rank: GameMasterRank::Debug, gm_rank: GameMasterRank::Debug,
online_status: OnlineStatus::GameMasterBlue, online_status: OnlineStatus::GameMasterBlue,
common: CommonSpawn { common: connection.get_player_common_spawn(exit_position, exit_rotation),
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()
},
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()
}; };
}
connection connection
.send_segment(PacketSegment { .send_segment(PacketSegment {
@ -538,7 +476,6 @@ async fn client_loop(
level: 100, level: 100,
one: 1, one: 1,
name: "INVALID".to_string(), name: "INVALID".to_string(),
fc_tag: "LOCAL".to_string(),
..Default::default() ..Default::default()
}], }],
}), }),

View file

@ -69,9 +69,7 @@ impl ChatHandler {
}) })
.await; .await;
} }
"!spawnactor" => { "!spawnplayer" => {
tracing::info!("Spawning actor...");
let config = get_config(); let config = get_config();
// send player spawn // send player spawn
@ -98,7 +96,6 @@ impl ChatHandler {
object_kind: ObjectKind::Player(PlayerSubKind::Player), object_kind: ObjectKind::Player(PlayerSubKind::Player),
spawn_index: connection.get_free_spawn_index(), spawn_index: connection.get_free_spawn_index(),
look: CUSTOMIZE_DATA, look: CUSTOMIZE_DATA,
fc_tag: "LOCAL".to_string(),
display_flags: DisplayFlag::INVISIBLE display_flags: DisplayFlag::INVISIBLE
| DisplayFlag::HIDE_HEAD | DisplayFlag::HIDE_HEAD
| DisplayFlag::UNK, | DisplayFlag::UNK,
@ -156,8 +153,6 @@ impl ChatHandler {
} }
} }
"!spawnnpc" => { "!spawnnpc" => {
// spawn another one of us
{
let ipc = ServerZoneIpcSegment { let ipc = ServerZoneIpcSegment {
unk1: 20, unk1: 20,
unk2: 0, unk2: 0,
@ -207,7 +202,6 @@ impl ChatHandler {
}) })
.await; .await;
} }
}
"!spawnmonster" => { "!spawnmonster" => {
let actor = Actor { let actor = Actor {
id: ObjectId(0x106ad804), id: ObjectId(0x106ad804),
@ -325,6 +319,35 @@ impl ChatHandler {
.unwrap() .unwrap()
.enter_territory(lua_player, connection.zone.as_ref().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!"), _ => tracing::info!("Unrecognized debug command!"),
} }
} }

View file

@ -22,9 +22,9 @@ use super::{
chat_handler::CUSTOMIZE_DATA, chat_handler::CUSTOMIZE_DATA,
ipc::{ ipc::{
ActorControlSelf, ActorMove, ActorSetPos, BattleNpcSubKind, ClientZoneIpcSegment, ActorControlSelf, ActorMove, ActorSetPos, BattleNpcSubKind, ClientZoneIpcSegment,
CommonSpawn, ContainerInfo, ContainerType, Equip, InitZone, ItemInfo, NpcSpawn, ObjectKind, CommonSpawn, ContainerInfo, ContainerType, DisplayFlag, Equip, InitZone, ItemInfo,
ServerZoneIpcData, ServerZoneIpcSegment, StatusEffect, StatusEffectList, UpdateClassInfo, NpcSpawn, ObjectKind, PlayerSubKind, ServerZoneIpcData, ServerZoneIpcSegment, StatusEffect,
WeatherChange, StatusEffectList, UpdateClassInfo, WeatherChange,
}, },
}; };
@ -714,4 +714,43 @@ impl ZoneConnection {
}) })
.await; .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()
}
}
} }