start the sandbox mode, just with sim controls. remove unused sim score

plugin.
This commit is contained in:
Dustin Carlino 2019-04-25 09:41:19 -07:00
parent ce25f8bbf6
commit 1fabf29747
10 changed files with 254 additions and 354 deletions

View File

@ -1,4 +1,5 @@
use crate::edit::EditMode;
use crate::sandbox::SandboxMode;
use crate::state::{Flags, UIState};
use crate::tutorial::TutorialMode;
use crate::ui::UI;
@ -23,9 +24,10 @@ pub struct GameState {
pub enum Mode {
SplashScreen(Wizard, Option<(Screensaver, XorShiftRng)>),
Playing,
Legacy,
Edit(EditMode),
Tutorial(TutorialMode),
Sandbox(SandboxMode),
}
impl GameState {
@ -33,7 +35,7 @@ impl GameState {
let splash = !flags.no_splash;
let mut rng = flags.sim_flags.make_rng();
let mut game = GameState {
mode: Mode::Playing,
mode: Mode::Legacy,
ui: UI::new(UIState::new(flags, prerender, true), canvas),
};
if splash {
@ -50,8 +52,7 @@ impl GameState {
}
impl GUI for GameState {
// TODO Don't display this unless mode is Playing! But that probably means we have to drag the
// management of more ezgui state here.
// TODO Totally get rid of this...
fn top_menu(&self, canvas: &Canvas) -> Option<TopMenu> {
self.ui.top_menu(canvas)
}
@ -76,7 +77,7 @@ impl GUI for GameState {
}
EventLoopMode::Animation
}
Mode::Playing => {
Mode::Legacy => {
let (event_mode, pause) = self.ui.new_event(ctx);
if pause {
self.mode = Mode::SplashScreen(Wizard::new(), None);
@ -85,6 +86,7 @@ impl GUI for GameState {
}
Mode::Edit(_) => EditMode::event(self, ctx),
Mode::Tutorial(_) => TutorialMode::event(self, ctx),
Mode::Sandbox(_) => SandboxMode::event(self, ctx),
}
}
@ -94,9 +96,10 @@ impl GUI for GameState {
self.ui.draw(g);
wizard.draw(g);
}
Mode::Playing => self.ui.draw(g),
Mode::Legacy => self.ui.draw(g),
Mode::Edit(_) => EditMode::draw(self, g),
Mode::Tutorial(_) => TutorialMode::draw(self, g),
Mode::Sandbox(_) => SandboxMode::draw(self, g),
}
}
@ -165,10 +168,11 @@ fn splash_screen(
maybe_screensaver: &mut Option<(Screensaver, XorShiftRng)>,
) -> Option<Mode> {
let mut wizard = raw_wizard.wrap(&mut ctx.input, ctx.canvas);
let play = "Play";
let sandbox = "Sandbox mode";
let load_map = "Load another map";
let edit = "Edit map";
let tutorial = "Tutorial";
let legacy = "Legacy mode (ignore this)";
let about = "About";
let quit = "Quit";
@ -177,11 +181,11 @@ fn splash_screen(
match wizard
.choose_string(
"Welcome to A/B Street!",
vec![play, load_map, edit, tutorial, about, quit],
vec![sandbox, load_map, edit, tutorial, legacy, about, quit],
)?
.as_str()
{
x if x == play => break Some(Mode::Playing),
x if x == sandbox => break Some(Mode::Sandbox(SandboxMode::new())),
x if x == load_map => {
let current_map = ui.state.primary.map.get_name().to_string();
if let Some((name, _)) = wizard.choose_something::<String>(
@ -197,7 +201,7 @@ fn splash_screen(
let mut flags = ui.state.primary.current_flags.clone();
flags.sim_flags.load = PathBuf::from(format!("../data/maps/{}.abst", name));
*ui = UI::new(UIState::new(flags, ctx.prerender, true), ctx.canvas);
break Some(Mode::Playing);
break Some(Mode::Sandbox(SandboxMode::new()));
} else if wizard.aborted() {
break Some(Mode::SplashScreen(Wizard::new(), maybe_screensaver.take()));
} else {
@ -210,6 +214,7 @@ fn splash_screen(
ctx.canvas.center_to_map_pt(),
)))
}
x if x == legacy => break Some(Mode::Legacy),
x if x == about => {
if wizard.acknowledge(LogScroller::new(
"About A/B Street".to_string(),

View File

@ -4,6 +4,7 @@ mod game;
mod objects;
mod plugins;
mod render;
mod sandbox;
mod state;
mod tutorial;
mod ui;

View File

@ -50,10 +50,6 @@ pub trait AmbientPlugin {
fn draw(&self, _g: &mut GfxCtx, _ctx: &DrawCtx) {}
}
pub trait AmbientPluginWithPrimaryPlugins {
fn ambient_event_with_plugins(&mut self, _ctx: &mut PluginCtx, _plugins: &mut PluginsPerMap);
}
pub trait NonblockingPlugin {
// True means active; false means done, please destroy.
fn nonblocking_event(&mut self, _ctx: &mut PluginCtx) -> bool;

View File

@ -1,209 +0,0 @@
use crate::plugins::{AmbientPluginWithPrimaryPlugins, PluginCtx};
use crate::state::PluginsPerMap;
use abstutil::elapsed_seconds;
use ezgui::EventLoopMode;
use geom::Duration;
use sim::{Benchmark, Sim, TIMESTEP};
use std::mem;
use std::time::Instant;
const ADJUST_SPEED: f64 = 0.1;
pub struct SimControls {
desired_speed: f64, // sim seconds per real second
state: State,
}
enum State {
Paused,
Running {
last_step: Instant,
benchmark: Benchmark,
speed: String,
},
}
impl SimControls {
pub fn new() -> SimControls {
SimControls {
desired_speed: 1.0,
state: State::Paused,
}
}
pub fn run_sim(&mut self, primary_sim: &mut Sim) {
self.state = State::Running {
last_step: Instant::now(),
benchmark: primary_sim.start_benchmark(),
speed: "running".to_string(),
};
}
}
impl AmbientPluginWithPrimaryPlugins for SimControls {
fn ambient_event_with_plugins(
&mut self,
ctx: &mut PluginCtx,
primary_plugins: &mut PluginsPerMap,
) {
if ctx.input.action_chosen("slow down sim") {
self.desired_speed -= ADJUST_SPEED;
self.desired_speed = self.desired_speed.max(0.0);
}
if ctx.input.action_chosen("speed up sim") {
self.desired_speed += ADJUST_SPEED;
}
if ctx.input.action_chosen("reset sim") {
// TODO Handle secondary sim
// TODO Will the sudden change mess up the state in other plugins, or will they detect
// the time change correctly?
// TODO savestate_every gets lost
ctx.primary.sim = Sim::new(
&ctx.primary.map,
ctx.primary.current_flags.sim_flags.run_name.clone(),
None,
);
self.state = State::Paused;
}
if ctx.secondary.is_some() && ctx.input.action_chosen("swap the primary/secondary sim") {
println!("Swapping primary/secondary sim");
// Check out this cool little trick. :D
let (mut secondary, mut secondary_plugins) = ctx.secondary.take().unwrap();
mem::swap(ctx.primary, &mut secondary);
mem::swap(primary_plugins, &mut secondary_plugins);
*ctx.secondary = Some((secondary, secondary_plugins));
*ctx.recalculate_current_selection = true;
}
match self.state {
State::Paused => {
if ctx.input.action_chosen("save sim state") {
ctx.primary.sim.save();
if let Some((s, _)) = ctx.secondary {
s.sim.save();
}
}
if ctx.input.action_chosen("load previous sim state") {
match ctx
.primary
.sim
.find_previous_savestate(ctx.primary.sim.time())
.and_then(|path| Sim::load_savestate(path, None).ok())
{
Some(new_sim) => {
// TODO From the perspective of other SimMode plugins, does this just
// look like the simulation stepping forwards?
ctx.primary.sim = new_sim;
*ctx.recalculate_current_selection = true;
if let Some((s, _)) = ctx.secondary {
s.sim = Sim::load_savestate(
s.sim.find_previous_savestate(s.sim.time()).unwrap(),
None,
)
.unwrap();
}
}
None => println!(
"Couldn't load previous savestate {:?}",
ctx.primary
.sim
.find_previous_savestate(ctx.primary.sim.time())
),
};
}
if ctx.input.action_chosen("load next sim state") {
match ctx
.primary
.sim
.find_next_savestate(ctx.primary.sim.time())
.and_then(|path| Sim::load_savestate(path, None).ok())
{
Some(new_sim) => {
ctx.primary.sim = new_sim;
*ctx.recalculate_current_selection = true;
if let Some((s, _)) = ctx.secondary {
s.sim = Sim::load_savestate(
s.sim.find_next_savestate(s.sim.time()).unwrap(),
None,
)
.unwrap();
}
}
None => println!(
"Couldn't load next savestate {:?}",
ctx.primary.sim.find_next_savestate(ctx.primary.sim.time())
),
};
}
if ctx.input.action_chosen("run/pause sim") {
self.run_sim(&mut ctx.primary.sim);
} else if ctx.input.action_chosen("run one step of sim") {
ctx.primary.sim.step(&ctx.primary.map);
*ctx.recalculate_current_selection = true;
if let Some((s, _)) = ctx.secondary {
s.sim.step(&s.map);
}
}
}
State::Running {
ref mut last_step,
ref mut benchmark,
ref mut speed,
} => {
if ctx.input.action_chosen("run/pause sim") {
self.state = State::Paused;
} else {
ctx.hints.mode = EventLoopMode::Animation;
if ctx.input.nonblocking_is_update_event() {
// TODO https://gafferongames.com/post/fix_your_timestep/
// TODO This doesn't interact correctly with the fixed 30 Update events
// sent per second. Even Benchmark is kind of wrong. I think we want to
// count the number of steps we've done in the last second, then stop if
// the speed says we should.
let dt_s = elapsed_seconds(*last_step);
if dt_s >= TIMESTEP.inner_seconds() / self.desired_speed {
ctx.input.use_update_event();
ctx.primary.sim.step(&ctx.primary.map);
*ctx.recalculate_current_selection = true;
if let Some((s, _)) = ctx.secondary {
s.sim.step(&s.map);
}
*last_step = Instant::now();
if benchmark.has_real_time_passed(Duration::seconds(1.0)) {
// I think the benchmark should naturally account for the delay of
// the secondary sim.
*speed = ctx.primary.sim.measure_speed(benchmark);
}
}
}
}
}
};
ctx.hints.osd.pad_if_nonempty();
ctx.hints.osd.add_line(ctx.primary.sim.summary());
if let Some((s, _)) = ctx.secondary {
ctx.hints.osd.add_line("A/B test running!".to_string());
ctx.hints.osd.add_line(s.sim.summary());
}
if let State::Running { ref speed, .. } = self.state {
ctx.hints.osd.add_line(format!(
"Speed: {0} / desired {1:.2}x",
speed, self.desired_speed
));
} else {
ctx.hints.osd.add_line(format!(
"Speed: paused / desired {0:.2}x",
self.desired_speed
));
}
}
}

View File

@ -1,5 +1,3 @@
pub mod controls;
pub mod diff_all;
pub mod diff_trip;
pub mod show_score;
pub mod time_travel;

View File

@ -1,84 +0,0 @@
use crate::objects::DrawCtx;
use crate::plugins::{NonblockingPlugin, PluginCtx};
use ezgui::{Color, GfxCtx, HorizontalAlignment, Text, VerticalAlignment};
use geom::Duration;
use sim::ScoreSummary;
pub struct ShowScoreState {
last_time: Duration,
txt: Text,
}
impl ShowScoreState {
pub fn new(ctx: &mut PluginCtx) -> Option<ShowScoreState> {
if ctx.input.action_chosen("show/hide sim info sidepanel") {
return Some(panel(ctx));
}
None
}
}
impl NonblockingPlugin for ShowScoreState {
fn nonblocking_event(&mut self, ctx: &mut PluginCtx) -> bool {
if ctx.input.action_chosen("show/hide sim info sidepanel") {
return false;
}
if self.last_time != ctx.primary.sim.time() {
*self = panel(ctx);
}
true
}
fn draw(&self, g: &mut GfxCtx, _ctx: &DrawCtx) {
g.draw_blocking_text(
&self.txt,
(HorizontalAlignment::Right, VerticalAlignment::BelowTopMenu),
);
}
}
fn panel(ctx: &mut PluginCtx) -> ShowScoreState {
let mut txt = Text::new();
if let Some((s, _)) = ctx.secondary {
// TODO More coloring
txt.add_line(ctx.primary.sim.get_name().to_string());
summarize(&mut txt, ctx.primary.sim.get_score());
txt.add_line(String::new());
txt.add_line(s.sim.get_name().to_string());
summarize(&mut txt, s.sim.get_score());
} else {
summarize(&mut txt, ctx.primary.sim.get_score());
}
ShowScoreState {
last_time: ctx.primary.sim.time(),
txt,
}
}
fn summarize(txt: &mut Text, summary: ScoreSummary) {
txt.add_styled_line(
"Walking".to_string(),
None,
Some(Color::RED.alpha(0.8)),
None,
);
txt.add_line(format!(
" {}/{} trips done",
(summary.total_walking_trips - summary.pending_walking_trips),
summary.pending_walking_trips
));
txt.add_line(format!(" {} total", summary.total_walking_trip_time));
txt.add_styled_line(
"Driving".to_string(),
None,
Some(Color::BLUE.alpha(0.8)),
None,
);
txt.add_line(format!(
" {}/{} trips done",
(summary.total_driving_trips - summary.pending_driving_trips),
summary.pending_driving_trips
));
txt.add_line(format!(" {} total", summary.total_driving_trip_time));
}

199
editor/src/sandbox/mod.rs Normal file
View File

@ -0,0 +1,199 @@
use crate::game::{GameState, Mode};
use abstutil::elapsed_seconds;
use ezgui::{Color, EventCtx, EventLoopMode, GfxCtx, Text, Wizard};
use geom::Duration;
use sim::{Benchmark, Sim, TripID};
use std::collections::HashMap;
use std::time::Instant;
const ADJUST_SPEED: f64 = 0.1;
pub struct SandboxMode {
desired_speed: f64, // sim seconds per real second
state: State,
following: Option<TripID>,
}
enum State {
Paused,
Running {
last_step: Instant,
benchmark: Benchmark,
speed: String,
},
}
impl SandboxMode {
pub fn new() -> SandboxMode {
SandboxMode {
desired_speed: 1.0,
state: State::Paused,
following: None,
}
}
pub fn event(state: &mut GameState, ctx: &mut EventCtx) -> EventLoopMode {
ctx.canvas.handle_event(ctx.input);
match state.mode {
// TODO confusing name? ;)
Mode::Sandbox(ref mut mode) => {
let mut txt = Text::new();
txt.add_styled_line("Sandbox Mode".to_string(), None, Some(Color::BLUE), None);
txt.add_line(state.ui.state.primary.sim.summary());
if let State::Running { ref speed, .. } = mode.state {
txt.add_line(format!(
"Speed: {0} / desired {1:.2}x",
speed, mode.desired_speed
));
} else {
txt.add_line(format!(
"Speed: paused / desired {0:.2}x",
mode.desired_speed
));
}
ctx.input
.set_mode_with_new_prompt("Sandbox Mode", txt, ctx.canvas);
if ctx.input.modal_action("quit") {
// TODO This shouldn't be necessary when we plumb state around instead of
// sharing it in the old structure.
state.ui.state.primary.sim = Sim::new(
&state.ui.state.primary.map,
state
.ui
.state
.primary
.current_flags
.sim_flags
.run_name
.clone(),
None,
);
state.mode = Mode::SplashScreen(Wizard::new(), None);
return EventLoopMode::InputOnly;
}
state.ui.handle_mouseover(ctx, None);
if ctx.input.modal_action("slow down sim") {
mode.desired_speed -= ADJUST_SPEED;
mode.desired_speed = mode.desired_speed.max(0.0);
}
if ctx.input.modal_action("speed up sim") {
mode.desired_speed += ADJUST_SPEED;
}
if ctx.input.modal_action("reset sim") {
// TODO savestate_every gets lost
state.ui.state.primary.sim = Sim::new(
&state.ui.state.primary.map,
state
.ui
.state
.primary
.current_flags
.sim_flags
.run_name
.clone(),
None,
);
mode.state = State::Paused;
}
match mode.state {
State::Paused => {
if ctx.input.modal_action("save sim state") {
state.ui.state.primary.sim.save();
}
if ctx.input.modal_action("load previous sim state") {
let prev_state = state
.ui
.state
.primary
.sim
.find_previous_savestate(state.ui.state.primary.sim.time());
match prev_state
.clone()
.and_then(|path| Sim::load_savestate(path, None).ok())
{
Some(new_sim) => {
state.ui.state.primary.sim = new_sim;
//*ctx.recalculate_current_selection = true;
}
None => {
println!("Couldn't load previous savestate {:?}", prev_state)
}
}
}
if ctx.input.modal_action("load next sim state") {
let next_state = state
.ui
.state
.primary
.sim
.find_next_savestate(state.ui.state.primary.sim.time());
match next_state
.clone()
.and_then(|path| Sim::load_savestate(path, None).ok())
{
Some(new_sim) => {
state.ui.state.primary.sim = new_sim;
//*ctx.recalculate_current_selection = true;
}
None => println!("Couldn't load next savestate {:?}", next_state),
}
}
if ctx.input.modal_action("run/pause sim") {
mode.state = State::Running {
last_step: Instant::now(),
benchmark: state.ui.state.primary.sim.start_benchmark(),
speed: "...".to_string(),
};
} else if ctx.input.modal_action("run one step of sim") {
state.ui.state.primary.sim.step(&state.ui.state.primary.map);
//*ctx.recalculate_current_selection = true;
}
EventLoopMode::InputOnly
}
State::Running {
ref mut last_step,
ref mut benchmark,
ref mut speed,
} => {
if ctx.input.modal_action("run/pause sim") {
mode.state = State::Paused;
} else if ctx.input.nonblocking_is_update_event() {
// TODO https://gafferongames.com/post/fix_your_timestep/
// TODO This doesn't interact correctly with the fixed 30 Update events sent
// per second. Even Benchmark is kind of wrong. I think we want to count the
// number of steps we've done in the last second, then stop if the speed says
// we should.
let dt_s = elapsed_seconds(*last_step);
if dt_s >= sim::TIMESTEP.inner_seconds() / mode.desired_speed {
ctx.input.use_update_event();
state.ui.state.primary.sim.step(&state.ui.state.primary.map);
//*ctx.recalculate_current_selection = true;
*last_step = Instant::now();
if benchmark.has_real_time_passed(Duration::seconds(1.0)) {
// I think the benchmark should naturally account for the delay of
// the secondary sim.
*speed =
state.ui.state.primary.sim.measure_speed(benchmark, false);
}
}
}
EventLoopMode::Animation
}
}
}
_ => unreachable!(),
}
}
pub fn draw(state: &GameState, g: &mut GfxCtx) {
state.ui.new_draw(g, None, HashMap::new());
}
}

View File

@ -2,8 +2,7 @@ use crate::colors::ColorScheme;
use crate::objects::{DrawCtx, RenderingHints, ID};
use crate::plugins;
use crate::plugins::{
debug, edit, view, AmbientPlugin, AmbientPluginWithPrimaryPlugins, BlockingPlugin,
NonblockingPlugin, PluginCtx,
debug, edit, view, AmbientPlugin, BlockingPlugin, NonblockingPlugin, PluginCtx,
};
use crate::render::DrawMap;
use abstutil::{MeasureMemory, Timer};
@ -59,11 +58,9 @@ pub struct UIState {
// These are stackable modal plugins. They can all coexist, and they don't block other modal
// plugins or ambient plugins.
show_score: Option<plugins::sim::show_score::ShowScoreState>,
pub legend: Option<plugins::view::legend::Legend>,
// Ambient plugins always exist, and they never block anything.
pub sim_controls: plugins::sim::controls::SimControls,
pub layers: debug::layers::ToggleableLayers,
pub enable_debug_controls: bool,
@ -83,9 +80,7 @@ impl UIState {
secondary: None,
exclusive_blocking_plugin: None,
exclusive_nonblocking_plugin: None,
show_score: None,
legend: None,
sim_controls: plugins::sim::controls::SimControls::new(),
layers: debug::layers::ToggleableLayers::new(),
enable_debug_controls,
cs,
@ -104,7 +99,7 @@ impl UIState {
// The exclusive_nonblocking_plugins don't color_obj.
// show_score, legend, hider, sim_controls, and layers don't color_obj.
// legend, hider, and layers don't color_obj.
for p in &self.primary_plugins.ambient_plugins {
if let Some(c) = p.color_for(id, ctx) {
return Some(c);
@ -264,18 +259,6 @@ impl UIState {
}
// Stackable modal plugins
if self.show_score.is_some() {
if !self
.show_score
.as_mut()
.unwrap()
.nonblocking_event(&mut ctx)
{
self.show_score = None;
}
} else if let Some(p) = plugins::sim::show_score::ShowScoreState::new(&mut ctx) {
self.show_score = Some(p);
}
if self.legend.is_some() {
if !self.legend.as_mut().unwrap().nonblocking_event(&mut ctx) {
self.legend = None;
@ -333,8 +316,6 @@ impl UIState {
}
// Ambient plugins
self.sim_controls
.ambient_event_with_plugins(&mut ctx, &mut self.primary_plugins);
for p in self.primary_plugins.ambient_plugins.iter_mut() {
p.ambient_event(&mut ctx);
}
@ -360,9 +341,6 @@ impl UIState {
}
// Stackable modals
if let Some(ref p) = self.show_score {
p.draw(g, ctx);
}
if let Some(ref p) = self.legend {
p.draw(g, ctx);
}

View File

@ -59,19 +59,9 @@ impl GUI for UI {
Folder::new(
"Simulation",
vec![
(Some(Key::LeftBracket), "slow down sim"),
(Some(Key::RightBracket), "speed up sim"),
(Some(Key::O), "save sim state"),
(Some(Key::Y), "load previous sim state"),
(Some(Key::U), "load next sim state"),
(Some(Key::Space), "run/pause sim"),
(Some(Key::M), "run one step of sim"),
(Some(Key::Dot), "show/hide sim info sidepanel"),
(Some(Key::T), "start time traveling"),
(Some(Key::D), "diff all A/B trips"),
(Some(Key::S), "seed the sim with agents"),
(Some(Key::LeftAlt), "swap the primary/secondary sim"),
(None, "reset sim"),
],
),
Folder::new(
@ -180,7 +170,7 @@ impl GUI for UI {
),
ModalMenu::new(
"Stop Sign Editor",
vec![(Key::Enter, "quit"), (Key::R, "reset to default")],
vec![(Key::Escape, "quit"), (Key::R, "reset to default")],
),
ModalMenu::new(
"Traffic Signal Editor",
@ -197,6 +187,20 @@ impl GUI for UI {
(Key::M, "add a new pedestrian scramble cycle"),
],
),
ModalMenu::new(
"Sandbox Mode",
vec![
(Key::Escape, "quit"),
(Key::LeftBracket, "slow down sim"),
(Key::RightBracket, "speed up sim"),
(Key::O, "save sim state"),
(Key::Y, "load previous sim state"),
(Key::U, "load next sim state"),
(Key::Space, "run/pause sim"),
(Key::M, "run one step of sim"),
(Key::R, "reset sim"),
],
),
]
}

View File

@ -421,14 +421,22 @@ impl Sim {
}
if benchmark.has_real_time_passed(Duration::seconds(1.0)) {
println!("{}, {}", self.summary(), self.measure_speed(&mut benchmark));
println!(
"{}, speed = {}",
self.summary(),
self.measure_speed(&mut benchmark, true)
);
}
callback(self, map);
if Some(self.time()) == time_limit {
panic!("Time limit {} hit", self.time);
}
if self.is_done() {
println!("{}, {}", self.summary(), self.measure_speed(&mut benchmark));
println!(
"{}, speed = {}",
self.summary(),
self.measure_speed(&mut benchmark, true)
);
break;
}
}
@ -458,7 +466,11 @@ impl Sim {
}
}
if benchmark.has_real_time_passed(Duration::seconds(1.0)) {
println!("{}, {}", self.summary(), self.measure_speed(&mut benchmark));
println!(
"{}, speed = {}",
self.summary(),
self.measure_speed(&mut benchmark, true)
);
}
if self.time() == time_limit {
panic!(
@ -530,19 +542,19 @@ impl Sim {
}
}
pub fn measure_speed(&self, b: &mut Benchmark) -> String {
pub fn measure_speed(&self, b: &mut Benchmark, details: bool) -> String {
let dt = Duration::seconds(abstutil::elapsed_seconds(b.last_real_time));
if dt == Duration::ZERO {
return format!("speed = instantly ({})", self.scheduler.describe_stats());
return "...".to_string();
}
let speed = (self.time - b.last_sim_time) / dt;
b.last_real_time = Instant::now();
b.last_sim_time = self.time;
format!(
"speed = {:.2}x ({})",
speed,
self.scheduler.describe_stats()
)
if details {
format!("{:.2}x ({})", speed, self.scheduler.describe_stats())
} else {
format!("{:.2}x", speed)
}
}
}