211 lines
9.9 KiB
Rust
211 lines
9.9 KiB
Rust
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<Self> {
|
|
let stream = TcpStream::connect(endpoint).await?;
|
|
stream.set_nodelay(true)?;
|
|
Ok(Self {
|
|
stream,
|
|
cipher: SessionCipher::scroll_rc4(),
|
|
})
|
|
}
|
|
async fn send<M: Payload + MessageMeta>(&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<u8>)> {
|
|
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<dyn std::error::Error>> {
|
|
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<String> = 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<String> = 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<String> = 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()),
|
|
}
|
|
}
|
|
}
|