Merge branch 'main' into feat/cli

This commit is contained in:
Cain 2023-11-08 20:42:07 +00:00
commit 963040fb84
28 changed files with 1071 additions and 348 deletions

View file

@ -1,5 +1,5 @@
<svg width="163.6" height="20" viewBox="0 0 1636 200" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Node game coverage: 12%">
<title>Node game coverage: 12%</title>
<svg width="163.6" height="20" viewBox="0 0 1636 200" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Node game coverage: 13%">
<title>Node game coverage: 13%</title>
<linearGradient id="a" x2="0" y2="100%">
<stop offset="0" stop-opacity=".1" stop-color="#EEE"/>
<stop offset="1" stop-opacity=".1"/>
@ -13,8 +13,8 @@
<g aria-hidden="true" fill="#fff" text-anchor="start" font-family="Verdana,DejaVu Sans,sans-serif" font-size="110">
<text x="60" y="148" textLength="1176" fill="#000" opacity="0.25">Node game coverage</text>
<text x="50" y="138" textLength="1176">Node game coverage</text>
<text x="1331" y="148" textLength="260" fill="#000" opacity="0.25">12%</text>
<text x="1321" y="138" textLength="260">12%</text>
<text x="1331" y="148" textLength="260" fill="#000" opacity="0.25">13%</text>
<text x="1321" y="138" textLength="260">13%</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

Before After
Before After

View file

@ -3,10 +3,24 @@ Who knows what the future holds...
# 0.X.Y - DD/MM/YYYY
### Changes:
Nothing yet.
Games:
- [Valheim](https://store.steampowered.com/app/892970/Valheim/) support.
- [The Front](https://store.steampowered.com/app/2285150/The_Front/) support.
- [Conan Exiles](https://store.steampowered.com/app/440900/Conan_Exiles/) support.
- Added a valve protocol query example.
Protocols:
- Added the unreal2 protocol and its associated games: Darkest Hour, Devastation, Killing Floor, Red Orchestra, Unreal Tournament 2003, Unreal Tournament 2004 (by @Douile).
### Breaking:
None, yaay!
Game:
- Changed identifications of the following games as they weren't properly expecting the naming rules:
- - Left 4 Dead: `left4dead` -> `l4d`.
- - 7 Days to Die: `7d2d` in definitions and `sd2d` in game declaration -> `sdtd`.
- - Quake 3 Arena: `quake3arena` -> `q3a`.
Protocols:
- Valve: Removed `SteamApp` due to it not being really useful at all, replaced all instances with `Engine`.
# 0.4.1 - 13/10/2023
### Changes:

View file

@ -52,18 +52,23 @@ and 2017 would be `swb22017`).
(`Day of Defeat` -> `dod`), then the new name should ignore rule #2
(`Day of Dragons` -> `dayofdragons`).
5. Roman numbering will be converted to arabic numbering (`XIV` -> `14`).
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`).
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.
6. Unless numbers (years included) are at the end of a name, they will be considered
words. If a number is not in the first position, its entire numeric digits will be
used instead of the acronym of that number's digits (`Left 4 Dead` -> `l4d`). If the
number is in the first position the longhand (words: 5 -> five) representation of the
number will be used to create an acronym (`7 Days to Die` -> `sdtd`). Other examples:
`Team Fortress 2` -> `teamfortress2`, `Unreal Tournament 2003` ->
`unrealtournament2003`.
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 base game id's: `<game_id><protocol_id>` (where the protocol id will follow all
rules except #2) (Minecraft is mainly divided by 2 editions, Java and Bedrock
which will be `minecraftjava` and `minecraftbedrock` respectively, but it also has
legacy versions, which use another protocol, an example would be the one for `1.6`,
so the name would be `Legacy 1.6` which its id will be `legacy16`, resulting in the
entry of `minecraftlegacy16`). 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.
8. If its actually about a mod that adds the ability for queries to be performed,
process only the mod name.

View file

@ -62,6 +62,15 @@ Beware of the `Notes` column, as it contains information about query port offset
| Creativerse | CREATIVERSE | Valve | Query Port offset: 1. |
| Garry's Mod | GARRYSMOD | Valve | |
| Barotrauma | BAROTRAUMA | Valve | Query Port offset: 1. |
| Valheim | VALHEIM | Valve | Query Port offset: 1. Does not respond to the A2S rules. |
| The Front | THEFRONT | Valve | Responds with wrong values on `name` (gives out a SteamID instead of the server name) and `players_maximum` (always 200). |
| Conan Exiles | CONANEXILES | Valve | Does not respond to the players query. |
| Darkest Hour: Europe '44-'45 | DARKESTHOUR | Unreal2 | Query port offset: 1 |
| Devastation | DEVASTATION | Unreal2 | Query port offset: 1 |
| Killing Floor | KILLINGFLOOR | Unreal2 | Query port offset: 1 |
| Red Orchestra | REDORCHESTRA | Unreal2 | Query port offset: 1 |
| Unreal Tournament 2003 | UT2003 | Unreal2 | Query port offset: 1 |
| Unreal Tournament 2004 | UT2004 | Unreal2 | Query port offset: 1 |
## Planned to add support:
_

View file

@ -7,6 +7,7 @@ A protocol is defined as proprietary if it is being used only for a single scope
| Minecraft | Games | Yes | Java: [List Server Protocol](https://wiki.vg/Server_List_Ping) <br> Bedrock: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/minecraftbedrock.js) | |
| GameSpy | Games | No | One: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy1.js) Two: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy2.js) Three: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy3.js) | These protocols are not really standardized, gamedig tries to get the most common fields amongst its supported games, if there are parsing problems, use the `query_vars` function. |
| Quake | Games | No | One: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake1.js) Two: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake2.js) Three: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake3.js) | |
| Unreal2 | Games | Yes | [Node-GameDig Source](https://github.com/gamedig/node-gamedig) | Sometimes servers send strings that node-gamedig would treat as latin1 that are UTF-8 encoded, when this happens the remove color code breaks because latin1 decodes the colour sequences differently. Some games provide additional info at the end of the server info packet, this is not currently handled (see the node implementation). Some games use a bot player to denote the team names, this is not currently handled. |
## Planned to add support:
_

View file

@ -89,3 +89,5 @@ Curious about the history and what changed between versions? Everything is in th
## Contributing
If you want to see your favorite game/service being supported here, open an issue, and I'll prioritize it (or do a pull request if you want to implement it yourself)!
Before contributing please read [CONTRIBUTING](CONTRIBUTING.md).

View file

@ -5,50 +5,53 @@ In the case that a field that performs the same function exists in the current c
# Response table
| Field | Generic | GameSpy(1) | GameSpy(2) | GameSpy(3) | Minecraft(Java) | Minecraft(Bedrock) | Valve | Quake | Proprietary: FFOW | Proprietary: TheShip | Proprietary: JC2MP |
|:---------------------|------------------|---------------------------|---------------|---------------------------|-----------------------|----------------------|----------------------------------|---------------------------|-------------------|--------------------------|--------------------|
| name | `Option<String>` | `String` | `String` | `String` | | `String` | `String` | `String` | `String` | `String` | `String` |
| description | `Option<String>` | | | | `String` | | | | `String` | | `String` |
| game_mode | `Option<String>` | `String` | | `String` | | `Option<GameMode>` | `String` | | `String` | `String` | |
| game_version | `Option<String>` | `String` | | `String` | `String` | | `String` | `String` | `String` | `String` | `String` |
| map | `Option<String>` | `String` | `String` | `String` | | `Option<String>` | `String` | `String` | `String` | `String` | |
| players_maxmimum | `u32` | `u32` | `u32` | `u32` | `u32` | `u32` | `u8` | `u8` | `u8` | `u8` | `u32` |
| players_online | `u32` | `u32` | `u32` | `u32` | `u32` | `u32` | `u8` | `u8` | `u8` | `u8` | `u32` |
| players_bots | `Option<u32>` | | | | | | `u8` | | | `u8` | |
| has_password | `Option<bool>` | `bool` | `bool` | `bool` | | | `bool` | | `bool` | `bool` | `bool` |
| players_minimum | | `Option<u8>` | `Option<u8>` | `Option<u8>` | | | | | | | |
| players | | `Vec<Player>` | `Vec<Player>` | `Vec<Player>` | `Option<Vec<Player>>` | | `Option<Vec<ServerPlayer>>` | `Vec<P>` | | `Vec<TheShipPlayer>` | `Vec<Player>` |
| tournament | | `bool` | | `bool` | | | | | | | |
| unused_entries | | `Hashmap<String, String>` | | `HashMap<String, String>` | | | | `HashMap<String, String>` | | | |
| teams | | | `Vec<Team>` | `Vec<Team>` | | | | | | | |
| protocol_version | | | | | `i32` | `String` | `u8` | | `u8` | `u8` | |
| server_type | | | | | `Server` | `Server` | `Server` | | | `Server` | |
| rules | | | | | | | `Option<HashMap<String,String>>` | | | `HashMap<String,String>` | |
| environment_type | | | | | | | `Environment` | | `Environment` | | |
| vac_secured | | | | | | | `bool` | | `bool` | `bool` | |
| map_title | | `Option<String>` | | | | | | | | | |
| admin_contact | | `Option<String>` | | | | | | | | | |
| admin_name | | `Option<String>` | | | | | | | | | |
| favicon | | | | | `Option<String>` | | | | | | |
| previews_chat | | | | | `Option<bool>` | | | | | | |
| enforces_secure_chat | | | | | `Option<bool>` | | | | | | |
| edition | | | | | | `String` | | | | | |
| id | | | | | | `String` | | | | | |
| the_ship | | | | | | | `Option<TheShip>` | | | | |
| is_mod | | | | | | | `bool` | | | | |
| extra_data | | | | | | | `Option<ExtraData>` | | | | |
| mod_data | | | | | | | `Option<ModData>` | | | | |
| folder | | | | | | | `String` | | | | |
| appid | | | | | | | `u32` | | | | |
| active_mod | | | | | | | | | `String` | | |
| round | | | | | | | | | `u8` | | |
| rounds_maximum | | | | | | | | | `u8` | | |
| time_left | | | | | | | | | `u16` | | |
| port | | | | | | | | | | `Option<u16>` | |
| steam_id | | | | | | | | | | `Option<u64>` | |
| tv_port | | | | | | | | | | `Option<u16>` | |
| tv_name | | | | | | | | | | `Option<String>` | |
| keywords | | | | | | | | | | `Option<string>` | |
| mode | | | | | | | | | | `u8` | |
| witnesses | | | | | | | | | | `u8` | |
| duration | | | | | | | | | | `u8` | |
| Field | Generic | GameSpy(1) | GameSpy(2) | GameSpy(3) | Minecraft(Java) | Minecraft(Bedrock) | Valve | Quake | Unreal2 | Proprietary: FFOW | Proprietary: TheShip | Proprietary: JC2MP |
|----------------------|------------------|---------------------------|---------------|---------------------------|-----------------------|--------------------|-----------------------------------|---------------------------|--------------------------------|-------------------|---------------------------|--------------------|
| name | `Option<String>` | `String` | `String` | `String` | | `String` | `String` | `String` | `String` | `String` | `String` | `String` |
| description | `Option<String>` | | | | `String` | | | | | `String` | | `String` |
| game_mode | `Option<String>` | `String` | | `String` | | `Option<GameMode>` | `String` | | `String` | `String` | `String` | |
| game_version | `Option<String>` | `String` | | `String` | `String` | | `String` | `String` | | `String` | `String` | `String` |
| map | `Option<String>` | `String` | `String` | `String` | | `Option<String>` | `String` | `String` | `String` | `String` | `String` | |
| players_maxmimum | `u32` | `u32` | `u32` | `u32` | `u32` | `u32` | `u8` | `u8` | `u32` | `u8` | `u8` | `u32` |
| players_online | `u32` | `u32` | `u32` | `u32` | `u32` | `u32` | `u8` | `u8` | `u32` | `u8` | `u8` | `u32` |
| players_bots | `Option<u32>` | | | | | | `u8` | | | | `u8` | |
| has_password | `Option<bool>` | `bool` | `bool` | `bool` | | | `bool` | | | `bool` | `bool` | `bool` |
| players_minimum | | `Option<u8>` | `Option<u8>` | `Option<u8>` | | | | | | | | |
| players | | `Vec<Player>` | `Vec<Player>` | `Vec<Player>` | `Option<Vec<Player>>` | | `Option<Vec<ServerPlayer>>` | `Vec<P>` | `Vec<Player>` | | `Vec<TheShipPlayer>` | `Vec<Player>` |
| tournament | | `bool` | | `bool` | | | | | | | | |
| unused_entries | | `Hashmap<String, String>` | | `HashMap<String, String>` | | | | `HashMap<String, String>` | | | | |
| teams | | | `Vec<Team>` | `Vec<Team>` | | | | | | | | |
| protocol_version | | | | | `i32` | `String` | `u8` | | | `u8` | `u8` | |
| server_type | | | | | `Server` | `Server` | `Server` | | | | `Server` | |
| rules | | | | | | | `Option<HashMap<String, String>>` | | `HashMap<String, Vec<String>>` | | `HashMap<String, String>` | |
| environment_type | | | | | | | `Environment` | | | `Environment` | | |
| vac_secured | | | | | | | `bool` | | | `bool` | `bool` | |
| map_title | | `Option<String>` | | | | | | | | | | |
| admin_contact | | `Option<String>` | | | | | | | | | | |
| admin_name | | `Option<String>` | | | | | | | | | | |
| favicon | | | | | `Option<String>` | | | | | | | |
| previews_chat | | | | | `Option<bool>` | | | | | | | |
| enforces_secure_chat | | | | | `Option<bool>` | | | | | | | |
| edition | | | | | | `String` | | | | | | |
| id | | | | | | `String` | | | `String` | | | |
| the_ship | | | | | | | `Option<TheShip>` | | | | | |
| is_mod | | | | | | | `bool` | | | | | |
| extra_data | | | | | | | `Option<ExtraData>` | | | | | |
| mod_data | | | | | | | `Option<ModData>` | | | | | |
| folder | | | | | | | `String` | | | | | |
| appid | | | | | | | `u32` | | | | | |
| active_mod | | | | | | | | | | `String` | | |
| round | | | | | | | | | | `u8` | | |
| rounds_maximum | | | | | | | | | | `u8` | | |
| time_left | | | | | | | | | | `u16` | | |
| port | | | | | | | | | `u32` | | `Option<u16>` | |
| steam_id | | | | | | | | | | | `Option<u64>` | |
| tv_port | | | | | | | | | | | `Option<u16>` | |
| tv_name | | | | | | | | | | | `Option<String>` | |
| keywords | | | | | | | | | | | `Option<String>` | |
| mode | | | | | | | | | | | `u8` | |
| witnesses | | | | | | | | | | | `u8` | |
| duration | | | | | | | | | | | `u8` | |
| query_port | | | | | | | | | `u32` | | | |
| ip | | | | | | | | | `String` | | | |
| mutators | | | | | | | | | `HashSet<String>` | | | |

View file

@ -2,23 +2,21 @@ use gamedig::{
protocols::types::{CommonResponse, ExtraRequestSettings, TimeoutSettings},
query_with_timeout_and_extra_settings,
GDResult,
Game,
GAMES,
};
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
/// Make a query given the name of a game
/// The `game` argument is taken from the [GAMES](gamedig::GAMES) map.
fn generic_query(
game_name: &str,
game: &Game,
addr: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
extra_settings: Option<ExtraRequestSettings>,
) -> GDResult<Box<dyn CommonResponse>> {
let game = GAMES
.get(game_name)
.expect("Game doesn't exist, run without arguments to see a list of games");
println!("Querying {:#?} with game {:#?}.", addr, game);
let response = query_with_timeout_and_extra_settings(game, addr, port, timeout_settings, extra_settings)?;
@ -51,14 +49,18 @@ fn main() {
)
.unwrap();
let extra_settings = ExtraRequestSettings::default()
let game = GAMES
.get(&game_name)
.expect("Game doesn't exist, run without arguments to see a list of games");
let extra_settings = game
.request_settings
.clone()
.set_hostname(hostname.to_string())
.set_gather_rules(true)
.set_gather_players(true)
.set_check_app_id(false);
generic_query(
&game_name,
game,
&addr.ip(),
port,
Some(timeout_settings),
@ -67,8 +69,7 @@ fn main() {
.unwrap();
} else {
// Without arguments print a list of games
for (name, game) in gamedig::games::GAMES.entries() {
for (name, game) in GAMES.entries() {
println!("{}\t{}", name, game.name);
}
}
@ -95,7 +96,12 @@ mod test {
)
.unwrap(),
);
assert!(generic_query(game_name, &ADDR, None, timeout_settings, None).is_err());
let game = GAMES
.get(game_name)
.expect("Game doesn't exist, run without arguments to see a list of games");
assert!(generic_query(game, &ADDR, None, timeout_settings, None).is_err());
}
#[test]
@ -108,7 +114,7 @@ mod test {
fn teamfortress2() { test_game("teamfortress2"); }
#[test]
fn quake() { test_game("quake3"); }
fn quake2() { test_game("quake2"); }
#[test]
fn all_games() {

View file

@ -0,0 +1,28 @@
use gamedig::protocols::types::TimeoutSettings;
use gamedig::protocols::valve;
use gamedig::protocols::valve::{Engine, GatheringSettings};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::time::Duration;
fn main() {
let address = &SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 27015);
let engine = Engine::Source(None); // We don't specify a steam app id, let the query try to find it.
let gather_settings = GatheringSettings {
players: true, // We want to query for players
rules: false, // We don't want to query for rules
check_app_id: false, // Loosen up the query a bit by not checking app id
};
let read_timeout = Duration::from_secs(2);
let write_timeout = Duration::from_secs(3);
let retries = 1; // does another request if the first one fails.
let timeout_settings = TimeoutSettings::new(Some(read_timeout), Some(write_timeout), retries).unwrap();
let response = valve::query(
address,
engine,
Some(gather_settings),
Some(timeout_settings),
);
println!("{response:#?}");
}

View file

@ -1,5 +1,6 @@
use crate::protocols::valve::Engine;
use crate::{
protocols::valve::{self, game, SteamApp},
protocols::valve::{self, game},
GDErrorKind::TypeParse,
GDResult,
};
@ -8,7 +9,7 @@ use std::net::{IpAddr, SocketAddr};
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<game::Response> {
let mut valve_response = valve::query(
&SocketAddr::new(*address, port.unwrap_or(7780)),
SteamApp::BATTALION1944.as_engine(),
Engine::new(489_940),
None,
None,
)?;

View file

@ -4,20 +4,31 @@ use crate::protocols::{
gamespy::GameSpyVersion,
minecraft::{LegacyGroup, Server},
quake::QuakeVersion,
valve::SteamApp,
valve::Engine,
Protocol,
};
use crate::Game;
use crate::protocols::types::ProprietaryProtocol;
use crate::protocols::valve::GatheringSettings;
use phf::{phf_map, Map};
macro_rules! game {
($name: literal, $default_port: literal, $protocol: expr) => {
game!(
$name,
$default_port,
$protocol,
GatheringSettings::default().into_extra()
)
};
($name: literal, $default_port: literal, $protocol: expr, $extra_request_settings: expr) => {
Game {
name: $name,
default_port: $default_port,
protocol: $protocol,
request_settings: $extra_request_settings,
}
};
}
@ -33,60 +44,77 @@ pub static GAMES: Map<&'static str, Game> = phf_map! {
"minecraftlegacy16" => game!("Minecraft (legacy v1.6)", 25565, Protocol::Minecraft(Some(Server::Legacy(LegacyGroup::V1_6)))),
"minecraftlegacy15" => game!("Minecraft (legacy v1.4-1.5)", 25565, Protocol::Minecraft(Some(Server::Legacy(LegacyGroup::V1_5)))),
"minecraftlegacy13" => game!("Minecraft (legacy vB1.8-1.3)", 25565, Protocol::Minecraft(Some(Server::Legacy(LegacyGroup::V1_3)))),
"alienswarm" => game!("Alien Swarm", 27015, Protocol::Valve(SteamApp::ALIENSWARM)),
"aoc" => game!("Age of Chivalry", 27015, Protocol::Valve(SteamApp::AOC)),
"a2oa" => game!("ARMA 2: Operation Arrowhead", 2304, Protocol::Valve(SteamApp::A2OA)),
"ase" => game!("ARK: Survival Evolved", 27015, Protocol::Valve(SteamApp::ASE)),
"asrd" => game!("Alien Swarm: Reactive Drop", 2304, Protocol::Valve(SteamApp::ASRD)),
"avorion" => game!("Avorion", 27020, Protocol::Valve(SteamApp::AVORION)),
"barotrauma" => game!("Barotrauma", 27016, Protocol::Valve(SteamApp::BAROTRAUMA)),
"battalion1944" => game!("Battalion 1944", 7780, Protocol::Valve(SteamApp::BATTALION1944)),
"brainbread2" => game!("BrainBread 2", 27015, Protocol::Valve(SteamApp::BRAINBREAD2)),
"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))),
"avorion" => game!("Avorion", 27020, Protocol::Valve(Engine::new(445_220))),
"barotrauma" => game!("Barotrauma", 27016, Protocol::Valve(Engine::new(602_960))),
"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)),
"blackmesa" => game!("Black Mesa", 27015, Protocol::Valve(SteamApp::BLACKMESA)),
"ballisticoverkill" => game!("Ballistic Overkill", 27016, Protocol::Valve(SteamApp::BALLISTICOVERKILL)),
"codenamecure" => game!("Codename CURE", 27015, Protocol::Valve(SteamApp::CODENAMECURE)),
"colonysurvival" => game!("Colony Survival", 27004, Protocol::Valve(SteamApp::COLONYSURVIVAL)),
"counterstrike" => game!("Counter-Strike", 27015, Protocol::Valve(SteamApp::COUNTERSTRIKE)),
"cscz" => game!("Counter Strike: Condition Zero", 27015, Protocol::Valve(SteamApp::CSCZ)),
"csgo" => game!("Counter-Strike: Global Offensive", 27015, Protocol::Valve(SteamApp::CSGO)),
"css" => game!("Counter-Strike: Source", 27015, Protocol::Valve(SteamApp::CSS)),
"creativerse" => game!("Creativerse", 26901, Protocol::Valve(SteamApp::CREATIVERSE)),
"blackmesa" => game!("Black Mesa", 27015, Protocol::Valve(Engine::new(362_890))),
"ballisticoverkill" => game!("Ballistic Overkill", 27016, Protocol::Valve(Engine::new(296_300))),
"codenamecure" => game!("Codename CURE", 27015, Protocol::Valve(Engine::new(355_180))),
"colonysurvival" => game!("Colony Survival", 27004, Protocol::Valve(Engine::new(366_090))),
"conanexiles" => game!("Conan Exiles", 27015, Protocol::Valve(Engine::new(440_900)), GatheringSettings {
players: false,
rules: true,
check_app_id: true,
}.into_extra()),
"counterstrike" => game!("Counter-Strike", 27015, Protocol::Valve(Engine::new_gold_src(false))),
"cscz" => game!("Counter Strike: Condition Zero", 27015, Protocol::Valve(Engine::new_gold_src(false))),
"csgo" => game!("Counter-Strike: Global Offensive", 27015, Protocol::Valve(Engine::new(730))),
"css" => game!("Counter-Strike: Source", 27015, Protocol::Valve(Engine::new(240))),
"creativerse" => game!("Creativerse", 26901, Protocol::Valve(Engine::new(280_790))),
"crysiswars" => game!("Crysis Wars", 64100, Protocol::Gamespy(GameSpyVersion::Three)),
"dod" => game!("Day of Defeat", 27015, Protocol::Valve(SteamApp::DOD)),
"dods" => game!("Day of Defeat: Source", 27015, Protocol::Valve(SteamApp::DODS)),
"doi" => game!("Day of Infamy", 27015, Protocol::Valve(SteamApp::DOI)),
"dst" => game!("Don't Starve Together", 27016, Protocol::Valve(SteamApp::DST)),
"dod" => game!("Day of Defeat", 27015, Protocol::Valve(Engine::new_gold_src(false))),
"dods" => game!("Day of Defeat: Source", 27015, Protocol::Valve(Engine::new(300))),
"doi" => game!("Day of Infamy", 27015, Protocol::Valve(Engine::new(447_820))),
"dst" => game!("Don't Starve Together", 27016, Protocol::Valve(Engine::new(322_320))),
"ffow" => game!("Frontlines: Fuel of War", 5478, Protocol::PROPRIETARY(ProprietaryProtocol::FFOW)),
"garrysmod" => game!("Garry's Mod", 27016, Protocol::Valve(SteamApp::GARRYSMOD)),
"hl2d" => game!("Half-Life 2 Deathmatch", 27015, Protocol::Valve(SteamApp::HL2D)),
"garrysmod" => game!("Garry's Mod", 27016, Protocol::Valve(Engine::new(4000))),
"hl2d" => game!("Half-Life 2 Deathmatch", 27015, Protocol::Valve(Engine::new(320))),
"hce" => game!("Halo: Combat Evolved", 2302, Protocol::Gamespy(GameSpyVersion::Two)),
"hlds" => game!("Half-Life Deathmatch: Source", 27015, Protocol::Valve(SteamApp::HLDS)),
"hll" => game!("Hell Let Loose", 26420, Protocol::Valve(SteamApp::HLL)),
"insurgency" => game!("Insurgency", 27015, Protocol::Valve(SteamApp::INSURGENCY)),
"imic" => game!("Insurgency: Modern Infantry Combat", 27015, Protocol::Valve(SteamApp::IMIC)),
"insurgencysandstorm" => game!("Insurgency: Sandstorm", 27131, Protocol::Valve(SteamApp::INSURGENCYSANDSTORM)),
"left4dead" => game!("Left 4 Dead", 27015, Protocol::Valve(SteamApp::LEFT4DEAD)),
"left4dead2" => game!("Left 4 Dead 2", 27015, Protocol::Valve(SteamApp::LEFT4DEAD2)),
"ohd" => game!("Operation: Harsh Doorstop", 27005, Protocol::Valve(SteamApp::OHD)),
"onset" => game!("Onset", 7776, Protocol::Valve(SteamApp::ONSET)),
"projectzomboid" => game!("Project Zomboid", 16261, Protocol::Valve(SteamApp::PROJECTZOMBOID)),
"hlds" => game!("Half-Life Deathmatch: Source", 27015, Protocol::Valve(Engine::new(360))),
"hll" => game!("Hell Let Loose", 26420, Protocol::Valve(Engine::new(686_810))),
"insurgency" => game!("Insurgency", 27015, Protocol::Valve(Engine::new(222_880))),
"imic" => game!("Insurgency: Modern Infantry Combat", 27015, Protocol::Valve(Engine::new(17700))),
"insurgencysandstorm" => game!("Insurgency: Sandstorm", 27131, Protocol::Valve(Engine::new(581_320))),
"l4d" => game!("Left 4 Dead", 27015, Protocol::Valve(Engine::new(500))),
"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))),
"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)),
"quake3" => game!("Quake 3: Arena", 27960, Protocol::Quake(QuakeVersion::Three)),
"ror2" => game!("Risk of Rain 2", 27016, Protocol::Valve(SteamApp::ROR2)),
"rust" => game!("Rust", 27015, Protocol::Valve(SteamApp::RUST)),
"sco" => game!("Sven Co-op", 27015, Protocol::Valve(SteamApp::SCO)),
"7d2d" => game!("7 Days To Die", 26900, Protocol::Valve(SteamApp::SD2D)),
"q3a" => game!("Quake 3 Arena", 27960, Protocol::Quake(QuakeVersion::Three)),
"ror2" => game!("Risk of Rain 2", 27016, Protocol::Valve(Engine::new(632_360))),
"rust" => game!("Rust", 27015, Protocol::Valve(Engine::new(252_490))),
"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)),
"theforest" => game!("The Forest", 27016, Protocol::Valve(SteamApp::THEFOREST)),
"teamfortress2" => game!("Team Fortress 2", 27015, Protocol::Valve(SteamApp::TEAMFORTRESS2)),
"tfc" => game!("Team Fortress Classic", 27015, Protocol::Valve(SteamApp::TFC)),
"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))),
"tfc" => game!("Team Fortress Classic", 27015, Protocol::Valve(Engine::new_gold_src(false))),
"theship" => game!("The Ship", 27015, Protocol::PROPRIETARY(ProprietaryProtocol::TheShip)),
"unturned" => game!("Unturned", 27015, Protocol::Valve(SteamApp::UNTURNED)),
"unturned" => game!("Unturned", 27015, Protocol::Valve(Engine::new(304_930))),
"unrealtournament" => game!("Unreal Tournament", 7778, Protocol::Gamespy(GameSpyVersion::One)),
"vrising" => game!("V Rising", 27016, Protocol::Valve(SteamApp::VRISING)),
"valheim" => game!("Valheim", 2457, Protocol::Valve(Engine::new(892_970)), GatheringSettings {
players: true,
rules: false,
check_app_id: true,
}.into_extra()),
"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),
"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),
};

View file

@ -5,10 +5,12 @@ use serde::{Deserialize, Serialize};
pub mod gamespy;
pub mod quake;
pub mod unreal2;
pub mod valve;
pub use gamespy::*;
pub use quake::*;
pub use unreal2::*;
pub use valve::*;
/// Battalion 1944
@ -39,6 +41,8 @@ pub struct Game {
pub default_port: u16,
/// The protocol the game's query uses
pub protocol: Protocol,
/// Request settings.
pub request_settings: ExtraRequestSettings,
}
#[cfg(feature = "game_defs")]
@ -74,11 +78,13 @@ pub fn query_with_timeout_and_extra_settings(
) -> GDResult<Box<dyn CommonResponse>> {
let socket_addr = SocketAddr::new(*address, port.unwrap_or(game.default_port));
Ok(match &game.protocol {
Protocol::Valve(steam_app) => {
Protocol::Valve(engine) => {
protocols::valve::query(
&socket_addr,
steam_app.as_engine(),
extra_settings.map(ExtraRequestSettings::into),
*engine,
extra_settings
.or(Option::from(game.request_settings.clone()))
.map(ExtraRequestSettings::into),
timeout_settings,
)
.map(Box::new)?
@ -125,6 +131,16 @@ pub fn query_with_timeout_and_extra_settings(
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 => {

View file

@ -4,6 +4,6 @@ use crate::protocols::quake::game_query_mod;
game_query_mod!(quake1, "Quake 1", one, 27500);
game_query_mod!(quake2, "Quake 2", two, 27910);
game_query_mod!(quake3, "Quake 3: Arena", three, 27960);
game_query_mod!(q3a, "Quake 3 Arena", three, 27960);
game_query_mod!(sof2, "Soldier of Fortune 2", three, 20100);
game_query_mod!(warsow, "Warsow", three, 44400);

View file

@ -1,7 +1,7 @@
use crate::{
protocols::{
types::{CommonPlayer, CommonResponse, GenericPlayer, TimeoutSettings},
valve::{self, get_optional_extracted_data, Server, ServerPlayer, SteamApp},
valve::{self, get_optional_extracted_data, Server, ServerPlayer},
GenericResponse,
},
GDErrorKind::PacketBad,
@ -11,6 +11,7 @@ use std::net::{IpAddr, SocketAddr};
use std::collections::HashMap;
use crate::protocols::valve::Engine;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
@ -136,7 +137,7 @@ pub fn query_with_timeout(
) -> GDResult<Response> {
let valve_response = valve::query(
&SocketAddr::new(*address, port.unwrap_or(27015)),
SteamApp::THESHIP.as_engine(),
Engine::new(2400),
None,
timeout_settings,
)?;

View file

@ -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);

View file

@ -2,55 +2,133 @@
use crate::protocols::valve::game_query_mod;
game_query_mod!(a2oa, "ARMA 2: Operation Arrowhead", A2OA, 2304);
game_query_mod!(alienswarm, "Alien Swarm", ALIENSWARM, 27015);
game_query_mod!(aoc, "Age of Chivalry", AOC, 27015);
game_query_mod!(ase, "ARK: Survival Evolved", ASE, 27015);
game_query_mod!(asrd, "Alien Swarm: Reactive Drop", ASRD, 2304);
game_query_mod!(avorion, "Avorion", AVORION, 27020);
game_query_mod!(
a2oa,
"ARMA 2: Operation Arrowhead",
Engine::new(33930),
2304
);
game_query_mod!(alienswarm, "Alien Swarm", Engine::new(630), 27015);
game_query_mod!(aoc, "Age of Chivalry", Engine::new(17510), 27015);
game_query_mod!(ase, "ARK: Survival Evolved", Engine::new(346_110), 27015);
game_query_mod!(
asrd,
"Alien Swarm: Reactive Drop",
Engine::new(563_560),
2304
);
game_query_mod!(avorion, "Avorion", Engine::new(445_220), 27020);
game_query_mod!(
ballisticoverkill,
"Ballistic Overkill",
BALLISTICOVERKILL,
Engine::new(296_300),
27016
);
game_query_mod!(barotrauma, "Barotrauma", BAROTRAUMA, 27016);
game_query_mod!(blackmesa, "Black Mesa", BLACKMESA, 27015);
game_query_mod!(brainbread2, "BrainBread 2", BRAINBREAD2, 27015);
game_query_mod!(codenamecure, "Codename CURE", CODENAMECURE, 27015);
game_query_mod!(colonysurvival, "Colony Survival", COLONYSURVIVAL, 27004);
game_query_mod!(counterstrike, "Counter-Strike", COUNTERSTRIKE, 27015);
game_query_mod!(creativerse, "Creativerse", CREATIVERSE, 26901);
game_query_mod!(cscz, "Counter Strike: Condition Zero", CSCZ, 27015);
game_query_mod!(csgo, "Counter-Strike: Global Offensive", CSGO, 27015);
game_query_mod!(css, "Counter-Strike: Source", CSS, 27015);
game_query_mod!(dod, "Day of Defeat", DOD, 27015);
game_query_mod!(dods, "Day of Defeat: Source", DODS, 27015);
game_query_mod!(doi, "Day of Infamy", DOI, 27015);
game_query_mod!(dst, "Don't Starve Together", DST, 27016);
game_query_mod!(garrysmod, "Garry's Mod", GARRYSMOD, 27016);
game_query_mod!(hl2d, "Half-Life 2 Deathmatch", HL2D, 27015);
game_query_mod!(hlds, "Half-Life Deathmatch: Source", HLDS, 27015);
game_query_mod!(hll, "Hell Let Loose", HLL, 26420);
game_query_mod!(imic, "Insurgency: Modern Infantry Combat", IMIC, 27015);
game_query_mod!(insurgency, "Insurgency", INSURGENCY, 27015);
game_query_mod!(barotrauma, "Barotrauma", Engine::new(602_960), 27016);
game_query_mod!(blackmesa, "Black Mesa", Engine::new(362_890), 27015);
game_query_mod!(brainbread2, "BrainBread 2", Engine::new(346_330), 27015);
game_query_mod!(codenamecure, "Codename CURE", Engine::new(355_180), 27015);
game_query_mod!(
colonysurvival,
"Colony Survival",
Engine::new(366_090),
27004
);
game_query_mod!(
conanexiles,
"Conan Exiles",
Engine::new(440_900),
27015,
GatheringSettings {
players: false,
rules: true,
check_app_id: true,
}
);
game_query_mod!(
counterstrike,
"Counter-Strike",
Engine::new_gold_src(false),
27015
);
game_query_mod!(creativerse, "Creativerse", Engine::new(280_790), 26901);
game_query_mod!(
cscz,
"Counter Strike: Condition Zero",
Engine::new_gold_src(false),
27015
);
game_query_mod!(
csgo,
"Counter-Strike: Global Offensive",
Engine::new(730),
27015
);
game_query_mod!(css, "Counter-Strike: Source", Engine::new(240), 27015);
game_query_mod!(dod, "Day of Defeat", Engine::new_gold_src(false), 27015);
game_query_mod!(dods, "Day of Defeat: Source", Engine::new(300), 27015);
game_query_mod!(doi, "Day of Infamy", Engine::new(447_820), 27015);
game_query_mod!(dst, "Don't Starve Together", Engine::new(322_320), 27016);
game_query_mod!(garrysmod, "Garry's Mod", Engine::new(4000), 27016);
game_query_mod!(hl2d, "Half-Life 2 Deathmatch", Engine::new(320), 27015);
game_query_mod!(
hlds,
"Half-Life Deathmatch: Source",
Engine::new(360),
27015
);
game_query_mod!(hll, "Hell Let Loose", Engine::new(686_810), 26420);
game_query_mod!(
imic,
"Insurgency: Modern Infantry Combat",
Engine::new(17700),
27015
);
game_query_mod!(insurgency, "Insurgency", Engine::new(222_880), 27015);
game_query_mod!(
insurgencysandstorm,
"Insurgency: Sandstorm",
INSURGENCYSANDSTORM,
Engine::new(581_320),
27131
);
game_query_mod!(left4dead, "Left 4 Dead", LEFT4DEAD, 27015);
game_query_mod!(left4dead2, "Left 4 Dead 2", LEFT4DEAD2, 27015);
game_query_mod!(ohd, "Operation: Harsh Doorstop", OHD, 27005);
game_query_mod!(onset, "Onset", ONSET, 7776);
game_query_mod!(projectzomboid, "Project Zomboid", PROJECTZOMBOID, 16261);
game_query_mod!(ror2, "Risk of Rain 2", ROR2, 27016);
game_query_mod!(rust, "Rust", RUST, 27015);
game_query_mod!(sco, "Sven Co-op", SCO, 27015);
game_query_mod!(sd2d, "7 Days To Die", SD2D, 26900);
game_query_mod!(teamfortress2, "Team Fortress 2", TEAMFORTRESS2, 27015);
game_query_mod!(tfc, "Team Fortress Classic", TFC, 27015);
game_query_mod!(theforest, "The Forest", THEFOREST, 27016);
game_query_mod!(unturned, "Unturned", UNTURNED, 27015);
game_query_mod!(vrising, "V Rising", VRISING, 27016);
game_query_mod!(l4d, "Left 4 Dead", Engine::new(500), 27015);
game_query_mod!(l4d2, "Left 4 Dead 2", Engine::new(550), 27015);
game_query_mod!(
ohd,
"Operation: Harsh Doorstop",
Engine::new_with_dedicated(736_590, 950_900),
27005
);
game_query_mod!(onset, "Onset", Engine::new(1_105_810), 7776);
game_query_mod!(
projectzomboid,
"Project Zomboid",
Engine::new(108_600),
16261
);
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!(teamfortress2, "Team Fortress 2", Engine::new(440), 27015);
game_query_mod!(
tfc,
"Team Fortress Classic",
Engine::new_gold_src(false),
27015
);
game_query_mod!(theforest, "The Forest", Engine::new(556_450), 27016);
game_query_mod!(thefront, "The Front", Engine::new(2_285_150), 27015);
game_query_mod!(unturned, "Unturned", Engine::new(304_930), 27015);
game_query_mod!(
valheim,
"Valheim",
Engine::new(892_970),
2457,
GatheringSettings {
players: true,
rules: false,
check_app_id: true,
}
);
game_query_mod!(vrising, "V Rising", Engine::new(1_604_030), 27016);

View file

@ -39,6 +39,7 @@ pub enum VersionedPlayer<'a> {
/// * `pretty_name` - The full name of the game, will be used as the
/// documentation for the created module.
/// * `gamespy_ver`, `default_port` - Passed through to [game_query_fn].
#[cfg(feature = "games")]
macro_rules! game_query_mod {
($mod_name: ident, $pretty_name: expr, $gamespy_ver: ident, $default_port: literal) => {
#[doc = $pretty_name]
@ -48,6 +49,7 @@ macro_rules! game_query_mod {
};
}
#[cfg(feature = "games")]
pub(crate) use game_query_mod;
// Allow generating doc comments:
@ -62,6 +64,7 @@ pub(crate) use game_query_mod;
/// use crate::protocols::gamespy::game_query_fn;
/// game_query_fn!(one, 7778);
/// ```
#[cfg(feature = "games")]
macro_rules! game_query_fn {
($gamespy_ver: ident, $default_port: literal) => {
crate::protocols::gamespy::game_query_fn! {@gen $gamespy_ver, $default_port, concat!(
@ -83,4 +86,5 @@ macro_rules! game_query_fn {
};
}
#[cfg(feature = "games")]
pub(crate) use game_query_fn;

View file

@ -3,6 +3,5 @@ pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use protocol::*;
pub use types::*;

View file

@ -31,11 +31,11 @@ pub fn query(
timeout_settings: Option<TimeoutSettings>,
request_settings: Option<RequestSettings>,
) -> GDResult<JavaResponse> {
if let Ok(response) = query_java(address, timeout_settings.clone(), request_settings) {
if let Ok(response) = query_java(address, timeout_settings, request_settings) {
return Ok(response);
}
if let Ok(response) = query_bedrock(address, timeout_settings.clone()) {
if let Ok(response) = query_bedrock(address, timeout_settings) {
return Ok(JavaResponse::from_bedrock_response(response));
}
@ -57,11 +57,11 @@ pub fn query_java(
/// Query a (Java) Legacy Server (1.6 -> 1.4 -> Beta 1.8).
pub fn query_legacy(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<JavaResponse> {
if let Ok(response) = query_legacy_specific(LegacyGroup::V1_6, address, timeout_settings.clone()) {
if let Ok(response) = query_legacy_specific(LegacyGroup::V1_6, address, timeout_settings) {
return Ok(response);
}
if let Ok(response) = query_legacy_specific(LegacyGroup::V1_5, address, timeout_settings.clone()) {
if let Ok(response) = query_legacy_specific(LegacyGroup::V1_5, address, timeout_settings) {
return Ok(response);
}

View file

@ -12,6 +12,8 @@ pub mod minecraft;
pub mod quake;
/// General types that are used by all protocols.
pub mod types;
/// Reference: [node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/unreal2.js)
pub mod unreal2;
/// Reference: [Server Query](https://developer.valvesoftware.com/wiki/Server_queries)
pub mod valve;

View file

@ -26,6 +26,7 @@ pub enum QuakeVersion {
/// * `pretty_name` - The full name of the game, will be used as the
/// documentation for the created module.
/// * `quake_ver`, `default_port` - Passed through to [game_query_fn].
#[cfg(feature = "games")]
macro_rules! game_query_mod {
($mod_name: ident, $pretty_name: expr, $quake_ver: ident, $default_port: literal) => {
#[doc = $pretty_name]
@ -35,6 +36,7 @@ macro_rules! game_query_mod {
};
}
#[cfg(feature = "games")]
pub(crate) use game_query_mod;
// Allow generating doc comments:
@ -49,6 +51,7 @@ pub(crate) use game_query_mod;
/// use crate::protocols::quake::game_query_fn;
/// game_query_fn!(one, 27500);
/// ```
#[cfg(feature = "games")]
macro_rules! game_query_fn {
($quake_ver: ident, $default_port: literal) => {
use crate::protocols::quake::$quake_ver::Player;
@ -71,4 +74,5 @@ macro_rules! game_query_fn {
};
}
#[cfg(feature = "games")]
pub(crate) use game_query_fn;

View file

@ -1,4 +1,4 @@
use crate::protocols::{gamespy, minecraft, quake, valve};
use crate::protocols::{gamespy, minecraft, quake, unreal2, valve};
use crate::GDErrorKind::InvalidInput;
use crate::GDResult;
@ -23,7 +23,8 @@ pub enum Protocol {
Gamespy(gamespy::GameSpyVersion),
Minecraft(Option<minecraft::types::Server>),
Quake(quake::QuakeVersion),
Valve(valve::SteamApp),
Valve(valve::Engine),
Unreal2,
#[cfg(feature = "games")]
PROPRIETARY(ProprietaryProtocol),
}
@ -35,6 +36,7 @@ pub enum GenericResponse<'a> {
Minecraft(minecraft::VersionedResponse<'a>),
Quake(quake::VersionedResponse<'a>),
Valve(&'a valve::Response),
Unreal2(&'a unreal2::Response),
#[cfg(feature = "games")]
TheShip(&'a crate::games::theship::Response),
#[cfg(feature = "games")]
@ -51,6 +53,7 @@ pub enum GenericPlayer<'a> {
QuakeTwo(&'a quake::two::Player),
Minecraft(&'a minecraft::Player),
Gamespy(gamespy::VersionedPlayer<'a>),
Unreal2(&'a unreal2::Player),
#[cfg(feature = "games")]
TheShip(&'a crate::games::theship::TheShipPlayer),
#[cfg(feature = "games")]

View file

@ -0,0 +1,58 @@
/// 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].
#[cfg(feature = "games")]
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);
}
};
}
#[cfg(feature = "games")]
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.
#[cfg(feature = "games")]
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)),
&crate::protocols::unreal2::GatheringSettings::default(),
None,
)
}
};
}
#[cfg(feature = "games")]
pub(crate) use game_query_fn;

View file

@ -0,0 +1,308 @@
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::{GatheringSettings, MutatorsAndRules, PacketKind, Players, Response, ServerInfo};
use std::net::SocketAddr;
use byteorder::{ByteOrder, LittleEndian};
use encoding_rs::{UTF_16LE, WINDOWS_1252};
/// Response packets don't seem to exceed 500 bytes, set to 1024 just to be
/// safe.
const PACKET_SIZE: usize = 1024;
/// Default amount of players to pre-allocate if numplayers was not included in
/// server info response.
const DEFAULT_PLAYER_PREALLOCATION: usize = 10;
/// Maximum amount of players to pre-allocate: if the server specifies a number
/// larger than this in serverinfo we don't allocate that many.
const MAXIMUM_PLAYER_PREALLOCATION: usize = 50;
/// 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(())
}
}
/// Send server info query.
pub fn query_server_info(&mut self) -> GDResult<ServerInfo> {
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)
}
/// Send mutators and rules query.
pub fn query_mutators_and_rules(&mut self) -> GDResult<MutatorsAndRules> {
// 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)?;
}
Ok(mutators_and_rules)
}
/// Send players query.
pub fn query_players(&mut self, server_info: Option<&ServerInfo>) -> GDResult<Players> {
// Pre-allocate the player arrays, but don't over allocate memory if the server
// specifies an insane number of players.
let num_players: Option<usize> = server_info.and_then(|i| i.num_players.try_into().ok());
let mut players = Players::with_capacity(
num_players
.unwrap_or(DEFAULT_PLAYER_PREALLOCATION)
.min(MAXIMUM_PLAYER_PREALLOCATION),
);
// 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)?;
if let Some(num_players) = num_players {
if players.total_len() >= num_players {
// If we have already received the amount of players specified in server info
// then we don't need to wait for more player packets to time out.
break;
}
}
// Receive next packet
players_data = self.socket.receive(Some(PACKET_SIZE));
}
Ok(players)
}
/// Make a full server query.
pub fn query(&mut self, gather_settings: &GatheringSettings) -> GDResult<Response> {
// Fetch the server info, this can only handle one response packet
let server_info = self.query_server_info()?;
let mutators_and_rules = if gather_settings.mutators_and_rules {
self.query_mutators_and_rules()?
} else {
MutatorsAndRules::default()
};
let players = if gather_settings.players {
self.query_players(Some(&server_info))?
} else {
Players::with_capacity(0)
};
// 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 result = 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"));
}
// When node decodes UCS2 it uses the UFT16LE encoding.
// https://github.com/nodejs/node/blob/2aaa21f9f684484edb54be30589c4af0b923cdef/lib/buffer.js#L637-L645
let (result, _, invalid_sequences) = UTF_16LE.decode(string_data);
if invalid_sequences {
return Err(PacketBad.context("UTF-8 string contained invalid character(s)"));
}
result
} 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;
// Decode as latin1
let (result, _, invalid_sequences) = WINDOWS_1252.decode(&data[0 .. position]);
if invalid_sequences {
return Err(PacketBad.context("latin1 string contained invalid character(s)"));
}
result
};
// 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,
gather_settings: &GatheringSettings,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let mut client = Unreal2Protocol::new(address, timeout_settings)?;
client.query(gather_settings)
}
// TODO: Add tests

View file

@ -0,0 +1,246 @@
use crate::buffer::Buffer;
use crate::errors::GDErrorKind::PacketBad;
use crate::protocols::types::{CommonPlayer, CommonResponse, ExtraRequestSettings, GenericPlayer};
use crate::protocols::GenericResponse;
use crate::{GDError, GDResult};
use super::Unreal2StringDecoder;
use std::collections::{HashMap, HashSet};
use byteorder::ByteOrder;
/// Unreal 2 packet types.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(u8)]
pub enum PacketKind {
ServerInfo = 0,
MutatorsAndRules = 1,
Players = 2,
}
impl TryFrom<u8> for PacketKind {
type Error = GDError;
fn try_from(value: u8) -> GDResult<Self> {
match value {
0 => Ok(Self::ServerInfo),
1 => Ok(Self::MutatorsAndRules),
2 => Ok(Self::Players),
_ => Err(PacketBad.context("Unknown packet type")),
}
}
}
/// Unreal 2 server info.
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ServerInfo {
pub server_id: u32,
pub ip: String,
pub game_port: u32,
pub query_port: u32,
pub name: String,
pub map: String,
pub game_type: String,
pub num_players: u32,
pub max_players: u32,
}
impl ServerInfo {
pub fn parse<B: ByteOrder>(buffer: &mut Buffer<B>) -> GDResult<Self> {
Ok(ServerInfo {
server_id: buffer.read()?,
ip: buffer.read_string::<Unreal2StringDecoder>(None)?,
game_port: buffer.read()?,
query_port: buffer.read()?,
name: buffer.read_string::<Unreal2StringDecoder>(None)?,
map: buffer.read_string::<Unreal2StringDecoder>(None)?,
game_type: buffer.read_string::<Unreal2StringDecoder>(None)?,
num_players: buffer.read()?,
max_players: buffer.read()?,
})
}
}
/// Unreal 2 mutators and rules.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MutatorsAndRules {
pub mutators: HashSet<String>,
pub rules: HashMap<String, Vec<String>>,
}
impl MutatorsAndRules {
pub fn parse<B: ByteOrder>(&mut self, buffer: &mut Buffer<B>) -> GDResult<()> {
while buffer.remaining_length() > 0 {
let key = buffer.read_string::<Unreal2StringDecoder>(None)?;
let value = buffer.read_string::<Unreal2StringDecoder>(None).ok();
if key.eq_ignore_ascii_case("mutator") {
if let Some(value) = value {
self.mutators.insert(value);
}
} else {
let rule_vec = self.rules.get_mut(&key);
let rule_vec = if let Some(rule_vec) = rule_vec {
rule_vec
} else {
self.rules.insert(key.clone(), Vec::default());
self.rules
.get_mut(&key)
.expect("Value should be in HashMap after we inserted")
};
if let Some(value) = value {
rule_vec.push(value);
}
}
}
Ok(())
}
}
/// Unreal 2 players and bots.
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Players {
/// List of players returned by server (without 0 ping).
pub players: Vec<Player>,
/// List of bots returned by server (players with 0 ping).
pub bots: Vec<Player>,
}
impl Players {
/// Pre-allocate the vectors inside the players struct based on the provided
/// capacity.
pub fn with_capacity(capacity: usize) -> Self {
Players {
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),
}
}
/// Parse a raw buffer of players into the current struct.
pub fn parse<B: ByteOrder>(&mut self, buffer: &mut Buffer<B>) -> GDResult<()> {
while buffer.remaining_length() > 0 {
let player = Player {
id: buffer.read()?,
name: buffer.read_string::<Unreal2StringDecoder>(None)?,
ping: buffer.read()?,
score: buffer.read()?,
stats_id: buffer.read()?,
};
// If ping is 0 the player is a bot
if player.ping == 0 {
self.bots.push(player);
} else {
self.players.push(player);
}
}
Ok(())
}
/// Length of both players and bots.
pub fn total_len(&self) -> usize { self.players.len() + self.bots.len() }
}
/// Unreal 2 player info.
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Player {
pub id: u32,
pub name: String,
pub ping: u32,
pub score: i32,
pub stats_id: u32,
}
impl CommonPlayer for Player {
fn name(&self) -> &str { &self.name }
fn score(&self) -> Option<i32> { Some(self.score) }
fn as_original(&self) -> GenericPlayer { GenericPlayer::Unreal2(self) }
}
/// Unreal 2 response.
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Response {
pub server_info: ServerInfo,
pub mutators_and_rules: MutatorsAndRules,
pub players: Players,
}
impl CommonResponse for Response {
fn map(&self) -> Option<&str> { Some(&self.server_info.map) }
fn name(&self) -> Option<&str> { Some(&self.server_info.name) }
fn game_mode(&self) -> Option<&str> { Some(&self.server_info.game_type) }
fn players_online(&self) -> u32 { self.server_info.num_players }
fn players_maximum(&self) -> u32 { self.server_info.max_players }
fn players(&self) -> Option<Vec<&dyn crate::protocols::types::CommonPlayer>> {
Some(
self.players
.players
.iter()
.map(|player| player as _)
.collect(),
)
}
fn as_original(&self) -> GenericResponse { GenericResponse::Unreal2(self) }
}
/// What data to gather, purely used only with the query function.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct GatheringSettings {
pub players: bool,
pub mutators_and_rules: bool,
}
impl GatheringSettings {
/// Default values are true for both the players and the rules.
pub const fn default() -> Self {
Self {
players: true,
mutators_and_rules: true,
}
}
pub const fn into_extra(self) -> ExtraRequestSettings {
ExtraRequestSettings {
hostname: None,
protocol_version: None,
gather_players: Some(self.players),
gather_rules: Some(self.mutators_and_rules),
check_app_id: None,
}
}
}
impl Default for GatheringSettings {
fn default() -> Self { GatheringSettings::default() }
}
impl From<ExtraRequestSettings> for GatheringSettings {
fn from(value: ExtraRequestSettings) -> Self {
let default = Self::default();
Self {
players: value.gather_players.unwrap_or(default.players),
mutators_and_rules: value.gather_rules.unwrap_or(default.mutators_and_rules),
}
}
}
// TODO: Add tests

View file

@ -13,42 +13,58 @@ pub use types::*;
/// * `pretty_name` - The full name of the game, will be used as the
/// documentation for the created module.
/// * `steam_app`, `default_port` - Passed through to [game_query_fn].
#[cfg(feature = "games")]
macro_rules! game_query_mod {
($mod_name: ident, $pretty_name: expr, $steam_app: ident, $default_port: literal) => {
($mod_name: ident, $pretty_name: expr, $engine: expr, $default_port: literal) => {
crate::protocols::valve::game_query_mod!(
$mod_name,
$pretty_name,
$engine,
$default_port,
GatheringSettings::default()
);
};
($mod_name: ident, $pretty_name: expr, $engine: expr, $default_port: literal, $gathering_settings: expr) => {
#[doc = $pretty_name]
pub mod $mod_name {
crate::protocols::valve::game_query_fn!($steam_app, $default_port);
use crate::protocols::valve::{Engine, GatheringSettings};
crate::protocols::valve::game_query_fn!($pretty_name, $engine, $default_port, $gathering_settings);
}
};
}
#[cfg(feature = "games")]
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.
///
/// * `steam_app` - The entry in the [SteamApp] enum that the game uses.
/// * `engine` - The [Engine] that the game uses.
/// * `default_port` - The default port the game uses.
///
/// ```rust,ignore
/// use crate::protocols::valve::game_query_fn;
/// game_query_fn!(TEAMFORTRESS2, 27015);
/// ```
#[cfg(feature = "games")]
macro_rules! game_query_fn {
($steam_app: ident, $default_port: literal) => {
crate::protocols::valve::game_query_fn!{@gen $steam_app, $default_port, concat!(
"Make a valve query for ", stringify!($steam_app), " 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.")}
($pretty_name: expr, $engine: expr, $default_port: literal, $gathering_settings: expr) => {
// TODO: By using $gathering_settings, also add to doc if a game doesnt respond to certain gathering settings
crate::protocols::valve::game_query_fn!{@gen $engine, $default_port, concat!(
"Make a valve query for ", $pretty_name, " 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."), $gathering_settings}
};
(@gen $steam_app: ident, $default_port: literal, $doc: expr) => {
(@gen $engine: expr, $default_port: literal, $doc: expr, $gathering_settings: expr) => {
#[doc = $doc]
pub fn query(address: &std::net::IpAddr, port: Option<u16>) -> crate::GDResult<crate::protocols::valve::game::Response> {
let valve_response = crate::protocols::valve::query(
&std::net::SocketAddr::new(*address, port.unwrap_or($default_port)),
crate::protocols::valve::SteamApp::$steam_app.as_engine(),
None,
$engine,
Some($gathering_settings),
None,
)?;
@ -57,4 +73,5 @@ macro_rules! game_query_fn {
};
}
#[cfg(feature = "games")]
pub(crate) use game_query_fn;

View file

@ -16,7 +16,6 @@ use crate::{
},
Engine,
ModData,
SteamApp,
},
},
socket::{Socket, UdpSocket},
@ -59,8 +58,8 @@ impl SplitPacket {
Engine::Source(_) => {
let total = buffer.read()?;
let number = buffer.read()?;
let size = match protocol == 7 && (*engine == SteamApp::CSS.as_engine()) {
// certain apps with protocol = 7 dont have this field
let size = match protocol == 7 && (*engine == Engine::new(240)) {
// certain apps with protocol = 7 dont have this field, such as CSS
false => buffer.read()?,
true => 1248,
};
@ -304,7 +303,7 @@ impl ValveProtocol {
let environment_type = Environment::from_gldsrc(buffer.read()?)?;
let has_password = buffer.read::<u8>()? == 1;
let vac_secured = buffer.read::<u8>()? == 1;
let the_ship = match *engine == SteamApp::THESHIP.as_engine() {
let the_ship = match *engine == Engine::new(2400) {
false => None,
true => {
Some(TheShip {
@ -389,11 +388,11 @@ impl ValveProtocol {
name: buffer.read_string::<Utf8Decoder>(None)?,
score: buffer.read()?,
duration: buffer.read()?,
deaths: match *engine == SteamApp::THESHIP.as_engine() {
deaths: match *engine == Engine::new(2400) {
false => None,
true => Some(buffer.read()?),
},
money: match *engine == SteamApp::THESHIP.as_engine() {
money: match *engine == Engine::new(2400) {
false => None,
true => Some(buffer.read()?),
},
@ -418,7 +417,8 @@ impl ValveProtocol {
rules.insert(name, value);
}
if *engine == SteamApp::ROR2.as_engine() {
if *engine == Engine::new(632_360) {
// ROR2
rules.remove("Test");
}

View file

@ -249,156 +249,20 @@ impl Request {
}
}
/// Supported steam apps
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum SteamApp {
/// Counter-Strike
COUNTERSTRIKE,
/// Creativerse
CREATIVERSE,
/// Team Fortress Classic
TFC,
/// Day of Defeat
DOD,
/// Counter-Strike: Condition Zero
CSCZ,
/// Counter-Strike: Source
CSS,
/// Day of Defeat: Source
DODS,
/// Half-Life 2 Deathmatch
HL2D,
/// Half-Life Deathmatch: Source
HLDS,
/// Team Fortress 2
TEAMFORTRESS2,
/// Left 4 Dead
LEFT4DEAD,
/// Left 4 Dead
LEFT4DEAD2,
/// Alien Swarm
ALIENSWARM,
/// Counter-Strike: Global Offensive
CSGO,
/// The Ship
THESHIP,
/// Garry's Mod
GARRYSMOD,
/// Age of Chivalry
AOC,
/// Insurgency: Modern Infantry Combat
IMIC,
/// ARMA 2: Operation Arrowhead
A2OA,
/// Project Zomboid
PROJECTZOMBOID,
/// Insurgency
INSURGENCY,
/// Sven Co-op
SCO,
/// 7 Days To Die
SD2D,
/// Rust
RUST,
/// Ballistic Overkill
BALLISTICOVERKILL,
/// Don't Starve Together
DST,
/// BrainBread 2
BRAINBREAD2,
/// Codename CURE
CODENAMECURE,
/// Black Mesa
BLACKMESA,
/// Colony Survival
COLONYSURVIVAL,
/// Avorion
AVORION,
/// Day of Infamy
DOI,
/// The Forest
THEFOREST,
/// Unturned
UNTURNED,
/// ARK: Survival Evolved
ASE,
/// Battalion 1944
BATTALION1944,
/// Insurgency: Sandstorm
INSURGENCYSANDSTORM,
/// Alien Swarm: Reactive Drop
ASRD,
/// Risk of Rain 2
ROR2,
/// Operation: Harsh Doorstop
OHD,
/// Onset
ONSET,
/// V Rising
VRISING,
/// Hell Let Loose
HLL,
/// Barotrauma
BAROTRAUMA,
}
impl SteamApp {
/// Get the specified app as engine.
pub const fn as_engine(&self) -> Engine {
match self {
Self::CSS => Engine::new_source(240),
Self::DODS => Engine::new_source(300),
Self::HL2D => Engine::new_source(320),
Self::HLDS => Engine::new_source(360),
Self::TEAMFORTRESS2 => Engine::new_source(440),
Self::LEFT4DEAD => Engine::new_source(500),
Self::LEFT4DEAD2 => Engine::new_source(550),
Self::ALIENSWARM => Engine::new_source(630),
Self::CSGO => Engine::new_source(730),
Self::THESHIP => Engine::new_source(2400),
Self::GARRYSMOD => Engine::new_source(4000),
Self::AOC => Engine::new_source(17510),
Self::IMIC => Engine::new_source(17700),
Self::A2OA => Engine::new_source(33930),
Self::PROJECTZOMBOID => Engine::new_source(108_600),
Self::INSURGENCY => Engine::new_source(222_880),
Self::SD2D => Engine::new_source(251_570),
Self::RUST => Engine::new_source(252_490),
Self::CREATIVERSE => Engine::new_source(280_790),
Self::BALLISTICOVERKILL => Engine::new_source(296_300),
Self::DST => Engine::new_source(322_320),
Self::BRAINBREAD2 => Engine::new_source(346_330),
Self::CODENAMECURE => Engine::new_source(355_180),
Self::BLACKMESA => Engine::new_source(362_890),
Self::COLONYSURVIVAL => Engine::new_source(366_090),
Self::AVORION => Engine::new_source(445_220),
Self::DOI => Engine::new_source(447_820),
Self::THEFOREST => Engine::new_source(556_450),
Self::UNTURNED => Engine::new_source(304_930),
Self::ASE => Engine::new_source(346_110),
Self::BATTALION1944 => Engine::new_source(489_940),
Self::INSURGENCYSANDSTORM => Engine::new_source(581_320),
Self::ASRD => Engine::new_source(563_560),
Self::BAROTRAUMA => Engine::new_source(602960),
Self::ROR2 => Engine::new_source(632_360),
Self::OHD => Engine::new_source_with_dedicated(736_590, 950_900),
Self::ONSET => Engine::new_source(1_105_810),
Self::VRISING => Engine::new_source(1_604_030),
Self::HLL => Engine::new_source(686_810),
_ => Engine::GoldSrc(false), // CS - 10, TFC - 20, DOD - 30, CSCZ - 80, SC - 225840
}
}
}
/// Engine type.
/// Every supported Valve game references this enum, represents the behaviour
/// of server requests and responses.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Engine {
/// A Source game, the argument represents the possible steam app ids, if
/// its **None**, let the query find it, if its **Some**, the query
/// fails if the response id is not the first one, which is the game app
/// id, or the other one, which is the dedicated server app id.
/// A Source game, the argument represents the possible steam app ids.
/// If its **None**, let the query find it (could come with some drawbacks,
/// some games do not respond on certain protocol versions (CSS on 7),
/// some have additional data (The Ship).
/// If its **Some**, the first value is the main steam app id, the second
/// could be a secondly used id, as some games use a different one for
/// dedicated servers. Beware if **check_app_id** is set to true in
/// [GatheringSettings], as the query will fail if the server doesnt respond
/// with the expected ids.
Source(Option<(u32, Option<u32>)>),
/// A GoldSrc game, the argument indicates whether to enforce
/// requesting the obsolete A2S_INFO response or not.
@ -406,9 +270,11 @@ pub enum Engine {
}
impl Engine {
pub const fn new_source(appid: u32) -> Self { Self::Source(Some((appid, None))) }
pub const fn new(appid: u32) -> Self { Self::Source(Some((appid, None))) }
pub const fn new_source_with_dedicated(appid: u32, dedicated_appid: u32) -> Self {
pub const fn new_gold_src(force: bool) -> Self { Self::GoldSrc(force) }
pub const fn new_with_dedicated(appid: u32, dedicated_appid: u32) -> Self {
Self::Source(Some((appid, Some(dedicated_appid))))
}
}
@ -422,15 +288,29 @@ pub struct GatheringSettings {
pub check_app_id: bool,
}
impl Default for GatheringSettings {
impl GatheringSettings {
/// Default values are true for both the players and the rules.
fn default() -> Self {
pub const fn default() -> Self {
Self {
players: true,
rules: true,
check_app_id: true,
}
}
pub const fn into_extra(self) -> ExtraRequestSettings {
ExtraRequestSettings {
hostname: None,
protocol_version: None,
gather_players: Some(self.players),
gather_rules: Some(self.rules),
check_app_id: Some(self.check_app_id),
}
}
}
impl Default for GatheringSettings {
fn default() -> Self { GatheringSettings::default() }
}
impl From<ExtraRequestSettings> for GatheringSettings {