Skip to main content

Horizon_Network_Common/
player.rs

1//! Player-related types shared across the Horizon ecosystem.
2//!
3//! These types represent players and their state as they move between
4//! different Horizon instances managed by Atlas.
5
6use serde::{Deserialize, Serialize};
7use chrono::{DateTime, Utc};
8
9use crate::spatial::WorldCoordinate;
10use crate::server::ServerId;
11
12/// Unique identifier for a player.
13/// Uses String for JSON API compatibility.
14#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
15pub struct PlayerId(pub String);
16
17impl PlayerId {
18    /// Creates a new random player ID using UUID v4.
19    pub fn new() -> Self {
20        Self(uuid::Uuid::new_v4().to_string())
21    }
22
23    /// Creates a player ID from a string.
24    pub fn from_string(s: impl Into<String>) -> Self {
25        Self(s.into())
26    }
27
28    /// Gets the inner string value.
29    pub fn as_str(&self) -> &str {
30        &self.0
31    }
32}
33
34impl std::fmt::Display for PlayerId {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        write!(f, "{}", self.0)
37    }
38}
39
40impl From<String> for PlayerId {
41    fn from(s: String) -> Self {
42        Self(s)
43    }
44}
45
46impl From<&str> for PlayerId {
47    fn from(s: &str) -> Self {
48        Self(s.to_string())
49    }
50}
51
52/// Authentication status of a player.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
54#[serde(rename_all = "snake_case")]
55pub enum AuthenticationStatus {
56    /// Player is not authenticated
57    #[default]
58    Unauthenticated,
59    /// Player is in the process of authenticating
60    Authenticating,
61    /// Player is successfully authenticated
62    Authenticated,
63    /// Player authentication failed
64    AuthenticationFailed,
65}
66
67/// Connection state of a player.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
69#[serde(rename_all = "snake_case")]
70pub enum ConnectionState {
71    /// Player is connecting
72    #[default]
73    Connecting,
74    /// Player is connected and active
75    Connected,
76    /// Player is being transferred to another server
77    Transferring,
78    /// Player is disconnecting
79    Disconnecting,
80    /// Player is disconnected
81    Disconnected,
82}
83
84/// Basic player information tracked by Atlas.
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct PlayerInfo {
87    /// Unique player identifier
88    pub id: PlayerId,
89    /// Player display name
90    pub name: String,
91    /// Current authentication status
92    pub auth_status: AuthenticationStatus,
93    /// Current connection state
94    pub connection_state: ConnectionState,
95    /// Server the player is currently connected to
96    pub current_server: Option<ServerId>,
97    /// Last known position in world coordinates
98    pub last_position: WorldCoordinate,
99    /// Timestamp of last position update
100    pub last_updated: DateTime<Utc>,
101}
102
103impl PlayerInfo {
104    /// Creates new player info.
105    pub fn new(id: PlayerId, name: String) -> Self {
106        Self {
107            id,
108            name,
109            auth_status: AuthenticationStatus::Unauthenticated,
110            connection_state: ConnectionState::Connecting,
111            current_server: None,
112            last_position: WorldCoordinate::zero(),
113            last_updated: Utc::now(),
114        }
115    }
116
117    /// Updates the player's position.
118    pub fn update_position(&mut self, position: WorldCoordinate) {
119        self.last_position = position;
120        self.last_updated = Utc::now();
121    }
122}
123
124/// Player state that can be serialized for transfer between servers.
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct PlayerState {
127    /// Player information
128    pub info: PlayerInfo,
129    /// Velocity vector
130    pub velocity: WorldCoordinate,
131    /// Player health (0.0 to 1.0)
132    pub health: f32,
133    /// Custom state data (game-specific)
134    #[serde(default)]
135    pub custom_data: std::collections::HashMap<String, serde_json::Value>,
136    /// Inventory or other persistent data
137    #[serde(default)]
138    pub persistent_data: serde_json::Value,
139}
140
141impl PlayerState {
142    /// Creates a new player state from player info.
143    pub fn new(info: PlayerInfo) -> Self {
144        Self {
145            info,
146            velocity: WorldCoordinate::zero(),
147            health: 1.0,
148            custom_data: std::collections::HashMap::new(),
149            persistent_data: serde_json::Value::Null,
150        }
151    }
152
153    /// Serializes the player state to JSON.
154    pub fn to_json(&self) -> Result<String, serde_json::Error> {
155        serde_json::to_string(self)
156    }
157
158    /// Deserializes player state from JSON.
159    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
160        serde_json::from_str(json)
161    }
162}
163
164/// Movement data for player position prediction.
165#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
166pub struct MovementData {
167    /// Current velocity
168    pub velocity: WorldCoordinate,
169    /// Acceleration
170    pub acceleration: WorldCoordinate,
171    /// Timestamp of this movement data
172    pub timestamp_ms: u64,
173}
174
175impl MovementData {
176    /// Predicts position after the given time delta.
177    pub fn predict_position(&self, current_pos: WorldCoordinate, delta_ms: u64) -> WorldCoordinate {
178        let t = delta_ms as f64 / 1000.0;
179        // Simple kinematic equation: p = p0 + v*t + 0.5*a*t^2
180        WorldCoordinate::new(
181            current_pos.x + self.velocity.x * t + 0.5 * self.acceleration.x * t * t,
182            current_pos.y + self.velocity.y * t + 0.5 * self.acceleration.y * t * t,
183            current_pos.z + self.velocity.z * t + 0.5 * self.acceleration.z * t * t,
184        )
185    }
186}
187
188/// Reasons for player disconnection.
189#[derive(Debug, Clone, Serialize, Deserialize)]
190#[serde(rename_all = "snake_case")]
191pub enum DisconnectReason {
192    /// Player initiated disconnection
193    ClientDisconnect,
194    /// Connection timed out
195    Timeout,
196    /// Server is shutting down
197    ServerShutdown,
198    /// Player was kicked
199    Kicked { reason: String },
200    /// Player is being transferred
201    Transfer { target_server: ServerId },
202    /// An error occurred
203    Error { message: String },
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_player_state_serialization() {
212        let info = PlayerInfo::new(PlayerId::new(), "TestPlayer".to_string());
213        let state = PlayerState::new(info);
214        let json = state.to_json().unwrap();
215        let restored = PlayerState::from_json(&json).unwrap();
216        assert_eq!(restored.info.name, "TestPlayer");
217    }
218
219    #[test]
220    fn test_movement_prediction() {
221        let movement = MovementData {
222            velocity: WorldCoordinate::new(10.0, 0.0, 0.0),
223            acceleration: WorldCoordinate::zero(),
224            timestamp_ms: 0,
225        };
226        let pos = WorldCoordinate::zero();
227        let predicted = movement.predict_position(pos, 1000); // 1 second
228        assert!((predicted.x - 10.0).abs() < 0.001);
229    }
230}