From 97cdc66ec76d3dc8d0b68d26825292d4af437c3a Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Wed, 18 Jun 2025 20:38:55 -0400 Subject: [PATCH] Make !spawnnpc debug command networked Next step is allocating actor IDs properly, instead of using a fixed value. --- src/world/chat_handler.rs | 88 +++---------------------- src/world/common.rs | 3 + src/world/server.rs | 134 +++++++++++++++++++++++++++++++------- 3 files changed, 124 insertions(+), 101 deletions(-) diff --git a/src/world/chat_handler.rs b/src/world/chat_handler.rs index 7d63a26..8f72a42 100644 --- a/src/world/chat_handler.rs +++ b/src/world/chat_handler.rs @@ -1,10 +1,9 @@ use crate::{ - common::{CustomizeData, ObjectId, ObjectTypeId, timestamp_secs}, + common::{ObjectId, ObjectTypeId, timestamp_secs}, inventory::Storage, ipc::zone::{ - ActorControl, ActorControlCategory, ActorControlSelf, BattleNpcSubKind, ChatMessage, - CommonSpawn, EventStart, NpcSpawn, ObjectKind, OnlineStatus, ServerZoneIpcData, - ServerZoneIpcSegment, + ActorControl, ActorControlCategory, ActorControlSelf, ChatMessage, EventStart, NpcSpawn, + OnlineStatus, ServerZoneIpcData, ServerZoneIpcSegment, }, opcodes::ServerZoneIpcType, packet::{PacketSegment, SegmentData, SegmentType}, @@ -13,35 +12,6 @@ use crate::{ use super::{LuaPlayer, ZoneConnection}; -pub const CUSTOMIZE_DATA: CustomizeData = CustomizeData { - race: 4, - gender: 1, - age: 1, - height: 50, - subrace: 7, - face: 3, - hair: 5, - enable_highlights: 0, - skin_tone: 10, - right_eye_color: 75, - hair_tone: 50, - highlights: 0, - facial_features: 1, - facial_feature_color: 19, - eyebrows: 1, - left_eye_color: 75, - eyes: 1, - nose: 0, - jaw: 1, - mouth: 1, - lips_tone_fur_pattern: 169, - race_feature_size: 100, - race_feature_type: 1, - bust: 100, - face_paint: 0, - face_paint_color: 167, -}; - pub struct ChatHandler {} impl ChatHandler { @@ -55,58 +25,18 @@ impl ChatHandler { let parts: Vec<&str> = chat_message.message.split(' ').collect(); match parts[0] { "!spawnnpc" => { - let ipc = ServerZoneIpcSegment { - op_code: ServerZoneIpcType::NpcSpawn, - 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() - }), - ..Default::default() - }; - connection - .send_segment(PacketSegment { - source_actor: 0x106ad804, - target_actor: connection.player_data.actor_id, - segment_type: SegmentType::Ipc, - data: SegmentData::Ipc { data: ipc }, - }) + .handle + .send(ToServer::DebugNewNpc( + connection.id, + connection.player_data.actor_id, + )) .await; } "!spawnmonster" => { connection .handle - .send(ToServer::DebugNewNpc( + .send(ToServer::DebugNewEnemy( connection.id, connection.player_data.actor_id, )) diff --git a/src/world/common.rs b/src/world/common.rs index a1a7fbb..f2abfbc 100644 --- a/src/world/common.rs +++ b/src/world/common.rs @@ -88,7 +88,10 @@ pub enum ToServer { Disconnected(ClientId), /// A fatal error occured. FatalError(std::io::Error), + /// Spawn a friendly debug NPC. DebugNewNpc(ClientId, u32), + /// Spawn an enemy debug NPC. + DebugNewEnemy(ClientId, u32), } #[derive(Clone, Debug)] diff --git a/src/world/server.rs b/src/world/server.rs index 4a95107..a4898d1 100644 --- a/src/world/server.rs +++ b/src/world/server.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use tokio::sync::mpsc::Receiver; use crate::{ - common::ObjectId, + common::{CustomizeData, ObjectId, ObjectTypeId}, ipc::zone::{ ActorControl, ActorControlCategory, ActorControlSelf, ActorControlTarget, BattleNpcSubKind, ClientTriggerCommand, CommonSpawn, NpcSpawn, ObjectKind, @@ -11,6 +11,36 @@ use crate::{ use super::{Actor, ClientHandle, ClientId, FromServer, ToServer}; +/// Used for the debug NPC. +pub const CUSTOMIZE_DATA: CustomizeData = CustomizeData { + race: 4, + gender: 1, + age: 1, + height: 50, + subrace: 7, + face: 3, + hair: 5, + enable_highlights: 0, + skin_tone: 10, + right_eye_color: 75, + hair_tone: 50, + highlights: 0, + facial_features: 1, + facial_feature_color: 19, + eyebrows: 1, + left_eye_color: 75, + eyes: 1, + nose: 0, + jaw: 1, + mouth: 1, + lips_tone_fur_pattern: 169, + race_feature_size: 100, + race_feature_type: 1, + bust: 100, + face_paint: 0, + face_paint_color: 167, +}; + #[derive(Debug, Clone)] enum NetworkedActor { Player(NpcSpawn), @@ -40,6 +70,7 @@ struct ClientState { #[derive(Default, Debug)] struct WorldServer { + to_remove: Vec, clients: HashMap, /// Indexed by zone id instances: HashMap, @@ -70,11 +101,24 @@ impl WorldServer { } None } + + /// Tell all the clients that a new NPC spawned. + fn send_npc(&mut self, spawn: NpcSpawn) { + // TODO: only send in the relevant instance + for (id, (handle, _)) in &mut self.clients { + let id = *id; + + let msg = FromServer::SpawnNPC(spawn.clone()); + + if handle.send(msg).is_err() { + self.to_remove.push(id); + } + } + } } pub async fn server_main_loop(mut recv: Receiver) -> Result<(), std::io::Error> { let mut data = WorldServer::default(); - let mut to_remove = Vec::new(); while let Some(msg) = recv.recv().await { match msg { @@ -162,7 +206,7 @@ pub async fn server_main_loop(mut recv: Receiver) -> Result<(), std::i ); if handle.send(msg).is_err() { - to_remove.push(id); + data.to_remove.push(id); } } } @@ -188,7 +232,7 @@ pub async fn server_main_loop(mut recv: Receiver) -> Result<(), std::i let msg = FromServer::ActorDespawn(actor_id); if handle.send(msg).is_err() { - to_remove.push(id); + data.to_remove.push(id); } } } @@ -203,7 +247,7 @@ pub async fn server_main_loop(mut recv: Receiver) -> Result<(), std::i let msg = FromServer::Message(msg.clone()); if handle.send(msg).is_err() { - to_remove.push(id); + data.to_remove.push(id); } } } @@ -232,7 +276,7 @@ pub async fn server_main_loop(mut recv: Receiver) -> Result<(), std::i let msg = FromServer::ActorMove(actor_id, position, rotation); if handle.send(msg).is_err() { - to_remove.push(id); + data.to_remove.push(id); } } } @@ -255,7 +299,7 @@ pub async fn server_main_loop(mut recv: Receiver) -> Result<(), std::i }); if handle.send(msg).is_err() { - to_remove.push(id); + data.to_remove.push(id); } } _ => {} @@ -275,7 +319,7 @@ pub async fn server_main_loop(mut recv: Receiver) -> Result<(), std::i ); if handle.send(msg).is_err() { - to_remove.push(id); + data.to_remove.push(id); } } ClientTriggerCommand::ChangePose { unk1, pose } => { @@ -290,7 +334,7 @@ pub async fn server_main_loop(mut recv: Receiver) -> Result<(), std::i ); if handle.send(msg).is_err() { - to_remove.push(id); + data.to_remove.push(id); } } ClientTriggerCommand::ReapplyPose { unk1, pose } => { @@ -305,7 +349,7 @@ pub async fn server_main_loop(mut recv: Receiver) -> Result<(), std::i ); if handle.send(msg).is_err() { - to_remove.push(id); + data.to_remove.push(id); } } _ => tracing::warn!("Server doesn't know what to do with {:#?}", trigger), @@ -313,6 +357,60 @@ pub async fn server_main_loop(mut recv: Receiver) -> Result<(), std::i } } ToServer::DebugNewNpc(_from_id, from_actor_id) => { + let spawn; + { + let Some(instance) = data.find_actor_instance_mut(from_actor_id) else { + break; + }; + + let Some(actor) = instance.find_actor(ObjectId(from_actor_id)) else { + break; + }; + + let NetworkedActor::Player(player) = actor else { + break; + }; + + spawn = NpcSpawn { + aggression_mode: 1, + common: CommonSpawn { + hp_curr: 100, + hp_max: 100, + mp_curr: 100, + mp_max: 100, + look: CUSTOMIZE_DATA, + bnpc_base: 13498, + bnpc_name: 10261, + object_kind: ObjectKind::BattleNpc(BattleNpcSubKind::Enemy), + target_id: ObjectTypeId { + object_id: ObjectId(from_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: player.common.pos, + ..Default::default() + }, + ..Default::default() + }; + + instance.insert_npc(ObjectId(1), spawn.clone()); + } + + data.send_npc(spawn); + } + ToServer::DebugNewEnemy(_from_id, from_actor_id) => { let spawn; { let Some(instance) = data.find_actor_instance_mut(from_actor_id) else { @@ -350,28 +448,20 @@ pub async fn server_main_loop(mut recv: Receiver) -> Result<(), std::i instance.insert_npc(ObjectId(1), spawn.clone()); } - for (id, (handle, _)) in &mut data.clients { - let id = *id; - - let msg = FromServer::SpawnNPC(spawn.clone()); - - if handle.send(msg).is_err() { - to_remove.push(id); - } - } + data.send_npc(spawn); } ToServer::Disconnected(from_id) => { - to_remove.push(from_id); + data.to_remove.push(from_id); } ToServer::FatalError(err) => return Err(err), } // Remove any clients that errored out - for remove_id in &to_remove { + for remove_id in data.to_remove.clone() { // remove any actors they had let mut actor_id = None; for (id, (handle, _)) in &mut data.clients { - if *id == *remove_id { + if *id == remove_id { actor_id = Some(handle.actor_id); } }