Cactus/consts/
mod.rs

1//! This module is where we store constants, like filepaths or the the version of the current
2//! Minecraft version that the server is implementing.
3// TODO: Maybe reimplement this with a real querying API, like a HashMap like object.
4
5/// Module where we store information relevant to the Minecraft server.
6pub mod minecraft {
7    pub const VERSION: &str = "1.21.4";
8    pub const PROTOCOL_VERSION: usize = 769;
9}
10
11/// Server logging messages.
12pub mod messages {
13    use colored::*;
14    use once_cell::sync::Lazy;
15
16    use super::minecraft::VERSION;
17
18    pub static SERVER_STARTING: Lazy<String> = Lazy::new(|| {
19        format!("Starting minecraft server version {}", VERSION)
20            .bold()
21            .to_string()
22    });
23
24    pub static SERVER_STARTED: Lazy<String> =
25        Lazy::new(|| "[ SERVER STARTED ]".bright_green().bold().to_string());
26
27    pub static SERVER_SHUTDOWN_SUCCESS: Lazy<String> =
28        Lazy::new(|| "[ SERVER SHUT DOWN ]".bright_red().bold().to_string());
29
30    pub static PLAY_PACKET_NOTIFIER: Lazy<String> =
31        Lazy::new(|| "[ SEND PLAY PACKET HERE! ]".bright_red().bold().to_string());
32
33    pub static SERVER_SHUTDOWN_ERROR: Lazy<String> = Lazy::new(|| {
34        "[ SERVER SHUT DOWN WITH ERROR ]"
35            .bright_red()
36            .bold()
37            .to_string()
38    });
39
40    pub static SERVER_SHUTDOWN_CTRL_C: Lazy<String> = Lazy::new(|| {
41        "[ SERVER SHUT DOWN WITH CTRL+C ]"
42            .bright_red()
43            .bold()
44            .to_string()
45    });
46
47    pub static GREET: Lazy<String> =
48        Lazy::new(|| "Hello, world from Cactus!".green().bold().to_string());
49}
50
51/// Module used to store file paths relative to the server binary.
52pub mod file_paths {
53    /// server.properties file, used to store server settings.
54    pub const PROPERTIES: &str = "server.properties";
55    pub const EULA: &str = "eula.txt";
56    pub const OPERATORS: &str = "ops.json";
57    pub const WHITELIST: &str = "whitelist.json";
58    pub const BANNED_IP: &str = "banned-ips.json";
59    pub const BANNED_PLAYERS: &str = "banned-players.json";
60    pub const USERCACHE: &str = "usercache.json";
61    pub const SESSION: &str = "session.lock";
62    pub const SERVER_ICON: &str = "server-icon.png";
63
64    /// SLP IP logger file.
65    pub const LOGGED_IPS: &str = "logged_ips.txt";
66    pub const LEVEL: &str = "world/level.dat";
67}
68
69pub mod cactus {
70    pub const DO_LOG_IPS_SLP: bool = true;
71}
72
73pub mod directory_paths {
74    pub const WORLDS_DIRECTORY: &str = "world/";
75    pub const THE_END: &str = "world/DIM1/";
76    pub const NETHER: &str = "world/DIM-1/";
77    pub const OVERWORLD: &str = "world/region/";
78    pub const LOGS: &str = "logs/";
79}
80
81pub mod file_contents {
82    use crate::time;
83
84    /// Returns the default content of the 'eula.txt' file.
85    pub fn eula() -> String {
86        let mut content = String::new();
87
88        content += "# By changing the setting below to 'true' you are indicating your agreement to our EULA (https://aka.ms/MinecraftEULA).\n";
89        content += &format!("# {}", time::get_formatted_time());
90        content += "\neula=false";
91        content
92    }
93
94    /// Returns the default content of the 'server.properties' file.
95    pub fn server_properties() -> String {
96        const SERVER_PROPERTIES_INNER: &str = r#"accepts-transfers=false
97allow-flight=false
98allow-nether=true
99broadcast-console-to-ops=true
100broadcast-rcon-to-ops=true
101bug-report-link=
102difficulty=normal
103enable-command-block=false
104enable-jmx-monitoring=false
105enable-query=false
106enable-rcon=false
107enable-status=true
108enforce-secure-profile=true
109enforce-whitelist=false
110entity-broadcast-range-percentage=100
111force-gamemode=false
112function-permission-level=2
113gamemode=survival
114generate-structures=true
115generator-settings={}
116hardcore=false
117hide-online-players=false
118initial-disabled-packs=
119initial-enabled-packs=vanilla
120level-name=world
121level-seed=
122level-type=minecraft\:normal
123log-ips=true
124max-chained-neighbor-updates=1000000
125max-players=20
126max-tick-time=60000
127max-world-size=29999984
128motd=A beautiful CactusMC server!
129network-compression-threshold=256
130online-mode=true
131op-permission-level=4
132player-idle-timeout=0
133prevent-proxy-connections=false
134pvp=true
135query.port=25565
136rate-limit=0
137rcon.password=
138rcon.port=25575
139region-file-compression=deflate
140require-resource-pack=false
141resource-pack=
142resource-pack-id=
143resource-pack-prompt=
144resource-pack-sha1=
145server-ip=
146server-port=25565
147simulation-distance=10
148spawn-animals=true
149spawn-monsters=true
150spawn-npcs=true
151spawn-protection=16
152sync-chunk-writes=true
153text-filtering-config=
154use-native-transport=true
155view-distance=10
156white-list=false"#;
157
158        format!(
159            "# Minecraft server properties\n# {}\n{}",
160            time::get_formatted_time(),
161            SERVER_PROPERTIES_INNER
162        )
163    }
164}
165
166/// Strings for packets
167pub mod protocol {
168    use base64::{engine::general_purpose, Engine};
169    use image::{GenericImageView, ImageFormat};
170    use log::error;
171    use serde_json::json;
172
173    use crate::{config::Settings, gracefully_exit};
174
175    use super::file_paths::SERVER_ICON;
176
177    /// Returns the Base64-encoded server icon.
178    /// The image must be a 64x64 PNG image as the file server-icon.png
179    fn get_favicon() -> Result<String, Box<dyn std::error::Error>> {
180        let file_data = std::fs::read(SERVER_ICON)?;
181
182        // Guess the image format
183        let format = image::guess_format(&file_data)?;
184        if format != ImageFormat::Png {
185            return Err("The server icon must be in PNG format".into());
186        }
187
188        // Load the image to verify dimensions
189        let img = image::load_from_memory_with_format(&file_data, format)?;
190        if img.dimensions() != (64, 64) {
191            return Err("The server icon must have dimensions of 64x64".into());
192        }
193
194        // Encode to Base64
195        let base64_icon = general_purpose::STANDARD.encode(file_data);
196
197        // Construct the Data URI
198        let favicon = format!("data:image/png;base64,{base64_icon}");
199        Ok(favicon)
200    }
201
202    /// Returns the Status Response JSON.
203    pub fn status_response_json() -> String {
204        let config = Settings::new();
205
206        let version_name = super::minecraft::VERSION;
207        let protocol = super::minecraft::PROTOCOL_VERSION;
208        let max_players = config.max_players;
209
210        // TODO: This does not mirror the server's current state.
211        let online_players = 0;
212
213        let description_text = config.motd;
214
215        // TODO: Implement logic such that, if no icon is provided, not include it in the JSON.
216        if let Err(err) = get_favicon() {
217            error!("Server icon not found: {err}. Shutting down the server...");
218            gracefully_exit(crate::ExitCode::Failure);
219        }
220        let favicon = get_favicon().unwrap();
221
222        let enforces_secure_chat = config.enforce_secure_profile;
223
224        let json_data = json!({
225            "version": {
226                "name": version_name,
227                "protocol": protocol
228            },
229            "players": {
230                "max": max_players,
231                "online": online_players,
232            },
233            "description": {
234                "text": description_text
235            },
236            "favicon": favicon,
237            "enforcesSecureChat": enforces_secure_chat
238        });
239
240        serde_json::to_string(&json_data).unwrap()
241    }
242}