From c3e2d948e81792e6fea5e0bdc0fae20707af030b Mon Sep 17 00:00:00 2001 From: CosminPerRam Date: Mon, 19 Jun 2023 22:10:49 +0300 Subject: [PATCH] [Game] Add JC2MP support. (#54) * [Game] Add JC2MP support. * [Game] Add game to changelog and games * [Games] Add generic support to JC2MP. * [Game] Add players_maximum and players_online --- CHANGELOG.md | 1 + GAMES.md | 109 +++++++++--------- examples/master_querant.rs | 2 + src/games/definitions.rs | 1 + src/games/jc2mp.rs | 106 +++++++++++++++++ src/games/mod.rs | 3 + src/protocols/gamespy/mod.rs | 2 +- .../gamespy/protocols/three/protocol.rs | 100 ++++++++++------ src/protocols/types.rs | 3 + 9 files changed, 236 insertions(+), 91 deletions(-) create mode 100644 src/games/jc2mp.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3050eaf..205b5f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Generic query: Games: - [Halo: Combat Evolved](https://en.wikipedia.org/wiki/Halo:_Combat_Evolved) support. +- [Just Cause 2: Multiplayer](https://store.steampowered.com/app/259080/Just_Cause_2_Multiplayer_Mod/) support. ### Breaking... Crate: diff --git a/GAMES.md b/GAMES.md index 42a01b3..96c6174 100644 --- a/GAMES.md +++ b/GAMES.md @@ -2,60 +2,61 @@ A supported game is defined as a game that has been successfully tested, other g Beware of the `Notes` column, as it contains information about query port offsets or other query requirements. # Supported games: -| Game | Use name | Protocol | Notes | -|------------------------------------|----------|------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Team Fortress 2 | TF2 | Valve Protocol | | -| The Ship | TS | Valve Protocol (*Altered) | | -| Counter-Strike: Global Offensive | CSGO | Valve Protocol | The server must have the cvar `host_players_show` set to `2` to get the full player list. | -| Counter-Strike: Source | CSS | Valve Protocol | | -| Day of Defeat: Source | DODS | Valve Protocol | | -| Left 4 Dead | L4D | Valve Protocol | | -| Left 4 Dead 2 | L4D2 | Valve Protocol | | -| Half-Life 2 Deathmatch | HL2DM | Valve Protocol | | -| Alien Swarm | ALIENS | Valve Protocol | | -| Alien Swarm: Reactive Drop | ASRD | Valve Protocol | | -| Insurgency | INS | Valve Protocol | | -| Insurgency: Sandstorm | INSS | Valve Protocol | Query port offset: 1. | -| Insurgency: Modern Infantry Combat | INSMIC | Valve Protocol | | -| Counter-Strike: Condition Zero | CSCZ | Valve Protocol (GoldSrc) | | -| Day of Defeat | DOD | Valve Protocol (GoldSrc) | | -| Minecraft | MC | Proprietary | Bedrock edition provides a different response compared to the Java edition, query specifically for bedrock to get them, otherwise, only matching fields will be provided. | -| 7 Days To Die | SDTD | Valve Protocol | | -| ARK: Survival Evolved | ASE | Valve Protocol | | -| Unturned | UNTURNED | Valve Protocol | | -| The Forest | TF | Valve Protocol (GoldSrc) | Query port offset: 1. | -| Team Fortress Classic | TFC | Valve Protocol | | -| Sven Co-op | SC | Valve Protocol (GoldSrc) | | -| Rust | RUST | Valve Protocol | | -| Counter-Strike | CS | Valve Protocol (GoldSrc) | | -| Arma 2: Operation Arrowhead | ARMA2OA | Valve Protocol | Query port offset: 1. | -| Day of Infamy | DOI | Valve Protocol | | -| Half-Life Deathmatch: Source | HLDMS | Valve Protocol | | -| Risk of Rain 2 | ROR2 | Valve Protocol | Query port offset: 1. | -| Battalion 1944 | BAT1944 | Valve Protocol | Query port offset: 3. It is strongly recommended to also query the rules, as it sends basic server info in them. | -| Black Mesa | BM | Valve Protocol | | -| Project Zomboid | PZ | Valve Protocol | | -| Age of Chivalry | AOC | Valve Protocol | | -| Don't Starve Together | DST | Valve Protocol | Query port is 27016. | -| Colony Survival | COLU | Valve Protocol | | -| Onset | ONSET | Valve Protocol | Query port is 7776. | -| Codename CURE | CCURE | Valve Protocol | | -| Ballistic Overkill | BO | Valve Protocol | Query port is 27016. | -| BrainBread 2 | BB2 | Valve Protocol | | -| Avorion | AVORION | Valve Protocol | Query port is 27020. | -| Operation: Harsh Doorstop | OHD | Valve Protocol | Query port is 27005. | -| V Rising | VR | Valve Protocol | Query port is 27016. | -| Unreal Tournament | UT | GameSpy 1 | Query Port offset: 1. | -| Battlefield 1942 | BF1942 | GameSpy 1 | Query port is 23000. | -| Serious Sam | SS | GameSpy 1 | Query Port offset: 1. | -| Frontlines: Fuel of War | FFOW | Valve Protocol (Proprietary) | Query Port offset: 2. | -| Crysis Wars | CW | GameSpy 3 | | -| Quake 2 | QUAKE2 | Quake 2 | | -| Quake 1 | QUAKE1 | Quake 1 | | -| Quake 3: Arena | QUAKE3A | Quake 3 | | -| Hell Let Loose | HLL | Valve Protocol | Query port is 26420. Note that on this port it might not send players data, as there might be another query port that does send players data. | -| Soldier of Fortune 2 | SOF2 | Quake 3 | | -| Halo: Combat Evolved | HALOCE | GameSpy 2 | | +| Game | Use name | Protocol | Notes | +|------------------------------------|----------|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Team Fortress 2 | TF2 | Valve Protocol | | +| The Ship | TS | Valve Protocol (*Altered) | | +| Counter-Strike: Global Offensive | CSGO | Valve Protocol | The server must have the cvar `host_players_show` set to `2` to get the full player list. | +| Counter-Strike: Source | CSS | Valve Protocol | | +| Day of Defeat: Source | DODS | Valve Protocol | | +| Left 4 Dead | L4D | Valve Protocol | | +| Left 4 Dead 2 | L4D2 | Valve Protocol | | +| Half-Life 2 Deathmatch | HL2DM | Valve Protocol | | +| Alien Swarm | ALIENS | Valve Protocol | | +| Alien Swarm: Reactive Drop | ASRD | Valve Protocol | | +| Insurgency | INS | Valve Protocol | | +| Insurgency: Sandstorm | INSS | Valve Protocol | Query port offset: 1. | +| Insurgency: Modern Infantry Combat | INSMIC | Valve Protocol | | +| Counter-Strike: Condition Zero | CSCZ | Valve Protocol (GoldSrc) | | +| Day of Defeat | DOD | Valve Protocol (GoldSrc) | | +| Minecraft | MC | Proprietary | Bedrock edition provides a different response compared to the Java edition, query specifically for bedrock to get them, otherwise, only matching fields will be provided. | +| 7 Days To Die | SDTD | Valve Protocol | | +| ARK: Survival Evolved | ASE | Valve Protocol | | +| Unturned | UNTURNED | Valve Protocol | | +| The Forest | TF | Valve Protocol (GoldSrc) | Query port offset: 1. | +| Team Fortress Classic | TFC | Valve Protocol | | +| Sven Co-op | SC | Valve Protocol (GoldSrc) | | +| Rust | RUST | Valve Protocol | | +| Counter-Strike | CS | Valve Protocol (GoldSrc) | | +| Arma 2: Operation Arrowhead | ARMA2OA | Valve Protocol | Query port offset: 1. | +| Day of Infamy | DOI | Valve Protocol | | +| Half-Life Deathmatch: Source | HLDMS | Valve Protocol | | +| Risk of Rain 2 | ROR2 | Valve Protocol | Query port offset: 1. | +| Battalion 1944 | BAT1944 | Valve Protocol | Query port offset: 3. It is strongly recommended to also query the rules, as it sends basic server info in them. | +| Black Mesa | BM | Valve Protocol | | +| Project Zomboid | PZ | Valve Protocol | | +| Age of Chivalry | AOC | Valve Protocol | | +| Don't Starve Together | DST | Valve Protocol | Query port is 27016. | +| Colony Survival | COLU | Valve Protocol | | +| Onset | ONSET | Valve Protocol | Query port is 7776. | +| Codename CURE | CCURE | Valve Protocol | | +| Ballistic Overkill | BO | Valve Protocol | Query port is 27016. | +| BrainBread 2 | BB2 | Valve Protocol | | +| Avorion | AVORION | Valve Protocol | Query port is 27020. | +| Operation: Harsh Doorstop | OHD | Valve Protocol | Query port is 27005. | +| V Rising | VR | Valve Protocol | Query port is 27016. | +| Unreal Tournament | UT | GameSpy 1 | Query Port offset: 1. | +| Battlefield 1942 | BF1942 | GameSpy 1 | Query port is 23000. | +| Serious Sam | SS | GameSpy 1 | Query Port offset: 1. | +| Frontlines: Fuel of War | FFOW | Valve Protocol (*Altered) | Query Port offset: 2. | +| Crysis Wars | CW | GameSpy 3 | | +| Quake 2 | QUAKE2 | Quake 2 | | +| Quake 1 | QUAKE1 | Quake 1 | | +| Quake 3: Arena | QUAKE3A | Quake 3 | | +| Hell Let Loose | HLL | Valve Protocol | Query port is 26420. Note that on this port it might not send players data, as there might be another query port that does send players data. | +| Soldier of Fortune 2 | SOF2 | Quake 3 | | +| Halo: Combat Evolved | HALOCE | GameSpy 2 | | +| Just Cause 2: Multiplayer | JC2MP | GameSpy 3 (*Altered) | | ## Planned to add support: _ diff --git a/examples/master_querant.rs b/examples/master_querant.rs index 15390fc..f10ff4e 100644 --- a/examples/master_querant.rs +++ b/examples/master_querant.rs @@ -34,6 +34,7 @@ use gamedig::{ ins, insmic, inss, + jc2mp, l4d, l4d2, mc, @@ -189,6 +190,7 @@ fn main() -> GDResult<()> { "sof2" => println!("{:#?}", sof2::query(ip, port)?), "_gamespy2" => println!("{:#?}", gamespy::two::query(address, None)), "haloce" => println!("{:#?}", haloce::query(ip, port)?), + "jc2mp" => println!("{:#?}", jc2mp::query(ip, port)?), _ => panic!("Undefined game: {}", args[1]), }; diff --git a/src/games/definitions.rs b/src/games/definitions.rs index d16ab25..d05496e 100644 --- a/src/games/definitions.rs +++ b/src/games/definitions.rs @@ -81,4 +81,5 @@ pub static GAMES: Map<&'static str, Game> = phf_map! { "unturned" => game!("Unturned", 27015, Protocol::Valve(SteamApp::UNTURNED)), "ut" => game!("Unreal Tournament", 7778, Protocol::Gamespy(GameSpyVersion::One)), "vr" => game!("V Rising", 27016, Protocol::Valve(SteamApp::VR)), + "jc2mp" => game!("Just Cause 2: Multiplayer", 7777, Protocol::JC2MP), }; diff --git a/src/games/jc2mp.rs b/src/games/jc2mp.rs new file mode 100644 index 0000000..b96c025 --- /dev/null +++ b/src/games/jc2mp.rs @@ -0,0 +1,106 @@ +use crate::bufferer::{Bufferer, Endianess}; +use crate::protocols::gamespy::common::has_password; +use crate::protocols::gamespy::three::{data_to_map, GameSpy3}; +use crate::protocols::types::SpecificResponse; +use crate::protocols::GenericResponse; +use crate::{GDError, GDResult}; +#[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, +} + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Response { + version: String, + description: String, + name: String, + has_password: bool, + players: Vec, + players_maximum: usize, + players_online: usize, +} + +impl From for GenericResponse { + fn from(r: Response) -> Self { + Self { + name: Some(r.name), + description: Some(r.description), + game: None, + game_version: Some(r.version), + map: None, + players_maximum: r.players_maximum as u64, + players_online: r.players_online as u64, + players_bots: None, + has_password: Some(r.has_password), + inner: SpecificResponse::JC2MP, + } + } +} + +fn parse_players_and_teams(packet: Vec) -> GDResult> { + let mut buf = Bufferer::new_with_data(Endianess::Big, &packet); + + let count = buf.get_u16()?; + let mut players = Vec::with_capacity(count as usize); + + while buf.remaining_length() > 0 { + players.push(Player { + name: buf.get_string_utf8()?, + steam_id: buf.get_string_utf8()?, + ping: buf.get_u16()?, + }) + } + + Ok(players) +} + +pub fn query(address: &IpAddr, port: Option) -> GDResult { + let mut client = GameSpy3::new_custom( + &SocketAddr::new(*address, port.unwrap_or(7777)), + None, + [0xFF, 0xFF, 0xFF, 0x02], + true, + )?; + + let packets = client.get_server_packets()?; + let data = packets.get(0).ok_or(GDError::PacketBad)?; + + 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(GDError::PacketBad)? + .parse() + .map_err(|_| GDError::TypeParse)?; + let players_online = match server_vars.remove("numplayers") { + None => players.len(), + Some(v) => { + let reported_players = v.parse().map_err(|_| GDError::TypeParse)?; + match reported_players < players.len() { + true => players.len(), + false => reported_players, + } + } + }; + + Ok(Response { + version: server_vars.remove("version").ok_or(GDError::PacketBad)?, + description: server_vars + .remove("description") + .ok_or(GDError::PacketBad)?, + name: server_vars.remove("hostname").ok_or(GDError::PacketBad)?, + has_password: has_password(&mut server_vars)?, + players, + players_maximum, + players_online, + }) +} diff --git a/src/games/mod.rs b/src/games/mod.rs index 6fa901e..50e1809 100644 --- a/src/games/mod.rs +++ b/src/games/mod.rs @@ -65,6 +65,8 @@ pub mod ins; pub mod insmic; /// Insurgency: Sandstorm pub mod inss; +/// Just Cause 2: Multiplayer +pub mod jc2mp; /// Left 4 Dead pub mod l4d; /// Left 4 Dead 2 @@ -171,5 +173,6 @@ pub fn query(game: &Game, address: &IpAddr, port: Option) -> GDResult ts::query(address, port).map(|r| r.into())?, Protocol::FFOW => ffow::query(address, port).map(|r| r.into())?, + Protocol::JC2MP => jc2mp::query(address, port).map(|r| r.into())?, }) } diff --git a/src/protocols/gamespy/mod.rs b/src/protocols/gamespy/mod.rs index 648a419..464ec8c 100644 --- a/src/protocols/gamespy/mod.rs +++ b/src/protocols/gamespy/mod.rs @@ -1,7 +1,7 @@ #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -mod common; +pub(crate) mod common; /// The implementations. pub mod protocols; diff --git a/src/protocols/gamespy/protocols/three/protocol.rs b/src/protocols/gamespy/protocols/three/protocol.rs index 2283203..c5adce6 100644 --- a/src/protocols/gamespy/protocols/three/protocol.rs +++ b/src/protocols/gamespy/protocols/three/protocol.rs @@ -36,18 +36,41 @@ impl RequestPacket { } } -struct GameSpy3 { +pub(crate) struct GameSpy3 { socket: UdpSocket, + payload: [u8; 4], + single_packets: bool, } const PACKET_SIZE: usize = 2048; +const DEFAULT_PAYLOAD: [u8; 4] = [0xFF, 0xFF, 0xFF, 0x01]; impl GameSpy3 { fn new(address: &SocketAddr, timeout_settings: Option) -> GDResult { let socket = UdpSocket::new(address)?; socket.apply_timeout(timeout_settings)?; - Ok(Self { socket }) + Ok(Self { + socket, + payload: DEFAULT_PAYLOAD, + single_packets: false, + }) + } + + pub(crate) fn new_custom( + address: &SocketAddr, + timeout_settings: Option, + payload: [u8; 4], + single_packets: bool, + ) -> GDResult { + let socket = UdpSocket::new(address)?; + socket.apply_timeout(timeout_settings)?; + + Ok(Self { + socket, + payload, + single_packets, + }) } fn receive(&mut self, size: Option, kind: u8) -> GDResult { @@ -97,54 +120,57 @@ impl GameSpy3 { kind: 0, session_id: THIS_SESSION_ID, challenge, - payload: Some([0xff, 0xff, 0xff, 0x01]), + payload: Some(self.payload), } .to_bytes(), ) } -} -fn get_server_packets(address: &SocketAddr, timeout_settings: Option) -> GDResult>> { - let mut gs3 = GameSpy3::new(address, timeout_settings)?; + pub(crate) fn get_server_packets(&mut self) -> GDResult>> { + let challenge = self.make_initial_handshake()?; + self.send_data_request(challenge)?; - let challenge = gs3.make_initial_handshake()?; - gs3.send_data_request(challenge)?; + let mut values: Vec> = Vec::new(); - let mut values: Vec> = Vec::new(); + let mut expected_number_of_packets: Option = None; - let mut expected_number_of_packets: Option = None; + while expected_number_of_packets.is_none() || values.len() != expected_number_of_packets.unwrap() { + let mut buf = self.receive(None, 0)?; - while expected_number_of_packets.is_none() || values.len() != expected_number_of_packets.unwrap() { - let mut buf = gs3.receive(None, 0)?; + if self.single_packets { + buf.move_position_ahead(11); + return Ok(vec![buf.remaining_data_vec()]); + } - if buf.get_string_utf8()? != "splitnum" { + if buf.get_string_utf8()? != "splitnum" { + return Err(GDError::PacketBad); + } + + let id = buf.get_u8()?; + let is_last = (id & 0x80) > 0; + let packet_id = (id & 0x7f) as usize; + buf.move_position_ahead(1); //unknown byte regarding packet no. + + if is_last { + expected_number_of_packets = Some(packet_id + 1); + } + + while values.len() <= packet_id { + values.push(Vec::new()); + } + + values[packet_id] = buf.remaining_data_vec(); + } + + if values.iter().any(|v| v.is_empty()) { return Err(GDError::PacketBad); } - let id = buf.get_u8()?; - let is_last = (id & 0x80) > 0; - let packet_id = (id & 0x7f) as usize; - buf.move_position_ahead(1); //unknown byte regarding packet no. - - if is_last { - expected_number_of_packets = Some(packet_id + 1); - } - - while values.len() <= packet_id { - values.push(Vec::new()); - } - - values[packet_id] = buf.remaining_data_vec(); + Ok(values) } - - if values.iter().any(|v| v.is_empty()) { - return Err(GDError::PacketBad); - } - - Ok(values) } -fn data_to_map(packet: &[u8]) -> GDResult<(HashMap, Vec)> { +pub(crate) fn data_to_map(packet: &[u8]) -> GDResult<(HashMap, Vec)> { let mut vars = HashMap::new(); let mut buf = Bufferer::new_with_data(Endianess::Big, packet); @@ -168,7 +194,8 @@ pub fn query_vars( address: &SocketAddr, timeout_settings: Option, ) -> GDResult> { - let packets = get_server_packets(address, timeout_settings)?; + let mut client = GameSpy3::new(address, timeout_settings)?; + let packets = client.get_server_packets()?; let mut vars = HashMap::new(); @@ -308,7 +335,8 @@ fn parse_players_and_teams(packets: Vec>) -> GDResult<(Vec, Vec< /// Providing None to the timeout settings results in using the default values. /// (TimeoutSettings::[default](TimeoutSettings::default)). pub fn query(address: &SocketAddr, timeout_settings: Option) -> GDResult { - let packets = get_server_packets(address, timeout_settings)?; + let mut client = GameSpy3::new(address, timeout_settings)?; + let packets = client.get_server_packets()?; let (mut server_vars, remaining_data) = data_to_map(packets.get(0).ok_or(GDError::PacketBad)?)?; diff --git a/src/protocols/types.rs b/src/protocols/types.rs index 580be63..cf417c4 100644 --- a/src/protocols/types.rs +++ b/src/protocols/types.rs @@ -16,6 +16,7 @@ pub enum Protocol { Valve(valve::SteamApp), TheShip, FFOW, + JC2MP, } /// A generic version of a response @@ -56,6 +57,8 @@ pub enum SpecificResponse { TheShip(crate::games::ts::ExtraResponse), #[cfg(not(feature = "no_games"))] FFOW(crate::games::ffow::ExtraResponse), + #[cfg(not(feature = "no_games"))] + JC2MP, } /// Timeout settings for socket operations