mirror of
https://git.ahines.net/joeyahines/Albatross.git
synced 2026-05-08 16:12:13 +00:00
Compare commits
No commits in common. "2283b6f851c4e61322540b09e4fe0562896fc74e" and "38630a4d5718011032085cab104d5c00f711724e" have entirely different histories.
2283b6f851
...
38630a4d57
42
.drone.yml
Normal file
42
.drone.yml
Normal file
@ -0,0 +1,42 @@
|
||||
---
|
||||
kind: pipeline
|
||||
name: compliance
|
||||
type: docker
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- pull_request
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
pull: always
|
||||
image: rust:1.55.0
|
||||
commands:
|
||||
- cargo build --verbose
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
name: release
|
||||
type: docker
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- master
|
||||
event:
|
||||
- push
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
pull: always
|
||||
image: rust:1.55.0
|
||||
commands:
|
||||
- cargo build --verbose --release
|
||||
- name: gitea-release
|
||||
pull: always
|
||||
image: jolheiser/drone-gitea-main:latest
|
||||
settings:
|
||||
token:
|
||||
from_secret: gitea_token
|
||||
base: https://git.canopymc.net
|
||||
files:
|
||||
- "target/release/albatross"
|
||||
@ -1,29 +0,0 @@
|
||||
name: Build and Test Albatross
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@v2
|
||||
with:
|
||||
repo-token: ${{ secrets.TASK_GITHUB_API_TOKEN }}
|
||||
- uses: actions/checkout@v2
|
||||
- name: Stable with rustfmt and clippy
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
components: rustfmt, clippy
|
||||
- name: Lint Code
|
||||
run: task check
|
||||
- name: Build
|
||||
run: task build
|
||||
- name: Run Unit Tests
|
||||
run: task test
|
||||
- name: Upload Built Binary
|
||||
uses: christopherHX/gitea-upload-artifact@v4
|
||||
with:
|
||||
name: albatross
|
||||
path: target/debug/albatross
|
||||
@ -1,23 +0,0 @@
|
||||
name: Build and Release Albatross
|
||||
on: [release]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@v2
|
||||
with:
|
||||
repo-token: ${{ secrets.TASK_GITHUB_API_TOKEN }}
|
||||
- uses: actions/checkout@v2
|
||||
- name: Stable with rustfmt and clippy
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
- name: Build Release
|
||||
run: task build:release
|
||||
- uses: https://gitea.com/actions/gitea-release-action@v1
|
||||
with:
|
||||
files: |-
|
||||
target/release/albatross
|
||||
663
Cargo.lock
generated
663
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "albatross"
|
||||
version = "0.7.1"
|
||||
version = "0.6.1"
|
||||
authors = ["Joey Hines <joey@ahines.net>"]
|
||||
edition = "2024"
|
||||
|
||||
@ -15,7 +15,7 @@ chrono = "0.4"
|
||||
regex = "1.3.9"
|
||||
flate2 = "1.0.14"
|
||||
tar = "0.4.28"
|
||||
reqwest = { version = "0.13.2", features = ["blocking", "json"] }
|
||||
reqwest = { version = "0.12.22", features = ["blocking", "json"] }
|
||||
discord-hooks-rs = { git = "https://github.com/joeyahines/discord-hooks-rs" }
|
||||
anvil-region = "0.8.1"
|
||||
ssh2 = "0.9.1"
|
||||
|
||||
45
README.md
45
README.md
@ -8,7 +8,7 @@ Backups can also be transferred to a remote server using SFTP.
|
||||
|
||||
## Help
|
||||
```
|
||||
albatross 0.7.0
|
||||
albatross 0.4.0
|
||||
Backup your Minecraft Server!
|
||||
|
||||
USAGE:
|
||||
@ -25,7 +25,10 @@ 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)
|
||||
restore Restore certain chunks from a backup```
|
||||
restore Restore certain chunks from a backup
|
||||
|
||||
Process finished with exit code 1
|
||||
|
||||
```
|
||||
|
||||
## Examples
|
||||
@ -46,44 +49,6 @@ Restoring a range of chunks (from -2,-2 to 2,2):
|
||||
`albatorss -c test.toml restore world backups/04-11-20_01.51.27_backup.tar.gz sp.tar.gz` (-2,-2) -u (2,2)
|
||||
|
||||
## Config
|
||||
|
||||
### For Minecraft Versions After 26.1
|
||||
```toml
|
||||
[backup]
|
||||
# Minecraft sever directory
|
||||
minecraft_dir = "/home/mc/server"
|
||||
# Optional Discord webhook
|
||||
discord_webhook = "https://discordapp.com/api/webhooks/"
|
||||
# Number of backups to keep
|
||||
backups_to_keep = 10
|
||||
|
||||
[backup.output_config]
|
||||
# Directory to place backups
|
||||
path = "./backups"
|
||||
|
||||
[world_26_plus_config]
|
||||
world_name = "world"
|
||||
|
||||
[[world_26_plus_config.dimensions]]
|
||||
# world name
|
||||
world_name = "minecraft/overworld"
|
||||
# world save radius (in blocks)
|
||||
save_radius = 1000
|
||||
|
||||
[[world_26_plus_config.dimensions]]
|
||||
# world name
|
||||
world_name = "minecraft/the_end"
|
||||
# world save radius (in blocks)
|
||||
save_radius = 1000
|
||||
|
||||
[[world_26_plus_config.dimensions]]
|
||||
# world name
|
||||
world_name = "minecraft/the_nether"
|
||||
# world save radius (in blocks)
|
||||
save_radius = 1000
|
||||
```
|
||||
|
||||
### For Minecraft Versions Before 26.1
|
||||
```toml
|
||||
# Local Backup Config
|
||||
[backup]
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
# yaml-language-server: $schema=https://taskfile.dev/schema.json
|
||||
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
version:
|
||||
desc: Print out Rust version info
|
||||
cmds:
|
||||
- cargo --version
|
||||
- rustc --version
|
||||
- cargo clippy --version
|
||||
build:
|
||||
desc: Debug build
|
||||
cmds:
|
||||
- cargo build --timings --tests --bins
|
||||
test:
|
||||
desc: Test code
|
||||
deps: [build]
|
||||
cmds:
|
||||
- cargo test --locked
|
||||
build:release:
|
||||
desc: Release build
|
||||
cmds:
|
||||
- cargo build --release --locked --timings
|
||||
fmt:
|
||||
desc: Format Rust code
|
||||
cmds:
|
||||
- cargo fmt
|
||||
check:
|
||||
desc: Lint code with Clippy
|
||||
cmds:
|
||||
- cargo check
|
||||
- cargo fmt --check
|
||||
- cargo clippy
|
||||
clean:
|
||||
desc: Purge Rust build cache
|
||||
cmds:
|
||||
- cargo clean
|
||||
152
src/backup.rs
152
src/backup.rs
@ -1,8 +1,6 @@
|
||||
use crate::config::{
|
||||
AlbatrossConfig, RemoteBackupConfig, World26PlusConfig, WorldConfig, WorldType,
|
||||
};
|
||||
use crate::config::{AlbatrossConfig, RemoteBackupConfig, WorldConfig, WorldType};
|
||||
use crate::discord::send_webhook;
|
||||
use crate::error::{AlbatrossError, Result};
|
||||
use crate::error::Result;
|
||||
use crate::region::Region;
|
||||
use crate::remote::RemoteBackupSite;
|
||||
use crate::remote::file::FileBackup;
|
||||
@ -45,35 +43,22 @@ pub fn backup_file(file_name: &str, world_path: &Path, backup_path: &Path) -> Re
|
||||
pub fn backup_dir(dir_name: &str, world_path: &Path, backup_path: &Path) -> Result<u64> {
|
||||
let src_dir = world_path.join(dir_name);
|
||||
|
||||
if !src_dir.exists() || !src_dir.is_dir() {
|
||||
if !src_dir.exists() {
|
||||
warn!("Directory '{dir_name}' does not exist in '{world_path:?}'");
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let backup_dir_path = backup_path.join(dir_name);
|
||||
create_dir(&backup_dir_path)?;
|
||||
let backup_dir = backup_path.join(dir_name);
|
||||
create_dir(&backup_dir)?;
|
||||
|
||||
let mut file_count = 0;
|
||||
for entry in src_dir.read_dir()? {
|
||||
let entry = entry?;
|
||||
let mut target = backup_dir_path.clone();
|
||||
let mut target = backup_dir.clone();
|
||||
target.push(entry.file_name());
|
||||
|
||||
if entry.path().is_dir() {
|
||||
let sub_dir = entry.file_name();
|
||||
let world_path = world_path.join(dir_name);
|
||||
let backup_path = backup_path.join(dir_name);
|
||||
|
||||
if !backup_path.exists() {
|
||||
create_dir(&backup_path)?;
|
||||
}
|
||||
|
||||
file_count += backup_dir(sub_dir.to_str().unwrap(), &world_path, &backup_path)?;
|
||||
} else {
|
||||
target.push(entry.file_name());
|
||||
|
||||
copy(entry.path(), target)?;
|
||||
file_count += 1;
|
||||
}
|
||||
copy(entry.path(), target)?;
|
||||
file_count += 1;
|
||||
}
|
||||
|
||||
Ok(file_count)
|
||||
@ -109,15 +94,14 @@ pub fn backup_region(
|
||||
let entry = entry?;
|
||||
let file_name = entry.file_name().to_str().unwrap().to_string();
|
||||
|
||||
if let Ok(region) = Region::try_from(file_name)
|
||||
&& region.x.abs() <= save_radius
|
||||
&& region.y.abs() <= save_radius
|
||||
{
|
||||
let mut target = backup_dir.clone();
|
||||
target.push(entry.file_name());
|
||||
if let Ok(region) = Region::try_from(file_name) {
|
||||
if region.x.abs() <= save_radius && region.y.abs() <= save_radius {
|
||||
let mut target = backup_dir.clone();
|
||||
target.push(entry.file_name());
|
||||
|
||||
copy(entry.path(), target)?;
|
||||
count += 1;
|
||||
copy(entry.path(), target)?;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -286,7 +270,7 @@ pub fn do_remote_backup(
|
||||
/// * `cfg` - config file
|
||||
pub fn do_backup(cfg: AlbatrossConfig, output: Option<PathBuf>) -> Result<()> {
|
||||
let server_base_dir = cfg.backup.minecraft_dir.clone();
|
||||
let timer = Instant::now();
|
||||
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!("{time_str}_backup.tar.gz");
|
||||
let mut output_archive = match output {
|
||||
@ -300,21 +284,15 @@ pub fn do_backup(cfg: AlbatrossConfig, output: Option<PathBuf>) -> Result<()> {
|
||||
|
||||
create_dir_all(tmp_dir.clone())?;
|
||||
|
||||
let timer = Instant::now();
|
||||
|
||||
send_webhook("**Albatross is swooping in to backup your worlds!**", &cfg);
|
||||
|
||||
let backup_res = if let Some(worlds) = &cfg.world_config {
|
||||
backup_worlds(&cfg, server_base_dir, worlds, &tmp_dir)
|
||||
} else if let Some(world_config) = &cfg.world_26_plus_config {
|
||||
backup_v26_plus_world_format(&cfg, world_config, &tmp_dir)
|
||||
} else {
|
||||
Err(AlbatrossError::MissingConfig)
|
||||
};
|
||||
|
||||
if let Err(err) = backup_res {
|
||||
send_webhook("Failed to backup worlds", &cfg);
|
||||
error!("Failed to backup worlds: {err}");
|
||||
return Err(err);
|
||||
}
|
||||
backup_worlds(&cfg, server_base_dir, worlds, &tmp_dir).map_err(|e| {
|
||||
send_webhook("Failed to copy worlds to backup location", &cfg);
|
||||
error!("Failed to copy worlds: {e}");
|
||||
e
|
||||
})?;
|
||||
|
||||
compress_backup(&tmp_dir, &output_archive).map_err(|e| {
|
||||
send_webhook("Failed to compress backup", &cfg);
|
||||
@ -324,7 +302,8 @@ pub fn do_backup(cfg: AlbatrossConfig, output: Option<PathBuf>) -> Result<()> {
|
||||
|
||||
remove_dir_all(&tmp_dir)?;
|
||||
|
||||
let mut local_backup = FileBackup::new(&cfg.backup.output_config, cfg.backup.backups_to_keep)?;
|
||||
let mut local_backup =
|
||||
FileBackup::new(&cfg.backup.output_config, cfg.backup.backups_to_keep).unwrap();
|
||||
|
||||
match local_backup.cleanup() {
|
||||
Ok(backups_removed) => {
|
||||
@ -363,81 +342,10 @@ pub fn do_backup(cfg: AlbatrossConfig, output: Option<PathBuf>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Backup a dimension
|
||||
///
|
||||
/// # Param
|
||||
/// * `world_path` - path to the world folder
|
||||
/// * `backup_path` - path to the backup folder
|
||||
/// * `world_config` - world config options
|
||||
pub fn backup_dimension(
|
||||
world_path: &Path,
|
||||
backup_path: &Path,
|
||||
world_config: &WorldConfig,
|
||||
) -> Result<u64> {
|
||||
let dimension_path = Path::new("dimensions").join(&world_config.world_name);
|
||||
let backup_path = backup_path.join(&dimension_path);
|
||||
let src_path = world_path.join(dimension_path);
|
||||
create_dir_all(backup_path.as_path())?;
|
||||
|
||||
backup_dir("data", &src_path, &backup_path)?;
|
||||
let region_count = backup_region("region", world_config.save_radius, &src_path, &backup_path)?;
|
||||
Ok(region_count)
|
||||
}
|
||||
|
||||
/// Backup v26.1 + version
|
||||
///
|
||||
/// # Param
|
||||
/// * `world_path` - path to the world folder
|
||||
/// * `backup_path` - path to the backup folder
|
||||
/// * `world_config` - world config options
|
||||
pub fn backup_v26_plus_world_format(
|
||||
cfg: &AlbatrossConfig,
|
||||
world_config: &World26PlusConfig,
|
||||
tmp_dir: &Path,
|
||||
) -> Result<()> {
|
||||
let world_path = cfg.backup.minecraft_dir.join(&world_config.world_name);
|
||||
let backup_path = tmp_dir.join(&world_config.world_name);
|
||||
|
||||
if !backup_path.exists() {
|
||||
create_dir(&backup_path)?;
|
||||
}
|
||||
|
||||
send_webhook(
|
||||
format!("Starting backup **{}**", world_config.world_name).as_str(),
|
||||
cfg,
|
||||
);
|
||||
|
||||
// Backup common files
|
||||
backup_dir("data", &world_path, &backup_path)?;
|
||||
backup_dir("datapacks", &world_path, &backup_path)?;
|
||||
backup_file("level.dat", &world_path, &backup_path)?;
|
||||
backup_file("level.dat_old", &world_path, &backup_path).ok();
|
||||
backup_file("session.lock", &world_path, &backup_path).ok();
|
||||
let player_count = backup_dir("players", &world_path, &backup_path)?;
|
||||
send_webhook(format!("Backed up {player_count} players").as_str(), cfg);
|
||||
info!("Backed up {player_count} players");
|
||||
|
||||
for world_config in &world_config.dimensions {
|
||||
send_webhook(
|
||||
format!("Starting backup of **{}**", world_config.world_name).as_str(),
|
||||
cfg,
|
||||
);
|
||||
info!(
|
||||
"Starting backup of dimension **{}**",
|
||||
world_config.world_name
|
||||
);
|
||||
let region_count = backup_dimension(&world_path, &backup_path, world_config)?;
|
||||
send_webhook(format!("{region_count} regions backed up.").as_str(), cfg);
|
||||
info!("{region_count} regions backed up.")
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn backup_worlds(
|
||||
cfg: &AlbatrossConfig,
|
||||
server_base_dir: PathBuf,
|
||||
worlds: &[WorldConfig],
|
||||
worlds: Vec<WorldConfig>,
|
||||
tmp_dir: &Path,
|
||||
) -> Result<()> {
|
||||
for world in worlds {
|
||||
@ -452,15 +360,15 @@ fn backup_worlds(
|
||||
let webhook_msg = match world_type {
|
||||
WorldType::Overworld => {
|
||||
let (region_count, player_count) =
|
||||
backup_overworld(&world_dir.clone(), tmp_dir, world)?;
|
||||
backup_overworld(&world_dir.clone(), tmp_dir, &world)?;
|
||||
format!("{region_count} regions and {player_count} player files backed up.")
|
||||
}
|
||||
WorldType::Nether => {
|
||||
let region_count = backup_nether(&world_dir, tmp_dir, world)?;
|
||||
let region_count = backup_nether(&world_dir, tmp_dir, &world)?;
|
||||
format!("{region_count} regions backed up.")
|
||||
}
|
||||
WorldType::End => {
|
||||
let region_count = backup_end(&world_dir, tmp_dir, world)?;
|
||||
let region_count = backup_end(&world_dir, tmp_dir, &world)?;
|
||||
format!("{region_count} regions backed up.")
|
||||
}
|
||||
};
|
||||
|
||||
@ -64,19 +64,11 @@ pub struct RemoteBackupConfig {
|
||||
pub file: Option<FileConfig>,
|
||||
}
|
||||
|
||||
/// Config for individual world configuration
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct World26PlusConfig {
|
||||
pub world_name: String,
|
||||
pub dimensions: Vec<WorldConfig>,
|
||||
}
|
||||
|
||||
/// Configs
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct AlbatrossConfig {
|
||||
pub backup: BackupConfig,
|
||||
pub world_config: Option<Vec<WorldConfig>>,
|
||||
pub world_26_plus_config: Option<World26PlusConfig>,
|
||||
pub remote: Option<RemoteBackupConfig>,
|
||||
}
|
||||
|
||||
|
||||
@ -11,7 +11,6 @@ pub enum AlbatrossError {
|
||||
ChronoParseError(chrono::ParseError),
|
||||
NoSSHAuth,
|
||||
FTPError(ftp::FtpError),
|
||||
MissingConfig,
|
||||
}
|
||||
|
||||
impl std::error::Error for AlbatrossError {}
|
||||
@ -28,7 +27,6 @@ impl std::fmt::Display for AlbatrossError {
|
||||
AlbatrossError::ChronoParseError(e) => write!(f, "Unable to parse time: {e}"),
|
||||
AlbatrossError::NoSSHAuth => write!(f, "No SSH auth methods provided in the config"),
|
||||
AlbatrossError::FTPError(e) => write!(f, "FTP error: {e}"),
|
||||
AlbatrossError::MissingConfig => write!(f, "Missing world config"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
108
src/main.rs
108
src/main.rs
@ -74,60 +74,64 @@ fn main() {
|
||||
let cfg = AlbatrossConfig::new(opt.config_path.into_os_string().to_str().unwrap())
|
||||
.expect("Config error");
|
||||
|
||||
match opt.sub_command {
|
||||
SubCommand::Backup { output } => {
|
||||
info!("Starting backup");
|
||||
match do_backup(cfg, output) {
|
||||
Ok(_) => info!("Backup complete!"),
|
||||
Err(e) => info!("Error doing backup: {e:?}"),
|
||||
};
|
||||
}
|
||||
SubCommand::Export {
|
||||
input_backup,
|
||||
output,
|
||||
} => {
|
||||
info!("Starting export");
|
||||
match convert_backup_to_sp(&cfg, &input_backup, &output) {
|
||||
Ok(_) => info!("Export complete!"),
|
||||
Err(e) => info!("Error exporting backup: {e:?}"),
|
||||
};
|
||||
}
|
||||
SubCommand::Restore {
|
||||
server_directory,
|
||||
world_name,
|
||||
backup_path,
|
||||
chunk,
|
||||
upper_bound,
|
||||
} => {
|
||||
info!("Starting restore");
|
||||
|
||||
let server_directory = match server_directory {
|
||||
Some(dir) => dir,
|
||||
None => cfg.backup.minecraft_dir,
|
||||
};
|
||||
|
||||
if let Some(upper_bound) = upper_bound {
|
||||
match restore_range_from_backup(
|
||||
world_name.as_str(),
|
||||
chunk,
|
||||
upper_bound,
|
||||
&backup_path,
|
||||
&server_directory,
|
||||
) {
|
||||
Ok(count) => info!("Restored {count} chunks!"),
|
||||
Err(e) => info!("Error restoring backup: {e:?}"),
|
||||
};
|
||||
} else {
|
||||
match restore_chunk_from_backup(
|
||||
world_name.as_str(),
|
||||
chunk,
|
||||
&backup_path,
|
||||
&server_directory,
|
||||
) {
|
||||
Ok(_) => info!("Restored chunk!"),
|
||||
Err(e) => info!("Error restoring backup: {e:?}"),
|
||||
if cfg.world_config.is_some() {
|
||||
match opt.sub_command {
|
||||
SubCommand::Backup { output } => {
|
||||
info!("Starting backup");
|
||||
match do_backup(cfg, output) {
|
||||
Ok(_) => info!("Backup complete!"),
|
||||
Err(e) => info!("Error doing backup: {e:?}"),
|
||||
};
|
||||
}
|
||||
SubCommand::Export {
|
||||
input_backup,
|
||||
output,
|
||||
} => {
|
||||
info!("Starting export");
|
||||
match convert_backup_to_sp(&cfg, &input_backup, &output) {
|
||||
Ok(_) => info!("Export complete!"),
|
||||
Err(e) => info!("Error exporting backup: {e:?}"),
|
||||
};
|
||||
}
|
||||
SubCommand::Restore {
|
||||
server_directory,
|
||||
world_name,
|
||||
backup_path,
|
||||
chunk,
|
||||
upper_bound,
|
||||
} => {
|
||||
info!("Starting restore");
|
||||
|
||||
let server_directory = match server_directory {
|
||||
Some(dir) => dir,
|
||||
None => cfg.backup.minecraft_dir,
|
||||
};
|
||||
|
||||
if let Some(upper_bound) = upper_bound {
|
||||
match restore_range_from_backup(
|
||||
world_name.as_str(),
|
||||
chunk,
|
||||
upper_bound,
|
||||
&backup_path,
|
||||
&server_directory,
|
||||
) {
|
||||
Ok(count) => info!("Restored {count} chunks!"),
|
||||
Err(e) => info!("Error restoring backup: {e:?}"),
|
||||
};
|
||||
} else {
|
||||
match restore_chunk_from_backup(
|
||||
world_name.as_str(),
|
||||
chunk,
|
||||
&backup_path,
|
||||
&server_directory,
|
||||
) {
|
||||
Ok(_) => info!("Restored chunk!"),
|
||||
Err(e) => info!("Error restoring backup: {e:?}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("No worlds specified in config file!")
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user