diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4cacb71..6b87f9c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,7 +3,11 @@ Who knows what the future holds...
# 0.X.Y - DD/MM/2023
### Changes:
-To be made...
+Protocols:
+- GameSpy 2 support.
+
+Games:
+- [Halo: Combat Evolved](https://en.wikipedia.org/wiki/Halo:_Combat_Evolved) support.
### Breaking...
Crate:
diff --git a/GAMES.md b/GAMES.md
index 0b9adfa..42a01b3 100644
--- a/GAMES.md
+++ b/GAMES.md
@@ -55,6 +55,7 @@ Beware of the `Notes` column, as it contains information about query port offset
| 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 | |
## Planned to add support:
_
diff --git a/PROTOCOLS.md b/PROTOCOLS.md
index 4a67279..0b838b3 100644
--- a/PROTOCOLS.md
+++ b/PROTOCOLS.md
@@ -1,12 +1,12 @@
A protocol is defined as proprietary if it is being used only for a single scope (or series, like Minecraft).
# Supported protocols:
-| Name | For | Proprietary? | Documentation reference | Notes |
-|----------------|-------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| Valve Protocol | Games | No | [Server Queries](https://developer.valvesoftware.com/wiki/Server_queries) | In some cases, the players details query might contain some 0-length named players. Multi-packet decompression not tested. |
-| Minecraft | Games | Yes | Java: [List Server Protocol](https://wiki.vg/Server_List_Ping)
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) 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) | |
+| Name | For | Proprietary? | Documentation reference | Notes |
+|----------------|-------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Valve Protocol | Games | No | [Server Queries](https://developer.valvesoftware.com/wiki/Server_queries) | In some cases, the players details query might contain some 0-length named players. Multi-packet decompression not tested. |
+| Minecraft | Games | Yes | Java: [List Server Protocol](https://wiki.vg/Server_List_Ping)
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) | |
## Planned to add support:
_
diff --git a/examples/master_querant.rs b/examples/master_querant.rs
index 109b9e7..15390fc 100644
--- a/examples/master_querant.rs
+++ b/examples/master_querant.rs
@@ -27,6 +27,7 @@ use gamedig::{
dst,
ffow,
gm,
+ haloce,
hl2dm,
hldms,
hll,
@@ -186,6 +187,8 @@ fn main() -> GDResult<()> {
"quake3a" => println!("{:#?}", quake3a::query(ip, port)?),
"hll" => println!("{:#?}", hll::query(ip, port)?),
"sof2" => println!("{:#?}", sof2::query(ip, port)?),
+ "_gamespy2" => println!("{:#?}", gamespy::two::query(address, None)),
+ "haloce" => println!("{:#?}", haloce::query(ip, port)?),
_ => panic!("Undefined game: {}", args[1]),
};
diff --git a/src/games/haloce.rs b/src/games/haloce.rs
new file mode 100644
index 0000000..6ab7af8
--- /dev/null
+++ b/src/games/haloce.rs
@@ -0,0 +1,8 @@
+use crate::protocols::gamespy;
+use crate::protocols::gamespy::two::Response;
+use crate::GDResult;
+use std::net::{IpAddr, SocketAddr};
+
+pub fn query(address: &IpAddr, port: Option) -> GDResult {
+ gamespy::two::query(&SocketAddr::new(*address, port.unwrap_or(2302)), None)
+}
diff --git a/src/games/mod.rs b/src/games/mod.rs
index 0a4e006..068492c 100644
--- a/src/games/mod.rs
+++ b/src/games/mod.rs
@@ -48,6 +48,8 @@ pub mod dst;
pub mod ffow;
/// Garry's Mod
pub mod gm;
+/// Halo: Combat Evolved
+pub mod haloce;
/// Half-Life 2 Deathmatch
pub mod hl2dm;
/// Half-Life Deathmatch: Source
diff --git a/src/protocols/gamespy/protocols/mod.rs b/src/protocols/gamespy/protocols/mod.rs
index c2a9530..bdd73c4 100644
--- a/src/protocols/gamespy/protocols/mod.rs
+++ b/src/protocols/gamespy/protocols/mod.rs
@@ -1,2 +1,3 @@
pub mod one;
pub mod three;
+pub mod two;
diff --git a/src/protocols/gamespy/protocols/two/mod.rs b/src/protocols/gamespy/protocols/two/mod.rs
new file mode 100644
index 0000000..14c6b48
--- /dev/null
+++ b/src/protocols/gamespy/protocols/two/mod.rs
@@ -0,0 +1,5 @@
+pub mod protocol;
+pub mod types;
+
+pub use protocol::*;
+pub use types::*;
diff --git a/src/protocols/gamespy/protocols/two/protocol.rs b/src/protocols/gamespy/protocols/two/protocol.rs
new file mode 100644
index 0000000..5b09d5f
--- /dev/null
+++ b/src/protocols/gamespy/protocols/two/protocol.rs
@@ -0,0 +1,185 @@
+use crate::bufferer::{Bufferer, Endianess};
+use crate::protocols::gamespy::two::{Player, Response, Team};
+use crate::protocols::types::TimeoutSettings;
+use crate::socket::{Socket, UdpSocket};
+use crate::{GDError, GDResult};
+use std::collections::HashMap;
+use std::net::SocketAddr;
+
+struct GameSpy2 {
+ socket: UdpSocket,
+}
+
+macro_rules! table_extract {
+ ($table:expr, $name:literal, $index:expr) => {
+ $table
+ .get($name)
+ .ok_or(GDError::PacketBad)?
+ .get($index)
+ .ok_or(GDError::PacketBad)?
+ };
+}
+
+macro_rules! table_extract_parse {
+ ($table:expr, $name:literal, $index:expr) => {
+ table_extract!($table, $name, $index)
+ .parse()
+ .map_err(|_| GDError::PacketBad)?
+ };
+}
+
+fn data_as_table(data: &mut Bufferer) -> GDResult<(HashMap>, usize)> {
+ if data.get_u8()? != 0 {
+ Err(GDError::PacketBad)?
+ }
+
+ let rows = data.get_u8()? as usize;
+
+ if rows == 0 {
+ return Ok((HashMap::new(), 0));
+ }
+
+ let mut column_heads = Vec::new();
+
+ let mut current_column = data.get_string_utf8()?;
+ while !current_column.is_empty() {
+ column_heads.push(current_column);
+ current_column = data.get_string_utf8()?;
+ }
+
+ let columns = column_heads.len();
+ let mut table = HashMap::with_capacity(columns);
+ for head in &column_heads {
+ table.insert(head.clone(), Vec::new()); // TODO: This doesn't look good nor it is performant, fix later
+ }
+
+ for _ in 0 .. rows {
+ for column in column_heads.iter() {
+ let value = data.get_string_utf8()?;
+ table.get_mut(column).ok_or(GDError::PacketBad)?.push(value);
+ }
+ }
+
+ Ok((table, rows))
+}
+
+impl GameSpy2 {
+ fn new(address: &SocketAddr, timeout_settings: Option) -> GDResult {
+ let socket = UdpSocket::new(address)?;
+ socket.apply_timeout(timeout_settings)?;
+
+ Ok(Self { socket })
+ }
+
+ fn request_data(&mut self) -> GDResult {
+ self.socket
+ .send(&[0xFE, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF])?;
+
+ let received = self.socket.receive(None)?;
+ let mut buf = Bufferer::new_with_data(Endianess::Big, &received);
+
+ if buf.get_u8()? != 0 {
+ return Err(GDError::PacketBad);
+ }
+
+ if buf.get_u32()? != 1 {
+ return Err(GDError::PacketBad);
+ }
+
+ Ok(buf)
+ }
+}
+
+fn get_server_vars(bufferer: &mut Bufferer) -> GDResult> {
+ let mut values = HashMap::new();
+
+ let mut done_processing_vars = false;
+ while !done_processing_vars && bufferer.remaining_length() > 0 {
+ let key = bufferer.get_string_utf8()?;
+ let value = bufferer.get_string_utf8_optional()?;
+
+ if key.is_empty() {
+ if value.is_empty() {
+ bufferer.move_position_backward(1);
+ done_processing_vars = true;
+ }
+
+ continue;
+ }
+
+ values.insert(key, value);
+ }
+
+ Ok(values)
+}
+
+fn get_teams(bufferer: &mut Bufferer) -> GDResult> {
+ let mut teams = Vec::new();
+
+ let (table, entries) = data_as_table(bufferer)?;
+
+ for index in 0 .. entries {
+ teams.push(Team {
+ name: table_extract!(table, "team_t", index).clone(),
+ score: table_extract_parse!(table, "score_t", index),
+ })
+ }
+
+ Ok(teams)
+}
+
+fn get_players(bufferer: &mut Bufferer) -> GDResult> {
+ let mut players = Vec::new();
+
+ let (table, entries) = data_as_table(bufferer)?;
+
+ for index in 0 .. entries {
+ players.push(Player {
+ name: table_extract!(table, "player_", index).clone(),
+ score: table_extract_parse!(table, "score_", index),
+ ping: table_extract_parse!(table, "ping_", index),
+ team_index: table_extract_parse!(table, "team_", index),
+ })
+ }
+
+ Ok(players)
+}
+
+pub fn query(address: &SocketAddr, timeout_settings: Option) -> GDResult {
+ let mut client = GameSpy2::new(address, timeout_settings)?;
+ let mut data = client.request_data()?;
+
+ let mut server_vars = get_server_vars(&mut data)?;
+ let players = get_players(&mut data)?;
+
+ 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,
+ }
+ }
+ };
+ let players_minimum = match server_vars.remove("minplayers") {
+ None => None,
+ Some(v) => Some(v.parse::().map_err(|_| GDError::TypeParse)?),
+ };
+
+ Ok(Response {
+ name: server_vars.remove("hostname").ok_or(GDError::PacketBad)?,
+ map: server_vars.remove("mapname").ok_or(GDError::PacketBad)?,
+ has_password: server_vars.remove("password").ok_or(GDError::PacketBad)? == "1",
+ teams: get_teams(&mut data)?,
+ players_maximum: server_vars
+ .remove("maxplayers")
+ .ok_or(GDError::PacketBad)?
+ .parse()
+ .map_err(|_| GDError::PacketBad)?,
+ players_online,
+ players_minimum,
+ players,
+ unused_entries: server_vars,
+ })
+}
diff --git a/src/protocols/gamespy/protocols/two/types.rs b/src/protocols/gamespy/protocols/two/types.rs
new file mode 100644
index 0000000..086204c
--- /dev/null
+++ b/src/protocols/gamespy/protocols/two/types.rs
@@ -0,0 +1,34 @@
+use std::collections::HashMap;
+
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct Team {
+ pub name: String,
+ pub score: u16,
+}
+
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct Player {
+ pub name: String,
+ pub score: u16,
+ pub ping: u16,
+ pub team_index: u16,
+}
+
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Response {
+ pub name: String,
+ pub map: String,
+ pub has_password: bool,
+ pub teams: Vec,
+ pub players_maximum: usize,
+ pub players_online: usize,
+ pub players_minimum: Option,
+ pub players: Vec,
+ pub unused_entries: HashMap,
+}