use std::time::Duration; use logic::{ message_type, AvailableServerCommandMessage, EndClientTurnMessage, KeepAliveMessage, LoginMessage, LoginOkMessage, LogicCommand, LogicCommandHeader, LogicDataTables, OwnHomeDataMessage, }; use titan::crypto::SessionCipher; use titan::{FrameCodec, FrameHeader, MessageFrame, MessageMeta, Payload, HEADER_LEN}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; struct Probe { stream: TcpStream, cipher: SessionCipher, } impl Probe { async fn connect(endpoint: &str) -> std::io::Result { let stream = TcpStream::connect(endpoint).await?; stream.set_nodelay(true)?; Ok(Self { stream, cipher: SessionCipher::scroll_rc4(), }) } async fn send(&mut self, message: &M) -> std::io::Result<()> { let payload = message.to_bytes().expect("encode"); let cipher_text = self.cipher.encrypt_outbound(&payload).expect("encrypt"); let frame = MessageFrame::new(M::MESSAGE_TYPE, M::MESSAGE_VERSION, cipher_text); let bytes = FrameCodec::encode(&frame).expect("frame"); self.stream.write_all(&bytes).await?; self.stream.flush().await } async fn receive(&mut self) -> std::io::Result<(u16, Vec)> { let mut header_bytes = [0u8; HEADER_LEN]; tokio::time::timeout( Duration::from_secs(10), self.stream.read_exact(&mut header_bytes), ) .await??; let header = FrameHeader::parse(&header_bytes); let mut payload = vec![0u8; header.payload_len]; tokio::time::timeout(Duration::from_secs(10), self.stream.read_exact(&mut payload)).await??; let plain = self.cipher.decrypt_inbound(&payload).expect("decrypt"); Ok((header.message_type, plain)) } } #[tokio::main] async fn main() -> Result<(), Box> { let endpoint = std::env::args().nth(1).unwrap_or_else(|| "127.0.0.1:9339".to_owned()); let account_high: i32 = std::env::args() .nth(2) .and_then(|value| value.parse().ok()) .unwrap_or(0); let account_low: i32 = std::env::args() .nth(3) .and_then(|value| value.parse().ok()) .unwrap_or(0); let pass_token = std::env::args().nth(4).filter(|token| token != "-"); let claim_free_chest = std::env::args().any(|arg| arg == "--claim-free-chest"); let desync = std::env::args().any(|arg| arg == "--desync"); let csv_root = std::env::var("SCROLL_CSV_ROOT").unwrap_or_else(|_| "assets".to_owned()); match LogicDataTables::load_from_dir(&csv_root) { Ok(tables) => { let count = tables.loaded_files().len(); LogicDataTables::install(std::sync::Arc::new(tables)); println!("loaded {count} data table(s) from {csv_root}"); } Err(error) => println!("no data tables ({error}), printing raw ids"), } println!("connecting to {endpoint}"); let mut probe = Probe::connect(&endpoint).await?; let login = LoginMessage { account_id: titan::LogicLong::new(account_high, account_low), pass_token, client_major_version: 2, client_minor_version: 0, client_build: 1306, resource_sha: Some("0".repeat(40)), udid: Some("scroll-probe".to_owned()), device: Some("probe".to_owned()), os_version: Some("0.0".to_owned()), preferred_device_language: "EN".to_owned(), ..LoginMessage::default() }; probe.send(&login).await?; println!("-> 10101 LoginMessage (2.0.1306)"); loop { let (message_type, payload) = probe.receive().await?; match message_type { message_type::LOGIN_OK => { let ok = LoginOkMessage::from_bytes(&payload)?; println!("<- 20104 LoginOkMessage"); println!(" account {}", ok.account_id); println!(" home {}", ok.home_id); println!(" pass token {}", ok.pass_token.as_deref().unwrap_or("-")); println!( " server {}.{}.{} content {}", ok.server_major_version, ok.server_minor_version, ok.server_build, ok.content_version ); println!(" sessions {}", ok.session_count); println!(" created {}", ok.account_created_date.as_deref().unwrap_or("-")); } message_type::LOGIN_FAILED => { let failed = logic::LoginFailedMessage::from_bytes(&payload)?; println!("<- 20103 LoginFailedMessage code {}", failed.error_code); return Ok(()); } message_type::OWN_HOME_DATA => { let home = OwnHomeDataMessage::from_bytes(&payload)?; println!("<- 24101 OwnHomeDataMessage ({} bytes)", payload.len()); println!(" home id {}", home.home.home_id); println!( " arena {} (trophy limit {})", home.home.arena, home.home .arena_data() .map(|arena| arena.trophy_limit()) .unwrap_or(-1) ); println!(" decks {}", home.home.decks.len()); for (index, deck) in home.home.decks.iter().enumerate() { let cards: Vec = deck .slots .iter() .map(|slot| match slot { Some(spell) => spell.data.to_string(), None => "-".to_owned(), }) .collect(); println!(" deck {index} {}", cards.join(" ")); } let collection: Vec = home .home .spell_collection .spells .iter() .map(|spell| spell.data.to_string()) .collect(); println!( " collection {} [{}]", collection.len(), collection.join(" ") ); println!(" chest slots {}", home.home.chest_slots.len()); println!(" name {}", home.avatar.name); println!(" level {}", home.avatar.exp_level); println!(" trophies {}", home.avatar.score); println!(" diamonds {}", home.avatar.diamonds); for (index, slots) in home.avatar.commodities.types.iter().enumerate() { if !slots.is_empty() { let entries: Vec = slots .iter() .map(|slot| format!("{}={}", slot.data, slot.count)) .collect(); println!(" commodity {index} {}", entries.join(" ")); } } println!(" random seed {}", home.random_seed); if claim_free_chest { let turn = EndClientTurnMessage { tick: 0, checksum: home.home.chest_id_counter + ((home.home.spell_collection.spells.len() as i32) << 16) + i32::from(desync), commands: vec![LogicCommand::CollectFreeChest { header: LogicCommandHeader::for_account(home.avatar.account_id), }], trailing: None, }; probe.send(&turn).await?; println!("-> 14102 EndClientTurnMessage [LogicCollectFreeChestCommand]"); } else { probe.send(&KeepAliveMessage::default()).await?; println!("-> 10108 KeepAliveMessage"); } } message_type::OUT_OF_SYNC => { let out_of_sync = logic::OutOfSyncMessage::from_bytes(&payload)?; println!("<- 24104 OutOfSyncMessage"); println!(" server checksum {}", out_of_sync.server_checksum); println!(" client checksum {}", out_of_sync.client_checksum); println!(" tick {}", out_of_sync.tick); } message_type::AVAILABLE_SERVER_COMMAND => { let envelope = AvailableServerCommandMessage::from_bytes(&payload)?; println!("<- 24111 AvailableServerCommandMessage ({} bytes)", payload.len()); if let LogicCommand::ClaimReward { command: claim } = envelope.command { println!(" chest source {}", claim.chest_source); println!(" chest id {}", claim.chest_id); if let Some(reward) = claim.reward { println!(" gold {}", reward.gold); println!(" diamonds {}", reward.diamonds); for spell in reward.spells.unwrap_or_default() { println!(" card {} x{}", spell.data, spell.count); } } } probe.send(&KeepAliveMessage::default()).await?; println!("-> 10108 KeepAliveMessage"); } message_type::KEEP_ALIVE_SERVER => { println!("<- 20108 KeepAliveServerMessage"); println!("lobby reached, probe done"); return Ok(()); } other => println!("<- {other} ({} bytes)", payload.len()), } } }