Added export subcommand
+ Takes a backup and exports it as a playable single player world + Changed file name format + Switched to using StructOpt for the argument parsing + Updated README.mdbackup_error_fix
parent
b54ac0efef
commit
b408ed4d2a
|
@ -35,7 +35,6 @@ name = "albatross"
|
|||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"config",
|
||||
"discord-hooks-rs",
|
||||
"flate2",
|
||||
|
@ -43,6 +42,7 @@ dependencies = [
|
|||
"regex",
|
||||
"reqwest",
|
||||
"serde 1.0.117",
|
||||
"structopt",
|
||||
"tar",
|
||||
]
|
||||
|
||||
|
@ -390,6 +390,15 @@ dependencies = [
|
|||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.13"
|
||||
|
@ -810,6 +819,30 @@ version = "0.2.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"version_check 0.9.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"version_check 0.9.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.24"
|
||||
|
@ -1089,6 +1122,30 @@ version = "0.8.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
||||
|
||||
[[package]]
|
||||
name = "structopt"
|
||||
version = "0.3.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "126d630294ec449fae0b16f964e35bf3c74f940da9dca17ee9b905f7b3112eb8"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"lazy_static 1.4.0",
|
||||
"structopt-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "structopt-derive"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65e51c492f9e23a220534971ff5afc14037289de430e3c83f9daf6a1b6ae91e8"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.46"
|
||||
|
@ -1256,6 +1313,12 @@ dependencies = [
|
|||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.7"
|
||||
|
|
|
@ -7,7 +7,7 @@ edition = "2018"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = "2.33.0"
|
||||
structopt = "0.3.20"
|
||||
serde = { version="1.0.116", features=["derive"] }
|
||||
config = "0.9"
|
||||
log = "0.4.8"
|
||||
|
|
24
README.md
24
README.md
|
@ -4,6 +4,30 @@ Back up what you care about in your Minecraft worlds.
|
|||
Albatross backs up player files and region files within a certain configurable radius. It can also send Discord
|
||||
webhooks. Backups are compressed and stored as `tar.gz` archives.
|
||||
|
||||
## Help
|
||||
```
|
||||
albatross 0.2.0
|
||||
Backup your Minecraft Server!
|
||||
|
||||
USAGE:
|
||||
albatross --config-path <config-path> <SUBCOMMAND>
|
||||
|
||||
FLAGS:
|
||||
-h, --help Prints help information
|
||||
-V, --version Prints version information
|
||||
|
||||
OPTIONS:
|
||||
-c, --config-path <config-path> Path to the Albatross config [env: ALBATROSS_CONFIG=]
|
||||
|
||||
SUBCOMMANDS:
|
||||
backup Backup a server
|
||||
export Export a backup as a single player world
|
||||
help Prints this message or the help of the given subcommand(s)
|
||||
|
||||
Process finished with exit code 1
|
||||
|
||||
```
|
||||
|
||||
## Config
|
||||
```toml
|
||||
[backup]
|
||||
|
|
193
src/backup.rs
193
src/backup.rs
|
@ -1,10 +1,18 @@
|
|||
use crate::config::WorldConfig;
|
||||
use crate::backup;
|
||||
use crate::config::{AlbatrossConfig, WorldConfig, WorldType};
|
||||
use crate::discord::send_webhook;
|
||||
use crate::region::Region;
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use flate2::read::GzDecoder;
|
||||
use flate2::write::GzEncoder;
|
||||
use flate2::Compression;
|
||||
use std::convert::TryFrom;
|
||||
use std::fs::{copy, create_dir, File};
|
||||
use std::fs::{
|
||||
copy, create_dir, create_dir_all, remove_dir_all, remove_file, rename, DirEntry, File,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
use tar::Archive;
|
||||
|
||||
/// Backup a file
|
||||
///
|
||||
|
@ -189,3 +197,184 @@ pub fn compress_backup(tmp_dir: &PathBuf, output_file: &PathBuf) -> Result<(), s
|
|||
tar_builder.append_dir_all(".", tmp_dir)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Takes an existing backup and converts it to a singleplayer world
|
||||
///
|
||||
/// # Param
|
||||
/// * config - Albatross config
|
||||
/// * backup - path of the backup to convert
|
||||
/// * output - output path
|
||||
pub fn convert_backup_to_sp(
|
||||
config: &AlbatrossConfig,
|
||||
backup: &PathBuf,
|
||||
output: &PathBuf,
|
||||
) -> Result<(), std::io::Error> {
|
||||
let backup_file = File::open(backup)?;
|
||||
let dec = GzDecoder::new(backup_file);
|
||||
let mut extract = Archive::new(dec);
|
||||
let extract_path = PathBuf::from("tmp");
|
||||
extract.unpack(&extract_path)?;
|
||||
|
||||
if let Some(worlds) = &config.world_config {
|
||||
for world in worlds {
|
||||
let world_type = match world.world_type.clone() {
|
||||
Some(world_type) => world_type,
|
||||
None => WorldType::OVERWORLD,
|
||||
};
|
||||
let src = PathBuf::from(&extract_path).join(&world.world_name);
|
||||
let dest = PathBuf::from(&extract_path);
|
||||
match world_type {
|
||||
WorldType::OVERWORLD => {
|
||||
rename(src.clone().join("poi"), dest.clone().join("poi"))?;
|
||||
rename(src.clone().join("region"), dest.clone().join("region"))?;
|
||||
}
|
||||
WorldType::NETHER => {
|
||||
rename(src, dest.clone().join("DIM-1"))?;
|
||||
}
|
||||
WorldType::END => {
|
||||
rename(src, dest.clone().join("DIM1"))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compress_backup(&extract_path, output)?;
|
||||
remove_dir_all(&extract_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the time of the backup from a file name
|
||||
///
|
||||
/// # Param
|
||||
/// * `archive_entry`: archive entry
|
||||
fn get_time_from_file_name(
|
||||
archive_entry: &DirEntry,
|
||||
) -> Result<Option<NaiveDateTime>, std::io::Error> {
|
||||
let file_name = archive_entry.file_name().to_str().unwrap().to_string();
|
||||
let name: Vec<&str> = file_name.split('_').collect();
|
||||
|
||||
Ok(chrono::NaiveDateTime::parse_from_str(name[0], "%d-%m-%y_%H.%M.%S").ok())
|
||||
}
|
||||
|
||||
/// Removes the old backups from the ouput directory
|
||||
///
|
||||
/// # Params
|
||||
/// * `output_dir` - output directory containing
|
||||
/// * `keep` - number of backups to keep
|
||||
fn remove_old_backups(output_dir: &PathBuf, keep: u64) -> Result<usize, std::io::Error> {
|
||||
let mut backups = vec![];
|
||||
let mut num_of_removed_backups: usize = 0;
|
||||
|
||||
for entry in output_dir.read_dir()? {
|
||||
let entry = entry?;
|
||||
|
||||
if let Some(ext) = entry.path().extension() {
|
||||
if ext == "gz" {
|
||||
backups.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if backups.len() > keep as usize {
|
||||
backups.sort_by(|a, b| {
|
||||
let a_time = get_time_from_file_name(a).unwrap().unwrap();
|
||||
let b_time = get_time_from_file_name(b).unwrap().unwrap();
|
||||
|
||||
b_time.cmp(&a_time)
|
||||
});
|
||||
|
||||
num_of_removed_backups = backups.len() - keep as usize;
|
||||
|
||||
for _i in 0..num_of_removed_backups {
|
||||
let oldest = backups.pop().unwrap();
|
||||
remove_file(oldest.path())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(num_of_removed_backups)
|
||||
}
|
||||
|
||||
/// Backup the configured worlds from a minecraft server
|
||||
///
|
||||
/// # Params
|
||||
/// * `cfg` - config file
|
||||
pub fn do_backup(cfg: AlbatrossConfig, output: Option<PathBuf>) -> Result<(), std::io::Error> {
|
||||
let server_base_dir = cfg.backup.minecraft_dir.clone();
|
||||
let worlds = cfg.world_config.clone().expect("No worlds configured");
|
||||
let time_str = Utc::now().format("%d-%m-%y_%H.%M.%S").to_string();
|
||||
let backup_name = format!("{}_backup.tar.gz", time_str);
|
||||
let mut output_archive = match output {
|
||||
Some(out_path) => out_path,
|
||||
None => cfg.backup.output_dir.clone(),
|
||||
};
|
||||
output_archive.push(backup_name);
|
||||
let mut tmp_dir = cfg.backup.output_dir.clone();
|
||||
tmp_dir.push("tmp");
|
||||
remove_dir_all(&tmp_dir).ok();
|
||||
|
||||
create_dir_all(tmp_dir.clone()).unwrap();
|
||||
|
||||
send_webhook("**Albatross is swooping in to backup your worlds!**", &cfg);
|
||||
let timer = Instant::now();
|
||||
for world in worlds {
|
||||
let mut world_dir = server_base_dir.clone();
|
||||
let world_name = world.world_name.clone();
|
||||
let world_type = match world.world_type.clone() {
|
||||
Some(world_type) => world_type,
|
||||
None => WorldType::OVERWORLD,
|
||||
};
|
||||
world_dir.push(world_name.clone());
|
||||
|
||||
if world_dir.exists() && world_dir.is_dir() {
|
||||
send_webhook(
|
||||
format!("Starting backup of **{}**", world_name).as_str(),
|
||||
&cfg,
|
||||
);
|
||||
let webhook_msg = match world_type {
|
||||
WorldType::OVERWORLD => {
|
||||
let (region_count, player_count) =
|
||||
backup_overworld(world_dir.clone(), tmp_dir.clone(), &world)?;
|
||||
format!(
|
||||
"{} regions and {} player files backed up.",
|
||||
region_count, player_count
|
||||
)
|
||||
}
|
||||
WorldType::NETHER => {
|
||||
let region_count = backup_nether(world_dir, tmp_dir.clone(), &world)?;
|
||||
format!("{} regions backed up.", region_count)
|
||||
}
|
||||
WorldType::END => {
|
||||
let region_count = backup_end(world_dir, tmp_dir.clone(), &world)?;
|
||||
format!("{} regions backed up.", region_count)
|
||||
}
|
||||
};
|
||||
|
||||
send_webhook(&webhook_msg, &cfg);
|
||||
} else {
|
||||
send_webhook(format!("Error: {} not found.", world_name).as_str(), &cfg);
|
||||
println!("World \"{}\" not found", world_name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
backup::compress_backup(&tmp_dir, &output_archive)?;
|
||||
|
||||
remove_dir_all(&tmp_dir)?;
|
||||
|
||||
let backups_removed = remove_old_backups(&cfg.backup.output_dir, cfg.backup.backups_to_keep)?;
|
||||
|
||||
if backups_removed > 0 {
|
||||
let msg = format!(
|
||||
"Albatross mistook **{}** of your old backups for some french fries and ate them!! SKRAWWWW",
|
||||
backups_removed
|
||||
);
|
||||
send_webhook(msg.as_str(), &cfg);
|
||||
}
|
||||
|
||||
let secs = timer.elapsed().as_secs();
|
||||
send_webhook(
|
||||
format!("**Full backup completed in {}s**! *SKREEEEEEEEEE*", secs).as_str(),
|
||||
&cfg,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
|
204
src/main.rs
204
src/main.rs
|
@ -1,178 +1,72 @@
|
|||
use chrono::{NaiveDateTime, Utc};
|
||||
use clap::{App, Arg};
|
||||
use std::fs::{create_dir_all, remove_dir_all, remove_file, DirEntry};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
use structopt::StructOpt;
|
||||
|
||||
mod backup;
|
||||
mod config;
|
||||
mod discord;
|
||||
mod region;
|
||||
|
||||
use crate::config::{AlbatrossConfig, WorldType};
|
||||
use backup::{backup_end, backup_nether, backup_overworld};
|
||||
use discord::send_webhook;
|
||||
use crate::backup::{convert_backup_to_sp, do_backup};
|
||||
use crate::config::AlbatrossConfig;
|
||||
|
||||
/// Get the time of the backup from a file name
|
||||
///
|
||||
/// # Param
|
||||
/// * `archive_entry`: archive entry
|
||||
fn get_time_from_file_name(
|
||||
archive_entry: &DirEntry,
|
||||
) -> Result<Option<NaiveDateTime>, std::io::Error> {
|
||||
let file_name = archive_entry.file_name().to_str().unwrap().to_string();
|
||||
let name: Vec<&str> = file_name.split('_').collect();
|
||||
|
||||
Ok(chrono::NaiveDateTime::parse_from_str(name[0], "%d-%m-%y-%H.%M.%S").ok())
|
||||
#[derive(Debug, StructOpt)]
|
||||
#[structopt(about = "Backup your Minecraft Server!")]
|
||||
struct Albatross {
|
||||
/// Path to the Albatross config
|
||||
#[structopt(short, long, env = "ALBATROSS_CONFIG", parse(from_os_str))]
|
||||
config_path: PathBuf,
|
||||
/// Subcommand
|
||||
#[structopt(subcommand)]
|
||||
sub_command: SubCommand,
|
||||
}
|
||||
|
||||
/// Removes the old backups from the ouput directory
|
||||
///
|
||||
/// # Params
|
||||
/// * `output_dir` - output directory containing
|
||||
/// * `keep` - number of backups to keep
|
||||
fn remove_old_backups(output_dir: &PathBuf, keep: u64) -> Result<usize, std::io::Error> {
|
||||
let mut backups = vec![];
|
||||
let mut num_of_removed_backups: usize = 0;
|
||||
#[derive(Debug, StructOpt)]
|
||||
enum SubCommand {
|
||||
/// Backup a server
|
||||
Backup {
|
||||
/// Output location override
|
||||
#[structopt(short = "o", long = "ouptut", parse(from_os_str))]
|
||||
output: Option<PathBuf>,
|
||||
},
|
||||
/// Export a backup as a single player world
|
||||
Export {
|
||||
/// Convert backup to singleplayer world
|
||||
#[structopt(parse(from_os_str))]
|
||||
input_backup: PathBuf,
|
||||
|
||||
for entry in output_dir.read_dir()? {
|
||||
let entry = entry?;
|
||||
|
||||
if let Some(ext) = entry.path().extension() {
|
||||
if ext == "gz" {
|
||||
backups.push(entry);
|
||||
}
|
||||
}
|
||||
/// Output location override
|
||||
#[structopt(parse(from_os_str))]
|
||||
output: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
if backups.len() > keep as usize {
|
||||
backups.sort_by(|a, b| {
|
||||
let a_time = get_time_from_file_name(a).unwrap().unwrap();
|
||||
let b_time = get_time_from_file_name(b).unwrap().unwrap();
|
||||
|
||||
b_time.cmp(&a_time)
|
||||
});
|
||||
|
||||
num_of_removed_backups = backups.len() - keep as usize;
|
||||
|
||||
for _i in 0..num_of_removed_backups {
|
||||
let oldest = backups.pop().unwrap();
|
||||
remove_file(oldest.path())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(num_of_removed_backups)
|
||||
}
|
||||
|
||||
/// Backup the configured worlds from a minecraft server
|
||||
///
|
||||
/// # Params
|
||||
/// * `cfg` - config file
|
||||
fn do_backup(cfg: AlbatrossConfig) -> Result<(), std::io::Error> {
|
||||
let server_base_dir = cfg.backup.minecraft_dir.clone();
|
||||
let worlds = cfg.world_config.clone().expect("No worlds configured");
|
||||
let time_str = Utc::now().format("%d-%m-%y-%H.%M.%S").to_string();
|
||||
let backup_name = format!("{}_backup.tar.gz", time_str);
|
||||
let mut output_archive = cfg.backup.output_dir.clone();
|
||||
output_archive.push(backup_name);
|
||||
let mut tmp_dir = cfg.backup.output_dir.clone();
|
||||
tmp_dir.push("tmp");
|
||||
remove_dir_all(&tmp_dir).ok();
|
||||
|
||||
create_dir_all(tmp_dir.clone()).unwrap();
|
||||
|
||||
send_webhook("**Albatross is swooping in to backup your worlds!**", &cfg);
|
||||
let timer = Instant::now();
|
||||
for world in worlds {
|
||||
let mut world_dir = server_base_dir.clone();
|
||||
let world_name = world.world_name.clone();
|
||||
let world_type = match world.world_type.clone() {
|
||||
Some(world_type) => world_type,
|
||||
None => WorldType::OVERWORLD,
|
||||
};
|
||||
world_dir.push(world_name.clone());
|
||||
|
||||
if world_dir.exists() && world_dir.is_dir() {
|
||||
send_webhook(
|
||||
format!("Starting backup of **{}**", world_name).as_str(),
|
||||
&cfg,
|
||||
);
|
||||
let webhook_msg = match world_type {
|
||||
WorldType::OVERWORLD => {
|
||||
let (region_count, player_count) =
|
||||
backup_overworld(world_dir.clone(), tmp_dir.clone(), &world)?;
|
||||
format!(
|
||||
"{} regions and {} player files backed up.",
|
||||
region_count, player_count
|
||||
)
|
||||
}
|
||||
WorldType::NETHER => {
|
||||
let region_count = backup_nether(world_dir, tmp_dir.clone(), &world)?;
|
||||
format!("{} regions backed up.", region_count)
|
||||
}
|
||||
WorldType::END => {
|
||||
let region_count = backup_end(world_dir, tmp_dir.clone(), &world)?;
|
||||
format!("{} regions backed up.", region_count)
|
||||
}
|
||||
};
|
||||
|
||||
send_webhook(&webhook_msg, &cfg);
|
||||
} else {
|
||||
send_webhook(format!("Error: {} not found.", world_name).as_str(), &cfg);
|
||||
println!("World \"{}\" not found", world_name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
backup::compress_backup(&tmp_dir, &output_archive)?;
|
||||
|
||||
remove_dir_all(&tmp_dir)?;
|
||||
|
||||
let backups_removed = remove_old_backups(&cfg.backup.output_dir, cfg.backup.backups_to_keep)?;
|
||||
|
||||
if backups_removed > 0 {
|
||||
let msg = format!(
|
||||
"Albatross mistook **{}** of your old backups for some french fries and ate them!! SKRAWWWW",
|
||||
backups_removed
|
||||
);
|
||||
send_webhook(msg.as_str(), &cfg);
|
||||
}
|
||||
|
||||
let secs = timer.elapsed().as_secs();
|
||||
send_webhook(
|
||||
format!("**Full backup completed in {}s**! *SKREEEEEEEEEE*", secs).as_str(),
|
||||
&cfg,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Albatross
|
||||
///
|
||||
/// Run backups of a Minecraft world
|
||||
fn main() {
|
||||
let mut app = App::new("Albatross").about("Backup your worlds").arg(
|
||||
Arg::with_name("config")
|
||||
.index(1)
|
||||
.short("c")
|
||||
.long("config")
|
||||
.value_name("CONFIG_PATH")
|
||||
.help("Config file path"),
|
||||
);
|
||||
// Get arg parser
|
||||
let matches = app.clone().get_matches();
|
||||
let opt = Albatross::from_args();
|
||||
|
||||
if let Some(cfg_path) = matches.value_of("config") {
|
||||
let cfg = AlbatrossConfig::new(cfg_path).expect("Config not found");
|
||||
let cfg = AlbatrossConfig::new(opt.config_path.into_os_string().to_str().unwrap())
|
||||
.expect("Config not found");
|
||||
|
||||
if cfg.world_config.is_some() {
|
||||
match opt.sub_command {
|
||||
SubCommand::Backup { output } => {
|
||||
println!("Starting backup");
|
||||
match do_backup(cfg) {
|
||||
Ok(_) => println!("Backup complete"),
|
||||
match do_backup(cfg, output) {
|
||||
Ok(_) => println!("Backup complete!"),
|
||||
Err(e) => println!("Error doing backup: {:?}", e),
|
||||
};
|
||||
}
|
||||
SubCommand::Export {
|
||||
input_backup,
|
||||
output,
|
||||
} => {
|
||||
println!("Starting export");
|
||||
match convert_backup_to_sp(&cfg, &input_backup, &output) {
|
||||
Ok(_) => println!("Export complete!"),
|
||||
Err(e) => println!("Error exporting backup: {:?}", e),
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("No worlds specified to backed up!")
|
||||
}
|
||||
} else {
|
||||
app.print_help().expect("Unable to print help");
|
||||
println!("No worlds specified in config file!")
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue