├── resources └── BBCS_White_Large.ico ├── .gitignore ├── manifest.xml ├── Cargo.toml ├── versioninfo.rc ├── LICENSE ├── README.md └── src └── main.rs /resources/BBCS_White_Large.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BattleBit-Community-Servers/Battlebit-Community-Server-Launcher/HEAD/resources/BBCS_White_Large.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore compiled files 2 | target/ 3 | 4 | # Ignore Cargo.lock for libraries 5 | Cargo.lock 6 | 7 | # Ignore build artifacts 8 | *.rlib 9 | *.o 10 | 11 | # Ignore logs 12 | *.log 13 | 14 | # Ignore SteamCMD files 15 | steamcmd/ 16 | steamcmd.zip -------------------------------------------------------------------------------- /manifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | true 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "battlebit-server-launcher" 3 | version = "3.0.0" 4 | edition = "2021" 5 | build = "build.rs" 6 | 7 | [dependencies] 8 | eframe = "0.29.1" 9 | tokio = { version = "1", features = ["full"] } 10 | serde = { version = "1.0", features = ["derive"] } 11 | toml = "0.8.19" 12 | futures = "0.3" 13 | reqwest = { version = "0.12.8", features = ["stream"] } 14 | zip = "2.2.0" 15 | rfd = "0.15.0" 16 | image = "0.25.2" 17 | sysinfo = "0.32.0" 18 | 19 | [target.'cfg(windows)'.dependencies] 20 | winreg = "0.52.0" 21 | 22 | [build-dependencies] 23 | embed-resource = "2.5" 24 | winres = "0.1.11" 25 | -------------------------------------------------------------------------------- /versioninfo.rc: -------------------------------------------------------------------------------- 1 | 1 ICON "resources/BBCS_White_Large.ico" 2 | 3 | 1 VERSIONINFO 4 | FILEVERSION 3,0,0,0 5 | PRODUCTVERSION 3,0,0,0 6 | FILEOS 0x4 7 | FILETYPE 0x1 8 | { 9 | BLOCK "StringFileInfo" 10 | { 11 | BLOCK "040904E4" 12 | { 13 | VALUE "FileDescription", "BattleBit Server Launcher" 14 | VALUE "CompanyName", "Your Company Name" 15 | VALUE "FileVersion", "3.0.0.0" 16 | VALUE "ProductName", "BattleBit Server Launcher" 17 | VALUE "ProductVersion", "3.0.0.0" 18 | } 19 | } 20 | BLOCK "VarFileInfo" 21 | { 22 | VALUE "Translation", 0x409, 1252 23 | } 24 | } 25 | 26 | 1 24 "manifest.xml" 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2024 Jellisy 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unofficial BattleBit Community Server Launcher 2 | >[!IMPORTANT] 3 | >Your Dedicated Server IP Must be Whitelisted to Connect To Master Server. 4 | > 5 | >Currently they are not accept more people into the community hosting pool 6 | 7 | >[!IMPORTANT] 8 | >This is currently in Early Alpha 9 | 10 | This is the **Unofficial** BattleBit Community Server Launcher. It provides a graphical interface to easily set up, manage, and launch BattleBit game servers. 11 | 12 | ## Features 13 | - Download and configure SteamCMD. 14 | - Install and manage the BattleBit server. 15 | - Easy server launch with configuration management. 16 | - Console log output and monitoring. 17 | 18 | ## Repository 19 | [Unofficial BattleBit Community Server Launcher GitHub Repo](https://github.com/BattleBit-Community-Servers/Battlebit-Community-Server-Launcher) 20 | 21 | ## How to Install 22 | 23 | 1. Clone the repository: 24 | ```bash 25 | git clone https://github.com/BattleBit-Community-Servers/Battlebit-Community-Server-Launcher.git 26 | cd Battlebit-Community-Server-Launcher 27 | ``` 28 | 29 | 2. Run the application: 30 | ```bash 31 | cargo run 32 | ``` 33 | 34 | ## License 35 | 36 | This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](./LICENSE) file for details. 37 | 38 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Battlebit-Community-Server-Launcher. 3 | * 4 | * Battlebit-Community-Server-Launcher is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Battlebit-Community-Server-Launcher is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with Battlebit-Community-Server-Launcher. If not, see . 16 | */ 17 | use eframe::{egui, App, NativeOptions}; 18 | use serde::{Deserialize, Serialize}; 19 | use std::fs; 20 | use std::path::{Path, PathBuf}; 21 | use std::sync::Arc; 22 | use std::process::{Command, Stdio}; 23 | use tokio; 24 | use tokio::sync::Mutex; 25 | use tokio::sync::RwLock; 26 | use tokio::time::Instant; 27 | use toml; 28 | use rfd::FileDialog; 29 | use std::io::{BufRead, BufReader}; 30 | #[cfg(windows)] 31 | use winreg::enums::*; 32 | #[cfg(windows)] 33 | use winreg::RegKey; 34 | use std::error::Error; 35 | 36 | // Constants for SteamCMD URL and configuration 37 | const STEAMCMD_URL: &str = "https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip"; 38 | const STEAMCMD_ZIP: &str = "steamcmd.zip"; 39 | const STEAMCMD_DIR: &str = "./steamcmd"; 40 | const CONFIG_FILE: &str = "config.toml"; 41 | 42 | // Struct for storing Steam credentials 43 | #[derive(Serialize, Deserialize, Default, Clone)] 44 | struct Credentials { 45 | username: String, 46 | password: String, 47 | } 48 | 49 | // Struct for storing the installation location of the server 50 | #[derive(Serialize, Deserialize, Default, Clone)] 51 | struct InstallLocation { 52 | path: PathBuf, 53 | } 54 | 55 | // Struct for storing game configuration settings 56 | #[derive(Serialize, Deserialize, Default, Clone)] 57 | struct GameConfig { 58 | port: String, 59 | hz: String, 60 | anticheat: String, 61 | max_ping: String, 62 | voxel_mode: String, 63 | api_endpoint: String, 64 | } 65 | 66 | // Overall config struct that holds game configuration, credentials, and installation location 67 | #[derive(Serialize, Deserialize, Default, Clone)] 68 | struct Config { 69 | #[serde(rename = "Game Config")] 70 | game_config: GameConfig, 71 | credentials: Credentials, 72 | install_location: InstallLocation, 73 | } 74 | 75 | // The main function to start the GUI application 76 | #[tokio::main] 77 | async fn main() -> Result<(), Box> { 78 | // Check if the configuration file exists, otherwise create it with default values 79 | if !Path::new(CONFIG_FILE).exists() { 80 | save_config(&Config::default())?; 81 | } 82 | 83 | // Detect Windows dark mode setting (only available on Windows) 84 | #[cfg(windows)] 85 | let is_dark_mode = { 86 | let hklm = RegKey::predef(HKEY_CURRENT_USER); 87 | let theme_key = hklm.open_subkey(r"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize").ok(); 88 | theme_key 89 | .and_then(|key| key.get_value::("AppsUseLightTheme").ok()) 90 | .map(|value| value == 0) 91 | .unwrap_or(false) 92 | }; 93 | 94 | #[cfg(not(windows))] 95 | let is_dark_mode = false; 96 | 97 | // Set the visual style (dark or light) based on the detected theme 98 | let options = NativeOptions::default(); 99 | 100 | // Launch the eframe GUI application 101 | eframe::run_native( 102 | "BattleBit Server Launcher", 103 | options, 104 | Box::new(move |cc| { 105 | if is_dark_mode { 106 | cc.egui_ctx.set_visuals(egui::Visuals::dark()); 107 | } else { 108 | cc.egui_ctx.set_visuals(egui::Visuals::light()); 109 | } 110 | Ok(Box::new(MyApp::default())) 111 | }), 112 | ) 113 | .map_err(|e| -> Box { Box::new(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())) })?; 114 | 115 | Ok(()) 116 | } 117 | 118 | // Struct to hold the application's state 119 | struct MyApp { 120 | credentials: Credentials, 121 | status: Arc>, 122 | install_location: Arc>, 123 | active_tab: String, 124 | setup_complete: Arc>, 125 | battlebit_console_output: Arc>, 126 | steamcmd_console_output: Arc>, 127 | console_input: String, 128 | uptime_timer: Arc>>, 129 | } 130 | 131 | // Default implementation for MyApp, loading configuration or creating a default one 132 | impl Default for MyApp { 133 | fn default() -> Self { 134 | let config = load_config().unwrap_or_default(); 135 | 136 | Self { 137 | credentials: config.credentials, 138 | status: Arc::new(Mutex::new(String::from("Waiting for user input..."))), 139 | install_location: Arc::new(Mutex::new(config.install_location.path)), 140 | active_tab: String::from("Credentials"), 141 | setup_complete: Arc::new(Mutex::new(false)), 142 | battlebit_console_output: Arc::new(RwLock::new(String::new())), 143 | steamcmd_console_output: Arc::new(RwLock::new(String::new())), 144 | console_input: String::new(), 145 | uptime_timer: Arc::new(Mutex::new(None)), 146 | } 147 | } 148 | } 149 | 150 | // Implementation of the eframe App trait for MyApp to handle the GUI 151 | impl App for MyApp { 152 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 153 | // Side panel for navigation between different tabs 154 | egui::SidePanel::left("side_panel").show(ctx, |ui| { 155 | ui.heading("Navigation"); 156 | if ui 157 | .selectable_label(self.active_tab == "Credentials", "Credentials") 158 | .clicked() 159 | { 160 | self.active_tab = String::from("Credentials"); 161 | } 162 | if ui 163 | .selectable_label(self.active_tab == "Setup", "Setup") 164 | .clicked() 165 | { 166 | self.active_tab = String::from("Setup"); 167 | } 168 | if ui 169 | .selectable_label(self.active_tab == "Server", "Server") 170 | .clicked() 171 | { 172 | self.active_tab = String::from("Server"); 173 | } 174 | if ui 175 | .selectable_label(self.active_tab == "About", "About") 176 | .clicked() 177 | { 178 | self.active_tab = String::from("About"); 179 | } 180 | }); 181 | 182 | // Central panel to display content based on the active tab 183 | egui::CentralPanel::default().show(ctx, |ui| { 184 | ui.heading("BattleBit Server Launcher"); 185 | ui.separator(); 186 | 187 | match self.active_tab.as_str() { 188 | "Credentials" => self.render_credentials_tab(ui, ctx), 189 | "Setup" => self.render_install_tab(ui, ctx), 190 | "Server" => self.render_server_tab(ui, ctx), 191 | "About" => self.render_about_tab(ui), 192 | _ => {} 193 | } 194 | }); 195 | } 196 | } 197 | 198 | // Implementation of various render and helper methods for MyApp 199 | impl MyApp { 200 | // Render the credentials tab for Steam username and password input 201 | fn render_credentials_tab(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) { 202 | ui.label("Enter your Steam username:"); 203 | ui.text_edit_singleline(&mut self.credentials.username); 204 | ui.label("Enter your Steam password:"); 205 | ui.add(egui::TextEdit::singleline(&mut self.credentials.password).password(true)); 206 | 207 | if ui.button("Save Credentials").clicked() { 208 | let install_location = self.install_location.clone(); 209 | let config = Config { 210 | game_config: GameConfig::default(), 211 | credentials: self.credentials.clone(), 212 | install_location: InstallLocation { 213 | path: tokio::task::block_in_place(|| install_location.blocking_lock().clone()), 214 | }, 215 | }; 216 | self.save_config_async(config, ctx); 217 | } 218 | } 219 | 220 | // Render the setup tab for SteamCMD and game server installation 221 | fn render_install_tab(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) { 222 | if ui.button("Set Server Location").clicked() { 223 | if let Some(folder) = FileDialog::new().pick_folder() { 224 | let mut install_location = tokio::task::block_in_place(|| self.install_location.blocking_lock()); 225 | *install_location = folder.clone(); 226 | 227 | let config = Config { 228 | game_config: GameConfig::default(), 229 | credentials: self.credentials.clone(), 230 | install_location: InstallLocation { path: folder }, 231 | }; 232 | self.save_config_async(config, ctx); 233 | } 234 | } 235 | 236 | if ui.button("Download and Server Setup").clicked() { 237 | self.download_and_install_steamcmd(ctx); 238 | } 239 | 240 | if ui.button("Install Game Server").clicked() { 241 | self.install_game_server(ctx); 242 | } 243 | 244 | if let Ok(setup_complete) = self.setup_complete.try_lock() { 245 | if *setup_complete { 246 | ui.horizontal(|ui| { 247 | ui.label("Done!"); 248 | }); 249 | } 250 | } 251 | } 252 | 253 | // Render the server tab for launching, stopping, and monitoring the server 254 | fn render_server_tab(&mut self, ui: &mut egui::Ui, _ctx: &egui::Context) { 255 | ui.horizontal(|ui| { 256 | if ui.button("Launch Server").clicked() { 257 | let install_location = self.install_location.clone(); 258 | let config = load_config().unwrap_or_default(); 259 | let server_path = tokio::task::block_in_place(|| install_location.blocking_lock().join("battlebit.exe")); 260 | if server_path.exists() { 261 | let port_arg = format!("-Port={}", config.game_config.port); 262 | let hz_arg = format!("-Hz={}", config.game_config.hz); 263 | let anticheat_arg = format!("-AntiCheat={}", config.game_config.anticheat); 264 | let max_ping_arg = format!("-MaxPing={}", config.game_config.max_ping); 265 | let voxel_mode_arg = format!("-VoxelMode={}", config.game_config.voxel_mode); 266 | let api_endpoint_arg = format!("-ApiEndPoint={}", config.game_config.api_endpoint); 267 | let args: Vec<&str> = vec![ 268 | "-batchmode", "-nographics", 269 | &port_arg, 270 | &hz_arg, 271 | &anticheat_arg, 272 | &max_ping_arg, 273 | &voxel_mode_arg, 274 | &api_endpoint_arg, 275 | ]; 276 | let battlebit_console_output_ref = self.battlebit_console_output.clone(); 277 | let uptime_timer_ref = self.uptime_timer.clone(); 278 | let command = Command::new(&server_path) 279 | .args(&args) 280 | .stdout(Stdio::piped()) 281 | .stderr(Stdio::piped()) 282 | .spawn(); 283 | if let Ok(mut child) = command { 284 | let stdout = BufReader::new(child.stdout.take().unwrap()); 285 | let stderr = BufReader::new(child.stderr.take().unwrap()); 286 | let battlebit_console_output_stdout = battlebit_console_output_ref.clone(); 287 | let battlebit_console_output_stderr = battlebit_console_output_ref.clone(); 288 | 289 | *tokio::task::block_in_place(|| uptime_timer_ref.blocking_lock()) = Some(Instant::now()); 290 | 291 | tokio::spawn(async move { 292 | for line in stdout.lines() { 293 | let line = line.unwrap_or_default(); 294 | let mut console = battlebit_console_output_stdout.write().await; 295 | console.push_str(&format!("{}\n", line)); 296 | } 297 | }); 298 | 299 | tokio::spawn(async move { 300 | for line in stderr.lines() { 301 | let line = line.unwrap_or_default(); 302 | let mut console = battlebit_console_output_stderr.write().await; 303 | console.push_str(&format!("{}\n", line)); 304 | } 305 | }); 306 | } else { 307 | ui.label("Failed to launch server."); 308 | } 309 | } else { 310 | ui.label("Error: battlebit.exe not found in the installation directory."); 311 | } 312 | } 313 | 314 | if ui.button("Kill Server").clicked() { 315 | if let Err(e) = kill_battlebit_process() { 316 | ui.label(&format!("Failed to kill server: {}", e)); 317 | } else { 318 | ui.label("Server process killed successfully."); 319 | *tokio::task::block_in_place(|| self.uptime_timer.blocking_lock()) = None; 320 | } 321 | } 322 | }); 323 | 324 | ui.separator(); 325 | ui.label("Console Output:"); 326 | ui.horizontal(|ui| { 327 | if let Some(start_time) = *tokio::task::block_in_place(|| self.uptime_timer.blocking_lock()) { 328 | let elapsed = Instant::now().duration_since(start_time); 329 | ui.label(format!("Uptime: {:.0?}", elapsed)); 330 | } else { 331 | ui.label("Uptime: Server is not running."); 332 | } 333 | }); 334 | egui::ScrollArea::vertical().max_height(200.0).show(ui, |ui| { 335 | let battlebit_console_output = self.battlebit_console_output.clone(); 336 | let mut console_text = tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(battlebit_console_output.read()).clone()); 337 | ui.add(egui::TextEdit::multiline(&mut console_text).desired_rows(10).lock_focus(true).desired_width(f32::INFINITY)); 338 | }); 339 | 340 | ui.separator(); 341 | ui.label("Console Input:"); 342 | ui.text_edit_singleline(&mut self.console_input); 343 | if ui.button("Send Command").clicked() { 344 | let command = self.console_input.clone(); 345 | self.console_input.clear(); 346 | let mut console_output = tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(self.battlebit_console_output.write())); 347 | console_output.push_str(&format!("> {}\n", command)); 348 | } 349 | } 350 | 351 | // Render the about tab with some information about the app 352 | fn render_about_tab(&self, ui: &mut egui::Ui) { 353 | ui.label("BattleBit Server Launcher v1.0\nDeveloped by Jellisy"); 354 | ui.label("This tool is designed to make managing game servers simple. It streamlines downloading SteamCMD, setting up game servers, and launching them effortlessly—all in one easy-to-use solution."); 355 | } 356 | 357 | // Asynchronous function to save the configuration to a file 358 | fn save_config_async(&self, config: Config, ctx: &egui::Context) { 359 | let status_ref = Arc::clone(&self.status); 360 | let ctx_clone = ctx.clone(); 361 | tokio::spawn(async move { 362 | let mut status = status_ref.lock().await; 363 | if let Err(e) = save_config(&config) { 364 | *status = format!("Failed to save config: {}", e); 365 | } else { 366 | *status = String::from("Config saved successfully."); 367 | } 368 | ctx_clone.request_repaint(); 369 | }); 370 | } 371 | 372 | // Asynchronous function to download and install SteamCMD 373 | fn download_and_install_steamcmd(&self, ctx: &egui::Context) { 374 | let status_ref = Arc::clone(&self.status); 375 | let ctx_clone = ctx.clone(); 376 | let setup_complete_ref = Arc::clone(&self.setup_complete); 377 | let steamcmd_console_output_ref = Arc::clone(&self.steamcmd_console_output); 378 | tokio::spawn(async move { 379 | { 380 | let mut status = status_ref.lock().await; 381 | *status = String::from("Downloading SteamCMD..."); 382 | } 383 | 384 | match download_steamcmd().await { 385 | Ok(_) => { 386 | { 387 | let mut status = status_ref.lock().await; 388 | *status = String::from("SteamCMD downloaded. Extracting..."); 389 | } 390 | 391 | if let Err(e) = extract_steamcmd() { 392 | let mut status = status_ref.lock().await; 393 | *status = format!("Failed to extract SteamCMD: {}", e); 394 | } else { 395 | { 396 | let mut status = status_ref.lock().await; 397 | *status = String::from("SteamCMD extracted successfully. Running initial setup..."); 398 | } 399 | 400 | if let Err(e) = run_steamcmd_once_with_output(steamcmd_console_output_ref.clone()).await { 401 | let mut status = status_ref.lock().await; 402 | *status = format!("Failed to run SteamCMD: {}", e); 403 | } else { 404 | let mut status = status_ref.lock().await; 405 | *status = String::from("SteamCMD setup completed successfully."); 406 | let mut setup_complete = setup_complete_ref.lock().await; 407 | *setup_complete = true; 408 | } 409 | } 410 | } 411 | Err(e) => { 412 | let mut status = status_ref.lock().await; 413 | *status = format!("Failed to download SteamCMD: {}", e); 414 | } 415 | } 416 | 417 | ctx_clone.request_repaint(); 418 | }); 419 | } 420 | 421 | // Asynchronous function to install the game server using SteamCMD 422 | fn install_game_server(&self, ctx: &egui::Context) { 423 | let credentials = self.credentials.clone(); 424 | let status_ref = Arc::clone(&self.status); 425 | let install_location_ref = Arc::clone(&self.install_location); 426 | let steamcmd_console_output_ref = Arc::clone(&self.steamcmd_console_output); 427 | let ctx_clone = ctx.clone(); 428 | 429 | tokio::spawn(async move { 430 | let install_location = tokio::task::block_in_place(|| install_location_ref.blocking_lock().clone()); 431 | let mut status = status_ref.lock().await; 432 | *status = String::from("Installing game server..."); 433 | drop(status); 434 | 435 | let result = tokio::task::block_in_place(|| install_game_server_with_output(&credentials.username, &credentials.password, &install_location, steamcmd_console_output_ref.clone())); 436 | 437 | let mut status = status_ref.lock().await; 438 | match result { 439 | Ok(_) => { 440 | *status = String::from("Server installed successfully."); 441 | } 442 | Err(e) => { 443 | *status = format!("Failed to install server: {}", e); 444 | } 445 | } 446 | ctx_clone.request_repaint(); 447 | }); 448 | } 449 | } 450 | 451 | // Save the configuration to a TOML file 452 | fn save_config(config: &Config) -> Result<(), Box> { 453 | let toml_str = toml::to_string(config)?; 454 | fs::write(CONFIG_FILE, toml_str)?; 455 | Ok(()) 456 | } 457 | 458 | // Load the configuration from a TOML file 459 | fn load_config() -> Result> { 460 | if !Path::new(CONFIG_FILE).exists() { 461 | return Ok(Config::default()); 462 | } 463 | let toml_str = fs::read_to_string(CONFIG_FILE)?; 464 | let config: Config = toml::from_str(&toml_str)?; 465 | Ok(config) 466 | } 467 | 468 | // Download SteamCMD using an HTTP request 469 | async fn download_steamcmd() -> Result<(), Box> { 470 | let response = reqwest::get(STEAMCMD_URL).await?; 471 | let bytes = response.bytes().await?; 472 | fs::write(STEAMCMD_ZIP, &bytes)?; 473 | Ok(()) 474 | } 475 | 476 | // Extract the downloaded SteamCMD zip file 477 | fn extract_steamcmd() -> Result<(), Box> { 478 | let file = fs::File::open(STEAMCMD_ZIP)?; 479 | let mut archive = zip::ZipArchive::new(file)?; 480 | archive.extract(STEAMCMD_DIR)?; 481 | Ok(()) 482 | } 483 | 484 | // Run SteamCMD once to complete its initial setup and log the output 485 | async fn run_steamcmd_once_with_output(console_output: Arc>) -> Result<(), Box> { 486 | let steamcmd_executable = format!("{}/steamcmd.exe", STEAMCMD_DIR); 487 | 488 | let mut child = Command::new(steamcmd_executable) 489 | .arg("+login") 490 | .arg("anonymous") 491 | .arg("+quit") 492 | .stdout(Stdio::piped()) 493 | .stderr(Stdio::piped()) 494 | .spawn()?; 495 | 496 | let stdout = BufReader::new(child.stdout.take().unwrap()); 497 | let stderr = BufReader::new(child.stderr.take().unwrap()); 498 | 499 | let console_output_stdout = console_output.clone(); 500 | let console_output_stderr = console_output.clone(); 501 | 502 | tokio::spawn(async move { 503 | for line in stdout.lines() { 504 | let line = line.unwrap_or_default(); 505 | let mut console = console_output_stdout.write().await; 506 | console.push_str(&format!("{}\n", line)); 507 | } 508 | }); 509 | 510 | tokio::spawn(async move { 511 | for line in stderr.lines() { 512 | let line = line.unwrap_or_default(); 513 | let mut console = console_output_stderr.write().await; 514 | console.push_str(&format!("{}\n", line)); 515 | } 516 | }); 517 | 518 | let output = child.wait_with_output()?; 519 | 520 | if !output.status.success() { 521 | return Err(format!( 522 | "Failed to run SteamCMD: {}", 523 | String::from_utf8_lossy(&output.stderr) 524 | ) 525 | .into()); 526 | } 527 | 528 | Ok(()) 529 | } 530 | 531 | // Install the game server using SteamCMD and log the output 532 | fn install_game_server_with_output( 533 | username: &str, 534 | password: &str, 535 | install_location: &Path, 536 | console_output: Arc>, 537 | ) -> Result<(), Box> { 538 | let steamcmd_executable = format!("{}/steamcmd.exe", STEAMCMD_DIR); 539 | 540 | let mut child = Command::new(steamcmd_executable) 541 | .arg("+login") 542 | .arg(username) 543 | .arg(password) 544 | .arg("+force_install_dir") 545 | .arg(install_location.to_str().unwrap()) 546 | .arg("+app_update") 547 | .arg("671860") 548 | .arg("validate") 549 | .arg("+quit") 550 | .stdout(Stdio::piped()) 551 | .stderr(Stdio::piped()) 552 | .spawn()?; 553 | 554 | let stdout = BufReader::new(child.stdout.take().unwrap()); 555 | let stderr = BufReader::new(child.stderr.take().unwrap()); 556 | 557 | for line in stdout.lines() { 558 | let line = line.unwrap_or_default(); 559 | let mut console = tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(console_output.write())); 560 | console.push_str(&format!("{}\n", line)); 561 | } 562 | 563 | for line in stderr.lines() { 564 | let line = line.unwrap_or_default(); 565 | let mut console = tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(console_output.write())); 566 | console.push_str(&format!("{}\n", line)); 567 | } 568 | 569 | let output = child.wait_with_output()?; 570 | 571 | if !output.status.success() { 572 | return Err(format!( 573 | "Failed to download server files: {}", 574 | String::from_utf8_lossy(&output.stderr) 575 | ) 576 | .into()); 577 | } 578 | 579 | Ok(()) 580 | } 581 | 582 | // Kill the battlebit.exe process using taskkill (on Windows) 583 | fn kill_battlebit_process() -> Result<(), Box> { 584 | let output = Command::new("taskkill") 585 | .args(&["/F", "/IM", "battlebit.exe"]) 586 | .output()?; 587 | 588 | if !output.status.success() { 589 | return Err(format!( 590 | "Failed to kill battlebit.exe: {}", 591 | String::from_utf8_lossy(&output.stderr) 592 | ) 593 | .into()); 594 | } 595 | 596 | Ok(()) 597 | } 598 | --------------------------------------------------------------------------------