-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
517 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
//! Unreal2 game query modules | ||
use crate::protocols::unreal2::game_query_mod; | ||
|
||
game_query_mod!(darkesthour, "Darkest Hour: Europe '44-'45 (2008)", 7758); | ||
game_query_mod!(devastation, "Devastation (2003)", 7778); | ||
game_query_mod!(killingfloor, "Killing Floor", 7708); | ||
game_query_mod!(redorchestra, "Red Orchestra", 7759); | ||
game_query_mod!(ut2003, "Unreal Tournament 2003", 7758); | ||
game_query_mod!(ut2004, "Unreal Tournament 2004", 7778); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
/// The implementation. | ||
pub mod protocol; | ||
/// All types used by the implementation. | ||
pub mod types; | ||
|
||
pub use protocol::*; | ||
pub use types::*; | ||
|
||
/// Generate a module containing a query function for a valve game. | ||
/// | ||
/// * `mod_name` - The name to be given to the game module (see ID naming | ||
/// conventions in CONTRIBUTING.md). | ||
/// * `pretty_name` - The full name of the game, will be used as the | ||
/// documentation for the created module. | ||
/// * `default_port` - Passed through to [game_query_fn]. | ||
macro_rules! game_query_mod { | ||
($mod_name: ident, $pretty_name: expr, $default_port: literal) => { | ||
#[doc = $pretty_name] | ||
pub mod $mod_name { | ||
crate::protocols::unreal2::game_query_fn!($default_port); | ||
} | ||
}; | ||
} | ||
|
||
pub(crate) use game_query_mod; | ||
|
||
// Allow generating doc comments: | ||
// https://users.rust-lang.org/t/macros-filling-text-in-comments/20473 | ||
/// Generate a query function for a valve game. | ||
/// | ||
/// * `default_port` - The default port the game uses. | ||
macro_rules! game_query_fn { | ||
($default_port: literal) => { | ||
crate::protocols::unreal2::game_query_fn! {@gen $default_port, concat!( | ||
"Make a Unreal2 query for with default timeout settings and default extra request settings.\n\n", | ||
"If port is `None`, then the default port (", stringify!($default_port), ") will be used.")} | ||
}; | ||
|
||
(@gen $default_port: literal, $doc: expr) => { | ||
#[doc = $doc] | ||
pub fn query( | ||
address: &std::net::IpAddr, | ||
port: Option<u16>, | ||
) -> crate::GDResult<crate::protocols::unreal2::Response> { | ||
crate::protocols::unreal2::query( | ||
&std::net::SocketAddr::new(*address, port.unwrap_or($default_port)), | ||
None, | ||
) | ||
} | ||
}; | ||
} | ||
|
||
pub(crate) use game_query_fn; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,246 @@ | ||
use crate::buffer::{Buffer, StringDecoder}; | ||
use crate::errors::GDErrorKind::PacketBad; | ||
use crate::protocols::types::TimeoutSettings; | ||
use crate::socket::{Socket, UdpSocket}; | ||
use crate::utils::retry_on_timeout; | ||
use crate::GDResult; | ||
|
||
use super::{MutatorsAndRules, PacketKind, Players, Response, ServerInfo}; | ||
|
||
use std::net::SocketAddr; | ||
|
||
use byteorder::{ByteOrder, LittleEndian}; | ||
|
||
// TODO: Validate this is the correct packet size | ||
pub const PACKET_SIZE: usize = 5012; | ||
|
||
/// The Unreal2 protocol implementation. | ||
pub(crate) struct Unreal2Protocol { | ||
socket: UdpSocket, | ||
retry_count: usize, | ||
} | ||
|
||
impl Unreal2Protocol { | ||
pub fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> { | ||
let socket = UdpSocket::new(address)?; | ||
let retry_count = timeout_settings | ||
.as_ref() | ||
.map(|t| t.get_retries()) | ||
.unwrap_or_else(|| TimeoutSettings::default().get_retries()); | ||
socket.apply_timeout(&timeout_settings)?; | ||
|
||
Ok(Self { | ||
socket, | ||
retry_count, | ||
}) | ||
} | ||
|
||
/// Send a request packet and recieve the first response (with retries). | ||
fn get_request_data(&mut self, packet_type: PacketKind) -> GDResult<Vec<u8>> { | ||
retry_on_timeout(self.retry_count, move || { | ||
self.get_request_data_impl(packet_type) | ||
}) | ||
} | ||
|
||
/// Send a request packet | ||
fn get_request_data_impl(&mut self, packet_type: PacketKind) -> GDResult<Vec<u8>> { | ||
let request = [0x79, 0, 0, 0, packet_type as u8]; | ||
self.socket.send(&request)?; | ||
|
||
let data = self.socket.receive(Some(PACKET_SIZE))?; | ||
|
||
Ok(data) | ||
} | ||
|
||
/// Consume the header part of a response packet, validate that the packet | ||
/// type matches what is expected. | ||
fn consume_response_headers<B: ByteOrder>( | ||
buffer: &mut Buffer<B>, | ||
expected_packet_type: PacketKind, | ||
) -> GDResult<()> { | ||
// Skip header | ||
buffer.move_cursor(4)?; | ||
|
||
let packet_type: u8 = buffer.read()?; | ||
|
||
let packet_type: PacketKind = packet_type.try_into()?; | ||
|
||
if packet_type != expected_packet_type { | ||
Err(PacketBad.context(format!( | ||
"Packet response ({:?}) didn't match request ({:?}) packet type", | ||
packet_type, expected_packet_type | ||
))) | ||
} else { | ||
Ok(()) | ||
} | ||
} | ||
|
||
/// Make a full server query. | ||
pub fn query(&mut self) -> GDResult<Response> { | ||
// Fetch the server info, this can only handle one response packet | ||
let server_info = { | ||
let data = self.get_request_data(PacketKind::ServerInfo)?; | ||
let mut buffer = Buffer::<LittleEndian>::new(&data); | ||
// TODO: Maybe put consume headers in individual packet parse methods | ||
Self::consume_response_headers(&mut buffer, PacketKind::ServerInfo)?; | ||
ServerInfo::parse(&mut buffer)? | ||
}; | ||
|
||
// TODO: Remove debug logging | ||
println!("{:#?}", server_info); | ||
|
||
// Fetch mutators and rules, this is a required packet so we validate that we | ||
// get at least one response. However there can be many packets in | ||
// response to a single request so we greedily handle packets until | ||
// we get a timeout (or any receive error). | ||
let mut mutators_and_rules = MutatorsAndRules::default(); | ||
{ | ||
let data = self.get_request_data(PacketKind::MutatorsAndRules)?; | ||
let mut buffer = Buffer::<LittleEndian>::new(&data); | ||
// TODO: Maybe put consume headers in individual packet parse methods | ||
Self::consume_response_headers(&mut buffer, PacketKind::MutatorsAndRules)?; | ||
mutators_and_rules.parse(&mut buffer)? | ||
}; | ||
|
||
// We could receive multiple packets in response | ||
while let Ok(data) = self.socket.receive(Some(PACKET_SIZE)) { | ||
let mut buffer = Buffer::<LittleEndian>::new(&data); | ||
|
||
let r = Self::consume_response_headers(&mut buffer, PacketKind::MutatorsAndRules); | ||
if r.is_err() { | ||
println!("{:?}", r); | ||
break; | ||
} | ||
|
||
mutators_and_rules.parse(&mut buffer)?; | ||
} | ||
|
||
// TODO: Remove debug logging | ||
println!("{:#?}", mutators_and_rules); | ||
|
||
// Pre-allocate the player arrays, but don't over allocate memory if the server | ||
// specifies an insane number of players. | ||
let mut players = Players::with_capacity(server_info.num_players.try_into().unwrap_or(10).min(50)); | ||
|
||
// Fetch first players packet (with retries) | ||
let mut players_data = self.get_request_data(PacketKind::Players); | ||
// Players are non required so if we don't get any responses we continue to | ||
// return | ||
while let Ok(data) = players_data { | ||
let mut buffer = Buffer::<LittleEndian>::new(&data); | ||
|
||
Self::consume_response_headers(&mut buffer, PacketKind::Players)?; | ||
|
||
players.parse(&mut buffer)?; | ||
|
||
// Receive next packet | ||
players_data = self.socket.receive(Some(PACKET_SIZE)); | ||
} | ||
|
||
// TODO: Handle extra info parsing when we detect certain game types (or maybe | ||
// include that in gather settings). | ||
|
||
Ok(Response { | ||
server_info, | ||
mutators_and_rules, | ||
players, | ||
}) | ||
} | ||
} | ||
|
||
/// Unreal 2 string decoder | ||
pub struct Unreal2StringDecoder; | ||
impl StringDecoder for Unreal2StringDecoder { | ||
type Delimiter = [u8; 1]; | ||
|
||
const DELIMITER: Self::Delimiter = [0x00]; | ||
|
||
fn decode_string(data: &[u8], cursor: &mut usize, delimiter: Self::Delimiter) -> GDResult<String> { | ||
let mut ucs2 = false; | ||
let mut length: usize = (*data | ||
.first() | ||
.ok_or(PacketBad.context("Tried to decode string without length"))?) | ||
.into(); | ||
|
||
let mut start = 0; | ||
|
||
// Check if it is a UCS-2 string | ||
if length >= 0x80 { | ||
ucs2 = true; | ||
|
||
length = (length & 0x7f) * 2; | ||
|
||
start += 1; | ||
|
||
// For UCS-2 strings, some unreal 2 games randomly insert an extra 0x01 here, | ||
// not included in the length. Skip it if present (hopefully this never happens | ||
// legitimately) | ||
if let Some(1) = data[start ..].first() { | ||
start += 1; | ||
} | ||
} | ||
|
||
// If UCS2 the first byte is the masked length of the string | ||
let string_data = if ucs2 { | ||
let string_data = &data[start .. start + length]; | ||
if string_data.len() != length { | ||
return Err(PacketBad.context("Not enough data in buffer to read string")); | ||
} | ||
string_data | ||
} else { | ||
// Else the string is null-delimited latin1 | ||
|
||
// TODO: Replace this with delimiter finder helper | ||
let position = data | ||
// Create an iterator over the data. | ||
.iter() | ||
// Find the position of the delimiter | ||
.position(|&b| b == delimiter.as_ref()[0]) | ||
// If the delimiter is not found, use the whole data slice. | ||
.unwrap_or(data.len()); | ||
|
||
length = position + 1; | ||
|
||
&data[1.min(position) .. position] | ||
}; | ||
|
||
// FIXME: This should be latin1 or usc2 | ||
let result = String::from_utf8_lossy(string_data); | ||
|
||
// Strip color encodings | ||
// TODO: Improve efficiency | ||
// TODO: There might be a nicer way to do this once string patterns are stable | ||
// https://github.com/rust-lang/rust/issues/27721 | ||
|
||
// After '0x1b' skip 3 characters (including the '0x1b') | ||
let mut char_skip = 0usize; | ||
let result: String = result | ||
.chars() | ||
.filter(|c: &char| { | ||
if '\x1b'.eq(c) { | ||
char_skip = 4; | ||
return false; | ||
} | ||
char_skip = char_skip.saturating_sub(1); | ||
char_skip == 0 | ||
}) | ||
.collect(); | ||
|
||
// Remove all characters between 0x00 and 0x1a | ||
let result = result.replace(|c: char| c > '\x00' && c <= '\x1a', ""); | ||
|
||
*cursor += start + length; | ||
|
||
// Strip delimiter that wasn't included in length | ||
Ok(result.trim_matches('\0').to_string()) | ||
} | ||
} | ||
|
||
/// Make an unreal2 query. | ||
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response> { | ||
let mut client = Unreal2Protocol::new(address, timeout_settings)?; | ||
|
||
client.query() | ||
} | ||
|
||
// TODO: Add tests |
Oops, something went wrong.