scroll.server/services/gateway/src/bin/scroll-probe.rs

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()),
}
}
}