Horizon-Network-Common 0.1.0

Shared network types and protocols for Horizon ecosystem (Horizon, Atlas, Maestro)
Documentation
//! Player transfer types for seamless server-to-server migration.
//!
//! These types handle the secure transfer of players between Horizon instances
//! when they move across region boundaries.

use serde::{Deserialize, Serialize};
use uuid::Uuid;
use chrono::{DateTime, Utc, Duration};

use crate::player::{PlayerId, PlayerState};
use crate::server::ServerId;
use crate::spatial::WorldCoordinate;

/// Transfer token that authorizes a player to connect to a new server.
///
/// This token is generated by Atlas and must be presented by the player
/// when connecting to the target server to prove they are authorized.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransferToken {
    /// Unique token identifier
    pub token_id: Uuid,
    /// Player being transferred
    pub player_id: PlayerId,
    /// Source server
    pub source_server: ServerId,
    /// Target server
    pub target_server: ServerId,
    /// When this token was created
    pub created_at: DateTime<Utc>,
    /// When this token expires
    pub expires_at: DateTime<Utc>,
    /// Cryptographic signature (HMAC or similar)
    pub signature: String,
    /// Serialized player state
    pub player_state: String,
}

impl TransferToken {
    /// Creates a new transfer token.
    ///
    /// The token is valid for the specified duration.
    pub fn new(
        player_id: PlayerId,
        source_server: ServerId,
        target_server: ServerId,
        player_state: PlayerState,
        valid_duration_secs: i64,
        secret_key: &[u8],
    ) -> Result<Self, TransferError> {
        let token_id = Uuid::new_v4();
        let created_at = Utc::now();
        let expires_at = created_at + Duration::seconds(valid_duration_secs);
        
        let player_state_json = serde_json::to_string(&player_state)
            .map_err(|e| TransferError::SerializationError(e.to_string()))?;
        
        // Create signature from token data
        let sign_data = format!(
            "{}:{}:{}:{}:{}",
            token_id, player_id, source_server, target_server, expires_at.timestamp()
        );
        let signature = Self::compute_signature(&sign_data, secret_key);
        
        Ok(Self {
            token_id,
            player_id,
            source_server,
            target_server,
            created_at,
            expires_at,
            signature,
            player_state: player_state_json,
        })
    }

    /// Verifies the token signature and expiration.
    pub fn verify(&self, secret_key: &[u8]) -> Result<(), TransferError> {
        // Check expiration
        if Utc::now() > self.expires_at {
            return Err(TransferError::TokenExpired);
        }

        // Verify signature
        let sign_data = format!(
            "{}:{}:{}:{}:{}",
            self.token_id, self.player_id, self.source_server, self.target_server, 
            self.expires_at.timestamp()
        );
        let expected_signature = Self::compute_signature(&sign_data, secret_key);
        
        if self.signature != expected_signature {
            return Err(TransferError::InvalidSignature);
        }

        Ok(())
    }

    /// Extracts the player state from the token.
    pub fn extract_player_state(&self) -> Result<PlayerState, TransferError> {
        serde_json::from_str(&self.player_state)
            .map_err(|e| TransferError::SerializationError(e.to_string()))
    }

    /// Simple signature computation (in production, use proper HMAC-SHA256).
    fn compute_signature(data: &str, key: &[u8]) -> String {
        use std::collections::hash_map::DefaultHasher;
        use std::hash::{Hash, Hasher};
        
        let mut hasher = DefaultHasher::new();
        data.hash(&mut hasher);
        key.hash(&mut hasher);
        format!("{:016x}", hasher.finish())
    }
}

/// Request to initiate a player transfer.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransferRequest {
    /// Player to transfer
    pub player_id: PlayerId,
    /// Current server
    pub source_server: ServerId,
    /// Target server
    pub target_server: ServerId,
    /// Target position in world coordinates
    pub target_position: WorldCoordinate,
    /// Reason for transfer
    pub reason: TransferReason,
    /// Priority (higher = more urgent)
    pub priority: u8,
}

/// Reason for initiating a transfer.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TransferReason {
    /// Player crossed region boundary
    RegionBoundary,
    /// Load balancing decision
    LoadBalancing,
    /// Server is shutting down
    ServerShutdown,
    /// Manual admin action
    AdminAction,
    /// Player teleport
    Teleport,
}

/// Result of a transfer operation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransferResult {
    /// Whether transfer succeeded
    pub success: bool,
    /// Transfer token if successful
    pub token: Option<TransferToken>,
    /// Error if failed
    pub error: Option<TransferError>,
    /// Time taken for transfer in milliseconds
    pub duration_ms: u64,
}

impl TransferResult {
    /// Creates a successful transfer result.
    pub fn success(token: TransferToken, duration_ms: u64) -> Self {
        Self {
            success: true,
            token: Some(token),
            error: None,
            duration_ms,
        }
    }

    /// Creates a failed transfer result.
    pub fn failure(error: TransferError) -> Self {
        Self {
            success: false,
            token: None,
            error: Some(error),
            duration_ms: 0,
        }
    }
}

/// Errors that can occur during transfer.
#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)]
pub enum TransferError {
    /// Target server is not available
    #[error("Target server unavailable: {0}")]
    TargetServerUnavailable(String),
    
    /// Player not found
    #[error("Player not found: {0}")]
    PlayerNotFound(String),
    
    /// Transfer token expired
    #[error("Transfer token expired")]
    TokenExpired,
    
    /// Invalid token signature
    #[error("Invalid token signature")]
    InvalidSignature,
    
    /// Serialization error
    #[error("Serialization error: {0}")]
    SerializationError(String),
    
    /// Transfer already in progress
    #[error("Transfer already in progress for player")]
    TransferInProgress,
    
    /// Target server rejected transfer
    #[error("Transfer rejected: {0}")]
    TransferRejected(String),
    
    /// Network error during transfer
    #[error("Network error: {0}")]
    NetworkError(String),
    
    /// Timeout waiting for transfer
    #[error("Transfer timeout")]
    Timeout,
}

/// Transfer notification sent to clients.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransferNotification {
    /// Player being transferred
    pub player_id: PlayerId,
    /// New server address to connect to
    pub target_address: String,
    /// Transfer token to present
    pub token: String,
    /// Suggested reconnect delay in milliseconds
    pub reconnect_delay_ms: u64,
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::player::PlayerInfo;

    #[test]
    fn test_transfer_token_creation_and_verification() {
        let player_id = PlayerId::new();
        let source = ServerId::new();
        let target = ServerId::new();
        let state = PlayerState::new(PlayerInfo::new(player_id, "TestPlayer".to_string()));
        let secret = b"test_secret_key";

        let token = TransferToken::new(player_id, source, target, state, 300, secret).unwrap();
        assert!(token.verify(secret).is_ok());
    }

    #[test]
    fn test_transfer_token_invalid_signature() {
        let player_id = PlayerId::new();
        let source = ServerId::new();
        let target = ServerId::new();
        let state = PlayerState::new(PlayerInfo::new(player_id, "TestPlayer".to_string()));

        let token = TransferToken::new(player_id, source, target, state, 300, b"key1").unwrap();
        assert!(matches!(
            token.verify(b"wrong_key"),
            Err(TransferError::InvalidSignature)
        ));
    }
}