diff --git a/pumpkin/icon.png b/assets/default_icon.png similarity index 100% rename from pumpkin/icon.png rename to assets/default_icon.png diff --git a/docs/config/basic.md b/docs/config/basic.md index 7d646c33c..7eda8546c 100644 --- a/docs/config/basic.md +++ b/docs/config/basic.md @@ -100,6 +100,22 @@ The server's description displayed on the status screen. motd=true ``` +## Use favicon + +Whether to use a server favicon or not + +```toml +use_favicon=true +``` + +## Favicon path + +The path to the server's favicon + +```toml +favicon_path=./icon.png +``` + ## Default gamemode The default game mode for players diff --git a/pumpkin-config/src/lib.rs b/pumpkin-config/src/lib.rs index 498218640..ebe9c99cf 100644 --- a/pumpkin-config/src/lib.rs +++ b/pumpkin-config/src/lib.rs @@ -97,6 +97,12 @@ pub struct BasicConfiguration { /// Whether to remove IPs from logs or not #[serde_inline_default(true)] pub scrub_ips: bool, + /// Whether to use a server favicon + #[serde_inline_default(true)] + pub use_favicon: bool, + /// Path to server favicon + #[serde_inline_default("icon.png".to_string())] + pub favicon_path: String, } fn default_server_address() -> SocketAddr { @@ -119,6 +125,8 @@ impl Default for BasicConfiguration { motd: "A Blazing fast Pumpkin Server!".to_string(), default_gamemode: GameMode::Survival, scrub_ips: true, + use_favicon: true, + favicon_path: "icon.png".to_string(), } } } diff --git a/pumpkin/Cargo.toml b/pumpkin/Cargo.toml index 8e737ab63..89091407b 100644 --- a/pumpkin/Cargo.toml +++ b/pumpkin/Cargo.toml @@ -53,7 +53,7 @@ thiserror = "1.0" # icon loading base64 = "0.22.1" -png = "0.17.14" +png = "0.17.14" # logging simple_logger = { version = "5.0.0", features = ["threads"] } diff --git a/pumpkin/src/server/connection_cache.rs b/pumpkin/src/server/connection_cache.rs index 1a57dffed..b90a7d16b 100644 --- a/pumpkin/src/server/connection_cache.rs +++ b/pumpkin/src/server/connection_cache.rs @@ -1,4 +1,10 @@ -use std::{fs::File, path::Path}; +use core::error; +use std::{ + fs::File, + io::{Cursor, Read}, + path::Path, + sync::LazyLock, +}; use base64::{engine::general_purpose, Engine as _}; use pumpkin_config::{BasicConfiguration, BASIC_CONFIG}; @@ -9,6 +15,29 @@ use pumpkin_protocol::{ use super::CURRENT_MC_VERSION; +static DEFAULT_ICON: LazyLock<&[u8]> = + LazyLock::new(|| include_bytes!("../../../assets/default_icon.png")); + +fn load_icon_from_file>(path: P) -> Result> { + let mut icon_file = File::open(path)?; + let mut buf = Vec::new(); + icon_file.read_to_end(&mut buf)?; + load_icon_from_bytes(&buf) +} + +fn load_icon_from_bytes(png_data: &[u8]) -> Result> { + let icon = png::Decoder::new(Cursor::new(&png_data)); + let reader = icon.read_info()?; + let info = reader.info(); + assert!(info.width == 64, "Icon width must be 64"); + assert!(info.height == 64, "Icon height must be 64"); + + // Reader consumes the image. Once we verify dimensions, we want to encode the entire raw image + let mut result = "data:image/png;base64,".to_owned(); + general_purpose::STANDARD.encode_string(png_data, &mut result); + Ok(result) +} + pub struct CachedStatus { _status_response: StatusResponse, // We cache the json response here so we don't parse it every time someone makes a Status request. @@ -57,10 +86,21 @@ impl CachedStatus { } pub fn build_response(config: &BasicConfiguration) -> StatusResponse { - let icon_path = "/icon.png"; - let icon = if Path::new(icon_path).exists() { - Some(Self::load_icon(icon_path)) + let icon = if config.use_favicon { + let icon_path = &config.favicon_path; + log::info!("Loading server favicon from '{}'", icon_path); + match load_icon_from_file(icon_path).or_else(|err| { + log::warn!("Failed to load icon from '{}': {}", icon_path, err); + load_icon_from_bytes(DEFAULT_ICON.as_ref()) + }) { + Ok(result) => Some(result), + Err(err) => { + log::warn!("Failed to load default icon: {}", err); + None + } + } } else { + log::info!("Not using a server favicon"); None }; @@ -82,21 +122,4 @@ impl CachedStatus { enforce_secure_chat: false, } } - - fn load_icon>(path: P) -> String { - let icon = png::Decoder::new(File::open(path).expect("Failed to load icon")); - let mut reader = icon.read_info().unwrap(); - let info = reader.info(); - assert!(info.width == 64, "Icon width must be 64"); - assert!(info.height == 64, "Icon height must be 64"); - // Allocate the output buffer. - let mut buf = vec![0; reader.output_buffer_size()]; - // Read the next frame. An APNG might contain multiple frames. - let info = reader.next_frame(&mut buf).unwrap(); - // Grab the bytes of the image. - let bytes = &buf[..info.buffer_size()]; - let mut result = "data:image/png;base64,".to_owned(); - general_purpose::STANDARD.encode_string(bytes, &mut result); - result - } }