1use std::path::{Path, PathBuf};
2use std::sync::mpsc::{Receiver, channel};
3use std::time::{Duration, SystemTime};
4
5use egui_wgpu::Renderer;
6use pfx_editor_shell::{App, Pgpu, Theme, Timing, Wake, Watch};
7use pfx_game::Config;
8use pfx_gpu::screens::ScreenPolicy;
9use pfx_gpu::{Gpu, OffscreenTarget};
10use pfx_input::{InputEvent, KeyLabels};
11use pfx_play::{Level, Reload};
12use pfx_project::Project;
13
14use crate::audio::Audio;
15use crate::editor::{Editor, Play, Request};
16use crate::input::Layout;
17use crate::view::{Stage, View};
18
19pub const STATUS_EVERY: Duration = Duration::from_secs(3);
20pub const MAX_SECONDS: f32 = 0.25;
21
22pub type Factory = pfx_game::Factory;
23
24pub const FLAT_SCENE: &str = "tmp/pfx/flat.scene.toml";
25
26pub fn free_policy() -> ScreenPolicy {
27 ScreenPolicy {
28 deck: Vec::new(),
29 desktop: Vec::new(),
30 ..ScreenPolicy::default()
31 }
32}
33
34pub fn policy_of(project: Option<&Project>) -> ScreenPolicy {
35 project
36 .and_then(Project::screen)
37 .and_then(|table| ScreenPolicy::from_table(table).ok())
38 .unwrap_or_else(free_policy)
39}
40
41pub fn empty_world(project: &Project) -> Result<PathBuf, String> {
42 let path = project.root().join(FLAT_SCENE);
43 if !path.is_file() {
44 let folder = path.parent().unwrap_or(project.root());
45 std::fs::create_dir_all(folder)
46 .map_err(|error| format!("{}: {error}", folder.display()))?;
47 std::fs::write(&path, "format = 1\n")
48 .map_err(|error| format!("{}: {error}", path.display()))?;
49 }
50 Ok(path)
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub struct Stamp {
55 modified: Option<SystemTime>,
56 len: u64,
57}
58
59fn stamp(file: &Path) -> Option<Stamp> {
60 let meta = std::fs::metadata(file).ok()?;
61 Some(Stamp {
62 modified: meta.modified().ok(),
63 len: meta.len(),
64 })
65}
66
67pub struct Session {
68 pub editor: Editor,
69 pub view: Option<Box<View>>,
70 setup: Setup,
71 factory: Option<Factory>,
72 config: Option<Config>,
73 wake: Option<Wake>,
74 status: Option<Receiver<String>>,
75 seconds: f32,
76 watched: Vec<PathBuf>,
77 modules: Vec<PathBuf>,
78 stamps: Vec<Option<Stamp>>,
79 held: bool,
80 drawn: bool,
81 modifiers: egui::Modifiers,
82 layout: Option<Layout>,
83 labels: KeyLabels,
84}
85
86#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87pub struct Setup {
88 pub pgpu: bool,
89 pub audio: bool,
90}
91
92pub fn status_watch(wake: impl Fn() + Send + 'static) -> Receiver<String> {
93 let (send, receive) = channel();
94 std::thread::spawn(move || {
95 loop {
96 let line = match crate::pgpu::status() {
97 Ok(status) => status.line(),
98 Err(error) => error,
99 };
100 if send.send(line).is_err() {
101 return;
102 }
103 wake();
104 std::thread::sleep(STATUS_EVERY);
105 }
106 });
107 receive
108}
109
110impl Session {
111 pub fn open(path: &Path, setup: Setup) -> Result<Session, String> {
112 let editor = Editor::open(path)?;
113 let watched = editor.files();
114 Ok(Session {
115 editor,
116 view: None,
117 setup,
118 factory: None,
119 config: None,
120 wake: None,
121 status: None,
122 seconds: 0.0,
123 watched,
124 modules: Vec::new(),
125 stamps: Vec::new(),
126 held: false,
127 drawn: false,
128 modifiers: egui::Modifiers::NONE,
129 layout: crate::input::layout(None),
130 labels: KeyLabels::new(),
131 })
132 }
133
134 pub fn open_project(
135 folder: &Path,
136 setup: Setup,
137 factory: Option<Factory>,
138 ) -> Result<Session, String> {
139 Session::open_with(folder, setup, factory, None)
140 }
141
142 pub fn open_game(
143 folder: &Path,
144 setup: Setup,
145 factory: Factory,
146 config: Config,
147 ) -> Result<Session, String> {
148 Session::open_with(folder, setup, Some(factory), Some(config))
149 }
150
151 fn open_with(
152 folder: &Path,
153 setup: Setup,
154 factory: Option<Factory>,
155 mut config: Option<Config>,
156 ) -> Result<Session, String> {
157 let project = Project::folder(folder)?;
158 match &config {
159 Some(config) => config.bind_text(),
160 None => {
161 pfx_load::scene::bind("name", project.name());
162 if let Some(version) = project.version() {
163 pfx_load::scene::bind("version", &version);
164 }
165 }
166 }
167 let flat = project.flat() || config.as_ref().is_some_and(|config| config.scene.is_none());
168 let (first, empty) = match project.first() {
169 Some(first) => (first, false),
170 None if flat => (empty_world(&project)?, true),
171 None => {
172 return Err(format!(
173 "{}: no *.scene.toml in this project to open",
174 project.root().display()
175 ));
176 }
177 };
178 let select = project.select().map(str::to_string);
179 let name = project.name().to_string();
180 let mut session = Session::open(&first, setup)?;
181 session.editor.set_project(project);
182 if empty {
183 session.editor.log.info(format!(
184 "project {name} is flat and has no scene: the editor shows an empty world, and F5 plays the game on it"
185 ));
186 }
187 if let Some(name) = select
188 && session.editor.scene().object(&name).is_some()
189 {
190 session
191 .editor
192 .select(Some(crate::outline::Item::Object(name)));
193 }
194 if let Some(config) = &config {
195 session.editor.app = crate::editor::version_line(&config.name, &config.version);
196 }
197 session.factory = factory;
198 session.layout =
199 crate::input::layout(config.as_mut().and_then(|config| config.layout.take()));
200 session.config = config;
201 Ok(session)
202 }
203
204 pub fn flat(&self) -> bool {
205 self.project().is_some_and(Project::flat)
206 || self
207 .config
208 .as_ref()
209 .is_some_and(|config| config.scene.is_none())
210 }
211
212 fn scene_config(&self) -> Config {
213 let name = match self.project() {
214 Some(project) => project.name().to_string(),
215 None => self.editor.name(),
216 };
217 Config::flat(name, env!("CARGO_PKG_VERSION")).screen(policy_of(self.project()))
218 }
219
220 pub fn project(&self) -> Option<&Project> {
221 self.editor.project.as_ref()
222 }
223
224 pub fn plays_a_game(&self) -> bool {
225 self.factory.is_some()
226 }
227
228 pub fn switch(&mut self, scene: &Path) -> Result<(), String> {
229 if let Some(view) = &mut self.view {
230 view.stop_play(&mut self.editor, false);
231 }
232 self.editor.end_group();
233 let mut editor = Editor::open(scene)?;
234 editor.muted = self.editor.muted;
235 editor.app = std::mem::take(&mut self.editor.app);
236 editor.pgpu = self.editor.pgpu.take();
237 editor.sound = self.editor.sound.take();
238 editor.sound_available = self.editor.sound_available;
239 editor.screen = self.editor.screen;
240 if let Some(wake) = &self.wake {
241 editor.viewport.set_wake(wake.clone());
242 }
243 if self.held {
244 editor.viewport.set_pgpu(Pgpu::Play);
245 }
246 let mut log = std::mem::take(&mut self.editor.log);
247 for entry in std::mem::take(&mut editor.log.entries) {
248 log.push(entry);
249 }
250 editor.log = log;
251 if let Some(project) = self.editor.project.take() {
252 editor.project = Some(project);
253 editor.frame_project();
254 editor.refresh();
255 }
256 self.editor = editor;
257 if let Some(view) = &mut self.view {
258 view.forget_trace();
259 }
260 self.drawn = false;
261 Ok(())
262 }
263
264 pub fn held(&self) -> bool {
265 self.held
266 }
267
268 pub fn pixels(&self) -> Result<Vec<u8>, String> {
269 let view = self.view.as_ref().ok_or("no frame has run yet")?;
270 if view.playing() || (self.editor.trace && view.trace_samples() > 0) {
271 return view.pixels();
272 }
273 let live = self
274 .editor
275 .viewport
276 .live()
277 .ok_or("the viewport has not drawn")?;
278 let texture = live.image().clone();
279 let [width, height] = live.size();
280 let target = OffscreenTarget {
281 view: texture.create_view(&Default::default()),
282 texture,
283 format: pfx_editor_viewport::live::FORMAT,
284 width,
285 height,
286 };
287 view.gpu.readback_bytes(&target)
288 }
289
290 fn playing(&self) -> bool {
291 self.view.as_deref().is_some_and(View::playing)
292 }
293
294 fn game_modules(&mut self) -> Option<&mut dyn Reload> {
295 self.view.as_mut()?.session()?.game_mut().modules()
296 }
297
298 pub fn poll_modules(&mut self) {
299 let files = self
300 .game_modules()
301 .map(|modules| modules.files())
302 .unwrap_or_default();
303 if files != self.modules {
304 if !files.is_empty() {
305 self.editor.log.info(format!(
306 "modules: watching {} built module{} while the game plays",
307 files.len(),
308 if files.len() == 1 { "" } else { "s" }
309 ));
310 }
311 self.stamps = files.iter().map(|file| stamp(file)).collect();
312 self.modules = files;
313 return;
314 }
315 let mut rebuilt = Vec::new();
316 for (file, last) in self.modules.iter().zip(&mut self.stamps) {
317 let now = stamp(file);
318 if now != *last {
319 *last = now;
320 if now.is_some() {
321 rebuilt.push(file.clone());
322 }
323 }
324 }
325 let Some(modules) = self.game_modules() else {
326 return;
327 };
328 for file in &rebuilt {
329 modules.changed(file);
330 }
331 for file in rebuilt {
332 let name = match &self.editor.project {
333 Some(project) => project.relative(&file),
334 None => file.display().to_string(),
335 };
336 self.editor.log.info(format!(
337 "modules: {name} was rebuilt; it swaps in at the next tick"
338 ));
339 }
340 }
341
342 fn module_notices(&mut self) {
343 let Some(driver) = self.view.as_mut().and_then(|view| view.driver()) else {
344 return;
345 };
346 let notices = driver.take_notices();
347 for notice in notices {
348 let text = format!("modules: {}", notice.text);
349 match notice.level {
350 Level::Info => self.editor.log.info(text),
351 Level::Warn => self.editor.log.warn(text),
352 Level::Error => self.editor.log.error(text),
353 }
354 }
355 }
356
357 fn size(&self, pixels_per_point: f32) -> [u32; 2] {
358 let ppp = pixels_per_point.max(0.25);
359 let screen = self.editor.screen;
360 [
361 (screen.width() * ppp).round().max(1.0) as u32,
362 (screen.height() * ppp).round().max(1.0) as u32,
363 ]
364 }
365
366 fn view(&mut self, gpu: &Gpu, pixels_per_point: f32) -> &mut View {
367 let size = self.size(pixels_per_point);
368 let audio = self.setup.audio;
369 self.view.get_or_insert_with(|| {
370 let audio = if audio { Audio::device() } else { Audio::off() };
371 Box::new(View::open(gpu.clone(), size, audio))
372 })
373 }
374
375 fn requests(&mut self) {
376 let mut requests = self.editor.take_requests();
377 let mut opens = Vec::new();
378 requests.retain(|request| match request {
379 Request::Open(scene) => {
380 opens.push(scene.clone());
381 false
382 }
383 _ => true,
384 });
385 if let Some(scene) = opens.pop() {
386 if let Err(error) = self.switch(&scene) {
387 self.editor.log.error(error);
388 }
389 return;
390 }
391 let flat = self.flat();
392 let fallback = (self.config.is_none() && requests.contains(&Request::Play))
393 .then(|| self.scene_config());
394 let Some(view) = &mut self.view else {
395 for request in requests {
396 if let Request::World(change) = request {
397 self.editor
398 .log
399 .warn(format!("no viewport for {}", change.describe()));
400 }
401 }
402 return;
403 };
404 for request in requests {
405 match request {
406 Request::Play => {
407 let Some(config) = self.config.as_ref().or(fallback.as_ref()) else {
408 continue;
409 };
410 let game = self.factory.as_ref().map(|factory| factory());
411 let stage = Stage {
412 config,
413 flat,
414 layout: self.layout.clone(),
415 labels: self.labels.clone(),
416 };
417 if let Err(error) = view.start_play(&mut self.editor, game, stage) {
418 self.editor.log.error(error);
419 }
420 }
421 Request::Pause => view.pause(&mut self.editor),
422 Request::Resume => view.resume(&mut self.editor),
423 Request::Step => view.step(&mut self.editor),
424 Request::Stop => view.stop_play(&mut self.editor, false),
425 Request::Keep => view.stop_play(&mut self.editor, true),
426 Request::Record => view.save_recording(&mut self.editor),
427 Request::World(change) => view.world_edit(&mut self.editor, &change),
428 Request::Open(_) => {}
429 }
430 }
431 }
432
433 pub fn update(
434 &mut self,
435 gpu: &Gpu,
436 painter: &mut Renderer,
437 pixels_per_point: f32,
438 ) -> Result<bool, String> {
439 if let Some(status) = &self.status
440 && let Some(line) = status.try_iter().last()
441 {
442 self.editor.pgpu = Some(line);
443 }
444 let size = self.size(pixels_per_point);
445 self.view(gpu, pixels_per_point);
446 self.requests();
447 self.poll_modules();
448 let seconds = self.seconds;
449 let mut more = false;
450 if let Some(view) = &mut self.view {
451 let resized = view.resize(painter, size)?;
452 view.sync(&mut self.editor);
453 if view.playing() {
454 if self.held {
455 self.held = false;
456 self.editor.viewport.set_pgpu(Pgpu::Free);
457 }
458 let drew = view.play(&mut self.editor, seconds);
459 if view.playing() {
460 self.editor.texture = view.shown().then(|| view.texture(painter));
461 self.drawn |= drew;
462 let more = drew || resized || view.running();
463 self.module_notices();
464 return Ok(more);
465 }
466 self.editor.texture = None;
467 more = true;
468 }
469 if self.editor.trace && !self.held {
470 self.editor.texture = Some(view.texture(painter));
471 let camera = self.editor.viewport.camera();
472 let scene = self.editor.viewport.shown();
473 if let (Some(camera), Some(scene)) = (camera, scene) {
474 let drew = view.trace_frame(scene, (camera, self.editor.revision))?;
475 self.editor.trace_samples = view.trace_samples();
476 more |= drew || resized;
477 }
478 } else {
479 self.editor.texture = None;
480 }
481 }
482 let drew = self.editor.viewport.frame(gpu, painter, pixels_per_point);
483 self.drawn |= drew;
484 Ok(more || drew)
485 }
486
487 fn feed(&mut self, ctx: &egui::Context) {
488 let was = self.modifiers;
489 self.modifiers = ctx.input(|input| input.modifiers);
490 if self.editor.play != Play::Playing || ctx.egui_wants_keyboard_input() {
491 return;
492 }
493 let Some(view) = self.view.as_deref_mut() else {
494 return;
495 };
496 let (events, ppp) = ctx.input(|input| (input.events.clone(), input.pixels_per_point));
497 if let Some(driver) = view.driver()
498 && crate::input::learn(driver.labels_mut(), &events)
499 {
500 self.labels = driver.labels().clone();
501 }
502 let Some(play) = view.session() else {
503 return;
504 };
505 let origin = self.editor.screen.min;
506 for event in crate::input::events(&events, ppp, was, self.modifiers) {
507 play.feed(match event {
508 InputEvent::PointerMoved { window: [x, y] } => InputEvent::PointerMoved {
509 window: [x - origin.x * ppp, y - origin.y * ppp],
510 },
511 event => event,
512 });
513 }
514 }
515}
516
517impl App for Session {
518 fn ui(&mut self, ui: &mut egui::Ui) {
519 let ctx = ui.ctx().clone();
520 self.seconds = ctx.input(|input| input.unstable_dt).clamp(0.0, MAX_SECONDS);
521 self.feed(&ctx);
522 self.editor.ui(ui);
523 }
524
525 fn viewport(&self) -> bool {
526 true
527 }
528
529 fn theme(&self) -> Theme {
530 crate::theme::pfx()
531 }
532
533 fn frame(&mut self, gpu: &Gpu, renderer: &mut Renderer, pixels_per_point: f32) -> bool {
534 match self.update(gpu, renderer, pixels_per_point) {
535 Ok(more) => more,
536 Err(error) => {
537 self.editor.log.error(error);
538 false
539 }
540 }
541 }
542
543 fn forget(&mut self) {
544 if let Some(view) = &mut self.view {
545 view.stop_play(&mut self.editor, false);
546 }
547 self.view = None;
548 self.editor.texture = None;
549 self.editor.viewport.forget();
550 self.drawn = false;
551 }
552
553 fn name(&self) -> &str {
554 "pfx edit"
555 }
556
557 fn label(&self) -> Option<String> {
558 Some(match &self.editor.project {
559 Some(project) => format!("{} ยท {}", project.name(), self.editor.name()),
560 None => self.editor.name(),
561 })
562 }
563
564 fn watched(&self) -> Watch {
565 Watch {
566 files: self.watched.clone(),
567 ..Watch::default()
568 }
569 }
570
571 fn changed(&mut self, files: &[PathBuf]) {
572 if files.iter().any(|file| !self.modules.contains(file)) {
573 self.editor.outside = true;
574 }
575 if files.iter().any(|file| self.modules.contains(file)) {
576 self.poll_modules();
577 }
578 }
579
580 fn waker(&mut self, wake: Wake) {
581 self.wake = Some(wake.clone());
582 self.editor.viewport.set_wake(wake.clone());
583 if self.setup.pgpu && self.status.is_none() {
584 self.status = Some(status_watch(move || wake()));
585 }
586 }
587
588 fn opened(&mut self) -> bool {
589 let mut files = self.editor.files();
590 files.extend(self.modules.iter().cloned());
591 if files == self.watched {
592 return false;
593 }
594 self.watched = files;
595 true
596 }
597
598 fn pgpu(&mut self, state: Pgpu) {
599 if !self.setup.pgpu {
600 return;
601 }
602 let held = state.paused() && !self.playing();
603 if held == self.held {
604 return;
605 }
606 self.held = held;
607 self.editor
608 .viewport
609 .set_pgpu(if held { state } else { Pgpu::Free });
610 if held {
611 self.editor
612 .log
613 .info("pgpu: a game is playing; the viewport waits");
614 } else {
615 self.editor.log.info("pgpu: the viewport draws again");
616 }
617 }
618
619 fn timing(&self) -> Timing {
620 match self.view.as_deref().and_then(View::renderer) {
621 Some(renderer) => {
622 let drops = renderer.timing_drops();
623 Timing {
624 gpu_ms: renderer.timings().and_then(|timings| timings.gpu_ms()),
625 drops: drops.ring + drops.late,
626 drawn: self.drawn,
627 }
628 }
629 None => self.editor.viewport.timing(),
630 }
631 }
632}
633
634impl Drop for Session {
635 fn drop(&mut self) {
636 if let Some(view) = &mut self.view {
637 view.stop_play(&mut self.editor, false);
638 }
639 self.editor.end_group();
640 }
641}