├── 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 |
--------------------------------------------------------------------------------