Minecraft bedrock support (#7)

* Added needed ground stuff

* Minecraft bedrock support!

* Documentation acknowledgements!

* Added utf8_le_undended test, some docs and modified master_querant

* Modified query function to comply with the others

Before: game query -> protocol query (get port or default port)
After: game query (get port or default port) -> protocol query

* Modified md files
This commit is contained in:
CosminPerRam 2022-12-05 18:47:35 +02:00 committed by GitHub
parent ae14e37e60
commit 91f8bbb9fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 319 additions and 107 deletions

View file

@ -0,0 +1,100 @@
/*
This file has code that has been documented by the NodeJS GameDig library (MIT) from
https://github.com/gamedig/node-gamedig/blob/master/protocols/minecraftbedrock.js
*/
use crate::{GDError, GDResult};
use crate::protocols::minecraft::{BedrockResponse, GameMode, Server};
use crate::protocols::types::TimeoutSettings;
use crate::socket::{Socket, UdpSocket};
use crate::utils::buffer::{get_string_utf8_le_unended, get_u16_be, get_u64_le, get_u8};
use crate::utils::error_by_expected_size;
pub struct Bedrock {
socket: UdpSocket
}
impl Bedrock {
fn new(address: &str, port: u16, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = UdpSocket::new(address, port)?;
socket.apply_timeout(timeout_settings)?;
Ok(Self {
socket
})
}
fn send_status_request(&mut self) -> GDResult<()> {
self.socket.send(&[
// Message ID, ID_UNCONNECTED_PING
0x01,
// Nonce / timestamp
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
// Magic
0x00, 0xff, 0xff, 0x00, 0xfe, 0xfe, 0xfe, 0xfe, 0xfd, 0xfd, 0xfd, 0xfd, 0x12, 0x34, 0x56, 0x78,
// Client GUID
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])?;
Ok(())
}
fn get_info(&mut self) -> GDResult<BedrockResponse> {
self.send_status_request()?;
let buf = self.socket.receive(None)?;
let mut pos = 0;
if get_u8(&buf, &mut pos)? != 0x1c {
return Err(GDError::PacketBad("Invalid message id.".to_string()));
}
// Checking for our nonce directly from a u64 (as the nonce is 8 bytes).
if get_u64_le(&buf, &mut pos)? != 9833440827789222417 {
return Err(GDError::PacketBad("Invalid nonce.".to_string()));
}
// These 8 bytes are identical to the serverId string we receive in decimal below
pos += 8;
// Verifying the magic value (as we need 16 bytes, cast to two u64 values)
if get_u64_le(&buf, &mut pos)? != 18374403896610127616 {
return Err(GDError::PacketBad("Invalid magic (part 1).".to_string()));
}
if get_u64_le(&buf, &mut pos)? != 8671175388723805693 {
return Err(GDError::PacketBad("Invalid magic (part 2).".to_string()));
}
let remaining_length = get_u16_be(&buf, &mut pos)? as usize;
error_by_expected_size(remaining_length, buf.len() - pos)?;
let binding = get_string_utf8_le_unended(&buf, &mut pos)?;
let status: Vec<&str> = binding.split(";").collect();
// We must have at least 6 values
if status.len() < 6 {
return Err(GDError::PacketBad("Not enough status parts.".to_string()));
}
Ok(BedrockResponse {
edition: status[0].to_string(),
name: status[1].to_string(),
version_name: status[3].to_string(),
version_protocol: status[2].to_string(),
max_players: status[5].parse().map_err(|_| GDError::TypeParse("couldn't parse.".to_string()))?,
online_players: status[4].parse().map_err(|_| GDError::TypeParse("couldn't parse.".to_string()))?,
id: status.get(6).and_then(|v| Some(v.to_string())),
map: status.get(7).and_then(|v| Some(v.to_string())),
game_mode: match status.get(8) {
None => None,
Some(v) => Some(GameMode::from_bedrock(v)?)
},
server_type: Server::Bedrock
})
}
pub fn query(address: &str, port: u16, timeout_settings: Option<TimeoutSettings>) -> GDResult<BedrockResponse> {
Bedrock::new(address, port, timeout_settings)?.get_info()
}
}

View file

@ -1,5 +1,6 @@
use crate::{GDError, GDResult};
use crate::protocols::minecraft::{LegacyGroup, Response, Server};
use crate::protocols::minecraft::{BedrockResponse, LegacyGroup, Response};
use crate::protocols::minecraft::protocol::bedrock::Bedrock;
use crate::protocols::minecraft::protocol::java::Java;
use crate::protocols::minecraft::protocol::legacy_v1_4::LegacyV1_4;
use crate::protocols::minecraft::protocol::legacy_v1_6::LegacyV1_6;
@ -10,36 +11,57 @@ mod java;
mod legacy_v1_4;
mod legacy_v1_6;
mod legacy_bv1_8;
mod bedrock;
/// Queries a Minecraft server with all the protocol variants one by one (Java -> Legacy (1.6 -> 1.4 -> Beta 1.8)).
/// Queries a Minecraft server with all the protocol variants one by one (Java -> Bedrock -> Legacy (1.6 -> 1.4 -> Beta 1.8)).
pub fn query(address: &str, port: u16, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response> {
if let Ok(response) = query_specific(Server::Java, address, port, timeout_settings.clone()) {
if let Ok(response) = query_java(address, port, timeout_settings.clone()) {
return Ok(response);
}
if let Ok(response) = query_specific(Server::Legacy(LegacyGroup::V1_6), address, port, timeout_settings.clone()) {
return Ok(response);
if let Ok(response) = query_bedrock(address, port, timeout_settings.clone()) {
return Ok(Response::from_bedrock_response(response));
}
if let Ok(response) = query_specific(Server::Legacy(LegacyGroup::V1_4), address, port, timeout_settings.clone()) {
return Ok(response);
}
if let Ok(response) = query_specific(Server::Legacy(LegacyGroup::VB1_8), address, port, timeout_settings.clone()) {
if let Ok(response) = query_legacy(address, port, timeout_settings) {
return Ok(response);
}
Err(GDError::AutoQuery)
}
/// Queries a specific Minecraft Server type.
pub fn query_specific(mc_type: Server, address: &str, port: u16, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response> {
match mc_type {
Server::Java => Java::query(address, port, timeout_settings),
Server::Legacy(category) => match category {
LegacyGroup::V1_6 => LegacyV1_6::query(address, port, timeout_settings),
LegacyGroup::V1_4 => LegacyV1_4::query(address, port, timeout_settings),
LegacyGroup::VB1_8 => LegacyBV1_8::query(address, port, timeout_settings),
}
/// Query a Java Server.
pub fn query_java(address: &str, port: u16, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response> {
Java::query(address, port, timeout_settings)
}
/// Query a (Java) Legacy Server (1.6 -> 1.4 -> Beta 1.8).
pub fn query_legacy(address: &str, port: u16, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response> {
if let Ok(response) = query_legacy_specific(LegacyGroup::V1_6, address, port, timeout_settings.clone()) {
return Ok(response);
}
if let Ok(response) = query_legacy_specific(LegacyGroup::V1_4, address, port, timeout_settings.clone()) {
return Ok(response);
}
if let Ok(response) = query_legacy_specific(LegacyGroup::VB1_8, address, port, timeout_settings) {
return Ok(response);
}
Err(GDError::AutoQuery)
}
/// Query a specific (Java) Legacy Server.
pub fn query_legacy_specific(group: LegacyGroup, address: &str, port: u16, timeout_settings: Option<TimeoutSettings>) -> GDResult<Response> {
match group {
LegacyGroup::V1_6 => LegacyV1_6::query(address, port, timeout_settings),
LegacyGroup::V1_4 => LegacyV1_4::query(address, port, timeout_settings),
LegacyGroup::VB1_8 => LegacyBV1_8::query(address, port, timeout_settings)
}
}
/// Query a Bedrock Server.
pub fn query_bedrock(address: &str, port: u16, timeout_settings: Option<TimeoutSettings>) -> GDResult<BedrockResponse> {
Bedrock::query(address, port, timeout_settings)
}

View file

@ -1,31 +1,8 @@
/*
This file contains lightly modified versions of the original code. (using only the varint parts)
Code reference: https://github.com/thisjaiden/golden_apple/blob/master/src/lib.rs
MIT License
Copyright (c) 2021-2022 Jaiden Bernard
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Although its a lightly modified version, this file contains code
by Jaiden Bernard (2021-2022 - MIT) from
https://github.com/thisjaiden/golden_apple/blob/master/src/lib.rs
*/
use crate::{GDError, GDResult};
@ -37,7 +14,9 @@ pub enum Server {
/// Java Edition.
Java,
/// Legacy Java.
Legacy(LegacyGroup)
Legacy(LegacyGroup),
/// Bedrock Edition.
Bedrock
}
/// Legacy Java (Versions) Groups.
@ -83,6 +62,67 @@ pub struct Response {
pub server_type: Server
}
/// A Bedrock Edition query response.
#[derive(Debug)]
pub struct BedrockResponse {
/// Server edition.
pub edition: String,
/// Server name.
pub name: String,
/// Version name, example: "1.19.40".
pub version_name: String,
/// Version protocol, example: 760 (for 1.19.2).
pub version_protocol: String,
/// Number of server capacity.
pub max_players: u32,
/// Number of online players.
pub online_players: u32,
/// Server id.
pub id: Option<String>,
/// The map.
pub map: Option<String>,
/// Game mode.
pub game_mode: Option<GameMode>,
/// Tell's the server type.
pub server_type: Server
}
impl Response {
pub fn from_bedrock_response(response: BedrockResponse) -> Self {
Self {
version_name: response.version_name,
version_protocol: 0,
max_players: response.max_players,
online_players: response.online_players,
sample_players: None,
description: response.name,
favicon: None,
previews_chat: None,
enforces_secure_chat: None,
server_type: Server::Bedrock
}
}
}
/// A server's game mode (used only by Bedrock servers).
#[derive(Debug)]
pub enum GameMode {
Survival, Creative, Hardcore, Spectator, Adventure
}
impl GameMode {
pub fn from_bedrock(value: &&str) -> GDResult<Self> {
match *value {
"Survival" => Ok(GameMode::Survival),
"Creative" => Ok(GameMode::Creative),
"Hardcore" => Ok(GameMode::Hardcore),
"Spectator" => Ok(GameMode::Spectator),
"Adventure" => Ok(GameMode::Adventure),
_ => Err(GDError::UnknownEnumCast)
}
}
}
pub fn get_varint(buf: &[u8], pos: &mut usize) -> GDResult<i32> {
let mut result = 0;