Skip to content

Instantly share code, notes, and snippets.

@Andoryuuta
Last active January 13, 2018 21:44
Show Gist options
  • Save Andoryuuta/2045593e88ca2336a3f9687d9ac2e5e2 to your computer and use it in GitHub Desktop.
Save Andoryuuta/2045593e88ca2336a3f9687d9ac2e5e2 to your computer and use it in GitHub Desktop.
Renegade GSA list viewer in rust
use std::net::UdpSocket;
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream};
use std::io::{BufRead, BufReader, BufWriter, Read, Write};
const GSA_ADDRESS: &str = "renmaster.cncnet.org:28900"; // or "master-gsa.renlist.n00b.hk:28900"
const EXPECTED_FIRST_RESP: &'static [u8] = b"\\basic\\\\secure\\IGNORE";
const GET_HOSTS_QUERY:&'static [u8] = b"\\gamename\\ccrenegade\\enctype\\0\\validate\\TVJFLK6J\\final\\\\queryid\\1.1\\list\\cmp\\gamename\\ccrenegade\\where\\\\final\\";
fn main() {
let hosts = get_hosts();
for host in hosts {
println!("Querying {}", host.);
let q = query_host_status(host);
println!("{:>3}/{:<3}\t\t{:<40}\t{}:{}", q["numplayers"], q["maxplayers"], q["hostname"].trim_left(), host.ip(), q["hostport"]);
}
}
fn get_hosts() -> Vec<SocketAddr> {
// Connect to the GameSpy Arcade master server.
let stream = TcpStream::connect(GSA_ADDRESS).expect("Failed to connect to GSA master server");
let mut reader = BufReader::new(&stream);
let mut writer = BufWriter::new(&stream);
// Check for GSA "\basic\\secure\IGNORE\" packet.
let mut first_packet = vec![0; EXPECTED_FIRST_RESP.len()];
reader
.read_exact(&mut first_packet)
.expect("Failed reading first GSA packet");
if first_packet != EXPECTED_FIRST_RESP {
// TODO(Andoryuuta): Make this return an error when converting into a library.
panic!("GSA master server didn't send the expected packet!");
}
// Query the GameSpy master server for game hosts.
let _ = writer.write(GET_HOSTS_QUERY);
// Read in the hosts data from the server.
let mut hosts = Vec::new();
{
let mut hosts_data = vec![];
// The host data packet is terminated with b"\\final\\", so just read until the first backslash.
let n = reader
.read_until(b'\\', &mut hosts_data)
.expect("Error reading hosts data");
// Remove the delimiter byte.
hosts_data.truncate(n - 1);
assert_eq!(hosts_data.len() % 6, 0);
for b in hosts_data.chunks(6) {
let ip = Ipv4Addr::new(b[0], b[1], b[2], b[3]);
let port: u16 = (b[4] as u16) << 8 | (b[5] as u16);
let socket_address = SocketAddr::new(IpAddr::from(ip), port);
hosts.push(socket_address);
}
}
hosts
}
fn query_host_status(host:SocketAddr) -> HashMap<String, String>{
// Bind and "connect" a UDP socket.
let sock = UdpSocket::bind("0.0.0.0:0").expect("Failed to get UDP bind address from OS");
sock.connect(format!("{}:{}", host.ip(), host.port())).expect("Failed to connect to game host");
// Send the status query.
sock.send(b"\\status\\").expect("Failed to send UDP datagram");
// Read in the full response query.
let mut query_string = String::new();
loop{
let mut buf:[u8; 1024*4] = [0; 1024*4];
match sock.recv(&mut buf) {
Ok(received) => {
let tmp_str = std::str::from_utf8(&buf[..received-1]).unwrap();
query_string.push_str(tmp_str);
if tmp_str.contains("\\final\\"){
break
}
},
Err(e) => println!("recv function failed: {:?}", e),
}
}
// Split string for converting to key-value pairs.
let query_split: Vec<&str> = query_string.split('\\').collect();
let mut query_fields = HashMap::new();
// There has got to be a better way to do a loop with a specific step.
let mut i = 1;
while i < query_split.len()-1{
query_fields.insert(query_split[i].to_owned(), query_split[i+1].to_owned());
i += 2;
}
query_fields
}
//query : \gametype\Infantry\mapname\C&C_Gobi.mix\nextmap\C&C_Ulake_D3.mix\timeleft\00.18.22\timeelapsed\00.11.30\timelimit\00.30.00\Starting Credits\50\FDS\Dragonade 1.8.1\Website\www.mpforums.com\ts\talk.cncirc.net\irc\irc.cncirc.net #mpf-newmaps\taunts\1\veteran system\1\Crates\1\Character Refunds\1\Donate\1\Loot\1\Infinite Ammo\1\Advanced Kill Messages\1\Parachutes\1\Points Distribution\1\Extra Radio Commands\1\Taunts\1\Vehicle Ownership\1\Vehicle Shells\1\Vehicle Queue\1\Sounds and Taunts\1\hostname\ MPF UltraAOW NewMaps\gamename\ccrenegade\gamever\838\hostport\1337\password\0\numplayers\16\maxplayers\50\queryid\12912.1\player_0\Nod\score_0\3255\kills_0\47\deaths_0\53\time_0\00.11.30\ping_0\3\team_0\Nod\player_1\GDI\score_1\3380\kills_1\53\deaths_1\45\time_1\00.11.30\ping_1\3\team_1\GDI\player_2\kirou11\score_2\339\kills_2\5\deaths_2\7\time_2\00.11.30\ping_2\171\team_2\Nod\player_3\nod398\score_3\443\kills_3\7\deaths_3\12\time_3\00.11.30\ping_3\128\team_3\Nod\player_4\RIDERSRULE\score_4\406\kills_4\3\deaths_4\7\time_4\00.11.30\ping_4\58\team_4\Nod\player_5\Cereal_Killer\score_5\725\kills_5\18\deaths_5\6\time_5\00.11.30\ping_5\54\team_5\GDI\player_6\Oxdradium\score_6\211\kills_6\9\deaths_6\5\time_6\00.11.30\ping_6\142\team_6\Nod\player_7\MPFwobbly196\score_7\184\kills_7\2\deaths_7\2\time_7\00.11.30\ping_7\140\team_7\GDI\player_8\viperexcl\score_8\430\kills_8\1\deaths_8\13\time_8\00.11.30\ping_8\173\team_8\GDI\player_9\dadukas\score_9\647\kills_9\8\deaths_9\7\time_9\00.11.30\ping_9\74\team_9\GDI\player_10\Christian\score_10\588\kills_10\14\deaths_10\4\time_10\00.11.30\ping_10\81\team_10\GDI\player_11\Redz85\score_11\358\kills_11\7\deaths_11\7\time_11\00.11.30\ping_11\176\team_11\Nod\queryid\12912.2\player_12\JO_MOMMA_USA\score_12\54\kills_12\3\deaths_12\0\time_12\00.11.30\ping_12\50\team_12\Nod\player_13\testrambo\score_13\812\kills_13\4\deaths_13\5\time_13\00.11.30\ping_13\124\team_13\Nod\player_14\defries2\score_14\571\kills_14\3\deaths_14\4\time_14\00.11.30\ping_14\137\team_14\GDI\player_15\DDoSer\score_15\613\kills_15\8\deaths_15\9\time_15\00.11.30\ping_15\188\team_15\Nod\player_16\aaaaaaaa\score_16\134\kills_16\3\deaths_16\5\time_16\00.11.27\ping_16\187\team_16\GDI\player_17\unknown\score_17\16\kills_17\1\deaths_17\2\time_17\00.06.19\ping_17\246\team_17\GDI\queryid\12912.3\final\\queryid\12912.4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment