diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index 44816d9a6..f8453e9a9 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -91,6 +91,9 @@ pub struct Level { pub shut_down_chunk_system: AtomicBool, pub should_save: AtomicBool, pub should_unload: AtomicBool, + /// Whether periodic autosaving is enabled. Toggled by `/save-off` and `/save-on`; + /// a manual `/save-all` still saves while this is `false`. + pub save_enabled: AtomicBool, /// Number of ticks between autosave checks. If 0, autosave is disabled. pub autosave_ticks: u64, @@ -262,6 +265,7 @@ impl Level { shut_down_chunk_system: AtomicBool::new(false), should_save: AtomicBool::new(false), should_unload: AtomicBool::new(false), + save_enabled: AtomicBool::new(true), autosave_ticks: level_config.autosave_ticks, pending_entity_generations, level_channel: level_channel.clone(), diff --git a/pumpkin/src/command/commands/mod.rs b/pumpkin/src/command/commands/mod.rs index de9d85d7d..ad63f13fc 100644 --- a/pumpkin/src/command/commands/mod.rs +++ b/pumpkin/src/command/commands/mod.rs @@ -40,6 +40,9 @@ mod plugins; mod pumpkin; mod random; mod rotate; +mod saveall; +mod saveoff; +mod saveon; mod say; mod seed; mod setblock; @@ -156,6 +159,9 @@ pub async fn default_dispatcher( random::register(&mut dispatcher, registry); list::register(&mut dispatcher, registry); seed::register(&mut dispatcher, registry); + saveall::register(&mut dispatcher, registry); + saveoff::register(&mut dispatcher, registry); + saveon::register(&mut dispatcher, registry); setidletimeout::register(&mut dispatcher, registry); spreadplayers::register(&mut dispatcher, registry); stop::register(&mut dispatcher, registry); diff --git a/pumpkin/src/command/commands/saveall.rs b/pumpkin/src/command/commands/saveall.rs new file mode 100644 index 000000000..47dd0c974 --- /dev/null +++ b/pumpkin/src/command/commands/saveall.rs @@ -0,0 +1,93 @@ +use std::sync::atomic::Ordering::Relaxed; + +use pumpkin_data::translation; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; +use pumpkin_util::text::TextComponent; +use tracing::error; + +use crate::command::argument_builder::{ArgumentBuilder, command}; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; + +const DESCRIPTION: &str = "Saves the server to disk."; + +const PERMISSION: &str = "minecraft:command.save-all"; + +const SAVE_FAILED_ERROR_TYPE: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_SAVE_FAILED, + translation::bedrock::COMMANDS_SAVE_FAILED, +); + +struct SaveAllExecutor; + +impl CommandExecutor for SaveAllExecutor { + fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> { + Box::pin(async move { + context + .source + .send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_SAVE_SAVING, + translation::bedrock::COMMANDS_SAVE_START, + [], + ), + false, + ) + .await; + + let server = context.server(); + + if let Err(err) = server.player_data_storage.save_all_players(server).await { + error!("Failed to save player data: {err}"); + return Err(SAVE_FAILED_ERROR_TYPE.create_without_context()); + } + + if let Err(err) = server + .advancement_manager + .save_all_players(&server.get_all_players()) + .await + { + error!("Failed to save player advancements: {err}"); + return Err(SAVE_FAILED_ERROR_TYPE.create_without_context()); + } + + // Request a save from every world's chunk scheduler. This works even + // while autosaving is disabled via /save-off, matching Vanilla. + for world in server.worlds.load().iter() { + world.level.should_save.store(true, Relaxed); + world.level.level_channel.notify(); + } + + context + .source + .send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_SAVE_SUCCESS, + translation::bedrock::COMMANDS_SAVE_SUCCESS, + [], + ), + true, + ) + .await; + + Ok(1) + }) + } +} + +pub fn register(dispatcher: &mut CommandDispatcher, registry: &mut PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Four), + )); + + dispatcher.register( + command("save-all", DESCRIPTION) + .requires(PERMISSION) + .executes(SaveAllExecutor), + ); +} diff --git a/pumpkin/src/command/commands/saveoff.rs b/pumpkin/src/command/commands/saveoff.rs new file mode 100644 index 000000000..964f2ad3a --- /dev/null +++ b/pumpkin/src/command/commands/saveoff.rs @@ -0,0 +1,68 @@ +use std::sync::atomic::Ordering::Relaxed; + +use pumpkin_data::translation; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; +use pumpkin_util::text::TextComponent; + +use crate::command::argument_builder::{ArgumentBuilder, command}; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; + +const DESCRIPTION: &str = "Disables automatic server saves."; + +const PERMISSION: &str = "minecraft:command.save-off"; + +const ALREADY_OFF_ERROR_TYPE: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_SAVE_ALREADYOFF, + translation::bedrock::COMMANDS_SAVE_OFF_ALREADYOFF, +); + +struct SaveOffExecutor; + +impl CommandExecutor for SaveOffExecutor { + fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> { + Box::pin(async move { + let mut any_disabled = false; + for world in context.server().worlds.load().iter() { + if world.level.save_enabled.swap(false, Relaxed) { + any_disabled = true; + } + } + + if !any_disabled { + return Err(ALREADY_OFF_ERROR_TYPE.create_without_context()); + } + + context + .source + .send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_SAVE_DISABLED, + translation::bedrock::COMMANDS_SAVE_DISABLED, + [], + ), + true, + ) + .await; + + Ok(1) + }) + } +} + +pub fn register(dispatcher: &mut CommandDispatcher, registry: &mut PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Four), + )); + + dispatcher.register( + command("save-off", DESCRIPTION) + .requires(PERMISSION) + .executes(SaveOffExecutor), + ); +} diff --git a/pumpkin/src/command/commands/saveon.rs b/pumpkin/src/command/commands/saveon.rs new file mode 100644 index 000000000..32b79da9b --- /dev/null +++ b/pumpkin/src/command/commands/saveon.rs @@ -0,0 +1,68 @@ +use std::sync::atomic::Ordering::Relaxed; + +use pumpkin_data::translation; +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; +use pumpkin_util::text::TextComponent; + +use crate::command::argument_builder::{ArgumentBuilder, command}; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; + +const DESCRIPTION: &str = "Enables automatic server saves."; + +const PERMISSION: &str = "minecraft:command.save-on"; + +const ALREADY_ON_ERROR_TYPE: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_SAVE_ALREADYON, + translation::bedrock::COMMANDS_SAVE_ON_ALREADYON, +); + +struct SaveOnExecutor; + +impl CommandExecutor for SaveOnExecutor { + fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> { + Box::pin(async move { + let mut any_enabled = false; + for world in context.server().worlds.load().iter() { + if !world.level.save_enabled.swap(true, Relaxed) { + any_enabled = true; + } + } + + if !any_enabled { + return Err(ALREADY_ON_ERROR_TYPE.create_without_context()); + } + + context + .source + .send_feedback( + TextComponent::translate_cross( + translation::java::COMMANDS_SAVE_ENABLED, + translation::bedrock::COMMANDS_SAVE_ENABLED, + [], + ), + true, + ) + .await; + + Ok(1) + }) + } +} + +pub fn register(dispatcher: &mut CommandDispatcher, registry: &mut PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Four), + )); + + dispatcher.register( + command("save-on", DESCRIPTION) + .requires(PERMISSION) + .executes(SaveOnExecutor), + ); +} diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 859c5a950..6ceb35313 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -1132,7 +1132,7 @@ impl World { } } } - if self.level.autosave_ticks > 0 { + if self.level.autosave_ticks > 0 && self.level.save_enabled.load(Relaxed) { let autosave = self.level.autosave_ticks as i64; if autosave > 0 && level_time.world_age % autosave == 0 { self.level.should_save.store(true, Relaxed);