extract server status publishing into its own file

This commit is contained in:
StripedMonkey 2024-09-09 22:00:54 -04:00
parent e7fc40632d
commit c4dc376bc9
6 changed files with 136 additions and 102 deletions

View File

@ -16,14 +16,14 @@ pub struct CConfigAddResourcePack<'a> {
impl<'a> CConfigAddResourcePack<'a> { impl<'a> CConfigAddResourcePack<'a> {
pub fn new( pub fn new(
uuid: UUID, uuid: uuid::Uuid,
url: &'a str, url: &'a str,
hash: &'a str, hash: &'a str,
forced: bool, forced: bool,
prompt_message: Option<TextComponent<'a>>, prompt_message: Option<TextComponent<'a>>,
) -> Self { ) -> Self {
Self { Self {
uuid, uuid: UUID(uuid),
url, url,
hash, hash,
forced, forced,

View File

@ -13,3 +13,9 @@ impl Serialize for UUID {
serializer.serialize_bytes(self.0.as_bytes()) serializer.serialize_bytes(self.0.as_bytes())
} }
} }
impl UUID {
pub fn new(data: &[u8]) -> Self {
Self(uuid::Uuid::new_v3(&uuid::Uuid::NAMESPACE_DNS, data))
}
}

View File

@ -92,7 +92,8 @@ pub fn unpack_textures(property: Property, config: &TextureConfig) -> Result<(),
for texture in textures.textures { for texture in textures.textures {
let url = let url =
Url::parse(&texture.1.url).map_err(|e| TextureError::InvalidURL(e.to_string()))?; Url::parse(&texture.1.url).map_err(|e| TextureError::InvalidURL(e.to_string()))?;
is_texture_url_valid(url, config)? } is_texture_url_valid(url, config)?
}
Ok(()) Ok(())
} }

View File

@ -7,7 +7,7 @@ use pumpkin_protocol::{
client::{ client::{
config::{CConfigAddResourcePack, CFinishConfig, CKnownPacks, CRegistryData}, config::{CConfigAddResourcePack, CFinishConfig, CKnownPacks, CRegistryData},
login::{CLoginSuccess, CSetCompression}, login::{CLoginSuccess, CSetCompression},
status::{CPingResponse, CStatusResponse}, status::CPingResponse,
}, },
server::{ server::{
config::{SAcknowledgeFinishConfig, SClientInformationConfig, SKnownPacks, SPluginMessage}, config::{SAcknowledgeFinishConfig, SClientInformationConfig, SKnownPacks, SPluginMessage},
@ -17,6 +17,7 @@ use pumpkin_protocol::{
}, },
ConnectionState, KnownPack, CURRENT_MC_PROTOCOL, ConnectionState, KnownPack, CURRENT_MC_PROTOCOL,
}; };
use uuid::Uuid;
use crate::{ use crate::{
client::authentication::{self, GameProfile}, client::authentication::{self, GameProfile},
@ -54,7 +55,7 @@ impl Client {
} }
pub fn handle_status_request(&self, server: &Arc<Server>, _status_request: SStatusRequest) { pub fn handle_status_request(&self, server: &Arc<Server>, _status_request: SStatusRequest) {
self.send_packet(&CStatusResponse::new(&server.status_response_json)); self.send_packet(&server.get_status());
} }
pub fn handle_ping_request(&self, _server: &Arc<Server>, ping_request: SStatusPingRequest) { pub fn handle_ping_request(&self, _server: &Arc<Server>, ping_request: SStatusPingRequest) {
@ -185,25 +186,26 @@ impl Client {
_login_acknowledged: SLoginAcknowledged, _login_acknowledged: SLoginAcknowledged,
) { ) {
self.connection_state.store(ConnectionState::Config); self.connection_state.store(ConnectionState::Config);
server.send_brand(self); self.send_packet(&server.get_branding());
let resource_config = &ADVANCED_CONFIG.resource_pack; let resource_config = &ADVANCED_CONFIG.resource_pack;
if resource_config.enabled { if resource_config.enabled {
let prompt_message = if resource_config.prompt_message.is_empty() { let resource_pack = CConfigAddResourcePack::new(
None Uuid::new_v3(
} else {
Some(TextComponent::text(&resource_config.prompt_message))
};
self.send_packet(&CConfigAddResourcePack::new(
pumpkin_protocol::uuid::UUID(uuid::Uuid::new_v3(
&uuid::Uuid::NAMESPACE_DNS, &uuid::Uuid::NAMESPACE_DNS,
resource_config.resource_pack_url.as_bytes(), resource_config.resource_pack_url.as_bytes(),
)), ),
&resource_config.resource_pack_url, &resource_config.resource_pack_url,
&resource_config.resource_pack_sha1, &resource_config.resource_pack_sha1,
resource_config.force, resource_config.force,
prompt_message, if !resource_config.prompt_message.is_empty() {
)); Some(TextComponent::text(&resource_config.prompt_message))
} else {
None
},
);
self.send_packet(&resource_pack);
} }
// known data packs // known data packs

View File

@ -0,0 +1,94 @@
use std::{io::Cursor, path::Path};
use base64::{engine::general_purpose, Engine as _};
use image::GenericImageView as _;
use pumpkin_config::{BasicConfiguration, BASIC_CONFIG};
use pumpkin_protocol::{
client::{config::CPluginMessage, status::CStatusResponse},
Players, Sample, StatusResponse, VarInt, Version, CURRENT_MC_PROTOCOL,
};
use super::CURRENT_MC_VERSION;
pub struct BikeShedServerListing {
_status_response: StatusResponse,
// We cache the json response here so we don't parse it every time someone makes a Status request.
// Keep in mind that we must parse this again, when the StatusResponse changes which usually happen when a player joins or leaves
status_response_json: String,
/// Cached Server brand buffer so we don't have to rebuild them every time a player joins
cached_server_brand: Vec<u8>,
}
impl BikeShedServerListing {
pub fn new() -> Self {
let status_response = Self::build_response(&BASIC_CONFIG);
let status_response_json = serde_json::to_string(&status_response)
.expect("Failed to parse Status response into JSON");
let cached_server_brand = Self::build_brand();
BikeShedServerListing {
_status_response: status_response,
status_response_json,
cached_server_brand,
}
}
pub fn get_branding(&self) -> CPluginMessage {
CPluginMessage::new("minecraft:brand", &self.cached_server_brand)
}
pub fn get_status(&self) -> CStatusResponse<'_> {
CStatusResponse::new(&self.status_response_json)
}
pub fn build_response(config: &BasicConfiguration) -> StatusResponse {
let icon_path = concat!(env!("CARGO_MANIFEST_DIR"), "/icon.png");
let icon = if Path::new(icon_path).exists() {
Some(Self::load_icon(icon_path))
} else {
None
};
StatusResponse {
version: Some(Version {
name: CURRENT_MC_VERSION.into(),
protocol: CURRENT_MC_PROTOCOL,
}),
players: Some(Players {
max: config.max_players,
online: 0,
sample: vec![Sample {
name: "".into(),
id: "".into(),
}],
}),
description: config.motd.clone(),
favicon: icon,
enforece_secure_chat: false,
}
}
fn load_icon(path: &str) -> String {
let icon = match image::open(path).map_err(|e| panic!("error loading icon: {}", e)) {
Ok(icon) => icon,
Err(_) => return "".into(),
};
let dimension = icon.dimensions();
assert!(dimension.0 == 64, "Icon width must be 64");
assert!(dimension.1 == 64, "Icon height must be 64");
let mut image = Vec::with_capacity(64 * 64 * 4);
icon.write_to(&mut Cursor::new(&mut image), image::ImageFormat::Png)
.unwrap();
let mut result = "data:image/png;base64,".to_owned();
general_purpose::STANDARD.encode_string(image, &mut result);
result
}
fn build_brand() -> Vec<u8> {
let brand = "Pumpkin";
let mut buf = vec![];
let _ = VarInt(brand.len() as i32).encode(&mut buf);
buf.extend_from_slice(brand.as_bytes());
buf
}
}

View File

@ -1,22 +1,17 @@
use base64::{engine::general_purpose, Engine};
use bikeshed_key_store::BikeShedKeyStore; use bikeshed_key_store::BikeShedKeyStore;
use image::GenericImageView; use bikeshed_server_listing::BikeShedServerListing;
use mio::Token; use mio::Token;
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
use pumpkin_config::{BasicConfiguration, BASIC_CONFIG}; use pumpkin_config::BASIC_CONFIG;
use pumpkin_core::GameMode; use pumpkin_core::GameMode;
use pumpkin_entity::EntityId; use pumpkin_entity::EntityId;
use pumpkin_plugin::PluginLoader; use pumpkin_plugin::PluginLoader;
use pumpkin_protocol::client::login::CEncryptionRequest; use pumpkin_protocol::client::login::CEncryptionRequest;
use pumpkin_protocol::{ use pumpkin_protocol::client::status::CStatusResponse;
client::config::CPluginMessage, ClientPacket, Players, Sample, StatusResponse, VarInt, Version, use pumpkin_protocol::{client::config::CPluginMessage, ClientPacket};
CURRENT_MC_PROTOCOL,
};
use pumpkin_world::dimension::Dimension; use pumpkin_world::dimension::Dimension;
use std::collections::HashMap; use std::collections::HashMap;
use std::{ use std::{
io::Cursor,
path::Path,
sync::{ sync::{
atomic::{AtomicI32, Ordering}, atomic::{AtomicI32, Ordering},
Arc, Arc,
@ -37,22 +32,16 @@ use crate::{
}; };
mod bikeshed_key_store; mod bikeshed_key_store;
mod bikeshed_server_listing;
pub const CURRENT_MC_VERSION: &str = "1.21.1"; pub const CURRENT_MC_VERSION: &str = "1.21.1";
pub struct Server { pub struct Server {
key_store: BikeShedKeyStore, key_store: BikeShedKeyStore,
server_listing: BikeShedServerListing,
pub plugin_loader: PluginLoader, pub plugin_loader: PluginLoader,
pub command_dispatcher: Arc<CommandDispatcher<'static>>, pub command_dispatcher: Arc<CommandDispatcher<'static>>,
pub worlds: Vec<Arc<World>>, pub worlds: Vec<Arc<World>>,
pub status_response: StatusResponse,
// We cache the json response here so we don't parse it every time someone makes a Status request.
// Keep in mind that we must parse this again, when the StatusResponse changes which usally happen when a player joins or leaves
pub status_response_json: String,
/// Cache the Server brand buffer so we don't have to rebuild them every time a player joins
pub cached_server_brand: Vec<u8>,
/// Cache the registry so we don't have to parse it every time a player joins /// Cache the registry so we don't have to parse it every time a player joins
pub cached_registry: Vec<Registry>, pub cached_registry: Vec<Registry>,
@ -68,13 +57,9 @@ pub struct Server {
impl Server { impl Server {
#[allow(clippy::new_without_default)] #[allow(clippy::new_without_default)]
pub fn new() -> Self { pub fn new() -> Self {
let status_response = Self::build_response(&BASIC_CONFIG);
let status_response_json = serde_json::to_string(&status_response)
.expect("Failed to parse Status response into JSON");
let cached_server_brand = Self::build_brand();
// TODO: only create when needed // TODO: only create when needed
let key_store = BikeShedKeyStore::new(); let key_store = BikeShedKeyStore::new();
let server_listing = BikeShedServerListing::new();
let auth_client = if BASIC_CONFIG.online_mode { let auth_client = if BASIC_CONFIG.online_mode {
Some( Some(
reqwest::Client::builder() reqwest::Client::builder()
@ -106,9 +91,7 @@ impl Server {
command_dispatcher: Arc::new(command_dispatcher), command_dispatcher: Arc::new(command_dispatcher),
auth_client, auth_client,
key_store, key_store,
status_response, server_listing,
status_response_json,
cached_server_brand,
} }
} }
@ -118,7 +101,7 @@ impl Server {
GameMode::Undefined => GameMode::Survival, GameMode::Undefined => GameMode::Survival,
game_mode => game_mode, game_mode => game_mode,
}; };
// Basicly the default world // Basically the default world
// TODO: select default from config // TODO: select default from config
let world = self.worlds[0].clone(); let world = self.worlds[0].clone();
@ -165,6 +148,14 @@ impl Server {
self.entity_id.fetch_add(1, Ordering::SeqCst) self.entity_id.fetch_add(1, Ordering::SeqCst)
} }
pub fn get_branding(&self) -> CPluginMessage<'_> {
self.server_listing.get_branding()
}
pub fn get_status(&self) -> CStatusResponse<'_> {
self.server_listing.get_status()
}
pub fn encryption_request<'a>( pub fn encryption_request<'a>(
&'a self, &'a self,
verification_token: &'a [u8; 4], verification_token: &'a [u8; 4],
@ -181,64 +172,4 @@ impl Server {
pub fn digest_secret(&self, secret: &[u8]) -> String { pub fn digest_secret(&self, secret: &[u8]) -> String {
self.key_store.get_digest(secret) self.key_store.get_digest(secret)
} }
pub fn build_brand() -> Vec<u8> {
let brand = "Pumpkin";
let mut buf = vec![];
let _ = VarInt(brand.len() as i32).encode(&mut buf);
buf.extend_from_slice(brand.as_bytes());
buf
}
pub fn send_brand(&self, client: &Client) {
// send server brand
client.send_packet(&CPluginMessage::new(
"minecraft:brand",
&self.cached_server_brand,
));
}
pub fn build_response(config: &BasicConfiguration) -> StatusResponse {
let icon_path = concat!(env!("CARGO_MANIFEST_DIR"), "/icon.png");
let icon = if Path::new(icon_path).exists() {
Some(Self::load_icon(icon_path))
} else {
None
};
StatusResponse {
version: Some(Version {
name: CURRENT_MC_VERSION.into(),
protocol: CURRENT_MC_PROTOCOL,
}),
players: Some(Players {
max: config.max_players,
online: 0,
sample: vec![Sample {
name: "".into(),
id: "".into(),
}],
}),
description: config.motd.clone(),
favicon: icon,
// TODO
enforece_secure_chat: false,
}
}
pub fn load_icon(path: &str) -> String {
let icon = match image::open(path).map_err(|e| panic!("error loading icon: {}", e)) {
Ok(icon) => icon,
Err(_) => return "".into(),
};
let dimension = icon.dimensions();
assert!(dimension.0 == 64, "Icon width must be 64");
assert!(dimension.1 == 64, "Icon height must be 64");
let mut image = Vec::with_capacity(64 * 64 * 4);
icon.write_to(&mut Cursor::new(&mut image), image::ImageFormat::Png)
.unwrap();
let mut result = "data:image/png;base64,".to_owned();
general_purpose::STANDARD.encode_string(image, &mut result);
result
}
} }