Skip to content

Implement smooth move to location broadcasting mechanism#20

Closed
artemijan wants to merge 7 commits intomasterfrom
fb-broadcast-move-to-location
Closed

Implement smooth move to location broadcasting mechanism#20
artemijan wants to merge 7 commits intomasterfrom
fb-broadcast-move-to-location

Conversation

@artemijan
Copy link
Copy Markdown
Owner

@artemijan artemijan commented Nov 30, 2025

Summary by CodeRabbit

  • New Features

    • Player movement system: start/restart controls, real-time position interpolation, travel-duration estimation, and automatic cancellation of background movement tasks.
    • Immediate broadcast on movement start plus periodic position updates while moving.
    • New configurable maximum movement distance (default provided).
  • Bug Fixes

    • Improved stop handling to reliably capture and update the final player position.
  • Tests

    • Unit tests for distance, duration, position calculations, and edge cases.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Nov 30, 2025

Walkthrough

Adds a movement subsystem: new movement module with MovementState; per-player movement_state in PlayerClient with start/stop APIs; MoveToLocation/StopMove handlers validate distance, start/cancel movement, and spawn a periodic task broadcasting interpolated positions; config adds max_movement_distance.

Changes

Cohort / File(s) Summary
Movement core & registration
game/src/main.rs, game/src/movement.rs
Registers mod movement; and adds MovementState (source/dest coords, start_time, speed, optional JoinHandle), ctor, distance/duration, interpolated position, arrival check, cancel_task, Drop cleanup, and unit tests.
PlayerClient integration
game/src/pl_client.rs
Adds movement_state: Option<MovementState> field; initializes it; adds start_movement(dest_x,dest_y,dest_z) -> anyhow::Result<(MovementState, i32, i32, i32)> and stop_movement() -> Option<(i32,i32,i32)> to manage movement lifecycle and select speed.
Packet handlers & actor messages
game/src/packets/from_client/move_to_location.rs, game/src/packets/from_client/stop_move.rs
MoveToLocation now validates 3D distance (overflow and config max_movement_distance), starts/restarts movement, sends initial movement packet, and spawns a periodic broadcast task that queries GetMovementPosition from PlayerClient until arrival. Adds public GetMovementPosition message/handler. StopMove stops movement, captures final coords, updates player location, logs info, and leaves a TODO to broadcast stop.
World/restart sync
game/src/packets/from_client/enter_world.rs, game/src/packets/from_client/restart.rs
Calls stop_movement() before transitioning/processing to ensure any ongoing movement is halted prior to world entry or restart.
Configuration & core config
config/game.yaml, l2-core/src/config/gs.rs
Adds commented hint max_movement_distance: 15000 to config/game.yaml and a new pub max_movement_distance: u32 with default 15000 in GSServerConfig (l2-core/src/config/gs.rs).

Sequence Diagram(s)

sequenceDiagram
    participant Client as Game Client
    participant Handler as MoveToLocation Handler
    participant Player as PlayerClient (actor)
    participant Movement as MovementState
    participant Task as Periodic Broadcast Task
    participant Nearby as Nearby Players

    Client->>Handler: Send MoveToLocation packet
    Handler->>Player: start_movement(dest_x,dest_y,dest_z)
    Player->>Movement: cancel existing / create new MovementState
    Player-->>Handler: Return current position
    Handler->>Nearby: Send initial movement packet
    Handler->>Task: Spawn periodic broadcast task

    loop until arrival or cancellation
        Task->>Player: GetMovementPosition
        Player->>Movement: calculate_current_position()
        Movement-->>Player: (x,y,z) + arrived?
        Player-->>Task: Reply position + arrival flag
        Task->>Nearby: Broadcast current position
    end

    alt arrival detected
        Task->>Task: Exit (task completes)
    else client requests stop
        Client->>Handler: Send StopMove packet
        Handler->>Player: stop_movement()
        Player->>Movement: cancel_task() / clear state
        Handler->>Nearby: (TODO) broadcast stop position
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Inspect async task spawn/cancellation and ownership of JoinHandle in game/src/movement.rs and game/src/packets/from_client/move_to_location.rs.
  • Verify interpolation math, travel duration, and zero-distance/overflow edge cases in game/src/movement.rs.
  • Check actor message (GetMovementPosition) handling and potential races when starting/stopping movement rapidly.
  • Confirm GSServerConfig deserialization/backward compatibility for the new max_movement_distance field.

Possibly related PRs

  • Implement basic move to location #14 — Prior MoveToLocation/CharMoveToLocation implementation that touches the move-to-location flow and likely relates to these movement lifecycle and broadcasting changes.

Poem

🐰 I hopped a new path in code tonight,
Steps that timestamp, packets bright,
I watch positions softly flow,
From source to dest I gently go.
Nibble hops — the broadcast light! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Implement smooth move to location broadcasting mechanism' directly corresponds to the main changes: adding MovementState struct, broadcasting position updates during movement, and implementing the movement lifecycle with periodic task broadcasts.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fb-broadcast-move-to-location

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
game/src/packets/from_client/move_to_location.rs (3)

54-54: Unused return value from start_movement.

The source_x, source_y, source_z tuple is captured but never used. If this is intentional (e.g., for future logging or validation), consider prefixing with _. Otherwise, simplify the call.

-        let (source_x, source_y, source_z) = self.start_movement(msg.x_to, msg.y_to, msg.z_to)?;
+        let _ = self.start_movement(msg.x_to, msg.y_to, msg.z_to)?;

92-97: Redundant actor call in broadcast loop.

Each tick calls GetCharInfo to get a full Player clone when position is already retrieved from GetMovementPosition. Consider caching the object_id before the loop or extending GetMovementPosition to return it, avoiding the extra actor round-trip every 300ms.


137-140: Side effect in getter method.

GetMovementPosition updates the player's stored location as a side effect, which is unexpected for a method named "Get...". Consider either renaming to UpdateAndGetMovementPosition or documenting this behavior clearly, as callers may not expect a query to mutate state.

game/src/movement.rs (2)

82-84: Consider rounding instead of truncating.

The as i32 cast truncates the interpolated value. For more accurate positioning, consider using .round() before casting:

-        let current_x = self.source_x + ((self.dest_x - self.source_x) as f64 * progress) as i32;
-        let current_y = self.source_y + ((self.dest_y - self.source_y) as f64 * progress) as i32;
-        let current_z = self.source_z + ((self.dest_z - self.source_z) as f64 * progress) as i32;
+        let current_x = self.source_x + ((self.dest_x - self.source_x) as f64 * progress).round() as i32;
+        let current_y = self.source_y + ((self.dest_y - self.source_y) as f64 * progress).round() as i32;
+        let current_z = self.source_z + ((self.dest_z - self.source_z) as f64 * progress).round() as i32;

128-136: Test has timing dependency.

test_calculate_current_position_at_start checks position immediately after creation but allows for "small movement" (x < 50). This test could be flaky on slow CI systems. Consider either:

  • Testing with zero speed (instant arrival)
  • Using a mock time source
  • Accepting this as sufficient for the current use case
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 20db175 and 588c474.

📒 Files selected for processing (5)
  • game/src/main.rs (1 hunks)
  • game/src/movement.rs (1 hunks)
  • game/src/packets/from_client/move_to_location.rs (2 hunks)
  • game/src/packets/from_client/stop_move.rs (3 hunks)
  • game/src/pl_client.rs (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
game/src/pl_client.rs (1)
game/src/movement.rs (1)
  • new (30-50)
game/src/packets/from_client/move_to_location.rs (3)
game/src/packets/from_client/stop_move.rs (2)
  • read (14-16)
  • handle (23-42)
game/src/movement.rs (2)
  • new (30-50)
  • has_arrived (90-94)
game/src/packets/to_client/char_move_to_location.rs (1)
  • new (16-34)
game/src/movement.rs (3)
game/src/pl_client.rs (5)
  • new (82-97)
  • handle (463-474)
  • handle (483-490)
  • handle (496-499)
  • handle (508-514)
game/src/packets/from_client/move_to_location.rs (2)
  • handle (44-118)
  • handle (128-146)
game/src/packets/from_client/stop_move.rs (1)
  • handle (23-42)
🔇 Additional comments (5)
game/src/main.rs (1)

21-21: LGTM!

Module declaration correctly wires the new movement subsystem into the game binary.

game/src/packets/from_client/stop_move.rs (1)

28-37: LGTM with a minor observation.

The handler correctly stops movement and updates position. Consider whether silently ignoring set_location errors (line 34) is intentional - if the character lookup fails, no position update occurs and no error is logged. This may be acceptable for a stop-move packet but worth noting.

game/src/pl_client.rs (2)

322-359: Well-structured movement initialization.

The start_movement method correctly:

  • Cancels any existing movement before starting new one (handles rerouting)
  • Calculates current interpolated position when rerouting
  • Selects appropriate speed based on run/walk state

One consideration: if try_get_selected_char() fails (line 339), movement_state has already been taken (line 329) but won't be restored. This seems acceptable since a missing character indicates a more fundamental problem.


361-369: LGTM!

Clean implementation of stop_movement with proper task cancellation and position interpolation.

game/src/movement.rs (1)

104-108: LGTM!

Good use of Drop trait to ensure task cleanup when MovementState is dropped, preventing orphaned broadcast tasks.

Comment on lines 112 to 115
// Store the task handle in movement state
if let Some(movement) = self.movement_state.as_mut() {
movement.task_handle = Some(task_handle);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Race condition: task handle stored after spawn.

There's a window between spawning the task (line 72) and storing its handle (line 114) where the task could complete. For very short movements, the task might finish before the handle is stored. Consider restructuring to set the handle atomically with movement creation, or accept this as benign since the task skips the first tick.

🤖 Prompt for AI Agents
In game/src/packets/from_client/move_to_location.rs around lines 112–115 there
is a race where the task is spawned before its JoinHandle is stored in
movement_state; a very short task can complete before the handle is saved. Fix
by making the spawned task wait on a small start signal so you can store the
handle first: create a tokio::sync::oneshot channel, spawn the task passing the
receiver and immediately set movement.task_handle = Some(join_handle) in
movement_state, then send the start signal on the sender so the task begins;
this ensures the handle is stored before the task can complete.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
game/src/packets/from_client/move_to_location.rs (1)

114-117: Race condition: task handle stored after spawn.

This issue was flagged in a previous review. There's a window between spawning the task (line 72) and storing its handle (line 114) where a very short movement could complete before the handle is stored. While the first tick is skipped, this is still a theoretical race. Consider using a oneshot channel to synchronize, or accept this as benign for the typical use case.

🧹 Nitpick comments (6)
game/src/movement.rs (2)

69-86: Potential truncation issues in position interpolation.

The cast (... as f64 * progress) as i32 may lose precision and behave unexpectedly for large coordinate differences. Using .round() before casting would provide more accurate interpolation.

-        let current_x = self.source_x + ((self.dest_x - self.source_x) as f64 * progress) as i32;
-        let current_y = self.source_y + ((self.dest_y - self.source_y) as f64 * progress) as i32;
-        let current_z = self.source_z + ((self.dest_z - self.source_z) as f64 * progress) as i32;
+        let current_x = self.source_x + ((self.dest_x - self.source_x) as f64 * progress).round() as i32;
+        let current_y = self.source_y + ((self.dest_y - self.source_y) as f64 * progress).round() as i32;
+        let current_z = self.source_z + ((self.dest_z - self.source_z) as f64 * progress).round() as i32;

109-142: Test coverage looks good, consider adding edge case tests.

The tests cover the basic scenarios well. Consider adding tests for:

  • Negative coordinates
  • Very large distances (to catch potential overflow)
  • Movement mid-progress (using a mock or adjusted start_time)
game/src/packets/from_client/move_to_location.rs (4)

72-112: Consider adding a timeout or maximum iteration guard.

The spawned task loops indefinitely until arrival or cancellation. If has_arrived never returns true due to a bug (e.g., speed is 0 but distance > 0), this task would loop forever. Consider adding a maximum duration check or iteration count as a safety net.

         let task_handle = tokio::spawn(async move {
             let mut tick = interval(Duration::from_millis(300));
             tick.tick().await; // Skip first immediate tick
+            let start = std::time::Instant::now();
+            let max_duration = Duration::from_secs(300); // 5 minute safety limit

             loop {
+                if start.elapsed() > max_duration {
+                    warn!("Movement task exceeded maximum duration, stopping");
+                    break;
+                }
+
                 // Check movement state BEFORE waiting...

127-148: Side effect in a query message handler.

GetMovementPosition is semantically a query (get position), but it also mutates state via player.set_location(x, y, z). This coupling of read and write operations can make the code harder to reason about and test. Consider separating the position update into a distinct message or documenting this behavior explicitly.

If keeping the current design, at minimum add a doc comment clarifying the side effect:

 /// Message to get current movement position and status
+/// Note: This also updates the player's actual position as a side effect
 #[derive(Debug)]
 pub struct GetMovementPosition;

140-142: Silently ignoring set_location errors.

The let _ = player.set_location(x, y, z); pattern discards any error. If setting location can fail, consider logging the error to aid debugging.

             // Update player's actual position
             if let Ok(player) = self.try_get_selected_char_mut() {
-                let _ = player.set_location(x, y, z);
+                if let Err(e) = player.set_location(x, y, z) {
+                    warn!("Failed to update player location: {}", e);
+                }
             }

91-96: Double actor message round-trip per tick.

Each iteration sends two ask messages: GetMovementPosition and GetCharInfo. The position query already has access to self and could return the player info needed for the packet, reducing latency and actor contention.

Consider extending GetMovementPosition to return the object ID directly, or create a combined message that returns all needed data in one round-trip.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 588c474 and c34578d.

📒 Files selected for processing (2)
  • game/src/movement.rs (1 hunks)
  • game/src/packets/from_client/move_to_location.rs (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
game/src/packets/from_client/move_to_location.rs (2)
game/src/movement.rs (2)
  • new (29-49)
  • has_arrived (89-93)
game/src/packets/to_client/char_move_to_location.rs (1)
  • new (16-34)
game/src/movement.rs (2)
game/src/pl_client.rs (5)
  • new (82-97)
  • handle (463-474)
  • handle (483-490)
  • handle (496-499)
  • handle (508-514)
game/src/packets/from_client/move_to_location.rs (2)
  • handle (44-120)
  • handle (130-148)
🔇 Additional comments (4)
game/src/movement.rs (3)

1-25: LGTM! Clean struct design.

The MovementState struct is well-documented and captures all necessary state for tracking player movement. The public fields allow direct access when needed while the task_handle being Option<JoinHandle<()>> correctly models the optional async task.


88-93: Edge case: has_arrived returns true immediately when duration is 0.

When speed > 0 but source equals destination, calculate_travel_duration() returns 0, and elapsed >= 0.0 is always true. This is correct behavior (covered by test), but worth noting for clarity.


103-107: Good use of Drop for cleanup.

The Drop implementation ensures the broadcast task is aborted when MovementState is dropped, preventing orphaned tasks. This is a solid pattern for resource cleanup.

game/src/packets/from_client/move_to_location.rs (1)

1-10: Imports look appropriate for the new functionality.

The added imports support the actor-based movement tracking, interval timing, and logging requirements.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
game/src/packets/from_client/move_to_location.rs (1)

159-162: Race condition between task spawn and handle storage.

This concern was raised in a previous review. The task is spawned before the handle is stored, creating a window where short movements could complete before the handle is saved.

game/src/movement.rs (1)

51-57: Potential overflow in distance calculation - inconsistent with move_to_location.rs.

This was flagged in a previous review. Note that move_to_location.rs (lines 14-24) now uses checked_sub for overflow protection, but this method still performs unchecked subtraction. Consider aligning both implementations.

Apply this diff to match the safer pattern:

     pub fn calculate_distance(&self) -> f64 {
-        let dx = f64::from(self.dest_x - self.source_x);
-        let dy = f64::from(self.dest_y - self.source_y);
-        let dz = f64::from(self.dest_z - self.source_z);
+        let dx = f64::from(self.dest_x) - f64::from(self.source_x);
+        let dy = f64::from(self.dest_y) - f64::from(self.source_y);
+        let dz = f64::from(self.dest_z) - f64::from(self.source_z);
         (dx * dx + dy * dy + dz * dz).sqrt()
     }
🧹 Nitpick comments (2)
game/src/packets/from_client/move_to_location.rs (2)

99-99: Unused return value from start_movement.

The tuple (source_x, source_y, source_z) returned by start_movement is assigned but never used. Consider prefixing with underscore or removing the binding.

-        let (source_x, source_y, source_z) = self.start_movement(msg.x_to, msg.y_to, msg.z_to)?;
+        let _ = self.start_movement(msg.x_to, msg.y_to, msg.z_to)?;

117-157: Consider logging failures in the broadcast loop.

When actor_ref.ask(GetCharInfo) fails at line 137, the error is silently ignored and the loop continues. While this prevents the task from crashing, repeated failures could indicate a problem. Consider adding a debug/trace log for visibility.

                        // Broadcast current position
-                        if let Ok(player) = actor_ref.ask(crate::pl_client::GetCharInfo).await {
+                        match actor_ref.ask(crate::pl_client::GetCharInfo).await {
+                            Ok(player) => {
                            if let Ok(packet) = CharMoveToLocation::new(&player, dest_x, dest_y, dest_z) {
                                controller.broadcast_packet(packet);
                            }
+                            }
+                            Err(e) => {
+                                tracing::trace!("Failed to get char info during movement broadcast: {}", e);
+                            }
                        }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c34578d and 11b297c.

📒 Files selected for processing (4)
  • config/game.yaml (1 hunks)
  • game/src/movement.rs (1 hunks)
  • game/src/packets/from_client/move_to_location.rs (2 hunks)
  • l2-core/src/config/gs.rs (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • config/game.yaml
🧰 Additional context used
🧬 Code graph analysis (1)
game/src/movement.rs (1)
game/src/packets/from_client/move_to_location.rs (3)
  • calculate_distance (15-25)
  • handle (59-165)
  • handle (175-193)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test-only
🔇 Additional comments (7)
l2-core/src/config/gs.rs (1)

51-64: LGTM!

The configuration addition follows the established patterns in this file. The max_movement_distance field with a default of 15000 integrates cleanly with the existing serde deserialization approach.

game/src/packets/from_client/move_to_location.rs (2)

13-25: LGTM!

Good use of checked_sub to handle potential overflow cases. The function safely returns None when coordinate subtraction would overflow, which is then properly handled by the caller.


172-193: LGTM with note on side effect.

The handler updates the player's position as a side effect of querying movement state (lines 185-187). This coupling ensures position synchronization during movement. The design is acceptable, though worth documenting that callers should be aware position is mutated on each query.

game/src/movement.rs (4)

4-25: LGTM!

The struct design is appropriate for tracking movement state. Using Instant for timing and Option<JoinHandle<()>> for task management is idiomatic Rust.


27-49: LGTM!

Clean constructor that properly initializes the movement state with the current instant as start time.


103-107: LGTM!

The Drop implementation ensures proper cleanup of the broadcast task, preventing resource leaks when MovementState is dropped.


109-142: Good test coverage.

The tests validate core movement calculations including distance (3-4-5 triangle), travel duration, initial position, and instant arrival edge case.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
game/src/packets/from_client/move_to_location.rs (1)

149-151: Race condition acknowledged.

The race between spawning the task and storing its handle was flagged in a previous review. For typical movement durations, this is benign since the task's first action involves an async ask that yields.

🧹 Nitpick comments (4)
game/src/packets/from_client/move_to_location.rs (3)

9-11: Unused import: interval is referenced inline.

interval from tokio::time is imported but line 118 uses tokio::time::interval directly. Either use the import or remove it.

 use std::time::Duration;
-use tokio::time::interval;
 use tracing::{info, instrument, warn};

99-99: Unused variables will trigger compiler warnings.

source_x, source_y, source_z are returned but never used. Prefix with underscore or use .. pattern to suppress warnings.

-        let (mut movement, source_x, source_y, source_z) = self.start_movement(msg.x_to, msg.y_to, msg.z_to)?;
+        let (mut movement, ..) = self.start_movement(msg.x_to, msg.y_to, msg.z_to)?;

173-176: Consider logging or propagating set_location errors.

Silently discarding the result could mask position update failures, potentially causing desync between movement interpolation and stored player position.

             // Update player's actual position
             if let Ok(player) = self.try_get_selected_char_mut() {
-                let _ = player.set_location(x, y, z);
+                if let Err(e) = player.set_location(x, y, z) {
+                    warn!("Failed to update player position: {:?}", e);
+                }
             }
game/src/pl_client.rs (1)

417-431: Missing movement task cleanup on actor stop.

If the player disconnects while moving, the spawned broadcast task continues running until actor_ref.ask fails. Consider canceling the movement task explicitly in on_stop for cleaner shutdown.

     async fn on_stop(
         &mut self,
         _actor_ref: WeakActorRef<Self>,
         _reason: ActorStopReason,
     ) -> anyhow::Result<()> {
         info!("Disconnecting Client...");
+        // Cancel any ongoing movement task
+        if let Some(mut movement) = self.movement_state.take() {
+            movement.cancel_task();
+        }
         if let Some(s) = self.packet_sender.as_ref() {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 29608c8 and 95e8e72.

📒 Files selected for processing (2)
  • game/src/packets/from_client/move_to_location.rs (2 hunks)
  • game/src/pl_client.rs (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
game/src/packets/from_client/move_to_location.rs (2)
game/src/movement.rs (2)
  • calculate_distance (52-57)
  • new (29-49)
game/src/packets/to_client/char_move_to_location.rs (1)
  • new (16-34)
game/src/pl_client.rs (1)
game/src/movement.rs (1)
  • new (29-49)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test-only
🔇 Additional comments (5)
game/src/packets/from_client/move_to_location.rs (2)

13-25: LGTM!

Overflow-safe distance calculation with proper checked_sub usage.


68-96: LGTM!

Distance validation with overflow protection and configurable max distance check is well implemented.

game/src/pl_client.rs (3)

70-70: LGTM!

Adding movement_state field to track ongoing player movement is appropriate.


322-358: LGTM with minor note.

The method correctly manages movement lifecycle. The design of returning MovementState without storing allows the caller to set the task_handle before storage, which is used in move_to_location.rs.


361-368: LGTM!

Clean implementation of movement cancellation with position retrieval.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
game/src/packets/from_client/move_to_location.rs (1)

110-144: Race condition between task spawn and movement state storage.

The task spawns at line 110 but self.movement_state isn't set until line 144. When the task immediately calls GetMovementPosition, it will find movement_state as None and exit. This effectively breaks the movement system.

Consider setting movement_state before spawning the task, then updating it with the handle afterward:

+        // Set movement state first so task can find it
+        self.movement_state = Some(movement);
+
         let task_handle = tokio::spawn(async move {
             // ... task body ...
         });

-        // Store the task handle in movement state and set it in client
-        movement.task_handle = Some(task_handle);
-        self.movement_state = Some(movement);
+        // Store task handle in existing movement state
+        if let Some(ref mut m) = self.movement_state {
+            m.task_handle = Some(task_handle);
+        }
🧹 Nitpick comments (3)
game/src/packets/from_client/move_to_location.rs (3)

9-11: Unused imports.

Duration and interval are imported but the code uses fully qualified paths (std::time::Duration::from_millis and tokio::time::interval) on line 111 instead.

Either use the imports:

-            let mut interval = tokio::time::interval(std::time::Duration::from_millis(300));
+            let mut interval = interval(Duration::from_millis(300));

Or remove the unused imports:

-use std::time::Duration;
-use tokio::time::interval;

99-99: Unused variables from start_movement return.

The source_x, source_y, source_z values are extracted but never used. If they're no longer needed, consider updating start_movement to not return them, or prefix with underscore.

-        let (mut movement, source_x, source_y, source_z) = self.start_movement(msg.x_to, msg.y_to, msg.z_to)?;
+        let (mut movement, _source_x, _source_y, _source_z) = self.start_movement(msg.x_to, msg.y_to, msg.z_to)?;

162-174: Side effect in getter message handler.

GetMovementPosition both queries the current position and mutates the player's location (lines 167-169). This couples reading and writing, which can lead to unexpected behavior if the message is used elsewhere for read-only purposes.

Consider separating concerns:

  1. Have GetMovementPosition only return the interpolated position without mutation
  2. Update the player's position when movement completes or in a dedicated update path

Alternatively, rename to UpdateAndGetMovementPosition to make the side effect explicit.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 95e8e72 and 49f9afb.

📒 Files selected for processing (3)
  • game/src/packets/from_client/enter_world.rs (1 hunks)
  • game/src/packets/from_client/move_to_location.rs (2 hunks)
  • game/src/packets/from_client/restart.rs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
game/src/packets/from_client/move_to_location.rs (2)
game/src/movement.rs (3)
  • calculate_distance (52-57)
  • has_arrived (89-93)
  • new (29-49)
game/src/packets/to_client/char_move_to_location.rs (1)
  • new (16-34)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test-only
🔇 Additional comments (5)
game/src/packets/from_client/enter_world.rs (1)

62-62: LGTM!

Stopping movement before transitioning to InGame ensures a clean state when the player enters the world, preventing any residual movement from causing issues.

game/src/packets/from_client/restart.rs (1)

31-31: LGTM!

Stopping movement at the start of restart handling ensures proper cleanup before character state transitions and broadcasting DeleteObject to other players.

game/src/packets/from_client/move_to_location.rs (3)

13-25: LGTM!

The overflow-safe distance calculation using checked_sub is a good defensive measure for validating client-supplied coordinates before trusting them.


68-96: LGTM!

The distance validation with overflow detection and max distance checking provides good protection against malicious movement requests. Returning silently (rather than erroring) on excessive distance is appropriate.


113-139: Loop structure is correct.

The broadcast logic is properly structured: immediate first broadcast followed by 300ms intervals. The previous concern about duplicate broadcasts no longer applies since there's no initial packet send outside the task.

@artemijan artemijan closed this Nov 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant