From 10c1369119daf73d71e3c88051fb619abe9bc8e4 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Mon, 14 Apr 2025 16:03:57 -0400 Subject: [PATCH] Add opcode for despawning actors, send that when changing zones This is to workaround a bigger bug where I don't properly enclose actors in their zone, so you can hit an assert while traveling between zones. But this is something that has been needed anyway, and also fixes that. --- resources/opcodes.json | 5 ++++ src/bin/kawari-world.rs | 36 ++++++++++++++++++++----- src/world/actor.rs | 1 + src/world/chat_handler.rs | 7 ----- src/world/connection.rs | 55 ++++++++++++++++++++++++++++++++++----- src/world/ipc/mod.rs | 2 ++ 6 files changed, 86 insertions(+), 20 deletions(-) diff --git a/resources/opcodes.json b/resources/opcodes.json index 91a1a39..75e63fd 100644 --- a/resources/opcodes.json +++ b/resources/opcodes.json @@ -154,6 +154,11 @@ "name": "Equip", "opcode": 468, "size": 72 + }, + { + "name": "ActorFreeSpawn", + "opcode": 144, + "size": 8 } ], "ClientZoneIpcType": [ diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index 96ebc55..2053739 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -65,8 +65,14 @@ async fn main_loop(mut recv: Receiver) -> Result<(), std::io::Error> { if id == from_id { // send existing player data for (id, common) in &data.actors { - let msg = - FromServer::ActorSpawn(Actor { id: *id, hp: 100 }, common.clone()); + let msg = FromServer::ActorSpawn( + Actor { + id: *id, + hp: 100, + spawn_index: 0, + }, + common.clone(), + ); handle.send(msg).unwrap(); } @@ -107,6 +113,23 @@ async fn main_loop(mut recv: Receiver) -> Result<(), std::io::Error> { } } } + ToServer::ActorDespawned(from_id, actor_id) => { + data.actors.remove(&ObjectId(actor_id)); + + for (id, handle) in &mut data.clients { + let id = *id; + + if id == from_id { + continue; + } + + let msg = FromServer::ActorDespawn(actor_id); + + if handle.send(msg).is_err() { + to_remove.push(id); + } + } + } ToServer::ActorMoved(from_id, actor_id, position, rotation) => { for (id, handle) in &mut data.clients { let id = *id; @@ -453,7 +476,7 @@ async fn client_loop( exit_rotation = None; // tell the other players we're here - connection.handle.send(ToServer::ActorSpawned(connection.id, Actor { id: ObjectId(connection.player_data.actor_id), hp: 100 }, connection.get_player_common_spawn(None, None))).await; + connection.handle.send(ToServer::ActorSpawned(connection.id, Actor { id: ObjectId(connection.player_data.actor_id), hp: 100, spawn_index: 0 }, connection.get_player_common_spawn(None, None))).await; } ClientZoneIpcData::Unk1 { category, param1, .. @@ -713,7 +736,7 @@ async fn client_loop( .copy_from_slice(&effects_builder.effects); if let Some(actor) = - connection.get_actor(request.target.object_id) + connection.get_actor_mut(request.target.object_id) { for effect in &effects_builder.effects { match effect.kind { @@ -1014,10 +1037,9 @@ async fn client_loop( msg = internal_recv.recv() => match msg { Some(msg) => match msg { FromServer::Message(msg)=> connection.send_message(&msg).await, - FromServer::ActorSpawn(actor, common) => { - connection.spawn_actor(actor, common).await - }, + FromServer::ActorSpawn(actor, common) => connection.spawn_actor(actor, common).await, FromServer::ActorMove(actor_id, position, rotation) => connection.set_actor_position(actor_id, position, rotation).await, + FromServer::ActorDespawn(actor_id) => connection.remove_actor(actor_id).await }, None => break, } diff --git a/src/world/actor.rs b/src/world/actor.rs index 059727e..964c557 100644 --- a/src/world/actor.rs +++ b/src/world/actor.rs @@ -4,4 +4,5 @@ use crate::common::ObjectId; pub struct Actor { pub id: ObjectId, pub hp: u32, + pub spawn_index: u32, // TODO: local to each connection, terrible place to put this } diff --git a/src/world/chat_handler.rs b/src/world/chat_handler.rs index 6246f80..7258968 100644 --- a/src/world/chat_handler.rs +++ b/src/world/chat_handler.rs @@ -203,13 +203,6 @@ impl ChatHandler { .await; } "!spawnmonster" => { - let actor = Actor { - id: ObjectId(0x106ad804), - hp: 100, - }; - - connection.add_actor(actor); - // spawn a tiny mandragora { let ipc = ServerZoneIpcSegment { diff --git a/src/world/connection.rs b/src/world/connection.rs index bb7ba47..ed8e55b 100644 --- a/src/world/connection.rs +++ b/src/world/connection.rs @@ -60,6 +60,8 @@ pub enum FromServer { ActorSpawn(Actor, CommonSpawn), /// An actor moved to a new position. ActorMove(u32, Position, f32), + // An actor has despawned. + ActorDespawn(u32), } #[derive(Debug, Clone)] @@ -98,6 +100,7 @@ pub enum ToServer { Message(ClientId, String), ActorSpawned(ClientId, Actor, CommonSpawn), ActorMoved(ClientId, u32, Position, f32), + ActorDespawned(ClientId, u32), ZoneLoaded(ClientId), Disconnected(ClientId), FatalError(std::io::Error), @@ -310,11 +313,12 @@ impl ZoneConnection { .await; } - pub async fn spawn_actor(&mut self, actor: Actor, mut common: CommonSpawn) { + pub async fn spawn_actor(&mut self, mut actor: Actor, mut common: CommonSpawn) { // There is no reason for us to spawn our own player again. It's probably a bug!' assert!(actor.id.0 != self.player_data.actor_id); - common.spawn_index = self.get_free_spawn_index(); + actor.spawn_index = self.get_free_spawn_index() as u32; + common.spawn_index = actor.spawn_index as u8; let ipc = ServerZoneIpcSegment { unk1: 20, @@ -334,6 +338,38 @@ impl ZoneConnection { segment_type: SegmentType::Ipc { data: ipc }, }) .await; + + self.actors.push(actor); + } + + pub async fn remove_actor(&mut self, actor_id: u32) { + if let Some(actor) = self.get_actor(ObjectId(actor_id)).cloned() { + let ipc = ServerZoneIpcSegment { + unk1: 20, + unk2: 0, + op_code: ServerZoneIpcType::ActorFreeSpawn, + server_id: 0, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::ActorFreeSpawn { + spawn_index: actor.spawn_index, + actor_id, + }, + }; + + self.send_segment(PacketSegment { + source_actor: actor.id.0, + target_actor: self.player_data.actor_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; + + self.actors.remove( + self.actors + .iter() + .position(|actor| actor.id == ObjectId(actor_id)) + .unwrap(), + ); + } } pub async fn update_class_info(&mut self) { @@ -359,6 +395,13 @@ impl ZoneConnection { } pub async fn change_zone(&mut self, new_zone_id: u16) { + // tell everyone we're gone + // TODO: check if we ever sent an initial ActorSpawn packet first, before sending this. + // the connection already checks to see if the actor already exists, so it's seems harmless if we do + self.handle + .send(ToServer::ActorDespawned(self.id, self.player_data.actor_id)) + .await; + { let mut game_data = self.gamedata.lock().unwrap(); self.zone = Some(Zone::load(&mut game_data.game_data, new_zone_id)); @@ -708,12 +751,12 @@ impl ZoneConnection { .await; } - pub fn add_actor(&mut self, actor: Actor) { - self.actors.push(actor); + pub fn get_actor_mut(&mut self, id: ObjectId) -> Option<&mut Actor> { + self.actors.iter_mut().find(|actor| actor.id == id) } - pub fn get_actor(&mut self, id: ObjectId) -> Option<&mut Actor> { - self.actors.iter_mut().find(|actor| actor.id == id) + pub fn get_actor(&mut self, id: ObjectId) -> Option<&Actor> { + self.actors.iter().find(|actor| actor.id == id) } pub async fn actor_control_self(&mut self, actor_control: ActorControlSelf) { diff --git a/src/world/ipc/mod.rs b/src/world/ipc/mod.rs index 538f34a..160b03f 100644 --- a/src/world/ipc/mod.rs +++ b/src/world/ipc/mod.rs @@ -214,6 +214,8 @@ pub enum ServerZoneIpcData { ActionResult(ActionResult), /// Sent to to the client to update their appearance Equip(Equip), + /// Sent to the client to free up a spawn index + ActorFreeSpawn { spawn_index: u32, actor_id: u32 }, } #[binrw]