use crate::crypter::{CrypterInput, CrypterUpdate, SLPcrypter};
use crate::dolphin::{DolphinEvent, SLPreader};
use crate::handshake::{HandshakeConfig, ParamRange};
use crate::msg::Msg;
use crate::net::GameNet;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Arc;
use tokio::sync::mpsc::error::TryRecvError;
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DiscoveryMode {
BootstrapDhtFallback,
BootstrapOnly,
DhtOnly,
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub enum NetworkProfile {
Aggressive,
#[default]
Standard,
Resilient,
}
#[derive(Debug, Clone)]
pub struct TimeoutConfig {
pub newgame_match_ms: u64,
pub handshake_secs: u64,
pub http_bootstrap_ms: u64,
}
impl From<NetworkProfile> for TimeoutConfig {
fn from(profile: NetworkProfile) -> Self {
match profile {
NetworkProfile::Aggressive => Self {
newgame_match_ms: 500,
handshake_secs: 2,
http_bootstrap_ms: 1000,
},
NetworkProfile::Standard => Self {
newgame_match_ms: 2000,
handshake_secs: 8,
http_bootstrap_ms: 4000,
},
NetworkProfile::Resilient => Self {
newgame_match_ms: 4000,
handshake_secs: 16,
http_bootstrap_ms: 8000,
},
}
}
}
impl Default for TimeoutConfig {
fn default() -> Self {
NetworkProfile::Standard.into()
}
}
#[derive(Debug, Clone)]
pub enum SessionError {
Disconnected,
}
impl std::fmt::Display for SessionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SessionError::Disconnected => write!(f, "Session has been disconnected"),
}
}
}
impl std::error::Error for SessionError {}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum SessionState {
InGame,
Idle,
Ended,
}
#[repr(u8)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ConnectionState {
Discovering = 0,
Discovered = 1,
}
impl ConnectionState {
fn from_u8(value: u8) -> Self {
match value {
1 => Self::Discovered,
_ => Self::Discovering,
}
}
}
pub struct Session {
session_state: Arc<Mutex<SessionState>>,
connection_state: Arc<AtomicU8>,
send_buf: Arc<Mutex<Vec<Vec<u8>>>>,
incoming_msgs_rx: UnboundedReceiver<Msg>,
}
impl Session {
pub fn try_recv(&mut self) -> Result<Option<Msg>, SessionError> {
match self.incoming_msgs_rx.try_recv() {
Ok(m) => Ok(Some(m)),
Err(TryRecvError::Empty) => Ok(None),
Err(TryRecvError::Disconnected) => Err(SessionError::Disconnected),
}
}
pub async fn send(&self, data: Vec<u8>) -> bool {
let mut buf = self.send_buf.lock().await;
buf.push(data);
true
}
pub fn connection_state(&self) -> ConnectionState {
ConnectionState::from_u8(self.connection_state.load(Ordering::Relaxed))
}
pub fn peer_discovered(&self) -> bool {
self.connection_state() == ConnectionState::Discovered
}
pub async fn state(&self) -> SessionState {
*self.session_state.lock().await
}
pub async fn in_game(&self) -> bool {
self.state().await == SessionState::InGame
}
}
pub(crate) const DEFAULT_RELAY_URL: &str = "http://slippi-ssp.net:3340";
pub(crate) const DEFAULT_BOOTSTRAP_URL: &str = "http://slippi-ssp.net:5000";
pub struct SessionBuilder {
encryption_enabled: bool,
handshake_config: HandshakeConfig,
bootstrap_url: Option<String>,
relay_url: Option<String>,
discovery_mode: DiscoveryMode,
timeouts: TimeoutConfig,
max_packet_length: usize,
dolphin_host: String,
dolphin_port: u16,
}
impl Default for SessionBuilder {
fn default() -> Self {
Self::new()
}
}
impl SessionBuilder {
pub fn new() -> Self {
Self {
encryption_enabled: true,
handshake_config: HandshakeConfig {
rollover: ParamRange::new(60, 120, 240),
offset: ParamRange::new(30, 60, 120),
slp_version_min: [0, 0, 0],
slp_version_max: [u8::MAX, u8::MAX, u8::MAX],
},
bootstrap_url: None,
relay_url: None,
discovery_mode: DiscoveryMode::BootstrapDhtFallback,
timeouts: TimeoutConfig::default(),
max_packet_length: 32768, dolphin_host: "127.0.0.1".to_string(),
dolphin_port: 51441,
}
}
pub fn set_dolphin_host(mut self, host: &str) -> Self {
self.dolphin_host = host.to_string();
self
}
pub fn set_dolphin_port(mut self, port: u16) -> Self {
self.dolphin_port = port;
self
}
pub fn set_encryption(mut self, enabled: bool) -> Self {
self.encryption_enabled = enabled;
self
}
pub fn set_bootstrap_url(mut self, url: &str) -> Self {
self.bootstrap_url = Some(url.trim_end_matches('/').to_string());
self
}
pub fn set_relay_url(mut self, url: &str) -> Self {
self.relay_url = Some(url.trim_end_matches('/').to_string());
self
}
pub fn set_discovery_mode(mut self, mode: DiscoveryMode) -> Self {
self.discovery_mode = mode;
self
}
pub fn set_network_profile(mut self, profile: NetworkProfile) -> Self {
self.timeouts = profile.into();
self
}
pub fn set_timeouts(mut self, timeouts: TimeoutConfig) -> Self {
self.timeouts = timeouts;
self
}
pub fn set_max_packet_length(mut self, len: usize) -> Self {
self.max_packet_length = len;
self
}
pub fn set_slp_version_filter(mut self, min: [u8; 3], max: [u8; 3]) -> Self {
self.handshake_config.slp_version_min = min;
self.handshake_config.slp_version_max = max;
self
}
pub fn set_key_rotation(mut self, interval: u64, rollback_offset: u64) -> Self {
self.handshake_config.rollover = ParamRange::new(interval, interval, interval);
self.handshake_config.offset =
ParamRange::new(rollback_offset, rollback_offset, rollback_offset);
self
}
pub fn set_rollover_range(mut self, min: u64, preferred: u64, max: u64) -> Self {
self.handshake_config.rollover = ParamRange::new(min, preferred, max);
self
}
pub fn set_offset_range(mut self, min: u64, preferred: u64, max: u64) -> Self {
self.handshake_config.offset = ParamRange::new(min, preferred, max);
self
}
pub async fn connect(self) -> Session {
let send_buf = Arc::new(Mutex::new(Vec::new()));
let (incoming_msgs_tx, incoming_msgs_rx) = unbounded_channel::<Msg>();
let (gameevent_tx, mut gameevent_rx) = unbounded_channel::<DolphinEvent>();
let session_cancel_token = CancellationToken::new();
let handshake_config = self.handshake_config;
let (crypter_key_rx, crypter_input_tx, handshake_succ_tx) = if self.encryption_enabled {
let (update_tx, update_rx) = unbounded_channel::<CrypterUpdate>();
let (input_tx, input_rx) = unbounded_channel::<CrypterInput>();
let (handshake_succ_tx, handshake_succ_rx) = unbounded_channel::<(u64, u64)>();
let crypter = SLPcrypter::new(
input_rx,
update_tx,
handshake_config.rollover.preferred,
handshake_config.offset.preferred,
Some(handshake_succ_rx),
);
tokio::spawn(crypter.start());
(Some(update_rx), Some(input_tx), Some(handshake_succ_tx))
} else {
(None, None, None)
};
let reader = SLPreader::new(
&self.dolphin_host,
self.dolphin_port,
crypter_input_tx,
gameevent_tx,
session_cancel_token.clone(),
);
tokio::spawn(async move {
reader.start().await;
});
let meta = loop {
match gameevent_rx.recv().await {
Some(DolphinEvent::NewGame(meta)) => break meta,
_ => continue,
}
};
let send_buf_clone = send_buf.clone();
let encryption_enabled = self.encryption_enabled;
let bootstrap_url = Some(
self.bootstrap_url
.unwrap_or_else(|| DEFAULT_BOOTSTRAP_URL.to_string()),
);
let relay_url = self
.relay_url
.unwrap_or_else(|| DEFAULT_RELAY_URL.to_string());
let discovery_mode = self.discovery_mode;
let session_state = Arc::new(Mutex::new(SessionState::InGame));
let net_session_state = session_state.clone();
let connection_state = Arc::new(AtomicU8::new(ConnectionState::Discovering as u8));
let net_connection_state = connection_state.clone();
let timeouts = self.timeouts;
let max_packet_length = self.max_packet_length;
tokio::spawn(async move {
let bootstrap_relay_url = match relay_url.parse::<iroh::RelayUrl>() {
Ok(url) => Some(url),
Err(e) => {
panic!("[net] invalid relay URL '{}': {}", relay_url, e);
}
};
GameNet::connect(
meta,
net_session_state,
net_connection_state,
encryption_enabled,
discovery_mode,
handshake_config,
bootstrap_url,
bootstrap_relay_url,
send_buf_clone,
incoming_msgs_tx,
crypter_key_rx,
gameevent_rx,
handshake_succ_tx,
session_cancel_token,
timeouts,
max_packet_length,
)
.await;
});
Session {
session_state,
connection_state,
send_buf,
incoming_msgs_rx,
}
}
}