blockworx/tutorial/
replay.rs1use std::time::Duration;
14
15use egui::{Rect, vec2};
16
17use blockworx_doc::document::{DocIndex, Document};
18
19use crate::grid::GRID_SIZE;
20use crate::schema::lower::SourceIds;
21use crate::script::driver::{SimClock, SimDriver, Speed};
22use crate::script::lowering::{Lowering, SimFrame};
23use crate::script::step::CueScope;
24
25use super::level::Level;
26
27pub struct Replay {
28 level: Level,
29 lowering: Lowering,
30 ids: SourceIds,
34 clock: SimClock,
35 driver: SimDriver,
38 finished: bool,
40 stopped: bool,
43}
44
45impl Replay {
46 pub fn new(level: Level, ids: SourceIds) -> Self {
47 let lowering = Lowering::new(&level.script);
48 Self {
49 level,
50 lowering,
51 ids,
52 clock: SimClock::default(),
53 driver: SimDriver::default(),
54 finished: false,
55 stopped: false,
56 }
57 }
58
59 pub fn level(&self) -> &Level {
60 &self.level
61 }
62
63 pub fn clock(&self) -> Duration {
65 self.clock.elapsed()
66 }
67
68 pub(crate) fn driver(&mut self) -> &mut SimDriver {
69 &mut self.driver
70 }
71
72 pub fn cue_scope<'a>(&'a self, doc: &'a Document, index: &'a mut DocIndex) -> CueScope<'a> {
75 CueScope::new(index.view(doc), &self.ids)
76 }
77
78 pub fn begin_show(&mut self, dt: Duration) -> u32 {
81 if self.done() {
82 return 0;
83 }
84 self.clock.begin_show(dt, Speed::NORMAL)
85 }
86
87 pub fn next_frame(&mut self, doc: &Document, index: &mut DocIndex) -> Option<SimFrame> {
91 let Self {
92 lowering,
93 ids,
94 clock,
95 level,
96 finished,
97 stopped,
98 ..
99 } = self;
100 match lowering.next(&CueScope::new(index.view(doc), ids)) {
101 Ok(Some(frame)) => {
102 clock.tick();
103 Some(frame)
104 }
105 Ok(None) => {
106 *finished = true;
107 None
108 }
109 Err(e) => {
110 tracing::error!(
111 "replay of `{}` stopped: demo step {} names a missing object ({:?})",
112 level.id,
113 e.step,
114 e.target
115 );
116 *stopped = true;
117 None
118 }
119 }
120 }
121
122 pub fn done(&self) -> bool {
126 self.stopped || (self.finished && !self.driver.enter_pending())
127 }
128}
129
130pub fn draw_instruction(
133 painter: &egui::Painter,
134 viewport: Rect,
135 text: &str,
136 visuals: &egui::Visuals,
137) {
138 if text.is_empty() {
139 return;
140 }
141 let galley = painter.layout(
142 text.to_owned(),
143 egui::FontId::proportional(18.0),
144 visuals.text_color(),
145 viewport.width() - 4.0 * GRID_SIZE,
146 );
147 let card = Rect::from_center_size(viewport.center(), galley.size() + vec2(24.0, 16.0));
148 painter.rect_filled(card, 8.0, visuals.window_fill.gamma_multiply(0.85));
149 painter.galley(card.min + vec2(12.0, 8.0), galley, visuals.text_color());
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 fn test_level() -> Level {
157 Level::parse(
158 r#"level "replay-test" title="T" {
159 instructions "i"
160 script {
161 camera x=0 y=0 w=10 h=10
162 pause secs=1.0
163 }
164 }"#,
165 )
166 .unwrap()
167 }
168
169 fn empty() -> (Document, DocIndex) {
171 (Document::default(), DocIndex::default())
172 }
173
174 #[test]
175 fn finished_script_reports_done_and_stops_advancing() {
176 let mut replay = Replay::new(test_level(), SourceIds::default());
177 let (doc, mut index) = empty();
178 assert!(!replay.done());
179 while replay.next_frame(&doc, &mut index).is_some() {}
181 assert!(replay.done());
182 assert_eq!(replay.begin_show(Duration::from_secs(1)), 0);
183 }
184
185 #[test]
186 fn done_waits_for_a_queued_editor_commit() {
187 let mut replay = Replay::new(test_level(), SourceIds::default());
188 let (doc, mut index) = empty();
189 while replay.next_frame(&doc, &mut index).is_some() {}
190 replay.driver().queue_enter();
191 assert!(!replay.done(), "the queued Enter still owes a frame");
192 assert!(replay.driver().idle_interaction().enter_pressed);
193 assert!(replay.done());
194 }
195}