Merge branch 'main' into feat/rootless-capture

This commit is contained in:
Cain 2024-01-18 01:56:14 +00:00 committed by GitHub
commit 36d957ceb4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 1483 additions and 457 deletions

View file

@ -150,6 +150,7 @@ fn resolve_ip_or_domain<T: AsRef<str>>(host: T, extra_options: &mut Option<Extra
set_hostname_if_missing(host_str, extra_options);
resolve_domain(host_str)
}
}
/// Resolve a domain name to one of its IP addresses (the first one returned).

View file

@ -0,0 +1,29 @@
[package]
name = "gamedig-id-tests"
version = "0.0.1"
edition = "2021"
authors = [
"rust-GameDig contributors [https://github.com/gamedig/rust-gamedig/contributors]",
"node-GameDig contributors [https://github.com/gamedig/node-gamedig/contributors]",
]
license = "MIT"
description = "Test if IDs match the gamedig rules"
homepage = "https://github.com/gamedig/rust-gamedig/CONTRIBUTING.md#naming"
repository = "https://github.com/gamedig/rust-gamedig"
readme = "README.md"
rust-version = "1.65.0"
[features]
cli = ["dep:serde_json", "dep:serde"]
default = ["cli"]
[[bin]]
name = "gamedig-id-tests"
required-features = ["cli"]
[dependencies]
number_to_words = "0.1"
roman_numeral = "0.1"
serde_json = { version = "1", optional = true }
serde = { version = "1", optional = true, features = ["derive"] }

451
crates/id-tests/src/lib.rs Normal file
View file

@ -0,0 +1,451 @@
use std::collections::HashMap;
mod utils;
use utils::{extract_bracketed_suffix, split_on_switch_between_alpha_numeric};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IDRule {
IDsMustBeLowerCase,
NumbersAreTheirOwnWord,
IfFirstWordNumberNoDigits,
IfLastWordNumberMustBeAppended,
ConvertRomanNumeralsToArabic,
TwoWordsOrLessUseFullWords,
MoreThanTwoWordsMakeAcronym,
IfIDDuplicateSameGameAppendYearToNewer,
IfIDDuplicateSameGameAppendProtocol,
IfIDDuplicateNoAcronym,
IfModForQueriesProcessOnlyModName,
NoDuplicates,
}
#[derive(Clone, Debug)]
pub struct IDFail {
pub game_id: String,
pub game_name: String,
pub expected_id: String,
pub rule_stack: Vec<IDRule>,
}
impl IDFail {
fn new(game_id: String, game_name: String, expected_id: String, rule_stack: Vec<IDRule>) -> Self {
Self {
game_id,
game_name,
expected_id,
rule_stack,
}
}
}
/// Test a single game against the rules
pub fn test_game_name_rule(
seen_ids: &mut HashMap<String, Vec<String>>,
id: &str,
mut game: GameNameParsed,
is_mod_name: bool,
) -> Vec<IDFail> {
let mut wrong_ids = Vec::new();
let mut rule_stack = Vec::new();
if is_mod_name {
rule_stack.push(IDRule::IfModForQueriesProcessOnlyModName);
}
let mut suffix = String::new();
// A game's identification is a lowercase alphanumeric string will and be forged
// following these rules:
if id.to_lowercase().ne(id) {
wrong_ids.push(IDFail::new(
id.to_owned(),
game.name.to_owned(),
id.to_lowercase(),
vec![IDRule::IDsMustBeLowerCase],
));
}
// 5. Roman numbering will be converted to arabic numbering (XIV -> 14).
game.words = {
let mut is_first = true;
game.words
.into_iter()
.map(|w| {
// First word will never be a numeral
if is_first {
is_first = false;
w
} else if let Ok(number) = roman_numeral::RomanNumeral::from_string(&w) {
rule_stack.push(IDRule::ConvertRomanNumeralsToArabic);
number.get_u32().to_string()
} else {
w
}
})
.collect()
};
// 6. Unless numbers are at the end of a name, they will be considered words,
// but digits will always be used instead of the acronym (counter to #2)
// (Left 4 Dead -> l4d) unless they at the start position (7 Days to Die ->
// sdtd), if they are at the end (such as sequel number or the year), always
// append them (Team Fortress 2 -> teamfortress2, Unreal Tournament 2003 ->
// unrealtournament2003).
game.words = game
.words
.into_iter()
.flat_map(|w| {
let n = split_on_switch_between_alpha_numeric(&w);
if n.len() > 1 {
rule_stack.push(IDRule::NumbersAreTheirOwnWord);
}
n
})
.collect();
// If first word is number make text
if !game.words.is_empty() && game.words[0].chars().next().unwrap().is_ascii_digit() {
game.words[0] = number_to_words::number_to_words(game.words[0].parse::<f64>().unwrap(), false);
rule_stack.push(IDRule::IfFirstWordNumberNoDigits);
}
// If last word is number append full number
if let Some(last_word) = game.words.last() {
if last_word.chars().all(|c| c.is_ascii_digit()) {
suffix += &game.words.pop().unwrap();
rule_stack.push(IDRule::IfLastWordNumberMustBeAppended);
}
}
let main = if game.words.len() <= 2 {
// 1. Names composed of a maximum of two words (unless #4 applies) will result
// in an id where the words are concatenated (Dead Cells -> deadcells),
// acronyms in the name count as a single word (S.T.A.L.K.E.R. -> stalker).
rule_stack.push(IDRule::TwoWordsOrLessUseFullWords);
game.words
.iter()
.map(|w| w.trim_matches('-').to_owned())
.collect::<Vec<_>>()
.join("")
} else {
// 2. Names of more than two words shall be made into an acronym made of the
// initial letters (The Binding of Isaac -> tboi), hypenation composed words
// don't count as a single word, but of how many parts they are made of (Dino
// D-Day, 3 words, so ddd).
rule_stack.push(IDRule::MoreThanTwoWordsMakeAcronym);
game.words
.iter()
.map(|w| w.chars().next().unwrap())
.filter(|c| c.is_alphanumeric())
.collect()
};
let mut expected_id = format!("{}{}", main, suffix).to_lowercase();
if let Some(other_game_name_words) = seen_ids.get(&expected_id) {
let mut game_names_same = other_game_name_words.len() == game.words.len();
// Check all words in game name are the same
if game_names_same {
for i in 0 .. game.words.len() {
if game.words[i].to_lowercase() != other_game_name_words[i].to_lowercase() {
game_names_same = false;
break;
}
}
}
if game_names_same {
if let Some(year) = game.year {
// 3. If a game has the exact name as a previously existing id's game (Star Wars
// Battlefront 2, the 2005 and 2017 one), append the release year to the
// newer id (2005 would be swbf2 (suppose we already have this one supported)
// and 2017 would be swbf22017).
rule_stack.push(IDRule::IfIDDuplicateSameGameAppendYearToNewer);
expected_id = format!("{}{}", expected_id, year).to_lowercase();
} else if let Some(protocol) = game.optional_parts.first() {
// 7. If a game supports multiple protocols, multiple entries will be done for
// said game where the edition/protocol name (first disposable in this order)
// will be appended to the game name (Minecraft is divided by 2 editions,
// Java and Bedrock which will be minecraftjava and minecraftbedrock
// respectively) and one more entry can be added by the base name of the game
// which queries in a group said supported protocols to make generic queries
// easier and disposable.
rule_stack.push(IDRule::IfIDDuplicateSameGameAppendProtocol);
// Parse the protocol as a game name so we can remove all non-valid characters
let protocol_parsed = extract_game_parts_from_name(protocol);
expected_id = format!("{}{}", expected_id, protocol_parsed.words.concat(),);
}
}
}
// 4. If a new id (Day of Dragons -> dod) results in an id that already exists
// (Day of Defeat -> dod), then the new name should ignore rule #2 (Day of
// Dragons -> dayofdragons).
if seen_ids.contains_key(&expected_id) {
rule_stack.push(IDRule::IfIDDuplicateNoAcronym);
let main = game
.words
.iter()
.map(|w| w.trim_matches('-').to_owned())
.collect::<Vec<_>>()
.join("");
expected_id = format!("{}{}", main, suffix).to_lowercase();
}
// 8. If its actually about a mod that adds the ability for queries to be
// performed, process only the mod name.
if !is_mod_name && id != expected_id {
if let Some((_, mod_game)) = game.name.split_once('-') {
let mut result = test_game_name_rule(seen_ids, id, extract_game_parts_from_name(mod_game), true);
if result.is_empty() {
return result;
} else {
wrong_ids.append(&mut result);
}
}
}
let duplicate = if seen_ids.insert(expected_id.clone(), game.words).is_some() {
rule_stack.push(IDRule::NoDuplicates);
true
} else {
false
};
// Check ID matches
if id != expected_id || duplicate {
wrong_ids.push(IDFail::new(
id.to_owned(),
game.name.to_owned(),
expected_id,
rule_stack,
));
}
wrong_ids
}
#[derive(Clone, Debug)]
pub struct GameNameParsed<'a> {
name: &'a str,
words: Vec<String>,
optional_parts: Vec<&'a str>,
year: Option<u16>,
}
pub fn extract_game_parts_from_name(game: &str) -> GameNameParsed {
// Separate game name into words
// NOTE: we have to leave "-" in to prevent hyphenated prefixes being parsed as
// numerals
let mut optional_game_name_parts = Vec::new();
let (game, paren) = extract_bracketed_suffix(game);
if let Some(paren) = paren {
optional_game_name_parts.push(paren);
}
let mut number_accumulator: Option<String> = None;
// Filter map necessary to move out words
#[allow(clippy::unnecessary_filter_map)]
let game_name_words: Vec<_> = game
// First split all text on space or dash
.split_inclusive(&[' ', '-'])
// Remove whitespace surrounding words (leave in dash because it is important information)
.map(|w| w.trim())
// If a word is entirely surrounded in brackets move it to optional parts
.filter_map(|w| {
if w.starts_with('(') && w.ends_with(')') {
optional_game_name_parts.push(w);
None
} else {
Some(w)
}
})
// Remove all characters that aren't alphanumeric or dashses
.map(|w| {
w.replace(
|c: char| !c.is_ascii_digit() && !c.is_alphabetic() && c != '-',
"",
)
})
// Remove words that are empty (discounting strings that are just dashes)
.filter(|w| !w.trim_matches('-').is_empty())
// Combine numbers that are seperated by dashes
// e.g. 44-45 = 4445
// Panics if there is text after number with trailing dash (44-text)
.filter_map(|w| {
if number_accumulator.is_some() {
if let Some(maybe_number) = w.strip_suffix('-') {
if maybe_number.chars().all(|c| c.is_ascii_digit()) {
number_accumulator.as_mut().unwrap().push_str(maybe_number);
return None;
} else {
panic!("Text after number-");
}
} else if w.chars().all(|c| c.is_ascii_digit()) {
let mut accumulator = number_accumulator.as_ref().unwrap().clone();
number_accumulator = None;
accumulator.push_str(&w);
return Some(accumulator);
} else {
panic!("Text after number-");
}
} else if let Some(maybe_number) = w.strip_suffix('-') {
if maybe_number.chars().all(|c| c.is_ascii_digit()) {
number_accumulator = Some(maybe_number.to_string());
return None;
}
}
Some(w)
})
.collect();
let mut game_year: Option<u16> = None;
for optional_part in &optional_game_name_parts {
if let Some(game_year_text) = optional_part
.strip_prefix('(')
.and_then(|s| s.strip_suffix(')'))
{
if let Ok(year) = game_year_text.parse() {
game_year = Some(year);
break;
}
} else if let Ok(year) = optional_part.parse() {
game_year = Some(year);
break;
}
}
GameNameParsed {
name: game,
words: game_name_words,
optional_parts: optional_game_name_parts,
year: game_year,
}
}
/// Iterate game entries and validate the id matches current rules
pub fn test_game_name_rules<'a, I: Iterator<Item = (&'a str, &'a str)>>(games: I) -> Vec<IDFail> {
let mut wrong_ids = Vec::with_capacity(games.size_hint().0);
let mut seen_ids: HashMap<String, Vec<String>> = HashMap::new();
// We must sort games by year so that rule 3 is applied correctly
let mut sorted_games: Vec<_> = games
.map(|(id, game)| {
let game = extract_game_parts_from_name(game);
(id, game)
})
.collect();
sorted_games.sort_by(|(_, a_game), (_, b_game)| {
a_game
.year
.cmp(&b_game.year)
.then(a_game.name.len().cmp(&b_game.name.len()))
});
let game_count = sorted_games.len();
for (id, game) in sorted_games {
wrong_ids.append(&mut test_game_name_rule(&mut seen_ids, id, game, false))
}
if !wrong_ids.is_empty() {
for fail in &wrong_ids {
println!("{:#?}", fail);
}
let percentage = (wrong_ids.len() * 100) / game_count;
println!(
"{} ({}%) IDs didn't match naming rules",
wrong_ids.len(),
percentage
);
}
wrong_ids
}
pub fn test_single_game_rule(id: &str, name: &str) -> Vec<IDFail> { test_game_name_rules(std::iter::once((id, name))) }
#[cfg(test)]
mod id_tests {
use super::{test_game_name_rules, test_single_game_rule};
#[test]
fn id_rule_one() {
assert!(test_single_game_rule("testgame", "Test Game").is_empty());
assert!(test_single_game_rule("testgame", "TestGame").is_empty());
assert!(test_single_game_rule("deadcells", "Dead Cells").is_empty());
assert!(test_single_game_rule("stalker", "S.T.A.L.K.E.R").is_empty());
}
#[test]
fn id_rule_two() {
assert!(test_single_game_rule("tgt", "Test Game Three").is_empty());
assert!(test_single_game_rule("tgt", "Test Game-Three").is_empty());
assert!(test_single_game_rule("tboi", "The Binding of Isaac").is_empty());
assert!(test_single_game_rule("ddd", "Dino D-Day").is_empty());
}
#[test]
fn id_rule_three() {
let games = vec![
("swb22017", "Star Wars Battlefront 2 (2017)"),
("swb2", "Star Wars Battlefront 2 (2015)"),
];
assert!(test_game_name_rules(games.into_iter()).is_empty());
}
#[test]
fn id_rule_four() {
let games = vec![("dod", "Day of Defeat"), ("dayofdragons", "Day of Dragons")];
assert!(test_game_name_rules(games.into_iter()).is_empty());
}
#[test]
fn id_rule_five() {
assert!(test_single_game_rule("gta14", "Grand Theft Auto XIV").is_empty());
}
#[test]
fn id_rule_six() {
assert!(test_single_game_rule("l4d", "Left 4 Dead").is_empty());
assert!(test_single_game_rule("sdtd", "7 Days to Die").is_empty());
assert!(test_single_game_rule("teamfortress2", "Team Fortress 2").is_empty());
assert!(test_single_game_rule("unrealtournament2003", "Unreal Tournament 2003").is_empty());
assert!(test_single_game_rule("dhe4445", "Darkest Hour: Europe '44-'45").is_empty());
}
#[test]
fn id_rule_seven() {
let games = vec![
("minecraft", "Minecraft"),
("minecraftjava", "Minecraft (java)"),
("minecraftbedrock", "Minecraft (bedrock)"),
];
assert!(test_game_name_rules(games.into_iter()).is_empty());
}
#[test]
fn id_rule_eight() {
assert!(test_single_game_rule("fivem", "Grand Theft Auto V - FiveM (2013)").is_empty());
assert!(test_single_game_rule("jc3m", "Just Cause 3 - Multiplayer").is_empty());
}
}

View file

@ -0,0 +1,31 @@
#![cfg(feature = "cli")]
use std::collections::HashMap;
/// Format for input games (the same as used in node-gamedig/lib/games.js).
type GamesInput = HashMap<String, Game>;
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
struct Game {
name: String,
}
use gamedig_id_tests::test_game_name_rules;
fn main() {
let games: GamesInput = if let Some(file) = std::env::args_os().skip(1).next() {
let file = std::fs::OpenOptions::new().read(true).open(file).unwrap();
serde_json::from_reader(file).unwrap()
} else {
serde_json::from_reader(std::io::stdin().lock()).unwrap()
};
let failed = test_game_name_rules(
games
.iter()
.map(|(key, game)| (key.as_str(), game.name.as_str())),
);
assert!(failed.is_empty());
}

View file

@ -0,0 +1,66 @@
/// Split a str when characters swap between being digits and not digits.
pub fn split_on_switch_between_alpha_numeric(text: &str) -> Vec<String> {
if text.is_empty() {
return vec![];
}
let mut parts = Vec::with_capacity(text.len());
let mut current = Vec::with_capacity(text.len());
let mut iter = text.chars();
let c = iter.next().unwrap();
let mut last_was_numeric = c.is_ascii_digit();
current.push(c);
for c in iter {
if c.is_ascii_digit() == last_was_numeric {
current.push(c);
} else {
parts.push(current.iter().collect());
current.clear();
current.push(c);
last_was_numeric = !last_was_numeric;
}
}
parts.push(current.into_iter().collect());
parts
}
#[test]
fn split_correctly() {
assert_eq!(
split_on_switch_between_alpha_numeric("2D45A"),
&["2", "D", "45", "A"]
);
}
#[test]
fn split_symbol_broken_numbers() {
let game_name = super::extract_game_parts_from_name("Darkest Hour: Europe '44-'45");
assert_eq!(game_name.words, &["Darkest", "Hour", "Europe", "4445"]);
}
/// Extract parts at end of string enclosed in brackets.
pub fn extract_bracketed_suffix(text: &str) -> (&str, Option<&str>) {
if let Some(text) = text.strip_suffix(')') {
if let Some((text, extra)) = text.rsplit_once('(') {
return (text, Some(extra));
}
}
(text, None)
}
#[test]
fn extract_brackets_correctly() {
assert_eq!(
extract_bracketed_suffix("no brackets here"),
("no brackets here", None)
);
assert_eq!(
extract_bracketed_suffix("Game name (with protocol here)"),
("Game name ", Some("with protocol here"))
);
}

View file

@ -42,6 +42,9 @@ pcap-file = { version = "2.0", optional = true }
pnet_packet = { version = "0.34", optional = true }
lazy_static = { version = "1.4", optional = true }
[dev-dependencies]
gamedig-id-tests = { path = "../id-tests", no-default-features = true }
# Examples
[[example]]
name = "minecraft"

View file

@ -1,8 +1,10 @@
use gamedig::{
protocols::types::{CommonResponse, ExtraRequestSettings, TimeoutSettings},
protocols::types::CommonResponse,
query_with_timeout_and_extra_settings,
ExtraRequestSettings,
GDResult,
Game,
TimeoutSettings,
GAMES,
};

View file

@ -1,6 +1,6 @@
use gamedig::protocols::types::TimeoutSettings;
use gamedig::protocols::valve;
use gamedig::protocols::valve::{Engine, GatheringSettings};
use gamedig::TimeoutSettings;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::time::Duration;

View file

@ -361,6 +361,55 @@ impl StringDecoder for Utf8Decoder {
}
}
/// A decoder for UTF-8 encoded strings prefixed by a single byte denoting the
/// string's length.
///
/// This decoder uses a single null byte (`0x00`) as the default delimiter.
pub struct Utf8LengthPrefixedDecoder;
impl StringDecoder for Utf8LengthPrefixedDecoder {
type Delimiter = [u8; 1];
const DELIMITER: Self::Delimiter = [0x00];
/// Decodes a UTF-8 string from the given data, updating the cursor position
/// accordingly.
fn decode_string(data: &[u8], cursor: &mut usize, delimiter: Self::Delimiter) -> GDResult<String> {
// Find the maximum length of the string
let length = *data
.first()
.ok_or(PacketBad.context("Length of string not found"))?;
// Find the position of the delimiter in the data. If the delimiter is not
// found, the length is returned.
let position = data
// Create an iterator over the data.
.iter()
.skip(1)
.take(length as usize)
// 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(length as usize);
// Convert the data until the found position into a UTF-8 string.
let result = std::str::from_utf8(
// Take a slice of data until the position.
&data[1 .. position + 1]
)
// If the data cannot be converted into a UTF-8 string, return an error
.map_err(|e| PacketBad.context(e))?
// Convert the resulting &str into a String
.to_owned();
// Update the cursor position
// The +1 is to skip t length
*cursor += position + 1;
Ok(result)
}
}
/// A decoder for UTF-16 encoded strings.
///
/// This decoder uses a pair of null bytes (`0x00, 0x00`) as the default

View file

@ -12,7 +12,7 @@ mod tests {
#[test]
fn test_gdresult_ok() {
let result: GDResult<u32> = Ok(42);
assert_eq!(result.unwrap(), 42);
assert_eq!(result, Ok(42));
}
// Testing Err variant of the GDResult type

View file

@ -9,7 +9,7 @@ use crate::protocols::valve::GatheringSettings;
use phf::{phf_map, Map};
macro_rules! game {
($name: literal, $default_port: literal, $protocol: expr) => {
($name: literal, $default_port: expr, $protocol: expr) => {
game!(
$name,
$default_port,
@ -18,7 +18,7 @@ macro_rules! game {
)
};
($name: literal, $default_port: literal, $protocol: expr, $extra_request_settings: expr) => {
($name: literal, $default_port: expr, $protocol: expr, $extra_request_settings: expr) => {
Game {
name: $name,
default_port: $default_port,
@ -39,13 +39,24 @@ pub static GAMES: Map<&'static str, Game> = phf_map! {
"minecraftlegacy16" => game!("Minecraft (legacy 1.6)", 25565, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(Some(Server::Legacy(LegacyGroup::V1_6))))),
"minecraftlegacy14" => game!("Minecraft (legacy 1.4)", 25565, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(Some(Server::Legacy(LegacyGroup::V1_4))))),
"minecraftlegacyb18" => game!("Minecraft (legacy b1.8)", 25565, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(Some(Server::Legacy(LegacyGroup::VB1_8))))),
"aapg" => game!("America's Army: Proving Grounds", 27020, Protocol::Valve(Engine::new(203_290)), GatheringSettings {
players: true,
rules: false,
check_app_id: true,
}.into_extra()),
"alienswarm" => game!("Alien Swarm", 27015, Protocol::Valve(Engine::new(630))),
"aoc" => game!("Age of Chivalry", 27015, Protocol::Valve(Engine::new(17510))),
"a2oa" => game!("ARMA 2: Operation Arrowhead", 2304, Protocol::Valve(Engine::new(33930))),
"ase" => game!("ARK: Survival Evolved", 27015, Protocol::Valve(Engine::new(346_110))),
"asrd" => game!("Alien Swarm: Reactive Drop", 2304, Protocol::Valve(Engine::new(563_560))),
"atlas" => game!("ATLAS", 57561, Protocol::Valve(Engine::new(834_910))),
"avorion" => game!("Avorion", 27020, Protocol::Valve(Engine::new(445_220))),
"barotrauma" => game!("Barotrauma", 27016, Protocol::Valve(Engine::new(602_960))),
"basedefense" => game!("Base Defense", 27015, Protocol::Valve(Engine::new(632_730)), GatheringSettings {
players: true,
rules: false,
check_app_id: true,
}.into_extra()),
"battalion1944" => game!("Battalion 1944", 7780, Protocol::Valve(Engine::new(489_940))),
"brainbread2" => game!("BrainBread 2", 27015, Protocol::Valve(Engine::new(346_330))),
"battlefield1942" => game!("Battlefield 1942", 23000, Protocol::Gamespy(GameSpyVersion::One)),
@ -81,16 +92,24 @@ pub static GAMES: Map<&'static str, Game> = phf_map! {
"l4d2" => game!("Left 4 Dead 2", 27015, Protocol::Valve(Engine::new(550))),
"ohd" => game!("Operation: Harsh Doorstop", 27005, Protocol::Valve(Engine::new_with_dedicated(736_590, 950_900))),
"onset" => game!("Onset", 7776, Protocol::Valve(Engine::new(1_105_810))),
"postscriptum" => game!("Post Scriptum", 10037, Protocol::Valve(Engine::new(736_220))),
"projectzomboid" => game!("Project Zomboid", 16261, Protocol::Valve(Engine::new(108_600))),
"quake1" => game!("Quake 1", 27500, Protocol::Quake(QuakeVersion::One)),
"quake2" => game!("Quake 2", 27910, Protocol::Quake(QuakeVersion::Two)),
"q3a" => game!("Quake 3 Arena", 27960, Protocol::Quake(QuakeVersion::Three)),
"risingworld" => game!("Rising World", 4254, Protocol::Valve(Engine::new(324_080)), GatheringSettings {
players: true,
rules: false,
check_app_id: true,
}.into_extra()),
"ror2" => game!("Risk of Rain 2", 27016, Protocol::Valve(Engine::new(632_360))),
"rust" => game!("Rust", 27015, Protocol::Valve(Engine::new(252_490))),
"savage2" => game!("Savage 2", 11235, Protocol::PROPRIETARY(ProprietaryProtocol::Savage2)),
"sco" => game!("Sven Co-op", 27015, Protocol::Valve(Engine::new_gold_src(false))),
"sdtd" => game!("7 Days to Die", 26900, Protocol::Valve(Engine::new(251_570))),
"sof2" => game!("Soldier of Fortune 2", 20100, Protocol::Quake(QuakeVersion::Three)),
"serioussam" => game!("Serious Sam", 25601, Protocol::Gamespy(GameSpyVersion::One)),
"squad" => game!("Squad", 27165, Protocol::Valve(Engine::new(393_380))),
"theforest" => game!("The Forest", 27016, Protocol::Valve(Engine::new(556_450))),
"thefront" => game!("The Front", 27015, Protocol::Valve(Engine::new(2_285_150))),
"teamfortress2" => game!("Team Fortress 2", 27015, Protocol::Valve(Engine::new(440))),
@ -106,10 +125,12 @@ pub static GAMES: Map<&'static str, Game> = phf_map! {
"vrising" => game!("V Rising", 27016, Protocol::Valve(Engine::new(1_604_030))),
"jc2m" => game!("Just Cause 2: Multiplayer", 7777, Protocol::PROPRIETARY(ProprietaryProtocol::JC2M)),
"warsow" => game!("Warsow", 44400, Protocol::Quake(QuakeVersion::Three)),
"darkesthour" => game!("Darkest Hour: Europe '44-'45 (2008)", 7758, Protocol::Unreal2),
"dhe4445" => game!("Darkest Hour: Europe '44-'45 (2008)", 7758, Protocol::Unreal2),
"devastation" => game!("Devastation (2003)", 7778, Protocol::Unreal2),
"killingfloor" => game!("Killing Floor", 7708, Protocol::Unreal2),
"redorchestra" => game!("Red Orchestra", 7759, Protocol::Unreal2),
"ut2003" => game!("Unreal Tournament 2003", 7758, Protocol::Unreal2),
"ut2004" => game!("Unreal Tournament 2004", 7778, Protocol::Unreal2),
"unrealtournament2003" => game!("Unreal Tournament 2003", 7758, Protocol::Unreal2),
"unrealtournament2004" => game!("Unreal Tournament 2004", 7778, Protocol::Unreal2),
"zps" => game!("Zombie Panic: Source", 27015, Protocol::Valve(Engine::new(17_500))),
"mindustry" => game!("Mindustry", crate::games::mindustry::DEFAULT_PORT, Protocol::PROPRIETARY(ProprietaryProtocol::Mindustry)),
};

View file

@ -0,0 +1,8 @@
/// The implementation.
/// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/ffow.js)
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use types::*;

View file

@ -1,64 +1,11 @@
use crate::buffer::{Buffer, Utf8Decoder};
use crate::protocols::types::{CommonResponse, TimeoutSettings};
use crate::games::ffow::types::Response;
use crate::protocols::types::TimeoutSettings;
use crate::protocols::valve::{Engine, Environment, Server, ValveProtocol};
use crate::protocols::GenericResponse;
use crate::GDResult;
use byteorder::LittleEndian;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::net::{IpAddr, SocketAddr};
/// The query response.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Response {
/// Protocol used by the server.
pub protocol_version: u8,
/// Name of the server.
pub name: String,
/// Map name.
pub active_mod: String,
/// Running game mode.
pub game_mode: String,
/// The version that the server is running on.
pub game_version: String,
/// Description of the server.
pub description: String,
/// Current map.
pub map: String,
/// Number of players on the server.
pub players_online: u8,
/// Maximum number of players the server reports it can hold.
pub players_maximum: u8,
/// Dedicated, NonDedicated or SourceTV
pub server_type: Server,
/// The Operating System that the server is on.
pub environment_type: Environment,
/// Indicates whether the server requires a password.
pub has_password: bool,
/// Indicates whether the server uses VAC.
pub vac_secured: bool,
/// Current round index.
pub round: u8,
/// Maximum amount of rounds.
pub rounds_maximum: u8,
/// Time left for the current round in seconds.
pub time_left: u16,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse { GenericResponse::FFOW(self) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) }
fn description(&self) -> Option<&str> { Some(&self.description) }
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
fn map(&self) -> Option<&str> { Some(&self.map) }
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
fn players_maximum(&self) -> u32 { self.players_maximum.into() }
fn players_online(&self) -> u32 { self.players_online.into() }
}
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, None) }
pub fn query_with_timeout(

View file

@ -0,0 +1,56 @@
use crate::protocols::types::CommonResponse;
use crate::protocols::valve::{Environment, Server};
use crate::protocols::GenericResponse;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// The query response.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Response {
/// Protocol used by the server.
pub protocol_version: u8,
/// Name of the server.
pub name: String,
/// Map name.
pub active_mod: String,
/// Running game mode.
pub game_mode: String,
/// The version that the server is running on.
pub game_version: String,
/// Description of the server.
pub description: String,
/// Current map.
pub map: String,
/// Number of players on the server.
pub players_online: u8,
/// Maximum number of players the server reports it can hold.
pub players_maximum: u8,
/// Dedicated, NonDedicated or SourceTV
pub server_type: Server,
/// The Operating System that the server is on.
pub environment_type: Environment,
/// Indicates whether the server requires a password.
pub has_password: bool,
/// Indicates whether the server uses VAC.
pub vac_secured: bool,
/// Current round index.
pub round: u8,
/// Maximum amount of rounds.
pub rounds_maximum: u8,
/// Time left for the current round in seconds.
pub time_left: u16,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse { GenericResponse::FFOW(self) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) }
fn description(&self) -> Option<&str> { Some(&self.description) }
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
fn map(&self) -> Option<&str> { Some(&self.map) }
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
fn players_maximum(&self) -> u32 { self.players_maximum.into() }
fn players_online(&self) -> u32 { self.players_online.into() }
}

View file

@ -1,129 +0,0 @@
use crate::buffer::{Buffer, Utf8Decoder};
use crate::protocols::gamespy::common::has_password;
use crate::protocols::gamespy::three::{data_to_map, GameSpy3};
use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer, TimeoutSettings};
use crate::protocols::GenericResponse;
use crate::GDErrorKind::{PacketBad, TypeParse};
use crate::{GDErrorKind, GDResult};
use byteorder::BigEndian;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::net::{IpAddr, SocketAddr};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Player {
name: String,
steam_id: String,
ping: u16,
}
impl CommonPlayer for Player {
fn as_original(&self) -> GenericPlayer { GenericPlayer::JCMP2(self) }
fn name(&self) -> &str { &self.name }
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Response {
game_version: String,
description: String,
name: String,
has_password: bool,
players: Vec<Player>,
players_maximum: u32,
players_online: u32,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse { GenericResponse::JC2M(self) }
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
fn description(&self) -> Option<&str> { Some(&self.description) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
fn players_maximum(&self) -> u32 { self.players_maximum }
fn players_online(&self) -> u32 { self.players_online }
fn players(&self) -> Option<Vec<&dyn crate::protocols::types::CommonPlayer>> {
Some(
self.players
.iter()
.map(|p| p as &dyn CommonPlayer)
.collect(),
)
}
}
fn parse_players_and_teams(packet: &[u8]) -> GDResult<Vec<Player>> {
let mut buf = Buffer::<BigEndian>::new(packet);
let count = buf.read::<u16>()?;
let mut players = Vec::with_capacity(count as usize);
while buf.remaining_length() != 0 {
players.push(Player {
name: buf.read_string::<Utf8Decoder>(None)?,
steam_id: buf.read_string::<Utf8Decoder>(None)?,
ping: buf.read::<u16>()?,
});
}
Ok(players)
}
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, None) }
pub fn query_with_timeout(
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let mut client = GameSpy3::new_custom(
&SocketAddr::new(*address, port.unwrap_or(7777)),
timeout_settings,
[0xFF, 0xFF, 0xFF, 0x02],
true,
)?;
let packets = client.get_server_packets()?;
let data = packets
.get(0)
.ok_or(PacketBad.context("First packet missing"))?;
let (mut server_vars, remaining_data) = data_to_map(data)?;
let players = parse_players_and_teams(&remaining_data)?;
let players_maximum = server_vars
.remove("maxplayers")
.ok_or(PacketBad.context("Server variables missing maxplayers"))?
.parse()
.map_err(|e| TypeParse.context(e))?;
let players_online = match server_vars.remove("numplayers") {
None => players.len(),
Some(v) => {
let reported_players = v.parse().map_err(|e| TypeParse.context(e))?;
match reported_players < players.len() {
true => players.len(),
false => reported_players,
}
}
} as u32;
Ok(Response {
game_version: server_vars
.remove("version")
.ok_or(GDErrorKind::PacketBad)?,
description: server_vars
.remove("description")
.ok_or(GDErrorKind::PacketBad)?,
name: server_vars
.remove("hostname")
.ok_or(GDErrorKind::PacketBad)?,
has_password: has_password(&mut server_vars)?,
players,
players_maximum,
players_online,
})
}

View file

@ -0,0 +1,8 @@
/// The implementation.
/// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/jc2mp.js)
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use types::*;

View file

@ -0,0 +1,75 @@
use crate::buffer::{Buffer, Utf8Decoder};
use crate::jc2m::{Player, Response};
use crate::protocols::gamespy::common::has_password;
use crate::protocols::gamespy::three::{data_to_map, GameSpy3};
use crate::protocols::types::TimeoutSettings;
use crate::GDErrorKind::{PacketBad, TypeParse};
use crate::GDResult;
use byteorder::BigEndian;
use std::net::{IpAddr, SocketAddr};
fn parse_players_and_teams(packet: &[u8]) -> GDResult<Vec<Player>> {
let mut buf = Buffer::<BigEndian>::new(packet);
let count = buf.read::<u16>()?;
let mut players = Vec::with_capacity(count as usize);
while buf.remaining_length() != 0 {
players.push(Player {
name: buf.read_string::<Utf8Decoder>(None)?,
steam_id: buf.read_string::<Utf8Decoder>(None)?,
ping: buf.read::<u16>()?,
});
}
Ok(players)
}
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, None) }
pub fn query_with_timeout(
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let mut client = GameSpy3::new_custom(
&SocketAddr::new(*address, port.unwrap_or(7777)),
timeout_settings,
[0xFF, 0xFF, 0xFF, 0x02],
true,
)?;
let packets = client.get_server_packets()?;
let data = packets
.get(0)
.ok_or_else(|| PacketBad.context("First packet missing"))?;
let (mut server_vars, remaining_data) = data_to_map(data)?;
let players = parse_players_and_teams(&remaining_data)?;
let players_maximum = server_vars
.remove("maxplayers")
.ok_or_else(|| PacketBad.context("Server variables missing maxplayers"))?
.parse()
.map_err(|e| TypeParse.context(e))?;
let players_online = match server_vars.remove("numplayers") {
None => players.len(),
Some(v) => {
let reported_players = v.parse().map_err(|e| TypeParse.context(e))?;
match reported_players < players.len() {
true => players.len(),
false => reported_players,
}
}
} as u32;
Ok(Response {
game_version: server_vars.remove("version").ok_or(PacketBad)?,
description: server_vars.remove("description").ok_or(PacketBad)?,
name: server_vars.remove("hostname").ok_or(PacketBad)?,
has_password: has_password(&mut server_vars)?,
players,
players_maximum,
players_online,
})
}

View file

@ -0,0 +1,50 @@
use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer};
use crate::protocols::GenericResponse;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Player {
pub name: String,
pub steam_id: String,
pub ping: u16,
}
impl CommonPlayer for Player {
fn as_original(&self) -> GenericPlayer { GenericPlayer::JCMP2(self) }
fn name(&self) -> &str { &self.name }
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Response {
pub game_version: String,
pub description: String,
pub name: String,
pub has_password: bool,
pub players: Vec<Player>,
pub players_maximum: u32,
pub players_online: u32,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse { GenericResponse::JC2M(self) }
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
fn description(&self) -> Option<&str> { Some(&self.description) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
fn players_maximum(&self) -> u32 { self.players_maximum }
fn players_online(&self) -> u32 { self.players_online }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> {
Some(
self.players
.iter()
.map(|p| p as &dyn CommonPlayer)
.collect(),
)
}
}

View file

@ -0,0 +1,25 @@
//! Mindustry game ping (v146)
//!
//! [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/ArcNetProvider.java#L225-L259)
use std::{net::IpAddr, net::SocketAddr};
use crate::{GDResult, TimeoutSettings};
use self::types::ServerData;
pub mod types;
pub mod protocol;
/// Default mindustry server port
///
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/Vars.java#L141-L142)
pub const DEFAULT_PORT: u16 = 6567;
/// Query a mindustry server.
pub fn query(ip: &IpAddr, port: Option<u16>, timeout_settings: &Option<TimeoutSettings>) -> GDResult<ServerData> {
let address = SocketAddr::new(*ip, port.unwrap_or(DEFAULT_PORT));
protocol::query_with_retries(&address, timeout_settings)
}

View file

@ -0,0 +1,58 @@
use std::net::SocketAddr;
use crate::{
buffer::{self, Buffer},
socket::{Socket, UdpSocket},
utils,
GDResult,
TimeoutSettings,
};
use super::types::ServerData;
/// Mindustry max datagram packet size.
pub const MAX_BUFFER_SIZE: usize = 500;
/// Send a ping packet.
///
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/ArcNetProvider.java#L248)
pub fn send_ping(socket: &mut UdpSocket) -> GDResult<()> { socket.send(&[-2i8 as u8, 1i8 as u8]) }
/// Parse server data.
///
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/NetworkIO.java#L122-L135)
pub fn parse_server_data<B: byteorder::ByteOrder, D: buffer::StringDecoder>(
buffer: &mut Buffer<B>,
) -> GDResult<ServerData> {
Ok(ServerData {
host: buffer.read_string::<D>(None)?,
map: buffer.read_string::<D>(None)?,
players: buffer.read()?,
wave: buffer.read()?,
version: buffer.read()?,
version_type: buffer.read_string::<D>(None)?,
gamemode: buffer.read::<u8>()?.try_into()?,
player_limit: buffer.read()?,
description: buffer.read_string::<D>(None)?,
mode_name: buffer.read_string::<D>(None).ok(),
})
}
/// Query a Mindustry server (without retries).
pub fn query(address: &SocketAddr, timeout_settings: &Option<TimeoutSettings>) -> GDResult<ServerData> {
let mut socket = UdpSocket::new(address, timeout_settings)?;
send_ping(&mut socket)?;
let socket_data = socket.receive(Some(MAX_BUFFER_SIZE))?;
let mut buffer = Buffer::new(&socket_data);
parse_server_data::<byteorder::BigEndian, buffer::Utf8LengthPrefixedDecoder>(&mut buffer)
}
/// Query a Mindustry server.
pub fn query_with_retries(address: &SocketAddr, timeout_settings: &Option<TimeoutSettings>) -> GDResult<ServerData> {
let retries = TimeoutSettings::get_retries_or_default(timeout_settings);
utils::retry_on_timeout(retries, || query(address, timeout_settings))
}

View file

@ -0,0 +1,108 @@
use crate::{
protocols::types::{CommonResponse, GenericResponse},
GDErrorKind,
};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// Mindustry sever data
///
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/NetworkIO.java#L122-L135)
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct ServerData {
pub host: String,
pub map: String,
pub players: i32,
pub wave: i32,
pub version: i32,
pub version_type: String,
pub gamemode: GameMode,
pub player_limit: i32,
pub description: String,
pub mode_name: Option<String>,
}
/// Mindustry game mode
///
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/game/Gamemode.java)
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub enum GameMode {
Survival,
Sandbox,
Attack,
PVP,
Editor,
}
impl TryFrom<u8> for GameMode {
type Error = GDErrorKind;
fn try_from(value: u8) -> Result<Self, Self::Error> {
use GameMode::*;
Ok(match value {
0 => Survival,
1 => Sandbox,
2 => Attack,
3 => PVP,
4 => Editor,
_ => return Err(GDErrorKind::TypeParse),
})
}
}
impl GameMode {
fn as_str(&self) -> &'static str {
use GameMode::*;
match self {
Survival => "survival",
Sandbox => "sandbox",
Attack => "attack",
PVP => "pvp",
Editor => "editor",
}
}
}
impl CommonResponse for ServerData {
fn as_original(&self) -> GenericResponse { GenericResponse::Mindustry(self) }
fn players_online(&self) -> u32 { self.players.try_into().unwrap_or(0) }
fn players_maximum(&self) -> u32 { self.player_limit.try_into().unwrap_or(0) }
fn game_mode(&self) -> Option<&str> { Some(self.gamemode.as_str()) }
fn map(&self) -> Option<&str> { Some(&self.map) }
fn description(&self) -> Option<&str> { Some(&self.description) }
}
#[cfg(test)]
mod test {
use crate::protocols::types::CommonResponse;
use super::ServerData;
#[test]
fn common_impl() {
let data = ServerData {
host: String::from("host"),
map: String::from("map"),
players: 5,
wave: 2,
version: 142,
version_type: String::from("steam"),
gamemode: super::GameMode::PVP,
player_limit: 20,
description: String::from("description"),
mode_name: Some(String::from("campaign")),
};
let common: &dyn CommonResponse = &data;
assert_eq!(common.players_online(), 5);
assert_eq!(common.players_maximum(), 20);
assert_eq!(common.game_mode(), Some("pvp"));
assert_eq!(common.map(), Some("map"));
assert_eq!(common.description(), Some("description"));
}
}

View file

@ -115,7 +115,7 @@ impl Default for RequestSettings {
impl RequestSettings {
/// Make a new *RequestSettings* with just the hostname, the protocol
/// version defaults to -1
pub fn new_just_hostname(hostname: String) -> Self {
pub const fn new_just_hostname(hostname: String) -> Self {
Self {
hostname,
protocol_version: -1,

View file

@ -1,8 +1,5 @@
//! Currently supported games.
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub mod gamespy;
pub mod quake;
pub mod unreal2;
@ -19,137 +16,23 @@ pub mod battalion1944;
pub mod ffow;
/// Just Cause 2: Multiplayer
pub mod jc2m;
/// Mindustry
pub mod mindustry;
/// Minecraft
pub mod minecraft;
/// Savage 2
pub mod savage2;
/// The Ship
pub mod theship;
use crate::protocols::gamespy::GameSpyVersion;
use crate::protocols::quake::QuakeVersion;
use crate::protocols::types::{CommonResponse, ExtraRequestSettings, ProprietaryProtocol, TimeoutSettings};
use crate::protocols::{self, Protocol};
use crate::GDResult;
use std::net::{IpAddr, SocketAddr};
pub mod types;
pub use types::*;
/// Definition of a game
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Game {
/// Full name of the game
pub name: &'static str,
/// Default port used by game
pub default_port: u16,
/// The protocol the game's query uses
pub protocol: Protocol,
/// Request settings.
pub request_settings: ExtraRequestSettings,
}
pub mod query;
pub use query::*;
#[cfg(feature = "game_defs")]
mod definitions;
#[cfg(feature = "game_defs")]
pub use definitions::GAMES;
/// Make a query given a game definition
#[inline]
pub fn query(game: &Game, address: &IpAddr, port: Option<u16>) -> GDResult<Box<dyn CommonResponse>> {
query_with_timeout_and_extra_settings(game, address, port, None, None)
}
/// Make a query given a game definition and timeout settings
#[inline]
pub fn query_with_timeout(
game: &Game,
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Box<dyn CommonResponse>> {
query_with_timeout_and_extra_settings(game, address, port, timeout_settings, None)
}
/// Make a query given a game definition, timeout settings, and extra settings
pub fn query_with_timeout_and_extra_settings(
game: &Game,
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
extra_settings: Option<ExtraRequestSettings>,
) -> GDResult<Box<dyn CommonResponse>> {
let socket_addr = SocketAddr::new(*address, port.unwrap_or(game.default_port));
Ok(match &game.protocol {
Protocol::Valve(engine) => {
protocols::valve::query(
&socket_addr,
*engine,
extra_settings
.or(Option::from(game.request_settings.clone()))
.map(ExtraRequestSettings::into),
timeout_settings,
)
.map(Box::new)?
}
Protocol::Gamespy(version) => {
match version {
GameSpyVersion::One => protocols::gamespy::one::query(&socket_addr, timeout_settings).map(Box::new)?,
GameSpyVersion::Two => protocols::gamespy::two::query(&socket_addr, timeout_settings).map(Box::new)?,
GameSpyVersion::Three => {
protocols::gamespy::three::query(&socket_addr, timeout_settings).map(Box::new)?
}
}
}
Protocol::Quake(version) => {
match version {
QuakeVersion::One => protocols::quake::one::query(&socket_addr, timeout_settings).map(Box::new)?,
QuakeVersion::Two => protocols::quake::two::query(&socket_addr, timeout_settings).map(Box::new)?,
QuakeVersion::Three => protocols::quake::three::query(&socket_addr, timeout_settings).map(Box::new)?,
}
}
Protocol::Unreal2 => {
protocols::unreal2::query(
&socket_addr,
&extra_settings
.map(ExtraRequestSettings::into)
.unwrap_or_default(),
timeout_settings,
)
.map(Box::new)?
}
Protocol::PROPRIETARY(protocol) => {
match protocol {
ProprietaryProtocol::TheShip => {
theship::query_with_timeout(address, port, timeout_settings).map(Box::new)?
}
ProprietaryProtocol::FFOW => ffow::query_with_timeout(address, port, timeout_settings).map(Box::new)?,
ProprietaryProtocol::JC2M => jc2m::query_with_timeout(address, port, timeout_settings).map(Box::new)?,
ProprietaryProtocol::Minecraft(version) => {
match version {
Some(minecraft::Server::Java) => {
minecraft::protocol::query_java(
&socket_addr,
timeout_settings,
extra_settings.map(ExtraRequestSettings::into),
)
.map(Box::new)?
}
Some(minecraft::Server::Bedrock) => {
minecraft::protocol::query_bedrock(&socket_addr, timeout_settings).map(Box::new)?
}
Some(minecraft::Server::Legacy(group)) => {
minecraft::protocol::query_legacy_specific(*group, &socket_addr, timeout_settings)
.map(Box::new)?
}
None => {
minecraft::protocol::query(
&socket_addr,
timeout_settings,
extra_settings.map(ExtraRequestSettings::into),
)
.map(Box::new)?
}
}
}
}
}
})
}

View file

@ -0,0 +1,118 @@
//! Generic query functions
use std::net::{IpAddr, SocketAddr};
use crate::games::types::Game;
use crate::games::{ffow, jc2m, mindustry, minecraft, savage2, theship};
use crate::protocols;
use crate::protocols::gamespy::GameSpyVersion;
use crate::protocols::quake::QuakeVersion;
use crate::protocols::types::{CommonResponse, ExtraRequestSettings, ProprietaryProtocol, Protocol, TimeoutSettings};
use crate::GDResult;
/// Make a query given a game definition
#[inline]
pub fn query(game: &Game, address: &IpAddr, port: Option<u16>) -> GDResult<Box<dyn CommonResponse>> {
query_with_timeout_and_extra_settings(game, address, port, None, None)
}
/// Make a query given a game definition and timeout settings
#[inline]
pub fn query_with_timeout(
game: &Game,
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Box<dyn CommonResponse>> {
query_with_timeout_and_extra_settings(game, address, port, timeout_settings, None)
}
/// Make a query given a game definition, timeout settings, and extra settings
pub fn query_with_timeout_and_extra_settings(
game: &Game,
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
extra_settings: Option<ExtraRequestSettings>,
) -> GDResult<Box<dyn CommonResponse>> {
let socket_addr = SocketAddr::new(*address, port.unwrap_or(game.default_port));
Ok(match &game.protocol {
Protocol::Valve(engine) => {
protocols::valve::query(
&socket_addr,
*engine,
extra_settings
.or_else(|| Option::from(game.request_settings.clone()))
.map(ExtraRequestSettings::into),
timeout_settings,
)
.map(Box::new)?
}
Protocol::Gamespy(version) => {
match version {
GameSpyVersion::One => protocols::gamespy::one::query(&socket_addr, timeout_settings).map(Box::new)?,
GameSpyVersion::Two => protocols::gamespy::two::query(&socket_addr, timeout_settings).map(Box::new)?,
GameSpyVersion::Three => {
protocols::gamespy::three::query(&socket_addr, timeout_settings).map(Box::new)?
}
}
}
Protocol::Quake(version) => {
match version {
QuakeVersion::One => protocols::quake::one::query(&socket_addr, timeout_settings).map(Box::new)?,
QuakeVersion::Two => protocols::quake::two::query(&socket_addr, timeout_settings).map(Box::new)?,
QuakeVersion::Three => protocols::quake::three::query(&socket_addr, timeout_settings).map(Box::new)?,
}
}
Protocol::Unreal2 => {
protocols::unreal2::query(
&socket_addr,
&extra_settings
.map(ExtraRequestSettings::into)
.unwrap_or_default(),
timeout_settings,
)
.map(Box::new)?
}
Protocol::PROPRIETARY(protocol) => {
match protocol {
ProprietaryProtocol::Savage2 => {
savage2::query_with_timeout(address, port, timeout_settings).map(Box::new)?
}
ProprietaryProtocol::TheShip => {
theship::query_with_timeout(address, port, timeout_settings).map(Box::new)?
}
ProprietaryProtocol::FFOW => ffow::query_with_timeout(address, port, timeout_settings).map(Box::new)?,
ProprietaryProtocol::JC2M => jc2m::query_with_timeout(address, port, timeout_settings).map(Box::new)?,
ProprietaryProtocol::Mindustry => mindustry::query(address, port, &timeout_settings).map(Box::new)?,
ProprietaryProtocol::Minecraft(version) => {
match version {
Some(minecraft::Server::Java) => {
minecraft::protocol::query_java(
&socket_addr,
timeout_settings,
extra_settings.map(ExtraRequestSettings::into),
)
.map(Box::new)?
}
Some(minecraft::Server::Bedrock) => {
minecraft::protocol::query_bedrock(&socket_addr, timeout_settings).map(Box::new)?
}
Some(minecraft::Server::Legacy(group)) => {
minecraft::protocol::query_legacy_specific(*group, &socket_addr, timeout_settings)
.map(Box::new)?
}
None => {
minecraft::protocol::query(
&socket_addr,
timeout_settings,
extra_settings.map(ExtraRequestSettings::into),
)
.map(Box::new)?
}
}
}
}
}
})
}

View file

@ -0,0 +1,8 @@
/// The implementation.
/// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/savage2.js)
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use types::*;

View file

@ -0,0 +1,37 @@
use crate::buffer::{Buffer, Utf8Decoder};
use crate::games::savage2::types::Response;
use crate::protocols::types::TimeoutSettings;
use crate::socket::{Socket, UdpSocket};
use crate::GDResult;
use byteorder::LittleEndian;
use std::net::{IpAddr, SocketAddr};
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, None) }
pub fn query_with_timeout(
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let addr = &SocketAddr::new(*address, port.unwrap_or(11235));
let mut socket = UdpSocket::new(addr, &timeout_settings)?;
socket.send(&[0x01])?;
let data = socket.receive(None)?;
let mut buffer = Buffer::<LittleEndian>::new(&data);
buffer.move_cursor(12)?;
Ok(Response {
name: buffer.read_string::<Utf8Decoder>(None)?,
players_online: buffer.read::<u8>()?,
players_maximum: buffer.read::<u8>()?,
time: buffer.read_string::<Utf8Decoder>(None)?,
map: buffer.read_string::<Utf8Decoder>(None)?,
next_map: buffer.read_string::<Utf8Decoder>(None)?,
location: buffer.read_string::<Utf8Decoder>(None)?,
players_minimum: buffer.read::<u8>()?,
game_mode: buffer.read_string::<Utf8Decoder>(None)?,
protocol_version: buffer.read_string::<Utf8Decoder>(None)?,
level_minimum: buffer.read::<u8>()?,
})
}

View file

@ -0,0 +1,30 @@
use crate::protocols::types::CommonResponse;
use crate::protocols::GenericResponse;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response {
pub name: String,
pub players_online: u8,
pub players_maximum: u8,
pub players_minimum: u8,
pub time: String,
pub map: String,
pub next_map: String,
pub location: String,
pub game_mode: String,
pub protocol_version: String,
pub level_minimum: u8,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse { GenericResponse::Savage2(self) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) }
fn map(&self) -> Option<&str> { Some(&self.map) }
fn players_maximum(&self) -> u32 { self.players_maximum.into() }
fn players_online(&self) -> u32 { self.players_online.into() }
}

View file

@ -0,0 +1,8 @@
/// The implementation.
/// Reference: [server queries](https://developer.valvesoftware.com/wiki/Server_queries)
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use types::*;

View file

@ -0,0 +1,23 @@
use crate::games::theship::types::Response;
use crate::protocols::types::TimeoutSettings;
use crate::protocols::valve;
use crate::protocols::valve::Engine;
use crate::GDResult;
use std::net::{IpAddr, SocketAddr};
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, None) }
pub fn query_with_timeout(
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let valve_response = valve::query(
&SocketAddr::new(*address, port.unwrap_or(27015)),
Engine::new(2400),
None,
timeout_settings,
)?;
Response::new_from_valve_response(valve_response)
}

View file

@ -1,17 +1,10 @@
use crate::{
protocols::{
types::{CommonPlayer, CommonResponse, GenericPlayer, TimeoutSettings},
valve::{self, get_optional_extracted_data, Server, ServerPlayer},
GenericResponse,
},
GDErrorKind::PacketBad,
GDResult,
};
use std::net::{IpAddr, SocketAddr};
use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer};
use crate::protocols::valve::{get_optional_extracted_data, Server, ServerPlayer};
use crate::protocols::{valve, GenericResponse};
use crate::GDErrorKind::PacketBad;
use crate::GDResult;
use std::collections::HashMap;
use crate::protocols::valve::Engine;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
@ -127,20 +120,3 @@ impl Response {
})
}
}
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, None) }
pub fn query_with_timeout(
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let valve_response = valve::query(
&SocketAddr::new(*address, port.unwrap_or(27015)),
Engine::new(2400),
None,
timeout_settings,
)?;
Response::new_from_valve_response(valve_response)
}

View file

@ -0,0 +1,20 @@
//! Game related types
use crate::protocols::types::{ExtraRequestSettings, Protocol};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// Definition of a game
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Game {
/// Full name of the game
pub name: &'static str,
/// Default port used by game
pub default_port: u16,
/// The protocol the game's query uses
pub protocol: Protocol,
/// Request settings.
pub request_settings: ExtraRequestSettings,
}

View file

@ -8,8 +8,20 @@ game_query_mod!(
Engine::new(33930),
2304
);
game_query_mod!(basedefense, "Base Defense", Engine::new(632_730), 27015);
game_query_mod!(alienswarm, "Alien Swarm", Engine::new(630), 27015);
game_query_mod!(aoc, "Age of Chivalry", Engine::new(17510), 27015);
game_query_mod!(
aapg,
"America's Army: Proving Grounds",
Engine::new(203_290),
27020,
GatheringSettings {
players: true,
rules: false,
check_app_id: true,
}
);
game_query_mod!(ase, "ARK: Survival Evolved", Engine::new(346_110), 27015);
game_query_mod!(
asrd,
@ -17,6 +29,7 @@ game_query_mod!(
Engine::new(563_560),
2304
);
game_query_mod!(atlas, "ATLAS", Engine::new(834_910), 57561);
game_query_mod!(avorion, "Avorion", Engine::new(445_220), 27020);
game_query_mod!(
ballisticoverkill,
@ -100,16 +113,19 @@ game_query_mod!(
27005
);
game_query_mod!(onset, "Onset", Engine::new(1_105_810), 7776);
game_query_mod!(postscriptum, "Post Scriptum", Engine::new(736_220), 10037);
game_query_mod!(
projectzomboid,
"Project Zomboid",
Engine::new(108_600),
16261
);
game_query_mod!(risingworld, "Rising World", Engine::new(324_080), 4254);
game_query_mod!(ror2, "Risk of Rain 2", Engine::new(632_360), 27016);
game_query_mod!(rust, "Rust", Engine::new(252_490), 27015);
game_query_mod!(sco, "Sven Co-op", Engine::new_gold_src(false), 27015);
game_query_mod!(sdtd, "7 Days to Die", Engine::new(251_570), 26900);
game_query_mod!(squad, "Squad", Engine::new(393_380), 27165);
game_query_mod!(teamfortress2, "Team Fortress 2", Engine::new(440), 27015);
game_query_mod!(
tfc,
@ -132,3 +148,4 @@ game_query_mod!(
}
);
game_query_mod!(vrising, "V Rising", Engine::new(1_604_030), 27016);
game_query_mod!(zps, "Zombie Panic: Source", Engine::new(17_500), 27015);

View file

@ -15,7 +15,7 @@
//!
//! ## Using a game definition
//! ```
//! use gamedig::games::{GAMES, query};
//! use gamedig::{GAMES, query};
//!
//! let game = GAMES.get("teamfortress2").unwrap(); // Get a game definition, the full list can be found in src/games/mod.rs
//! let response = query(game, &"127.0.0.1".parse().unwrap(), None); // None will use the default port
@ -51,5 +51,10 @@ pub mod capture;
pub use errors::*;
#[cfg(feature = "games")]
pub use games::*;
#[cfg(feature = "games")]
pub use query::*;
#[cfg(feature = "services")]
pub use services::*;
// Re-export types needed to call games::query::query in the root
pub use protocols::types::{ExtraRequestSettings, TimeoutSettings};

View file

@ -4,7 +4,7 @@ use std::collections::HashMap;
pub fn has_password(server_vars: &mut HashMap<String, String>) -> GDResult<bool> {
let password_value = server_vars
.remove("password")
.ok_or(GDErrorKind::PacketBad.context("Missing password (exists) field"))?
.ok_or_else(|| GDErrorKind::PacketBad.context("Missing password (exists) field"))?
.to_lowercase();
if let Ok(has) = password_value.parse::<bool>() {

View file

@ -55,7 +55,7 @@ fn get_server_values_impl(socket: &mut UdpSocket) -> GDResult<HashMap<String, St
let key = splited[position].clone();
let value = splited
.get(position + 1)
.map_or_else(String::new, |v| v.clone());
.map_or_else(String::new, Clone::clone);
server_values.insert(key, value);
}

View file

@ -282,7 +282,7 @@ fn parse_players_and_teams(packets: Vec<Vec<u8>>) -> GDResult<(Vec<Player>, Vec<
}
let entry_data = data.get_mut(offset).ok_or(PacketBad)?;
entry_data.insert(field_name.to_string(), item);
entry_data.insert((*field_name).to_string(), item);
offset += 1;
}

View file

@ -78,7 +78,7 @@ fn get_server_values(bufferer: &mut Buffer<LittleEndian>) -> GDResult<HashMap<St
if let Some(k) = key {
if let Some(v) = value {
vars.insert(k.to_string(), v.to_string());
vars.insert((*k).to_string(), (*v).to_string());
}
}
}
@ -116,23 +116,23 @@ pub fn client_query<Client: QuakeClient>(
Ok(Response {
name: server_vars
.remove("hostname")
.or(server_vars.remove("sv_hostname"))
.or_else(|| server_vars.remove("sv_hostname"))
.ok_or(GDErrorKind::PacketBad)?,
map: server_vars
.remove("mapname")
.or(server_vars.remove("map"))
.or_else(|| server_vars.remove("map"))
.ok_or(GDErrorKind::PacketBad)?,
players_online: players.len() as u8,
players_maximum: server_vars
.remove("maxclients")
.or(server_vars.remove("sv_maxclients"))
.or_else(|| server_vars.remove("sv_maxclients"))
.ok_or(GDErrorKind::PacketBad)?
.parse()
.map_err(|e| TypeParse.context(e))?,
players,
game_version: server_vars
.remove("version")
.or(server_vars.remove("*version")),
.or_else(|| server_vars.remove("*version")),
unused_entries: server_vars,
})
}

View file

@ -18,6 +18,8 @@ pub enum ProprietaryProtocol {
Minecraft(Option<minecraft::types::Server>),
FFOW,
JC2M,
Savage2,
Mindustry,
}
/// Enumeration of all valid protocol types
@ -41,6 +43,8 @@ pub enum GenericResponse<'a> {
Valve(&'a valve::Response),
Unreal2(&'a unreal2::Response),
#[cfg(feature = "games")]
Mindustry(&'a crate::games::mindustry::types::ServerData),
#[cfg(feature = "games")]
Minecraft(minecraft::VersionedResponse<'a>),
#[cfg(feature = "games")]
TheShip(&'a crate::games::theship::Response),
@ -48,6 +52,8 @@ pub enum GenericResponse<'a> {
FFOW(&'a crate::games::ffow::Response),
#[cfg(feature = "games")]
JC2M(&'a crate::games::jc2m::Response),
#[cfg(feature = "games")]
Savage2(&'a crate::games::savage2::Response),
}
/// All player types
@ -228,33 +234,33 @@ impl TimeoutSettings {
/// Get the number of retries if there are timeout settings else fall back
/// to the default
pub const fn get_retries_or_default(timeout_settings: &Option<TimeoutSettings>) -> usize {
pub const fn get_retries_or_default(timeout_settings: &Option<Self>) -> usize {
if let Some(timeout_settings) = timeout_settings {
timeout_settings.get_retries()
} else {
TimeoutSettings::const_default().get_retries()
Self::const_default().get_retries()
}
}
/// Get the read and write durations if there are timeout settings else fall
/// back to the defaults
pub const fn get_read_and_write_or_defaults(
timeout_settings: &Option<TimeoutSettings>,
timeout_settings: &Option<Self>,
) -> (Option<Duration>, Option<Duration>) {
if let Some(timeout_settings) = timeout_settings {
(timeout_settings.get_read(), timeout_settings.get_write())
} else {
let default = TimeoutSettings::const_default();
let default = Self::const_default();
(default.get_read(), default.get_write())
}
}
/// Get the connect duration given timeout settings or get the default.
pub const fn get_connect_or_default(timeout_settings: &Option<TimeoutSettings>) -> Option<Duration> {
pub const fn get_connect_or_default(timeout_settings: &Option<Self>) -> Option<Duration> {
if let Some(timeout_settings) = timeout_settings {
timeout_settings.get_connect()
} else {
TimeoutSettings::const_default().get_connect()
Self::const_default().get_connect()
}
}
@ -337,22 +343,22 @@ impl ExtraRequestSettings {
}
/// [Sets protocol
/// version](ExtraRequestSettings#structfield.protocol_version)
pub fn set_protocol_version(mut self, protocol_version: i32) -> Self {
pub const fn set_protocol_version(mut self, protocol_version: i32) -> Self {
self.protocol_version = Some(protocol_version);
self
}
/// [Sets gather players](ExtraRequestSettings#structfield.gather_players)
pub fn set_gather_players(mut self, gather_players: bool) -> Self {
pub const fn set_gather_players(mut self, gather_players: bool) -> Self {
self.gather_players = Some(gather_players);
self
}
/// [Sets gather rules](ExtraRequestSettings#structfield.gather_rules)
pub fn set_gather_rules(mut self, gather_rules: bool) -> Self {
pub const fn set_gather_rules(mut self, gather_rules: bool) -> Self {
self.gather_rules = Some(gather_rules);
self
}
/// [Sets check app ID](ExtraRequestSettings#structfield.check_app_id)
pub fn set_check_app_id(mut self, check_app_id: bool) -> Self {
pub const fn set_check_app_id(mut self, check_app_id: bool) -> Self {
self.check_app_id = Some(check_app_id);
self
}

View file

@ -33,10 +33,10 @@ pub(crate) struct Unreal2Protocol {
impl Unreal2Protocol {
pub fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = UdpSocket::new(address, &timeout_settings)?;
let retry_count = timeout_settings
.as_ref()
.map(|t| t.get_retries())
.unwrap_or_else(|| TimeoutSettings::default().get_retries());
let retry_count = timeout_settings.as_ref().map_or_else(
|| TimeoutSettings::default().get_retries(),
TimeoutSettings::get_retries,
);
Ok(Self {
socket,
@ -209,7 +209,7 @@ impl StringDecoder for Unreal2StringDecoder {
let mut ucs2 = false;
let mut length: usize = (*data
.first()
.ok_or(PacketBad.context("Tried to decode string without length"))?)
.ok_or_else(|| PacketBad.context("Tried to decode string without length"))?)
.into();
let mut start = 0;
@ -225,7 +225,7 @@ impl StringDecoder for Unreal2StringDecoder {
// 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() {
if data[start ..].first() == Some(&1) {
start += 1;
}
}

View file

@ -50,7 +50,7 @@ pub struct ServerInfo {
impl ServerInfo {
pub fn parse<B: ByteOrder>(buffer: &mut Buffer<B>) -> GDResult<Self> {
Ok(ServerInfo {
Ok(Self {
server_id: buffer.read()?,
ip: buffer.read_string::<Unreal2StringDecoder>(None)?,
game_port: buffer.read()?,
@ -118,7 +118,7 @@ impl Players {
/// Pre-allocate the vectors inside the players struct based on the provided
/// capacity.
pub fn with_capacity(capacity: usize) -> Self {
Players {
Self {
players: Vec::with_capacity(capacity),
// Allocate half as many bots as we don't expect there to be as many
bots: Vec::with_capacity(capacity / 2),
@ -234,7 +234,7 @@ impl GatheringSettings {
}
impl Default for GatheringSettings {
fn default() -> Self { GatheringSettings::default() }
fn default() -> Self { Self::default() }
}
impl From<ExtraRequestSettings> for GatheringSettings {

View file

@ -127,10 +127,10 @@ static PACKET_SIZE: usize = 6144;
impl ValveProtocol {
pub fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = UdpSocket::new(address, &timeout_settings)?;
let retry_count = timeout_settings
.as_ref()
.map(|t| t.get_retries())
.unwrap_or_else(|| TimeoutSettings::default().get_retries());
let retry_count = timeout_settings.as_ref().map_or_else(
|| TimeoutSettings::default().get_retries(),
|t| t.get_retries(),
);
Ok(Self {
socket,
@ -271,7 +271,7 @@ impl ValveProtocol {
has_password,
vac_secured,
the_ship: None,
game_version: "".to_string(), // a version field only for the mod
game_version: String::new(), // a version field only for the mod
extra_data: None,
is_mod,
mod_data,

View file

@ -310,7 +310,7 @@ impl GatheringSettings {
}
impl Default for GatheringSettings {
fn default() -> Self { GatheringSettings::default() }
fn default() -> Self { Self::default() }
}
impl From<ExtraRequestSettings> for GatheringSettings {

View file

@ -18,7 +18,7 @@ pub fn default_master_address() -> SocketAddr {
fn construct_payload(region: Region, filters: &Option<SearchFilters>, last_ip: &str, last_port: u16) -> Vec<u8> {
let filters_bytes: Vec<u8> = filters
.as_ref()
.map_or_else(|| vec![0x00], |f| f.to_bytes());
.map_or_else(|| vec![0x00], SearchFilters::to_bytes);
let region_byte = &[region as u8];

View file

@ -75,11 +75,10 @@ pub struct TcpSocketImpl {
impl Socket for TcpSocketImpl {
fn new(address: &SocketAddr, timeout_settings: &Option<TimeoutSettings>) -> GDResult<Self> {
let socket = if let Some(timeout) = TimeoutSettings::get_connect_or_default(timeout_settings) {
net::TcpStream::connect_timeout(address, timeout)
} else {
net::TcpStream::connect(address)
};
let socket = TimeoutSettings::get_connect_or_default(timeout_settings).map_or_else(
|| net::TcpStream::connect(address),
|timeout| net::TcpStream::connect_timeout(address, timeout),
);
let socket = Self {
socket: socket.map_err(|e| SocketConnect.context(e))?,

View file

@ -0,0 +1,11 @@
#![cfg(all(test, feature = "game_defs"))]
use gamedig::GAMES;
use gamedig_id_tests::test_game_name_rules;
#[test]
fn check_definitions_match_name_rules() {
let wrong = test_game_name_rules(GAMES.entries().map(|(id, game)| (id.to_owned(), game.name)));
assert!(wrong.is_empty());
}