Horizon-Network-Common 0.2.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 std::time::{SystemTime, UNIX_EPOCH};

use crate::player::PlayerId;
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: String,
    /// Player being transferred
    pub player_id: PlayerId,
    /// Source server
    pub source_server: ServerId,
    /// Target server
    pub target_server: ServerId,
    /// Target server address
    pub target_address: String,
    /// When this token was created (ms since epoch)
    pub created_at_ms: u64,
    /// When this token expires (ms since epoch)
    pub expires_at_ms: u64,
    /// Cryptographic signature (HMAC or similar)
    pub signature: String,
}

/// Token validity duration in seconds
const DEFAULT_TOKEN_VALIDITY_SECS: u64 = 60;

impl TransferToken {
    /// Creates a new transfer token.
    pub fn new(
        player_id: PlayerId,
        source_server: ServerId,
        target_server: ServerId,
        target_address: String,
        secret_key: &[u8],
    ) -> Self {
        Self::with_validity(player_id, source_server, target_server, target_address, DEFAULT_TOKEN_VALIDITY_SECS, secret_key)
    }

    /// Creates a new transfer token with custom validity duration.
    pub fn with_validity(
        player_id: PlayerId,
        source_server: ServerId,
        target_server: ServerId,
        target_address: String,
        valid_duration_secs: u64,
        secret_key: &[u8],
    ) -> Self {
        let now_ms = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_millis() as u64;

        let token_id = format!("txfr-{}-{}", now_ms, Self::rand_component());
        let expires_at_ms = now_ms + (valid_duration_secs * 1000);

        let mut token = Self {
            token_id,
            player_id,
            source_server,
            target_server,
            target_address,
            created_at_ms: now_ms,
            expires_at_ms,
            signature: String::new(),
        };

        token.signature = token.compute_signature(secret_key);
        token
    }

    /// Verifies the token signature and expiration.
    pub fn verify(&self, secret_key: &[u8]) -> Result<(), TransferError> {
        let now_ms = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_millis() as u64;

        if now_ms > self.expires_at_ms {
            return Err(TransferError::TokenExpired);
        }

        let expected_signature = self.compute_signature(secret_key);
        if self.signature != expected_signature {
            return Err(TransferError::InvalidSignature);
        }

        Ok(())
    }

    /// Computes signature for this token.
    fn compute_signature(&self, key: &[u8]) -> String {
        use std::collections::hash_map::DefaultHasher;
        use std::hash::{Hash, Hasher};

        let data = format!(
            "{}:{}:{}:{}:{}",
            self.token_id, self.player_id, self.source_server, self.target_server, self.expires_at_ms
        );

        let mut hasher = DefaultHasher::new();
        data.hash(&mut hasher);
        key.hash(&mut hasher);
        format!("{:016x}", hasher.finish())
    }

    /// Generates a random component for token IDs.
    fn rand_component() -> u32 {
        use std::collections::hash_map::DefaultHasher;
        use std::hash::{Hash, Hasher};

        let mut hasher = DefaultHasher::new();
        std::time::Instant::now().hash(&mut hasher);
        (hasher.finish() % 1000000) as u32
    }

    /// Serializes the token to JSON.
    pub fn to_json(&self) -> Result<String, TransferError> {
        serde_json::to_string(self)
            .map_err(|e| TransferError::SerializationError(e.to_string()))
    }

    /// Deserializes a token from JSON.
    pub fn from_json(json: &str) -> Result<Self, TransferError> {
        serde_json::from_str(json)
            .map_err(|e| TransferError::SerializationError(e.to_string()))
    }
}

/// 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::*;

    #[test]
    fn test_transfer_token_creation_and_verification() {
        let player_id = PlayerId::new();
        let source = ServerId::new();
        let target = ServerId::new();
        let secret = b"test_secret_key";

        let token = TransferToken::new(
            player_id.clone(), 
            source, 
            target, 
            "127.0.0.1:8080".to_string(),
            secret
        );
        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 token = TransferToken::new(
            player_id, 
            source, 
            target, 
            "127.0.0.1:8080".to_string(),
            b"key1"
        );
        assert!(matches!(
            token.verify(b"wrong_key"),
            Err(TransferError::InvalidSignature)
        ));
    }
}