Skip to main content

Horizon_Network_Common/
transfer.rs

1//! Player transfer types for seamless server-to-server migration.
2//!
3//! These types handle the secure transfer of players between Horizon instances
4//! when they move across region boundaries.
5
6use serde::{Deserialize, Serialize};
7use std::time::{SystemTime, UNIX_EPOCH};
8
9use crate::player::PlayerId;
10use crate::server::ServerId;
11use crate::spatial::WorldCoordinate;
12
13/// Transfer token that authorizes a player to connect to a new server.
14///
15/// This token is generated by Atlas and must be presented by the player
16/// when connecting to the target server to prove they are authorized.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct TransferToken {
19    /// Unique token identifier
20    pub token_id: String,
21    /// Player being transferred
22    pub player_id: PlayerId,
23    /// Source server
24    pub source_server: ServerId,
25    /// Target server
26    pub target_server: ServerId,
27    /// Target server address
28    pub target_address: String,
29    /// When this token was created (ms since epoch)
30    pub created_at_ms: u64,
31    /// When this token expires (ms since epoch)
32    pub expires_at_ms: u64,
33    /// Cryptographic signature (HMAC or similar)
34    pub signature: String,
35}
36
37/// Token validity duration in seconds
38const DEFAULT_TOKEN_VALIDITY_SECS: u64 = 60;
39
40impl TransferToken {
41    /// Creates a new transfer token.
42    pub fn new(
43        player_id: PlayerId,
44        source_server: ServerId,
45        target_server: ServerId,
46        target_address: String,
47        secret_key: &[u8],
48    ) -> Self {
49        Self::with_validity(player_id, source_server, target_server, target_address, DEFAULT_TOKEN_VALIDITY_SECS, secret_key)
50    }
51
52    /// Creates a new transfer token with custom validity duration.
53    pub fn with_validity(
54        player_id: PlayerId,
55        source_server: ServerId,
56        target_server: ServerId,
57        target_address: String,
58        valid_duration_secs: u64,
59        secret_key: &[u8],
60    ) -> Self {
61        let now_ms = SystemTime::now()
62            .duration_since(UNIX_EPOCH)
63            .unwrap_or_default()
64            .as_millis() as u64;
65
66        let token_id = format!("txfr-{}-{}", now_ms, Self::rand_component());
67        let expires_at_ms = now_ms + (valid_duration_secs * 1000);
68
69        let mut token = Self {
70            token_id,
71            player_id,
72            source_server,
73            target_server,
74            target_address,
75            created_at_ms: now_ms,
76            expires_at_ms,
77            signature: String::new(),
78        };
79
80        token.signature = token.compute_signature(secret_key);
81        token
82    }
83
84    /// Verifies the token signature and expiration.
85    pub fn verify(&self, secret_key: &[u8]) -> Result<(), TransferError> {
86        let now_ms = SystemTime::now()
87            .duration_since(UNIX_EPOCH)
88            .unwrap_or_default()
89            .as_millis() as u64;
90
91        if now_ms > self.expires_at_ms {
92            return Err(TransferError::TokenExpired);
93        }
94
95        let expected_signature = self.compute_signature(secret_key);
96        if self.signature != expected_signature {
97            return Err(TransferError::InvalidSignature);
98        }
99
100        Ok(())
101    }
102
103    /// Computes signature for this token.
104    fn compute_signature(&self, key: &[u8]) -> String {
105        use std::collections::hash_map::DefaultHasher;
106        use std::hash::{Hash, Hasher};
107
108        let data = format!(
109            "{}:{}:{}:{}:{}",
110            self.token_id, self.player_id, self.source_server, self.target_server, self.expires_at_ms
111        );
112
113        let mut hasher = DefaultHasher::new();
114        data.hash(&mut hasher);
115        key.hash(&mut hasher);
116        format!("{:016x}", hasher.finish())
117    }
118
119    /// Generates a random component for token IDs.
120    fn rand_component() -> u32 {
121        use std::collections::hash_map::DefaultHasher;
122        use std::hash::{Hash, Hasher};
123
124        let mut hasher = DefaultHasher::new();
125        std::time::Instant::now().hash(&mut hasher);
126        (hasher.finish() % 1000000) as u32
127    }
128
129    /// Serializes the token to JSON.
130    pub fn to_json(&self) -> Result<String, TransferError> {
131        serde_json::to_string(self)
132            .map_err(|e| TransferError::SerializationError(e.to_string()))
133    }
134
135    /// Deserializes a token from JSON.
136    pub fn from_json(json: &str) -> Result<Self, TransferError> {
137        serde_json::from_str(json)
138            .map_err(|e| TransferError::SerializationError(e.to_string()))
139    }
140}
141
142/// Request to initiate a player transfer.
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct TransferRequest {
145    /// Player to transfer
146    pub player_id: PlayerId,
147    /// Current server
148    pub source_server: ServerId,
149    /// Target server
150    pub target_server: ServerId,
151    /// Target position in world coordinates
152    pub target_position: WorldCoordinate,
153    /// Reason for transfer
154    pub reason: TransferReason,
155    /// Priority (higher = more urgent)
156    pub priority: u8,
157}
158
159/// Reason for initiating a transfer.
160#[derive(Debug, Clone, Serialize, Deserialize)]
161#[serde(rename_all = "snake_case")]
162pub enum TransferReason {
163    /// Player crossed region boundary
164    RegionBoundary,
165    /// Load balancing decision
166    LoadBalancing,
167    /// Server is shutting down
168    ServerShutdown,
169    /// Manual admin action
170    AdminAction,
171    /// Player teleport
172    Teleport,
173}
174
175/// Result of a transfer operation.
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct TransferResult {
178    /// Whether transfer succeeded
179    pub success: bool,
180    /// Transfer token if successful
181    pub token: Option<TransferToken>,
182    /// Error if failed
183    pub error: Option<TransferError>,
184    /// Time taken for transfer in milliseconds
185    pub duration_ms: u64,
186}
187
188impl TransferResult {
189    /// Creates a successful transfer result.
190    pub fn success(token: TransferToken, duration_ms: u64) -> Self {
191        Self {
192            success: true,
193            token: Some(token),
194            error: None,
195            duration_ms,
196        }
197    }
198
199    /// Creates a failed transfer result.
200    pub fn failure(error: TransferError) -> Self {
201        Self {
202            success: false,
203            token: None,
204            error: Some(error),
205            duration_ms: 0,
206        }
207    }
208}
209
210/// Errors that can occur during transfer.
211#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)]
212pub enum TransferError {
213    /// Target server is not available
214    #[error("Target server unavailable: {0}")]
215    TargetServerUnavailable(String),
216    
217    /// Player not found
218    #[error("Player not found: {0}")]
219    PlayerNotFound(String),
220    
221    /// Transfer token expired
222    #[error("Transfer token expired")]
223    TokenExpired,
224    
225    /// Invalid token signature
226    #[error("Invalid token signature")]
227    InvalidSignature,
228    
229    /// Serialization error
230    #[error("Serialization error: {0}")]
231    SerializationError(String),
232    
233    /// Transfer already in progress
234    #[error("Transfer already in progress for player")]
235    TransferInProgress,
236    
237    /// Target server rejected transfer
238    #[error("Transfer rejected: {0}")]
239    TransferRejected(String),
240    
241    /// Network error during transfer
242    #[error("Network error: {0}")]
243    NetworkError(String),
244    
245    /// Timeout waiting for transfer
246    #[error("Transfer timeout")]
247    Timeout,
248}
249
250/// Transfer notification sent to clients.
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct TransferNotification {
253    /// Player being transferred
254    pub player_id: PlayerId,
255    /// New server address to connect to
256    pub target_address: String,
257    /// Transfer token to present
258    pub token: String,
259    /// Suggested reconnect delay in milliseconds
260    pub reconnect_delay_ms: u64,
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn test_transfer_token_creation_and_verification() {
269        let player_id = PlayerId::new();
270        let source = ServerId::new();
271        let target = ServerId::new();
272        let secret = b"test_secret_key";
273
274        let token = TransferToken::new(
275            player_id.clone(), 
276            source, 
277            target, 
278            "127.0.0.1:8080".to_string(),
279            secret
280        );
281        assert!(token.verify(secret).is_ok());
282    }
283
284    #[test]
285    fn test_transfer_token_invalid_signature() {
286        let player_id = PlayerId::new();
287        let source = ServerId::new();
288        let target = ServerId::new();
289
290        let token = TransferToken::new(
291            player_id, 
292            source, 
293            target, 
294            "127.0.0.1:8080".to_string(),
295            b"key1"
296        );
297        assert!(matches!(
298            token.verify(b"wrong_key"),
299            Err(TransferError::InvalidSignature)
300        ));
301    }
302}