Skip to main content

pfx_editor/
session.rs

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}