diff --git a/abstutil/src/io.rs b/abstutil/src/io.rs index 8d0640294d..36fa131fcd 100644 --- a/abstutil/src/io.rs +++ b/abstutil/src/io.rs @@ -345,3 +345,7 @@ pub fn basename(path: &str) -> String { .into_string() .unwrap() } + +pub fn file_exists(path: String) -> bool { + Path::new(&path).exists() +} diff --git a/abstutil/src/lib.rs b/abstutil/src/lib.rs index 6580522a6d..02994e844a 100644 --- a/abstutil/src/lib.rs +++ b/abstutil/src/lib.rs @@ -15,10 +15,10 @@ pub use crate::collections::{ }; pub use crate::error::Error; pub use crate::io::{ - basename, deserialize_btreemap, deserialize_multimap, find_next_file, find_prev_file, - list_all_objects, load_all_objects, maybe_read_binary, maybe_read_json, read_binary, read_json, - serialize_btreemap, serialize_multimap, serialized_size_bytes, to_json, write_binary, - write_json, FileWithProgress, + basename, deserialize_btreemap, deserialize_multimap, file_exists, find_next_file, + find_prev_file, list_all_objects, load_all_objects, maybe_read_binary, maybe_read_json, + read_binary, read_json, serialize_btreemap, serialize_multimap, serialized_size_bytes, to_json, + write_binary, write_json, FileWithProgress, }; pub use crate::logs::Warn; pub use crate::random::{fork_rng, WeightedUsizeChoice}; diff --git a/game/assets/tools/save.svg b/game/assets/tools/save.svg new file mode 100644 index 0000000000..28c6f0eaaa --- /dev/null +++ b/game/assets/tools/save.svg @@ -0,0 +1,3 @@ + + + diff --git a/game/src/abtest/setup.rs b/game/src/abtest/setup.rs index d6ee6f266b..820487241c 100644 --- a/game/src/abtest/setup.rs +++ b/game/src/abtest/setup.rs @@ -139,7 +139,6 @@ fn launch_test(test: &ABTest, ui: &mut UI, ctx: &mut EventCtx) -> ABTestMode { ui, MapEdits::load(&test.map_name, &test.edits1_name, &mut timer), ); - ui.primary.map.mark_edits_fresh(); ui.primary .map .recalculate_pathfinding_after_edits(&mut timer); @@ -193,7 +192,6 @@ fn launch_test(test: &ABTest, ui: &mut UI, ctx: &mut EventCtx) -> ABTestMode { MapEdits::load(&test.map_name, &test.edits2_name, &mut timer), ); std::mem::swap(&mut ui.primary, &mut secondary); - secondary.map.mark_edits_fresh(); secondary .map .recalculate_pathfinding_after_edits(&mut timer); diff --git a/game/src/edit/mod.rs b/game/src/edit/mod.rs index 428aedbc3b..da8b73716c 100644 --- a/game/src/edit/mod.rs +++ b/game/src/edit/mod.rs @@ -62,6 +62,10 @@ impl EditMode { .recalculate_pathfinding_after_edits(&mut timer); // Parking state might've changed ui.primary.clear_sim(); + // Autosave + if ui.primary.map.get_edits().edits_name != "no_edits" { + ui.primary.map.save_edits(); + } Transition::PopThenReplace(Box::new(SandboxMode::new(ctx, ui, self.mode.clone()))) }) } @@ -105,6 +109,10 @@ impl State for EditMode { match self.composite.event(ctx) { Some(Outcome::Clicked(x)) => match x.as_ref() { "load edits" => { + // Autosave first + if ui.primary.map.get_edits().edits_name != "no_edits" { + ui.primary.map.save_edits(); + } return Transition::Push(make_load_edits( self.composite.rect_of("load edits").clone(), self.mode.clone(), @@ -113,9 +121,9 @@ impl State for EditMode { "finish editing" => { return self.quit(ctx, ui); } - "save edits" => { + "save edits as" => { return Transition::Push(WizardState::new(Box::new(|wiz, ctx, ui| { - save_edits(&mut wiz.wrap(ctx), ui)?; + save_edits_as(&mut wiz.wrap(ctx), ui)?; Some(Transition::Pop) }))); } @@ -198,13 +206,18 @@ impl State for EditMode { } } -pub fn save_edits(wizard: &mut WrappedWizard, ui: &mut UI) -> Option<()> { +pub fn save_edits_as(wizard: &mut WrappedWizard, ui: &mut UI) -> Option<()> { let map = &mut ui.primary.map; + let new_default_name = if map.get_edits().edits_name == "no_edits" { + "untitled edits".to_string() + } else { + format!("copy of {}", map.get_edits().edits_name) + }; - let rename = if map.get_edits().edits_name == "no_edits" { - Some(wizard.input_something( - "Name these map edits", - None, + let name = loop { + let candidate = wizard.input_something( + "Name the new copy of these edits", + Some(new_default_name.clone()), Box::new(|l| { if l.contains("/") || l == "no_edits" || l == "" { None @@ -212,27 +225,28 @@ pub fn save_edits(wizard: &mut WrappedWizard, ui: &mut UI) -> Option<()> { Some(l) } }), - )?) - } else { - None + )?; + if abstutil::file_exists(abstutil::path_edits(map.get_name(), &candidate)) { + let overwrite = "Overwrite"; + let rename = "Rename"; + if wizard + .choose_string(&format!("Edits named {} already exist", candidate), || { + vec![overwrite, rename] + })? + .as_str() + == overwrite + { + break candidate; + } + } else { + break candidate; + } }; - // TODO Do it this weird way to avoid saving edits on every event. :P - // TODO Do some kind of versioning? Don't ask this if the file doesn't exist yet? - let save = "save edits"; - let cancel = "cancel"; - if wizard - .choose_string("Overwrite edits?", || vec![save, cancel])? - .as_str() - == save - { - if let Some(name) = rename { - let mut edits = map.get_edits().clone(); - edits.edits_name = name; - map.apply_edits(edits, &mut Timer::new("name map edits")); - } - map.save_edits(); - } + let mut edits = map.get_edits().clone(); + edits.edits_name = name; + map.apply_edits(edits, &mut Timer::new("name map edits")); + map.save_edits(); Some(()) } @@ -240,7 +254,9 @@ fn make_load_edits(btn: ScreenRectangle, mode: GameplayMode) -> Box { WizardState::new(Box::new(move |wiz, ctx, ui| { let mut wizard = wiz.wrap(ctx); - if ui.primary.map.get_edits().dirty { + if ui.primary.map.get_edits().edits_name == "no_edits" + && !ui.primary.map.get_edits().commands.is_empty() + { let save = "save edits"; let discard = "discard"; if wizard @@ -248,13 +264,14 @@ fn make_load_edits(btn: ScreenRectangle, mode: GameplayMode) -> Box { .as_str() == save { - save_edits(&mut wizard, ui)?; + save_edits_as(&mut wizard, ui)?; wizard.reset(); } } // TODO Exclude current - let map_name = ui.primary.map.get_name().to_string(); + let current_edits_name = ui.primary.map.get_edits().edits_name.clone(); + let map_name = ui.primary.map.get_name().clone(); let (_, new_edits) = wizard.choose_exact( ( HorizontalAlignment::Centered(btn.center().x), @@ -265,7 +282,9 @@ fn make_load_edits(btn: ScreenRectangle, mode: GameplayMode) -> Box { let mut list = Choice::from( abstutil::load_all_objects(abstutil::path_all_edits(&map_name)) .into_iter() - .filter(|(_, edits)| mode.allows(edits)) + .filter(|(_, edits)| { + mode.allows(edits) && edits.edits_name != current_edits_name + }) .collect(), ); list.push(Choice::new("no_edits", MapEdits::new(map_name.clone()))); @@ -273,7 +292,6 @@ fn make_load_edits(btn: ScreenRectangle, mode: GameplayMode) -> Box { }, )?; apply_map_edits(ctx, ui, new_edits); - ui.primary.map.mark_edits_fresh(); Some(Transition::Pop) })) } @@ -301,6 +319,13 @@ fn make_topcenter(ctx: &mut EventCtx, ui: &UI) -> Composite { "load edits", ) .margin(5), + WrappedComposite::svg_button( + ctx, + "assets/tools/save.svg", + "save edits as", + lctrl(Key::S), + ) + .margin(5), (if !ui.primary.map.get_edits().commands.is_empty() { WrappedComposite::svg_button( ctx, @@ -316,9 +341,10 @@ fn make_topcenter(ctx: &mut EventCtx, ui: &UI) -> Composite { ) }) .margin(15), - ]), - WrappedComposite::text_button(ctx, "save edits", lctrl(Key::S)), - WrappedComposite::text_button(ctx, "finish editing", hotkey(Key::Escape)), + ]) + .centered(), + WrappedComposite::text_button(ctx, "finish editing", hotkey(Key::Escape)) + .centered_horiz(), ]) .bg(colors::PANEL_BG), ) @@ -326,8 +352,7 @@ fn make_topcenter(ctx: &mut EventCtx, ui: &UI) -> Composite { .build(ctx) } -pub fn apply_map_edits(ctx: &mut EventCtx, ui: &mut UI, mut edits: MapEdits) { - edits.dirty = true; +pub fn apply_map_edits(ctx: &mut EventCtx, ui: &mut UI, edits: MapEdits) { let mut timer = Timer::new("apply map edits"); let (lanes_changed, roads_changed, turns_deleted, turns_added, mut modified_intersections) = diff --git a/game/src/sandbox/mod.rs b/game/src/sandbox/mod.rs index 11bde6ccdb..3535bc569c 100644 --- a/game/src/sandbox/mod.rs +++ b/game/src/sandbox/mod.rs @@ -6,7 +6,7 @@ use crate::colors; use crate::common::{tool_panel, CommonState, Minimap, Overlays, ShowBusRoute}; use crate::debug::DebugMode; use crate::edit::{ - apply_map_edits, can_edit_lane, save_edits, EditMode, LaneEditor, StopSignEditor, + apply_map_edits, can_edit_lane, save_edits_as, EditMode, LaneEditor, StopSignEditor, TrafficSignalEditor, }; use crate::game::{DrawBaselayer, State, Transition, WizardState}; @@ -319,11 +319,12 @@ impl State for SandboxMode { fn exit_sandbox(wiz: &mut Wizard, ctx: &mut EventCtx, ui: &mut UI) -> Option { let mut wizard = wiz.wrap(ctx); - let dirty = ui.primary.map.get_edits().dirty; + let unsaved = ui.primary.map.get_edits().edits_name == "no_edits" + && !ui.primary.map.get_edits().commands.is_empty(); let (resp, _) = wizard.choose("Sure you want to abandon the current challenge?", || { let mut choices = Vec::new(); choices.push(Choice::new("keep playing", ())); - if dirty { + if unsaved { choices.push(Choice::new("save edits and quit", ())); } choices.push(Choice::new("quit challenge", ()).key(Key::Q)); @@ -334,12 +335,13 @@ fn exit_sandbox(wiz: &mut Wizard, ctx: &mut EventCtx, ui: &mut UI) -> Option, - - #[serde(skip_serializing, skip_deserializing)] - pub dirty: bool, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -65,14 +62,9 @@ impl MapEdits { original_lts: BTreeMap::new(), reversed_lanes: BTreeSet::new(), changed_intersections: BTreeSet::new(), - dirty: false, } } - pub fn is_empty(&self) -> bool { - self.edits_name == "no_edits" && self.commands.is_empty() - } - pub fn load(map_name: &str, edits_name: &str, timer: &mut Timer) -> MapEdits { if edits_name == "no_edits" { return MapEdits::new(map_name.to_string()); @@ -80,14 +72,12 @@ impl MapEdits { abstutil::read_json(abstutil::path_edits(map_name, edits_name), timer) } - // TODO Version these + // TODO Version these? Or it's unnecessary, since we have a command stack. pub(crate) fn save(&mut self, map: &Map) { self.compress(map); - assert!(self.dirty); assert_ne!(self.edits_name, "no_edits"); abstutil::write_json(abstutil::path_edits(&self.map_name, &self.edits_name), self); - self.dirty = false; } pub fn original_it(&self, i: IntersectionID) -> IntersectionType { diff --git a/map_model/src/map.rs b/map_model/src/map.rs index 9f52231c59..ac49568b9c 100644 --- a/map_model/src/map.rs +++ b/map_model/src/map.rs @@ -630,11 +630,6 @@ impl Map { &self.edits } - pub fn mark_edits_fresh(&mut self) { - assert!(self.edits.dirty); - self.edits.dirty = false; - } - pub fn save_edits(&mut self) { let mut edits = std::mem::replace(&mut self.edits, MapEdits::new(self.name.clone())); edits.save(self); diff --git a/sim/src/make/load.rs b/sim/src/make/load.rs index e977640380..546d75cd23 100644 --- a/sim/src/make/load.rs +++ b/sim/src/make/load.rs @@ -72,7 +72,6 @@ impl SimFlags { MapEdits::load(map.get_name(), &sim.edits_name, timer), timer, ); - map.mark_edits_fresh(); map.recalculate_pathfinding_after_edits(timer); } sim.restore_paths(&map, timer);