mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #2969 from kacpherek/fix/backup-before-world-upgrade
🤖 Agent: needs-proof check by Ludovic's AI agent (Claude), not by Ludovic himself. Passed on a union of 11 contributor PRs on main 430e5e4c9: fmt, clippy -D warnings, the horizon-client and horizon-items tests (all pass), and lan_e2e tab chest drops signs frames containers bubble-column with the host on its server thread and in-frame (digging and frames passed on their rerun, after #2745's checks had left water behind). A world with DataVersion 3955 shows the backup prompt (vanilla text and buttons). Create Backup and Load writes backups/<date>_W.zip and opens the world, now at 5023. Parity: 46 of 268 harnesses passed locally before the Mac mini got too busy, so CI's run-parity on this head decides.
This commit is contained in:
5 files changed
+127
-8
No files matched your search
@@ -13712,6 +13712,14 @@ impl ApplicationHandler for Game<'_> {
|
||||
event_loop.exit();
|
||||
return;
|
||||
}
|
||||
// The world a backup made before its upgrade opens (EditWorldScreen's callback).
|
||||
if let Some(folder) = self.title.as_mut().and_then(|t| t.take_open_request())
|
||||
&& let Err(error) = self.open_world(&folder, None, None, false, None)
|
||||
{
|
||||
self.error = Some(error);
|
||||
event_loop.exit();
|
||||
return;
|
||||
}
|
||||
self.tick_server_packs();
|
||||
self.poll_control(event_loop);
|
||||
if self.pending_save.is_some() {
|
||||
|
||||
@@ -597,6 +597,9 @@ impl Game<'_> {
|
||||
Mode::Delete { world } => {
|
||||
json!({ "mode": "delete-world", "folder": world.folder, "name": world.name })
|
||||
}
|
||||
Mode::BackupBeforeUpgrade { world } => {
|
||||
json!({ "mode": "backup-before-upgrade", "folder": world.folder, "name": world.name })
|
||||
}
|
||||
Mode::Error { message, .. } => {
|
||||
json!({ "mode": "world-operation-error", "message": message })
|
||||
}
|
||||
|
||||
@@ -185,6 +185,19 @@ pub fn upgrade_older_world(world: &Path) -> Result<()> {
|
||||
.map_err(|e| format!("upgrading {}: {e}", world.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
/// Whether opening the world runs file fixes, which vanilla asks for a backup
|
||||
/// first (`BackupStatus.FILE_FIXING_REQUIRED`). Saves of 1.8 and before have no
|
||||
/// DataVersion, read as -1.
|
||||
pub fn needs_file_fixes(world: &Path) -> bool {
|
||||
let Some(root) = read_dat(&world.join("level.dat")) else {
|
||||
return false;
|
||||
};
|
||||
let Some(data) = root.get("Data") else {
|
||||
return false;
|
||||
};
|
||||
let version = number(data.get("DataVersion")).unwrap_or(-1);
|
||||
i32::try_from(version).is_ok_and(horizon_world::datafix::files::requires_file_fixing)
|
||||
}
|
||||
/// Move a world saved by an older Horizon build (pre-26.3 layout with a
|
||||
/// DataVersion that keeps vanilla from upgrading it) into the 26.3 layout.
|
||||
pub fn migrate_legacy(world: &Path) -> Result<()> {
|
||||
|
||||
@@ -96,6 +96,10 @@ pub enum Mode {
|
||||
Backup {
|
||||
world: WorldEntry,
|
||||
},
|
||||
/// BackupConfirmScreen over a world whose file fixes are due.
|
||||
BackupBeforeUpgrade {
|
||||
world: WorldEntry,
|
||||
},
|
||||
Delete {
|
||||
world: WorldEntry,
|
||||
},
|
||||
@@ -153,6 +157,10 @@ pub struct Title {
|
||||
message_lines: Cell<usize>,
|
||||
backup: Option<mpsc::Receiver<Result<u64, String>>>,
|
||||
backup_shown: Cell<bool>,
|
||||
/// The backup a `BackupBeforeUpgrade` asked for opens its world when done.
|
||||
backup_then_open: bool,
|
||||
/// The folder to open next, once that backup has finished.
|
||||
open_request: Option<String>,
|
||||
/// EditWorldScreen's `SystemToast` (`WORLD_BACKUP`) and its slide.
|
||||
toast: Option<(crate::toasts::Slide, BackupToast)>,
|
||||
icons: RefCell<std::collections::HashMap<PathBuf, (std::time::SystemTime, Option<Vec<u8>>)>>,
|
||||
@@ -258,6 +266,8 @@ impl Title {
|
||||
message_lines: Cell::new(1),
|
||||
backup: None,
|
||||
backup_shown: Cell::new(false),
|
||||
backup_then_open: false,
|
||||
open_request: None,
|
||||
toast: None,
|
||||
icons: RefCell::new(std::collections::HashMap::new()),
|
||||
splash: choose_splash(
|
||||
@@ -523,6 +533,10 @@ impl Title {
|
||||
if let Mode::Backup { world } = &self.mode {
|
||||
let folder = world.folder.clone();
|
||||
self.return_to_list(Some(&folder));
|
||||
// EditWorldScreen opens the world after the backup, failed or not.
|
||||
if std::mem::take(&mut self.backup_then_open) {
|
||||
self.open_request = Some(folder.clone());
|
||||
}
|
||||
let toast = match result {
|
||||
Ok(bytes) => BackupToast::Created { folder, bytes },
|
||||
Err(error) => BackupToast::Failed(error),
|
||||
@@ -549,6 +563,21 @@ impl Title {
|
||||
}
|
||||
})
|
||||
}
|
||||
/// The world a finished backup made before its upgrade opens next.
|
||||
pub fn take_open_request(&mut self) -> Option<String> {
|
||||
self.open_request.take()
|
||||
}
|
||||
/// `WorldOpenFlows.openWorldCheckVersionCompatibility`: a world whose file
|
||||
/// fixes are due asks for a backup first; any other opens at once.
|
||||
fn play(&mut self, world: WorldEntry) -> Action {
|
||||
if crate::save::needs_file_fixes(&self.saves.join(&world.folder)) {
|
||||
self.mode = Mode::BackupBeforeUpgrade { world };
|
||||
return Action::None;
|
||||
}
|
||||
Action::Play {
|
||||
folder: world.folder,
|
||||
}
|
||||
}
|
||||
fn start_backup(&mut self, world: WorldEntry) {
|
||||
let (send, receive) = mpsc::channel();
|
||||
let saves = self.saves.clone();
|
||||
@@ -908,6 +937,39 @@ impl Title {
|
||||
true,
|
||||
39,
|
||||
)],
|
||||
Mode::BackupBeforeUpgrade { .. } => {
|
||||
// BackupConfirmScreen.init: the buttons sit under the text at fixed offsets.
|
||||
let y = 109.0 + self.message_lines.get() as f32 * 9.0;
|
||||
vec![
|
||||
b(
|
||||
(w / 2.0).floor() - 155.0,
|
||||
y,
|
||||
150.0,
|
||||
&lang::get_or(
|
||||
"selectWorld.backupJoinConfirmButton",
|
||||
"Create Backup and Load",
|
||||
),
|
||||
true,
|
||||
40,
|
||||
),
|
||||
b(
|
||||
(w / 2.0).floor() + 5.0,
|
||||
y,
|
||||
150.0,
|
||||
&lang::get_or("selectWorld.backupJoinSkipButton", "I Know What I'm Doing!"),
|
||||
true,
|
||||
41,
|
||||
),
|
||||
b(
|
||||
(w / 2.0).floor() - 75.0,
|
||||
y + 24.0,
|
||||
150.0,
|
||||
&lang::get_or("gui.cancel", "Cancel"),
|
||||
true,
|
||||
42,
|
||||
),
|
||||
]
|
||||
}
|
||||
Mode::Backup { .. } => Vec::new(),
|
||||
Mode::Multiplayer(screen) => screen
|
||||
.buttons(w, h)
|
||||
@@ -1192,6 +1254,24 @@ impl Title {
|
||||
centered(atlas, batch, line, top + 17.0 + i as f32 * 9.0, u32::MAX);
|
||||
}
|
||||
}
|
||||
Mode::BackupBeforeUpgrade { .. } => {
|
||||
crate::ui::menu_background(batch, atlas, 0.0, 0.0, w, h, false);
|
||||
let question = lang::get_or(
|
||||
"selectWorld.backupQuestion.file_fixing_required",
|
||||
"Create a backup before upgrading this world?",
|
||||
);
|
||||
let warning = lang::get_or(
|
||||
"selectWorld.backupWarning.file_fixing_required",
|
||||
"This world needs to be upgraded before you can play it. We strongly suggest creating a backup before continuing in case you experience world corruption.",
|
||||
);
|
||||
let lines = wrapped(atlas, &warning, w - 50.0);
|
||||
self.message_lines.set(lines.len());
|
||||
// BackupConfirmScreen.extractRenderState: the title at 50, the text from 70.
|
||||
centered(atlas, batch, &question, 50.0, u32::MAX);
|
||||
for (i, line) in lines.iter().enumerate() {
|
||||
centered(atlas, batch, line, 70.0 + i as f32 * 9.0, u32::MAX);
|
||||
}
|
||||
}
|
||||
Mode::Backup { .. } => {
|
||||
self.backup_shown.set(true);
|
||||
crate::ui::menu_background(batch, atlas, 0.0, 0.0, w, h, false);
|
||||
@@ -1383,13 +1463,25 @@ impl Title {
|
||||
(Mode::Main, Some(7)) => Action::Accessibility,
|
||||
(Mode::Main, Some(6)) => Action::Language,
|
||||
(Mode::Select { worlds, selected }, Some(10)) => {
|
||||
match selected.and_then(|i| worlds.get(i)) {
|
||||
Some(world) => Action::Play {
|
||||
folder: world.folder.clone(),
|
||||
},
|
||||
match selected.and_then(|i| worlds.get(i)).cloned() {
|
||||
Some(world) => self.play(world),
|
||||
None => Action::None,
|
||||
}
|
||||
}
|
||||
(Mode::BackupBeforeUpgrade { world }, Some(40)) => {
|
||||
let world = world.clone();
|
||||
self.backup_then_open = true;
|
||||
self.start_backup(world);
|
||||
Action::None
|
||||
}
|
||||
(Mode::BackupBeforeUpgrade { world }, Some(41)) => Action::Play {
|
||||
folder: world.folder.clone(),
|
||||
},
|
||||
(Mode::BackupBeforeUpgrade { world }, Some(42)) => {
|
||||
let folder = world.folder.clone();
|
||||
self.return_to_list(Some(&folder));
|
||||
Action::None
|
||||
}
|
||||
(Mode::Select { .. }, Some(11)) => {
|
||||
self.create_return_to_main = false;
|
||||
self.mode = Self::new_world();
|
||||
@@ -1453,13 +1545,13 @@ impl Title {
|
||||
.world_row_at(worlds, (x, y), (w, h))
|
||||
.and_then(|(row, rel)| {
|
||||
let (index, world) = self.visible_worlds(worlds).nth(row)?;
|
||||
Some((index, world.folder.clone(), rel))
|
||||
Some((index, world.clone(), rel))
|
||||
});
|
||||
if let Some((index, folder, (rx, ry))) = chosen {
|
||||
if let Some((index, world, (rx, ry))) = chosen {
|
||||
// WorldListEntry.mouseClicked: the icon joins at once.
|
||||
let on_icon = (0.0..32.0).contains(&rx) && (0.0..32.0).contains(&ry);
|
||||
if on_icon || *selected == Some(index) {
|
||||
return Action::Play { folder };
|
||||
return self.play(world);
|
||||
}
|
||||
if let Mode::Select { selected, .. } = &mut self.mode {
|
||||
*selected = Some(index);
|
||||
@@ -1788,7 +1880,9 @@ impl Title {
|
||||
}
|
||||
match &self.mode {
|
||||
Mode::Backup { .. } => {}
|
||||
Mode::Edit { world, .. } | Mode::Delete { world } => {
|
||||
Mode::Edit { world, .. }
|
||||
| Mode::Delete { world }
|
||||
| Mode::BackupBeforeUpgrade { world } => {
|
||||
let folder = world.folder.clone();
|
||||
self.return_to_list(Some(&folder));
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ impl Fixture {
|
||||
"Data",
|
||||
Tag::compound([
|
||||
("LevelName", Tag::String(name.into())),
|
||||
("DataVersion", Tag::Int(horizon_world::DATA_VERSION)),
|
||||
("GameType", Tag::Int(1)),
|
||||
("allowCommands", Tag::Byte(1)),
|
||||
(
|
||||
|
||||
Reference in new issue
Block a user