Skip to content

Commit cbbebca

Browse files
authored
Merge pull request #21 from artemijan/fb-broadcast-move-to-location
Fb broadcast move to location
2 parents 20db175 + 4102151 commit cbbebca

File tree

9 files changed

+376
-14
lines changed

9 files changed

+376
-14
lines changed

config/game.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ max_players: 5000
1818
rates:
1919
vitality_exp_multiplier: 2
2020
enable_vitality: false
21+
# Maximum distance a player can move in a single request (anti-cheat)
22+
# Default: 15000 game units
23+
#max_movement_distance: 15000
2124
enable_encryption: true
2225
#ip_config:
2326
# - subnet: 192.168.0.0/0

game/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ mod cp_factory;
1818
mod ls_client;
1919
mod lsp_factory;
2020
pub mod managers;
21+
mod movement;
2122
mod packets;
2223
mod pl_client;
2324
mod test_utils;

game/src/movement.rs

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
use std::time::Instant;
2+
use tokio::task::JoinHandle;
3+
4+
/// Calculate Euclidean distance between two 3D points (x1,y1,z1) -> (x2,y2,z2)
5+
/// Returns None if coordinate subtraction overflows
6+
pub fn calculate_distance(x1: i32, y1: i32, z1: i32, x2: i32, y2: i32, z2: i32) -> Option<f64> {
7+
let dx = x2.checked_sub(x1)?;
8+
let dy = y2.checked_sub(y1)?;
9+
let dz = z2.checked_sub(z1)?;
10+
11+
let dx = f64::from(dx);
12+
let dy = f64::from(dy);
13+
let dz = f64::from(dz);
14+
15+
Some((dx * dx + dy * dy + dz * dz).sqrt())
16+
}
17+
18+
/// Represents the current movement state of a player
19+
#[derive(Debug)]
20+
pub struct MovementState {
21+
/// Starting position (x, y, z)
22+
pub source_x: i32,
23+
pub source_y: i32,
24+
pub source_z: i32,
25+
26+
/// Destination position (x, y, z)
27+
pub dest_x: i32,
28+
pub dest_y: i32,
29+
pub dest_z: i32,
30+
31+
/// When the movement started
32+
pub start_time: Instant,
33+
34+
/// Movement speed in game units per second
35+
pub speed: f64,
36+
37+
/// Handle to the periodic broadcast task
38+
pub task_handle: Option<JoinHandle<()>>,
39+
}
40+
41+
impl MovementState {
42+
/// Create a new movement state
43+
pub fn new(
44+
source_x: i32,
45+
source_y: i32,
46+
source_z: i32,
47+
dest_x: i32,
48+
dest_y: i32,
49+
dest_z: i32,
50+
speed: u16,
51+
) -> Self {
52+
Self {
53+
source_x,
54+
source_y,
55+
source_z,
56+
dest_x,
57+
dest_y,
58+
dest_z,
59+
start_time: Instant::now(),
60+
speed: f64::from(speed),
61+
task_handle: None,
62+
}
63+
}
64+
65+
/// Calculate the total distance to travel
66+
pub fn total_distance(&self) -> Option<f64> {
67+
calculate_distance(
68+
self.source_x,
69+
self.source_y,
70+
self.source_z,
71+
self.dest_x,
72+
self.dest_y,
73+
self.dest_z,
74+
)
75+
}
76+
77+
/// Calculate how long the entire journey should take (in seconds)
78+
pub fn calculate_travel_duration(&self) -> f64 {
79+
let Some(distance) = self.total_distance() else {
80+
return 0.0;
81+
};
82+
if self.speed > 0.0 {
83+
distance / self.speed
84+
} else {
85+
0.0
86+
}
87+
}
88+
89+
/// Calculate the current interpolated position based on elapsed time
90+
pub fn calculate_current_position(&self) -> (i32, i32, i32) {
91+
let elapsed = self.start_time.elapsed().as_secs_f64();
92+
let duration = self.calculate_travel_duration();
93+
94+
if duration <= 0.0 || elapsed >= duration {
95+
// Already arrived or instant movement
96+
return (self.dest_x, self.dest_y, self.dest_z);
97+
}
98+
99+
let progress = (elapsed / duration).min(1.0);
100+
101+
let current_x =
102+
self.source_x + ((f64::from(self.dest_x) - f64::from(self.source_x)) * progress) as i32;
103+
let current_y =
104+
self.source_y + ((f64::from(self.dest_y) - f64::from(self.source_y)) * progress) as i32;
105+
let current_z =
106+
self.source_z + ((f64::from(self.dest_z) - f64::from(self.source_z)) * progress) as i32;
107+
108+
(current_x, current_y, current_z)
109+
}
110+
111+
/// Check if the player has arrived at the destination
112+
pub fn has_arrived(&self) -> bool {
113+
let elapsed = self.start_time.elapsed().as_secs_f64();
114+
let duration = self.calculate_travel_duration();
115+
elapsed >= duration
116+
}
117+
118+
/// Cancel the periodic broadcast task if it exists
119+
pub fn cancel_task(&mut self) {
120+
if let Some(handle) = self.task_handle.take() {
121+
handle.abort();
122+
}
123+
}
124+
}
125+
126+
impl Drop for MovementState {
127+
fn drop(&mut self) {
128+
self.cancel_task();
129+
}
130+
}
131+
132+
#[cfg(test)]
133+
mod tests {
134+
use super::*;
135+
136+
#[test]
137+
fn test_calculate_distance() {
138+
let state = MovementState::new(0, 0, 0, 300, 400, 0, 100);
139+
let distance = state.total_distance().unwrap();
140+
//float values can give 0.00000001 error, so we check for a range of 0.001
141+
assert!((distance - 500.0).abs() < 0.001); // 3-4-5 triangle
142+
}
143+
144+
#[test]
145+
fn test_calculate_travel_duration() {
146+
let state = MovementState::new(0, 0, 0, 500, 0, 0, 100);
147+
let duration = state.calculate_travel_duration();
148+
//float values can give 0.00000001 error, so we check for a range of 0.001
149+
assert!((duration - 5.0).abs() < 0.1); // 500 units at 100 units/sec = 5 seconds
150+
}
151+
152+
#[test]
153+
fn test_calculate_current_position_at_start() {
154+
let state = MovementState::new(0, 0, 0, 1000, 0, 0, 100);
155+
let (x, y, z) = state.calculate_current_position();
156+
// Should be at or very near start position
157+
assert!((0..10).contains(&x)); // Allow small movement
158+
assert_eq!(y, 0);
159+
assert_eq!(z, 0);
160+
}
161+
162+
#[test]
163+
fn test_has_arrived_immediately() {
164+
let state = MovementState::new(0, 0, 0, 0, 0, 0, 100);
165+
assert!(state.has_arrived()); // No distance = instant arrival
166+
}
167+
}

game/src/packets/from_client/enter_world.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ impl Message<EnterWorld> for PlayerClient {
5959
if self.get_status() != &ClientStatus::Entering {
6060
bail!("Not in entering state")
6161
}
62+
self.stop_movement();
6263
self.set_status(ClientStatus::InGame);
6364
let mut addresses = Vec::with_capacity(5);
6465
for i in 0..5 {

game/src/packets/from_client/move_to_location.rs

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
use crate::packets::to_client::CharMoveToLocation;
21
use bytes::BytesMut;
32
use kameo::message::{Context, Message};
43
use l2_core::shared_packets::common::ReadablePacket;
54
use l2_core::shared_packets::read::ReadablePacketBuffer;
6-
use tracing::{info, instrument};
5+
use tracing::{instrument, warn};
6+
use crate::movement::calculate_distance;
77

88
use crate::pl_client::PlayerClient;
99

@@ -36,22 +36,49 @@ impl ReadablePacket for RequestMoveToLocation {
3636

3737
impl Message<RequestMoveToLocation> for PlayerClient {
3838
type Reply = anyhow::Result<()>;
39-
#[instrument(skip(self, _ctx))]
39+
#[instrument(skip(self, ctx))]
4040
async fn handle(
4141
&mut self,
4242
msg: RequestMoveToLocation,
43-
_ctx: &mut Context<Self, Self::Reply>,
43+
ctx: &mut Context<Self, Self::Reply>,
4444
) -> anyhow::Result<()> {
45-
info!("Received MoveToLocation packet {:?}", msg);
4645
//TODO check with geodata if the location is valid.
47-
//todo: we need to
48-
{
49-
let selected_char = self.try_get_selected_char_mut()?;
50-
selected_char.set_location(msg.x_to, msg.y_to, msg.z_to)?;
46+
47+
// Get the effective current position for distance validation
48+
// This aligns with start_movement starting point logic (mid-move retargets included)
49+
let (current_x, current_y, current_z) = self.effective_current_position()?;
50+
51+
// Calculate distance
52+
let Some(distance) = calculate_distance(
53+
current_x, current_y, current_z, msg.x_to, msg.y_to, msg.z_to,
54+
) else {
55+
let player = self.try_get_selected_char()?;
56+
warn!(
57+
"Player {} attempted movement causing coordinate overflow. Pos: ({},{},{}) -> Dest: ({},{},{})",
58+
player.char_model.name,
59+
current_x,
60+
current_y,
61+
current_z,
62+
msg.x_to,
63+
msg.y_to,
64+
msg.z_to
65+
);
66+
return Ok(());
67+
};
68+
69+
// Check against max distance from config
70+
let cfg = self.controller.get_cfg();
71+
if cfg.max_movement_distance > 0 && distance > f64::from(cfg.max_movement_distance) {
72+
let player = self.try_get_selected_char()?;
73+
warn!(
74+
"Player {} attempted to move excessive distance: {:.2} (max: {})",
75+
player.char_model.name, distance, cfg.max_movement_distance
76+
);
77+
return Ok(());
5178
}
52-
let p =
53-
CharMoveToLocation::new(self.try_get_selected_char()?, msg.x_to, msg.y_to, msg.z_to)?;
54-
self.controller.broadcast_packet(p); //broadcast to all players including self
79+
80+
self.start_movement(msg.x_to, msg.y_to, msg.z_to, ctx.actor_ref().clone())?;
81+
5582
Ok(())
5683
}
5784
}

game/src/packets/from_client/restart.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ impl Message<RequestRestart> for PlayerClient {
2828
_ctx: &mut Context<Self, Self::Reply>,
2929
) -> anyhow::Result<()> {
3030
//todo: if can logout (olymp, pvp flag, events, etc.)
31+
self.stop_movement();
3132
let session_id = self.try_get_session_key()?.get_play_session_id();
3233
let chars = self.try_get_account_chars()?;
3334
let user_name = self.try_get_user()?.username.clone();

game/src/packets/from_client/stop_move.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use bytes::BytesMut;
33
use kameo::message::Context;
44
use kameo::prelude::Message;
55
use l2_core::shared_packets::common::ReadablePacket;
6-
use tracing::{instrument, warn};
6+
use tracing::{info, instrument};
77

88
#[derive(Debug, Clone)]
99
pub struct StopMove {}
@@ -15,6 +15,7 @@ impl ReadablePacket for StopMove {
1515
Ok(Self {})
1616
}
1717
}
18+
1819
impl Message<StopMove> for PlayerClient {
1920
type Reply = anyhow::Result<()>;
2021

@@ -24,7 +25,19 @@ impl Message<StopMove> for PlayerClient {
2425
_: StopMove,
2526
_ctx: &mut Context<Self, Self::Reply>,
2627
) -> anyhow::Result<()> {
27-
warn!("TODO: stop move");
28+
info!("Received StopMove packet");
29+
30+
// Stop movement and get final interpolated position
31+
if let Some((final_x, final_y, final_z)) = self.stop_movement() {
32+
// Update player's location to the stopped position
33+
if let Ok(player) = self.try_get_selected_char_mut() {
34+
player.set_location(final_x, final_y, final_z)?;
35+
}
36+
info!("Movement stopped at ({}, {}, {})", final_x, final_y, final_z);
37+
}
38+
39+
// TODO: Broadcast StopMove packet to nearby players once that packet is implemented
40+
2841
Ok(())
2942
}
3043
}

0 commit comments

Comments
 (0)