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);