mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-12-24 23:15:24 +03:00
'save as' feature for map edits. autosave otherwise.
This commit is contained in:
parent
1147d29d98
commit
872cd0cba6
@ -345,3 +345,7 @@ pub fn basename(path: &str) -> String {
|
||||
.into_string()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn file_exists(path: String) -> bool {
|
||||
Path::new(&path).exists()
|
||||
}
|
||||
|
@ -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};
|
||||
|
3
game/assets/tools/save.svg
Normal file
3
game/assets/tools/save.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.875 0.625H2.375C1.40375 0.625 0.625 1.4125 0.625 2.375V14.625C0.625 15.5875 1.40375 16.375 2.375 16.375H14.625C15.5875 16.375 16.375 15.5875 16.375 14.625V4.125L12.875 0.625ZM8.5 14.625C7.0475 14.625 5.875 13.4525 5.875 12C5.875 10.5475 7.0475 9.375 8.5 9.375C9.9525 9.375 11.125 10.5475 11.125 12C11.125 13.4525 9.9525 14.625 8.5 14.625ZM11.125 5.875H2.375V2.375H11.125V5.875Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 510 B |
@ -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);
|
||||
|
@ -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<dyn State> {
|
||||
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<dyn State> {
|
||||
.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<dyn State> {
|
||||
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<dyn State> {
|
||||
},
|
||||
)?;
|
||||
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) =
|
||||
|
@ -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<Transition> {
|
||||
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<Tra
|
||||
}
|
||||
let map_name = ui.primary.map.get_name().to_string();
|
||||
if resp == "save edits and quit" {
|
||||
save_edits(&mut wizard, ui)?;
|
||||
save_edits_as(&mut wizard, ui)?;
|
||||
}
|
||||
ctx.loading_screen("reset map and sim", |ctx, mut timer| {
|
||||
if !ui.primary.map.get_edits().is_empty() {
|
||||
if ui.primary.map.get_edits().edits_name != "no_edits"
|
||||
|| !ui.primary.map.get_edits().commands.is_empty()
|
||||
{
|
||||
apply_map_edits(ctx, ui, MapEdits::new(map_name));
|
||||
ui.primary.map.mark_edits_fresh();
|
||||
ui.primary
|
||||
.map
|
||||
.recalculate_pathfinding_after_edits(&mut timer);
|
||||
|
@ -19,9 +19,6 @@ pub struct MapEdits {
|
||||
|
||||
// Edits without these are player generated.
|
||||
pub proposal_description: Vec<String>,
|
||||
|
||||
#[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 {
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user