mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-12-25 07:25:47 +03:00
Express the SandboxMode constructor as a full state machine, with the
ability to defer things like pushing the time warp state onto the stack of states. This fixes the edit mode case of #367, but other callers of SandboxMode::new still need updating.
This commit is contained in:
parent
20b4f15e86
commit
5ef48a5d1c
@ -101,11 +101,22 @@ impl EditMode {
|
|||||||
app.primary.sim.handle_live_edits(&app.primary.map);
|
app.primary.sim.handle_live_edits(&app.primary.map);
|
||||||
Transition::Pop
|
Transition::Pop
|
||||||
} else {
|
} else {
|
||||||
app.primary.clear_sim();
|
let resume_time = old_sim.time();
|
||||||
Transition::Multi(vec![
|
Transition::Multi(vec![
|
||||||
Transition::Pop,
|
Transition::Pop,
|
||||||
Transition::Replace(SandboxMode::new(ctx, app, self.mode.clone())),
|
Transition::Replace(SandboxMode::async_new(
|
||||||
Transition::Push(TimeWarpScreen::new(ctx, app, old_sim.time(), None)),
|
ctx,
|
||||||
|
app,
|
||||||
|
self.mode.clone(),
|
||||||
|
Box::new(move |ctx, app| {
|
||||||
|
vec![Transition::Push(TimeWarpScreen::new(
|
||||||
|
ctx,
|
||||||
|
app,
|
||||||
|
resume_time,
|
||||||
|
None,
|
||||||
|
))]
|
||||||
|
}),
|
||||||
|
)),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -3,9 +3,8 @@ use maplit::btreeset;
|
|||||||
pub use speed::{SpeedControls, TimePanel};
|
pub use speed::{SpeedControls, TimePanel};
|
||||||
pub use time_warp::TimeWarpScreen;
|
pub use time_warp::TimeWarpScreen;
|
||||||
|
|
||||||
use abstutil::Timer;
|
|
||||||
use geom::Time;
|
use geom::Time;
|
||||||
use sim::AgentType;
|
use sim::{AgentType, Analytics, Scenario};
|
||||||
use widgetry::{
|
use widgetry::{
|
||||||
lctrl, Btn, Choice, EventCtx, GfxCtx, HorizontalAlignment, Key, Line, Outcome, Panel, Text,
|
lctrl, Btn, Choice, EventCtx, GfxCtx, HorizontalAlignment, Key, Line, Outcome, Panel, Text,
|
||||||
TextExt, UpdateType, VerticalAlignment, Widget,
|
TextExt, UpdateType, VerticalAlignment, Widget,
|
||||||
@ -52,190 +51,25 @@ pub struct SandboxControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SandboxMode {
|
impl SandboxMode {
|
||||||
|
// TODO Audit all callers
|
||||||
pub fn new(ctx: &mut EventCtx, app: &mut App, mode: GameplayMode) -> Box<dyn State> {
|
pub fn new(ctx: &mut EventCtx, app: &mut App, mode: GameplayMode) -> Box<dyn State> {
|
||||||
// TODO This is quite convoluted to support loading files on the web. Each step is a State.
|
SandboxMode::async_new(ctx, app, mode, Box::new(|_, _| Vec::new()))
|
||||||
// The overview is:
|
|
||||||
//
|
|
||||||
// 1) load the map
|
|
||||||
// 2) figure out the scenario from the GameplayMode
|
|
||||||
// 3) if it's a file, go load it
|
|
||||||
// 4) if there's prebaked data, load it
|
|
||||||
//
|
|
||||||
// Need to keep working on this to express these with less nesting, ideally like futures,
|
|
||||||
// with .and_then() or something.
|
|
||||||
|
|
||||||
app.primary.clear_sim();
|
|
||||||
MapLoader::new(
|
|
||||||
ctx,
|
|
||||||
app,
|
|
||||||
mode.map_name().to_string(),
|
|
||||||
Box::new(move |ctx, app| {
|
|
||||||
let mut timer = Timer::new("load scenario");
|
|
||||||
match mode.scenario(
|
|
||||||
&app.primary.map,
|
|
||||||
app.primary.current_flags.num_agents,
|
|
||||||
app.primary.current_flags.sim_flags.make_rng(),
|
|
||||||
&mut timer,
|
|
||||||
) {
|
|
||||||
gameplay::LoadScenario::Nothing => {
|
|
||||||
Transition::Replace(SandboxMode::mode_to_sandbox(ctx, app, mode.clone()))
|
|
||||||
}
|
|
||||||
gameplay::LoadScenario::Scenario(scenario) => {
|
|
||||||
ctx.loading_screen("instantiate scenario", |_, mut timer| {
|
|
||||||
scenario.instantiate(
|
|
||||||
&mut app.primary.sim,
|
|
||||||
&app.primary.map,
|
|
||||||
&mut app.primary.current_flags.sim_flags.make_rng(),
|
|
||||||
&mut timer,
|
|
||||||
);
|
|
||||||
app.primary
|
|
||||||
.sim
|
|
||||||
.tiny_step(&app.primary.map, &mut app.primary.sim_cb);
|
|
||||||
});
|
|
||||||
|
|
||||||
Transition::Replace(SandboxMode::mode_to_sandbox(ctx, app, mode.clone()))
|
|
||||||
}
|
|
||||||
gameplay::LoadScenario::Path(path) => {
|
|
||||||
let mode = mode.clone();
|
|
||||||
Transition::Replace(
|
|
||||||
// Let's get nesty...
|
|
||||||
FileLoader::<sim::Scenario>::new(
|
|
||||||
ctx,
|
|
||||||
path,
|
|
||||||
Box::new(move |ctx, app, scenario| {
|
|
||||||
let mode = mode.clone();
|
|
||||||
let scenario = ctx.loading_screen(
|
|
||||||
"instantiate scenario",
|
|
||||||
|_, mut timer| {
|
|
||||||
// TODO Handle corrupt files
|
|
||||||
let mut scenario = scenario.unwrap();
|
|
||||||
if let GameplayMode::PlayScenario(_, _, ref modifiers) =
|
|
||||||
mode.clone()
|
|
||||||
{
|
|
||||||
let mut rng =
|
|
||||||
app.primary.current_flags.sim_flags.make_rng();
|
|
||||||
for m in modifiers {
|
|
||||||
scenario = m.apply(
|
|
||||||
&app.primary.map,
|
|
||||||
scenario,
|
|
||||||
&mut rng,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scenario.instantiate(
|
|
||||||
&mut app.primary.sim,
|
|
||||||
&app.primary.map,
|
|
||||||
&mut app.primary.current_flags.sim_flags.make_rng(),
|
|
||||||
&mut timer,
|
|
||||||
);
|
|
||||||
app.primary.sim.tiny_step(
|
|
||||||
&app.primary.map,
|
|
||||||
&mut app.primary.sim_cb,
|
|
||||||
);
|
|
||||||
|
|
||||||
scenario
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Maybe we've already got prebaked data for this map+scenario.
|
|
||||||
if !app
|
|
||||||
.has_prebaked()
|
|
||||||
.map(|(m, s)| {
|
|
||||||
m == &scenario.map_name && s == &scenario.scenario_name
|
|
||||||
})
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
// Oh, you thought we were done?
|
|
||||||
let mode = mode.clone();
|
|
||||||
Transition::Replace(FileLoader::<sim::Analytics>::new(
|
|
||||||
ctx,
|
|
||||||
abstutil::path_prebaked_results(
|
|
||||||
&scenario.map_name,
|
|
||||||
&scenario.scenario_name,
|
|
||||||
),
|
|
||||||
Box::new(move |ctx, app, prebaked| {
|
|
||||||
if let Some(prebaked) = prebaked {
|
|
||||||
app.set_prebaked(Some((
|
|
||||||
scenario.map_name.clone(),
|
|
||||||
scenario.scenario_name.clone(),
|
|
||||||
prebaked,
|
|
||||||
)));
|
|
||||||
} else {
|
|
||||||
warn!(
|
|
||||||
"No prebaked simulation results for \
|
|
||||||
\"{}\" scenario on {} map. This means \
|
|
||||||
trip dashboards can't compare current \
|
|
||||||
times to any kind of baseline.",
|
|
||||||
scenario.scenario_name, scenario.map_name
|
|
||||||
);
|
|
||||||
app.set_prebaked(None);
|
|
||||||
}
|
|
||||||
Transition::Replace(SandboxMode::mode_to_sandbox(
|
|
||||||
ctx,
|
|
||||||
app,
|
|
||||||
mode.clone(),
|
|
||||||
))
|
|
||||||
}),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Transition::Replace(SandboxMode::mode_to_sandbox(
|
|
||||||
ctx,
|
|
||||||
app,
|
|
||||||
mode.clone(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mode_to_sandbox(ctx: &mut EventCtx, app: &mut App, mode: GameplayMode) -> Box<dyn State> {
|
/// This does not immediately initialize anything (like loading the correct map, instantiating
|
||||||
let gameplay = mode.initialize(ctx, app);
|
/// the scenario, etc). That means if you're chaining this call with other transitions, you
|
||||||
Box::new(SandboxMode {
|
/// probably need to defer running them using `finalize`.
|
||||||
controls: SandboxControls {
|
pub fn async_new(
|
||||||
common: if gameplay.has_common() {
|
_: &mut EventCtx,
|
||||||
Some(CommonState::new())
|
app: &mut App,
|
||||||
} else {
|
mode: GameplayMode,
|
||||||
None
|
finalize: Box<dyn Fn(&mut EventCtx, &mut App) -> Vec<Transition>>,
|
||||||
},
|
) -> Box<dyn State> {
|
||||||
route_preview: if gameplay.can_examine_objects() {
|
app.primary.clear_sim();
|
||||||
Some(RoutePreview::new())
|
Box::new(SandboxLoader {
|
||||||
} else {
|
stage: Some(LoadStage::LoadingMap),
|
||||||
None
|
mode,
|
||||||
},
|
finalize,
|
||||||
tool_panel: if gameplay.has_tool_panel() {
|
|
||||||
Some(tool_panel(ctx))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
time_panel: if gameplay.has_time_panel() {
|
|
||||||
Some(TimePanel::new(ctx, app))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
speed: if gameplay.has_speed() {
|
|
||||||
Some(SpeedControls::new(ctx, app))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
agent_meter: if gameplay.has_agent_meter() {
|
|
||||||
Some(AgentMeter::new(ctx, app))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
minimap: if gameplay.has_minimap() {
|
|
||||||
Some(Minimap::new(ctx, app))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
},
|
|
||||||
gameplay,
|
|
||||||
gameplay_mode: mode,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -662,3 +496,213 @@ impl ContextualActions for Actions {
|
|||||||
self.gameplay.clone()
|
self.gameplay.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO Setting SandboxMode up is quite convoluted, all in order to support asynchronously loading
|
||||||
|
// files on the web. Each LoadStage is followed in order, with some optional short-circuiting.
|
||||||
|
//
|
||||||
|
// Ideally there'd be a much simpler way to express this using Rust's async, to let the compiler
|
||||||
|
// express this state machine for us.
|
||||||
|
|
||||||
|
enum LoadStage {
|
||||||
|
LoadingMap,
|
||||||
|
LoadingScenario,
|
||||||
|
GotScenario(Scenario),
|
||||||
|
// Scenario name
|
||||||
|
LoadingPrebaked(String),
|
||||||
|
// Scenario name, maybe prebaked data
|
||||||
|
GotPrebaked(String, Option<Analytics>),
|
||||||
|
Finalizing,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SandboxLoader {
|
||||||
|
// Always exists, just a way to avoid clones
|
||||||
|
stage: Option<LoadStage>,
|
||||||
|
mode: GameplayMode,
|
||||||
|
finalize: Box<dyn Fn(&mut EventCtx, &mut App) -> Vec<Transition>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State for SandboxLoader {
|
||||||
|
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
|
||||||
|
loop {
|
||||||
|
match self.stage.take().unwrap() {
|
||||||
|
LoadStage::LoadingMap => {
|
||||||
|
return Transition::Push(MapLoader::new(
|
||||||
|
ctx,
|
||||||
|
app,
|
||||||
|
self.mode.map_name().to_string(),
|
||||||
|
Box::new(|_, _| {
|
||||||
|
Transition::Multi(vec![
|
||||||
|
Transition::Pop,
|
||||||
|
Transition::ModifyState(Box::new(|state, _, _| {
|
||||||
|
let loader = state.downcast_mut::<SandboxLoader>().unwrap();
|
||||||
|
loader.stage = Some(LoadStage::LoadingScenario);
|
||||||
|
})),
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
LoadStage::LoadingScenario => {
|
||||||
|
match ctx.loading_screen("load scenario", |_, mut timer| {
|
||||||
|
self.mode.scenario(
|
||||||
|
&app.primary.map,
|
||||||
|
app.primary.current_flags.num_agents,
|
||||||
|
app.primary.current_flags.sim_flags.make_rng(),
|
||||||
|
&mut timer,
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
gameplay::LoadScenario::Nothing => {
|
||||||
|
app.set_prebaked(None);
|
||||||
|
self.stage = Some(LoadStage::Finalizing);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
gameplay::LoadScenario::Scenario(scenario) => {
|
||||||
|
self.stage = Some(LoadStage::GotScenario(scenario));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
gameplay::LoadScenario::Path(path) => {
|
||||||
|
return Transition::Push(FileLoader::<Scenario>::new(
|
||||||
|
ctx,
|
||||||
|
path,
|
||||||
|
Box::new(|_, _, scenario| {
|
||||||
|
// TODO Handle corrupt files
|
||||||
|
let scenario = scenario.unwrap();
|
||||||
|
Transition::Multi(vec![
|
||||||
|
Transition::Pop,
|
||||||
|
Transition::ModifyState(Box::new(|state, _, _| {
|
||||||
|
let loader =
|
||||||
|
state.downcast_mut::<SandboxLoader>().unwrap();
|
||||||
|
loader.stage = Some(LoadStage::GotScenario(scenario));
|
||||||
|
})),
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LoadStage::GotScenario(mut scenario) => {
|
||||||
|
let scenario_name = scenario.scenario_name.clone();
|
||||||
|
ctx.loading_screen("instantiate scenario", |_, mut timer| {
|
||||||
|
if let GameplayMode::PlayScenario(_, _, ref modifiers) = self.mode {
|
||||||
|
let mut rng = app.primary.current_flags.sim_flags.make_rng();
|
||||||
|
for m in modifiers {
|
||||||
|
scenario = m.apply(&app.primary.map, scenario, &mut rng);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scenario.instantiate(
|
||||||
|
&mut app.primary.sim,
|
||||||
|
&app.primary.map,
|
||||||
|
&mut app.primary.current_flags.sim_flags.make_rng(),
|
||||||
|
&mut timer,
|
||||||
|
);
|
||||||
|
app.primary
|
||||||
|
.sim
|
||||||
|
.tiny_step(&app.primary.map, &mut app.primary.sim_cb);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.stage = Some(LoadStage::LoadingPrebaked(scenario_name));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
LoadStage::LoadingPrebaked(scenario_name) => {
|
||||||
|
// Maybe we've already got prebaked data for this map+scenario.
|
||||||
|
if app
|
||||||
|
.has_prebaked()
|
||||||
|
.map(|(m, s)| m == app.primary.map.get_name() && s == &scenario_name)
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
self.stage = Some(LoadStage::Finalizing);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Transition::Push(FileLoader::<Analytics>::new(
|
||||||
|
ctx,
|
||||||
|
abstutil::path_prebaked_results(app.primary.map.get_name(), &scenario_name),
|
||||||
|
Box::new(move |_, _, prebaked| {
|
||||||
|
let scenario_name = scenario_name.clone();
|
||||||
|
Transition::Multi(vec![
|
||||||
|
Transition::Pop,
|
||||||
|
Transition::ModifyState(Box::new(move |state, _, _| {
|
||||||
|
let loader = state.downcast_mut::<SandboxLoader>().unwrap();
|
||||||
|
loader.stage = Some(LoadStage::GotPrebaked(
|
||||||
|
scenario_name.clone(),
|
||||||
|
prebaked,
|
||||||
|
));
|
||||||
|
})),
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
LoadStage::GotPrebaked(scenario_name, prebaked) => {
|
||||||
|
if let Some(prebaked) = prebaked {
|
||||||
|
app.set_prebaked(Some((
|
||||||
|
app.primary.map.get_name().clone(),
|
||||||
|
scenario_name,
|
||||||
|
prebaked,
|
||||||
|
)));
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"No prebaked simulation results for \"{}\" scenario on {} map. This \
|
||||||
|
means trip dashboards can't compare current times to any kind of \
|
||||||
|
baseline.",
|
||||||
|
scenario_name,
|
||||||
|
app.primary.map.get_name()
|
||||||
|
);
|
||||||
|
app.set_prebaked(None);
|
||||||
|
}
|
||||||
|
self.stage = Some(LoadStage::Finalizing);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
LoadStage::Finalizing => {
|
||||||
|
let gameplay = self.mode.initialize(ctx, app);
|
||||||
|
let sandbox = Box::new(SandboxMode {
|
||||||
|
controls: SandboxControls {
|
||||||
|
common: if gameplay.has_common() {
|
||||||
|
Some(CommonState::new())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
route_preview: if gameplay.can_examine_objects() {
|
||||||
|
Some(RoutePreview::new())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
tool_panel: if gameplay.has_tool_panel() {
|
||||||
|
Some(tool_panel(ctx))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
time_panel: if gameplay.has_time_panel() {
|
||||||
|
Some(TimePanel::new(ctx, app))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
speed: if gameplay.has_speed() {
|
||||||
|
Some(SpeedControls::new(ctx, app))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
agent_meter: if gameplay.has_agent_meter() {
|
||||||
|
Some(AgentMeter::new(ctx, app))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
minimap: if gameplay.has_minimap() {
|
||||||
|
Some(Minimap::new(ctx, app))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gameplay,
|
||||||
|
gameplay_mode: self.mode.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut transitions = vec![Transition::Replace(sandbox)];
|
||||||
|
transitions.extend((self.finalize)(ctx, app));
|
||||||
|
return Transition::Multi(transitions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&self, _: &mut GfxCtx, _: &App) {}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user