mirror of
https://github.com/tribufu/rust-gamedig
synced 2026-05-18 09:35:50 +00:00
Add support for Mindustry (#178)
* buffer: Add UTF8LengthPrefixed string decoder * games: Use expression for default port This allows us to refer to constants for the default ports if we want to (literals will still work). * games: Add support for mindustry
This commit is contained in:
parent
ba92466ae1
commit
07de5168f4
9 changed files with 251 additions and 3 deletions
|
|
@ -17,6 +17,7 @@ Games:
|
|||
- [Zombie Panic: Source](https://store.steampowered.com/app/17500/Zombie_Panic_Source/) support.
|
||||
- Added a valve protocol query example.
|
||||
- Made all of Just Cause 2: Multiplayer Response and Player fields public.
|
||||
- [Mindustry](https://mindustrygame.github.io/) support.
|
||||
|
||||
Protocols:
|
||||
- Added the unreal2 protocol and its associated games: Darkest Hour, Devastation, Killing Floor, Red Orchestra, Unreal Tournament 2003, Unreal Tournament 2004 (by @Douile).
|
||||
|
|
|
|||
|
|
@ -361,6 +361,55 @@ impl StringDecoder for Utf8Decoder {
|
|||
}
|
||||
}
|
||||
|
||||
/// A decoder for UTF-8 encoded strings prefixed by a single byte denoting the
|
||||
/// string's length.
|
||||
///
|
||||
/// This decoder uses a single null byte (`0x00`) as the default delimiter.
|
||||
pub struct Utf8LengthPrefixedDecoder;
|
||||
|
||||
impl StringDecoder for Utf8LengthPrefixedDecoder {
|
||||
type Delimiter = [u8; 1];
|
||||
|
||||
const DELIMITER: Self::Delimiter = [0x00];
|
||||
|
||||
/// Decodes a UTF-8 string from the given data, updating the cursor position
|
||||
/// accordingly.
|
||||
fn decode_string(data: &[u8], cursor: &mut usize, delimiter: Self::Delimiter) -> GDResult<String> {
|
||||
// Find the maximum length of the string
|
||||
let length = *data
|
||||
.first()
|
||||
.ok_or(PacketBad.context("Length of string not found"))?;
|
||||
|
||||
// Find the position of the delimiter in the data. If the delimiter is not
|
||||
// found, the length is returned.
|
||||
let position = data
|
||||
// Create an iterator over the data.
|
||||
.iter()
|
||||
.skip(1)
|
||||
.take(length as usize)
|
||||
// Find the position of the delimiter
|
||||
.position(|&b| b == delimiter.as_ref()[0])
|
||||
// If the delimiter is not found, use the whole data slice.
|
||||
.unwrap_or(length as usize);
|
||||
|
||||
// Convert the data until the found position into a UTF-8 string.
|
||||
let result = std::str::from_utf8(
|
||||
// Take a slice of data until the position.
|
||||
&data[1 .. position + 1]
|
||||
)
|
||||
// If the data cannot be converted into a UTF-8 string, return an error
|
||||
.map_err(|e| PacketBad.context(e))?
|
||||
// Convert the resulting &str into a String
|
||||
.to_owned();
|
||||
|
||||
// Update the cursor position
|
||||
// The +1 is to skip t length
|
||||
*cursor += position + 1;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// A decoder for UTF-16 encoded strings.
|
||||
///
|
||||
/// This decoder uses a pair of null bytes (`0x00, 0x00`) as the default
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use crate::protocols::valve::GatheringSettings;
|
|||
use phf::{phf_map, Map};
|
||||
|
||||
macro_rules! game {
|
||||
($name: literal, $default_port: literal, $protocol: expr) => {
|
||||
($name: literal, $default_port: expr, $protocol: expr) => {
|
||||
game!(
|
||||
$name,
|
||||
$default_port,
|
||||
|
|
@ -18,7 +18,7 @@ macro_rules! game {
|
|||
)
|
||||
};
|
||||
|
||||
($name: literal, $default_port: literal, $protocol: expr, $extra_request_settings: expr) => {
|
||||
($name: literal, $default_port: expr, $protocol: expr, $extra_request_settings: expr) => {
|
||||
Game {
|
||||
name: $name,
|
||||
default_port: $default_port,
|
||||
|
|
@ -132,4 +132,5 @@ pub static GAMES: Map<&'static str, Game> = phf_map! {
|
|||
"unrealtournament2003" => game!("Unreal Tournament 2003", 7758, Protocol::Unreal2),
|
||||
"unrealtournament2004" => game!("Unreal Tournament 2004", 7778, Protocol::Unreal2),
|
||||
"zps" => game!("Zombie Panic: Source", 27015, Protocol::Valve(Engine::new(17_500))),
|
||||
"mindustry" => game!("Mindustry", crate::games::mindustry::DEFAULT_PORT, Protocol::PROPRIETARY(ProprietaryProtocol::Mindustry)),
|
||||
};
|
||||
|
|
|
|||
25
crates/lib/src/games/mindustry/mod.rs
Normal file
25
crates/lib/src/games/mindustry/mod.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
//! Mindustry game ping (v146)
|
||||
//!
|
||||
//! [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/ArcNetProvider.java#L225-L259)
|
||||
|
||||
use std::{net::IpAddr, net::SocketAddr};
|
||||
|
||||
use crate::{GDResult, TimeoutSettings};
|
||||
|
||||
use self::types::ServerData;
|
||||
|
||||
pub mod types;
|
||||
|
||||
pub mod protocol;
|
||||
|
||||
/// Default mindustry server port
|
||||
///
|
||||
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/Vars.java#L141-L142)
|
||||
pub const DEFAULT_PORT: u16 = 6567;
|
||||
|
||||
/// Query a mindustry server.
|
||||
pub fn query(ip: &IpAddr, port: Option<u16>, timeout_settings: &Option<TimeoutSettings>) -> GDResult<ServerData> {
|
||||
let address = SocketAddr::new(*ip, port.unwrap_or(DEFAULT_PORT));
|
||||
|
||||
protocol::query_with_retries(&address, timeout_settings)
|
||||
}
|
||||
58
crates/lib/src/games/mindustry/protocol.rs
Normal file
58
crates/lib/src/games/mindustry/protocol.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
use std::net::SocketAddr;
|
||||
|
||||
use crate::{
|
||||
buffer::{self, Buffer},
|
||||
socket::{Socket, UdpSocket},
|
||||
utils,
|
||||
GDResult,
|
||||
TimeoutSettings,
|
||||
};
|
||||
|
||||
use super::types::ServerData;
|
||||
|
||||
/// Mindustry max datagram packet size.
|
||||
pub const MAX_BUFFER_SIZE: usize = 500;
|
||||
|
||||
/// Send a ping packet.
|
||||
///
|
||||
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/ArcNetProvider.java#L248)
|
||||
pub fn send_ping(socket: &mut UdpSocket) -> GDResult<()> { socket.send(&[-2i8 as u8, 1i8 as u8]) }
|
||||
|
||||
/// Parse server data.
|
||||
///
|
||||
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/NetworkIO.java#L122-L135)
|
||||
pub fn parse_server_data<B: byteorder::ByteOrder, D: buffer::StringDecoder>(
|
||||
buffer: &mut Buffer<B>,
|
||||
) -> GDResult<ServerData> {
|
||||
Ok(ServerData {
|
||||
host: buffer.read_string::<D>(None)?,
|
||||
map: buffer.read_string::<D>(None)?,
|
||||
players: buffer.read()?,
|
||||
wave: buffer.read()?,
|
||||
version: buffer.read()?,
|
||||
version_type: buffer.read_string::<D>(None)?,
|
||||
gamemode: buffer.read::<u8>()?.try_into()?,
|
||||
player_limit: buffer.read()?,
|
||||
description: buffer.read_string::<D>(None)?,
|
||||
mode_name: buffer.read_string::<D>(None).ok(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Query a Mindustry server (without retries).
|
||||
pub fn query(address: &SocketAddr, timeout_settings: &Option<TimeoutSettings>) -> GDResult<ServerData> {
|
||||
let mut socket = UdpSocket::new(address, timeout_settings)?;
|
||||
|
||||
send_ping(&mut socket)?;
|
||||
|
||||
let socket_data = socket.receive(Some(MAX_BUFFER_SIZE))?;
|
||||
let mut buffer = Buffer::new(&socket_data);
|
||||
|
||||
parse_server_data::<byteorder::BigEndian, buffer::Utf8LengthPrefixedDecoder>(&mut buffer)
|
||||
}
|
||||
|
||||
/// Query a Mindustry server.
|
||||
pub fn query_with_retries(address: &SocketAddr, timeout_settings: &Option<TimeoutSettings>) -> GDResult<ServerData> {
|
||||
let retries = TimeoutSettings::get_retries_or_default(timeout_settings);
|
||||
|
||||
utils::retry_on_timeout(retries, || query(address, timeout_settings))
|
||||
}
|
||||
108
crates/lib/src/games/mindustry/types.rs
Normal file
108
crates/lib/src/games/mindustry/types.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
use crate::{
|
||||
protocols::types::{CommonResponse, GenericResponse},
|
||||
GDErrorKind,
|
||||
};
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Mindustry sever data
|
||||
///
|
||||
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/NetworkIO.java#L122-L135)
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ServerData {
|
||||
pub host: String,
|
||||
pub map: String,
|
||||
pub players: i32,
|
||||
pub wave: i32,
|
||||
pub version: i32,
|
||||
pub version_type: String,
|
||||
pub gamemode: GameMode,
|
||||
pub player_limit: i32,
|
||||
pub description: String,
|
||||
pub mode_name: Option<String>,
|
||||
}
|
||||
|
||||
/// Mindustry game mode
|
||||
///
|
||||
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/game/Gamemode.java)
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum GameMode {
|
||||
Survival,
|
||||
Sandbox,
|
||||
Attack,
|
||||
PVP,
|
||||
Editor,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for GameMode {
|
||||
type Error = GDErrorKind;
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
use GameMode::*;
|
||||
Ok(match value {
|
||||
0 => Survival,
|
||||
1 => Sandbox,
|
||||
2 => Attack,
|
||||
3 => PVP,
|
||||
4 => Editor,
|
||||
_ => return Err(GDErrorKind::TypeParse),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl GameMode {
|
||||
fn as_str(&self) -> &'static str {
|
||||
use GameMode::*;
|
||||
match self {
|
||||
Survival => "survival",
|
||||
Sandbox => "sandbox",
|
||||
Attack => "attack",
|
||||
PVP => "pvp",
|
||||
Editor => "editor",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CommonResponse for ServerData {
|
||||
fn as_original(&self) -> GenericResponse { GenericResponse::Mindustry(self) }
|
||||
|
||||
fn players_online(&self) -> u32 { self.players.try_into().unwrap_or(0) }
|
||||
fn players_maximum(&self) -> u32 { self.player_limit.try_into().unwrap_or(0) }
|
||||
|
||||
fn game_mode(&self) -> Option<&str> { Some(self.gamemode.as_str()) }
|
||||
|
||||
fn map(&self) -> Option<&str> { Some(&self.map) }
|
||||
fn description(&self) -> Option<&str> { Some(&self.description) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::protocols::types::CommonResponse;
|
||||
|
||||
use super::ServerData;
|
||||
|
||||
#[test]
|
||||
fn common_impl() {
|
||||
let data = ServerData {
|
||||
host: String::from("host"),
|
||||
map: String::from("map"),
|
||||
players: 5,
|
||||
wave: 2,
|
||||
version: 142,
|
||||
version_type: String::from("steam"),
|
||||
gamemode: super::GameMode::PVP,
|
||||
player_limit: 20,
|
||||
description: String::from("description"),
|
||||
mode_name: Some(String::from("campaign")),
|
||||
};
|
||||
|
||||
let common: &dyn CommonResponse = &data;
|
||||
|
||||
assert_eq!(common.players_online(), 5);
|
||||
assert_eq!(common.players_maximum(), 20);
|
||||
assert_eq!(common.game_mode(), Some("pvp"));
|
||||
assert_eq!(common.map(), Some("map"));
|
||||
assert_eq!(common.description(), Some("description"));
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,8 @@ pub mod battalion1944;
|
|||
pub mod ffow;
|
||||
/// Just Cause 2: Multiplayer
|
||||
pub mod jc2m;
|
||||
/// Mindustry
|
||||
pub mod mindustry;
|
||||
/// Minecraft
|
||||
pub mod minecraft;
|
||||
/// Savage 2
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
use crate::games::types::Game;
|
||||
use crate::games::{ffow, jc2m, minecraft, savage2, theship};
|
||||
use crate::games::{ffow, jc2m, mindustry, minecraft, savage2, theship};
|
||||
use crate::protocols;
|
||||
use crate::protocols::gamespy::GameSpyVersion;
|
||||
use crate::protocols::quake::QuakeVersion;
|
||||
|
|
@ -84,6 +84,7 @@ pub fn query_with_timeout_and_extra_settings(
|
|||
}
|
||||
ProprietaryProtocol::FFOW => ffow::query_with_timeout(address, port, timeout_settings).map(Box::new)?,
|
||||
ProprietaryProtocol::JC2M => jc2m::query_with_timeout(address, port, timeout_settings).map(Box::new)?,
|
||||
ProprietaryProtocol::Mindustry => mindustry::query(address, port, &timeout_settings).map(Box::new)?,
|
||||
ProprietaryProtocol::Minecraft(version) => {
|
||||
match version {
|
||||
Some(minecraft::Server::Java) => {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ pub enum ProprietaryProtocol {
|
|||
FFOW,
|
||||
JC2M,
|
||||
Savage2,
|
||||
Mindustry,
|
||||
}
|
||||
|
||||
/// Enumeration of all valid protocol types
|
||||
|
|
@ -42,6 +43,8 @@ pub enum GenericResponse<'a> {
|
|||
Valve(&'a valve::Response),
|
||||
Unreal2(&'a unreal2::Response),
|
||||
#[cfg(feature = "games")]
|
||||
Mindustry(&'a crate::games::mindustry::types::ServerData),
|
||||
#[cfg(feature = "games")]
|
||||
Minecraft(minecraft::VersionedResponse<'a>),
|
||||
#[cfg(feature = "games")]
|
||||
TheShip(&'a crate::games::theship::Response),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue