Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions game/src/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ pub struct GameController {
pub base_stats_table: BaseStat,
pub hero_list: DashMap<i32, character::Model>,
pub clan_ally_manager: Arc<RwLock<ClanAllyManager>>,
// Global registry: world object_id -> player actor
player_by_object_id: DashMap<i32, ActorRef<PlayerClient>>,
}

impl GameController {
Expand All @@ -58,6 +60,7 @@ impl GameController {
hero_list: DashMap::new(),
online_chars: DashMap::new(),
clan_ally_manager: Arc::new(RwLock::new(ClanAllyManager::new(db_pool.clone()).await)),
player_by_object_id: DashMap::new(),
}
}
pub async fn set_ls_actor(&self, actor: ActorRef<LoginServerClient>) {
Expand Down Expand Up @@ -178,6 +181,22 @@ impl GameController {
pub fn broadcast_packet(&self, packet: impl SendablePacket + Clone + Send + 'static) {
self.broadcast_packet_with_filter(packet, None);
}

/// Register a player actor by its global `object_id`.
/// Returns previous actor if any was registered for that id.
pub fn register_player_object(&self, object_id: i32, actor: ActorRef<PlayerClient>) -> Option<ActorRef<PlayerClient>> {
self.player_by_object_id.insert(object_id, actor)
}

/// Remove a player from the registry by its `object_id`.
pub fn unregister_player_object(&self, object_id: i32) {
self.player_by_object_id.remove(&object_id);
}

/// Get a player actor by global `object_id`.
pub fn get_player_by_object_id(&self, object_id: i32) -> Option<ActorRef<PlayerClient>> {
self.player_by_object_id.get(&object_id).map(|r| r.clone())
}
}

#[cfg(test)]
Expand All @@ -201,6 +220,7 @@ impl GameController {
hero_list: DashMap::new(),
online_chars: DashMap::new(),
clan_ally_manager: Arc::new(RwLock::new(ClanAllyManager::default())),
player_by_object_id: DashMap::new(),
}
}
}
Expand Down
7 changes: 6 additions & 1 deletion game/src/cp_factory.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::packets::from_client::action::Action;
use crate::packets::from_client::auth::AuthLogin;
use crate::packets::from_client::char_create::CreateCharRequest;
use crate::packets::from_client::char_restore::RestoreChar;
Expand Down Expand Up @@ -48,6 +49,7 @@ pub enum PlayerPackets {
ReqSkillCoolTime(ReqSkillCoolTime),
ValidatePosition(ValidatePosition),
StopMove(StopMove),
Action(Action),
}

pub fn build_client_packet(mut data: BytesMut) -> anyhow::Result<PlayerPackets> {
Expand All @@ -68,7 +70,9 @@ pub fn build_client_packet(mut data: BytesMut) -> anyhow::Result<PlayerPackets>
CreateCharRequest::PACKET_ID => Ok(PlayerPackets::CreateCharRequest(
CreateCharRequest::read(data)?,
)),
RequestMoveToLocation::PACKET_ID => Ok(PlayerPackets::MoveToLocation(RequestMoveToLocation::read(data)?)),
RequestMoveToLocation::PACKET_ID => Ok(PlayerPackets::MoveToLocation(
RequestMoveToLocation::read(data)?,
)),
RequestRestart::PACKET_ID => Ok(PlayerPackets::ReqRestart(RequestRestart::read(data)?)),
Logout::PACKET_ID => Ok(PlayerPackets::Logout(Logout::read(data)?)),
DeleteChar::PACKET_ID => Ok(PlayerPackets::DeleteChar(DeleteChar::read(data)?)),
Expand All @@ -81,6 +85,7 @@ pub fn build_client_packet(mut data: BytesMut) -> anyhow::Result<PlayerPackets>
ReqSkillCoolTime::PACKET_ID => Ok(PlayerPackets::ReqSkillCoolTime(ReqSkillCoolTime::read(
data,
)?)),
Action::PACKET_ID => Ok(PlayerPackets::Action(Action::read(data)?)),
0xD0 => build_ex_client_packet(data),
_ => {
error!("Unknown Player packet ID: 0x{:02X}", packet_id[0]);
Expand Down
77 changes: 77 additions & 0 deletions game/src/packets/from_client/action.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use crate::packets::to_client::TargetSelected;
use crate::pl_client::{GetCharInfo, PlayerClient};
use bytes::BytesMut;
use kameo::message::{Context, Message};
use l2_core::shared_packets::common::ReadablePacket;
use l2_core::shared_packets::read::ReadablePacketBuffer;
use l2_core::shared_packets::write::SendablePacketBuffer;
use tracing::{error, instrument};

#[derive(Debug, Clone)]
pub struct Action {
pub buffer: SendablePacketBuffer,
pub object_id: i32,
pub origin_x: i32,
pub origin_y: i32,
pub origin_z: i32,
pub action: u8,
}

impl ReadablePacket for Action {
const PACKET_ID: u8 = 0x1F;
const EX_PACKET_ID: Option<u16> = None;
fn read(data: BytesMut) -> anyhow::Result<Self> {
let mut buffer = ReadablePacketBuffer::new(data);
let object_id = buffer.read_i32()?;
let origin_x = buffer.read_i32()?;
let origin_y = buffer.read_i32()?;
let origin_z = buffer.read_i32()?;
let action = buffer.read_byte()?;
Ok(Self {
buffer: SendablePacketBuffer::empty(),
object_id,
origin_x,
origin_y,
origin_z,
action,
})
}
}

impl Message<Action> for PlayerClient {
type Reply = anyhow::Result<()>;
#[instrument(skip(self, _ctx))]
async fn handle(
&mut self,
msg: Action,
_ctx: &mut Context<Self, Self::Reply>,
) -> anyhow::Result<()> {
let level = self.try_get_selected_char()?.char_model.level;
match msg.action {
0 => {
if let Some(target_actor) = self.controller.get_player_by_object_id(msg.object_id) {
// store selected target mapping
let other_pl = target_actor.ask(GetCharInfo).await?;
self.selected_target = Some((msg.object_id, target_actor));
// notify client about target selection
self.send_packet(TargetSelected::new(
msg.object_id,
i16::from(level - other_pl.char_model.level),
)?)
.await?;
} else {
// the target not found in world registry; ignore or clear selection
self.selected_target = None;
}
}
1 => { //shift
}
_ => {
//invalid action
error!("Invalid action: {}", msg.action);
}
}

Ok(())
}
}
4 changes: 4 additions & 0 deletions game/src/packets/from_client/enter_world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ impl Message<EnterWorld> for PlayerClient {
//todo: send message about premium items (maybe premium account or so?)
//todo: check if offline trade and cancel it

// register this player by global object_id in world registry
self.controller
.register_player_object(player.get_object_id(), ctx.actor_ref().clone());

self.controller
.add_player_to_world(&player, ctx.actor_ref())
.await?;
Expand Down
17 changes: 9 additions & 8 deletions game/src/packets/from_client/mod.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
pub mod protocol;
pub mod action;
pub mod auth;
pub mod noop;
pub mod new_char_request;
pub mod extended;
pub mod char_create;
pub mod logout;
pub mod delete_char;
pub mod char_restore;
pub mod char_select;
pub mod delete_char;
pub mod enter_world;
pub mod extended;
pub mod logout;
pub mod move_to_location;
pub mod restart;
pub mod new_char_request;
pub mod noop;
pub mod protocol;
pub mod req_skill_cooltime;
pub mod restart;
pub mod stop_move;
pub mod validate_position;
pub mod stop_move;
81 changes: 41 additions & 40 deletions game/src/packets/to_client/mod.rs
Original file line number Diff line number Diff line change
@@ -1,56 +1,57 @@
mod protocol_response;
mod login_response;
mod char_selection;
mod new_char_response;
pub mod extended;
mod abnormal_status_update;
mod acquire_skill_list;
mod char_create_fail;
mod char_create_ok;
mod char_delete_fail;
mod char_selected;
mod user_info;
mod macro_list;
mod item_list;
mod shortcuts_init;
mod skill_list;
mod henna_info;
mod char_etc_status_update;
mod quest_list;
mod char_info;
mod char_move_to_location;
mod char_selected;
mod char_selection;
mod delete_object;
pub mod extended;
mod friend_list;
mod acquire_skill_list;
mod henna_info;
mod item_list;
mod login_response;
mod macro_list;
mod move_to;
mod abnormal_status_update;
mod system_message;
mod char_move_to_location;
mod new_char_response;
mod protocol_response;
mod quest_list;
mod relation_changed;
mod restart_resp;
mod shortcuts_init;
mod skill_cooltime;
mod char_info;
mod relation_changed;
mod delete_object;

pub use protocol_response::*;
pub use login_response::*;
pub use char_selection::*;
pub use new_char_response::*;
mod skill_list;
mod system_message;
mod target_selected;
mod user_info;
pub use abnormal_status_update::*;
pub use acquire_skill_list::*;
pub use char_create_fail::*;
pub use char_create_ok::*;
pub use char_delete_fail::*;
pub use char_etc_status_update::*;
pub use char_info::*;
pub use char_move_to_location::*;
pub use char_selected::*;
pub use user_info::*;
pub use macro_list::*;
pub use item_list::*;
pub use shortcuts_init::*;
pub use skill_list::*;
pub use char_selection::*;
pub use delete_object::*;
pub use friend_list::*;
pub use henna_info::*;
pub use char_etc_status_update::*;
pub use item_list::*;
pub use login_response::*;
pub use macro_list::*;
pub use move_to::*;
pub use new_char_response::*;
pub use protocol_response::*;
pub use quest_list::*;
pub use friend_list::*;
pub use relation_changed::*;
pub use restart_resp::*;
pub use shortcuts_init::*;
pub use skill_cooltime::*;
pub use acquire_skill_list::*;
pub use move_to::*;
pub use abnormal_status_update::*;
pub use skill_list::*;
pub use system_message::*;
pub use char_move_to_location::*;
pub use restart_resp::*;
pub use char_info::*;
pub use relation_changed::*;
pub use delete_object::*;
pub use target_selected::*;
pub use user_info::*;
38 changes: 38 additions & 0 deletions game/src/packets/to_client/target_selected.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use l2_core::shared_packets::write::SendablePacketBuffer;
use macro_common::SendablePacket;
use std::fmt::Debug;

#[derive(Debug, Clone, SendablePacket)]
pub struct TargetSelected {
pub buffer: SendablePacketBuffer,
}

impl TargetSelected {
pub const PACKET_ID: u8 = 0xB9;

pub fn new(target_id: i32, level_diff: i16) -> anyhow::Result<Self> {
let mut inst = Self {
buffer: SendablePacketBuffer::new(),
};
inst.buffer.write(Self::PACKET_ID)?;
inst.buffer.write_i32(1)?; // Grand Crusade
inst.buffer.write_i32(target_id)?;
inst.buffer.write_i16(level_diff)?;
inst.buffer.write_i32(0)?;
Ok(inst)
}
}

#[cfg(test)]
mod test {
use super::*;

#[tokio::test]
async fn test_target_selected() {
let mut packet = TargetSelected::new(1, 9).unwrap();
assert_eq!(
[185, 1, 0, 0, 0, 1, 0, 0, 0, 9, 0, 0, 0, 0, 0],
packet.buffer.get_data_mut(false)[2..]
);
}
}
10 changes: 10 additions & 0 deletions game/src/pl_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ pub struct PlayerClient {
status: ClientStatus,
account_chars: Option<Vec<Player>>,
selected_char: Option<i32>,
pub selected_target: Option<(i32, ActorRef<PlayerClient>)>,
pub packet_sender: Option<ActorRef<ConnectionActor<Self>>>,
session_key: Option<SessionKey>,
user: Option<user::Model>,
Expand Down Expand Up @@ -101,6 +102,7 @@ impl PlayerClient {
account_chars: None,
protocol: None,
user: None,
selected_target: None,
session_key: None,
selected_char: None,
packet_sender: None,
Expand Down Expand Up @@ -466,6 +468,10 @@ impl Actor for PlayerClient {
err: PanicError,
) -> anyhow::Result<ControlFlow<ActorStopReason>> {
error!("Player client {} panicked: {:?}", self.ip, &err);
// Ensure we cleanup world registries even on panic
if let Ok(player) = self.try_get_selected_char() {
self.controller.unregister_player_object(player.get_object_id());
}
if let Some(sender) = self.packet_sender.take() {
let _ = sender.stop_gracefully().await;
sender.wait_for_shutdown().await;
Expand All @@ -492,6 +498,10 @@ impl Actor for PlayerClient {
}
s.wait_for_shutdown().await;
}
// Always attempt to unregister the player from world registry if we have it
if let Ok(player) = self.try_get_selected_char() {
self.controller.unregister_player_object(player.get_object_id());
}
let Some(user) = self.user.as_ref() else {
return Ok(());
};
Expand Down