all_is_cubes_ui/apps/
session.rs

1use alloc::boxed::Box;
2use alloc::string::ToString as _;
3use alloc::sync::{Arc, Weak};
4use core::fmt;
5use core::mem;
6use core::pin::Pin;
7use core::task::{Context, Poll, Waker};
8use std::sync::RwLock;
9
10use flume::TryRecvError;
11use futures_core::future::BoxFuture;
12use sync_wrapper::SyncWrapper;
13
14use all_is_cubes::arcstr::{self, ArcStr, literal};
15use all_is_cubes::character::{self, Character, Cursor};
16use all_is_cubes::fluff::Fluff;
17use all_is_cubes::inv::ToolError;
18use all_is_cubes::listen::{self, Listen as _, Listener as _, Source as _};
19use all_is_cubes::save::WhenceUniverse;
20use all_is_cubes::sound;
21use all_is_cubes::space::{self, Space};
22use all_is_cubes::time::{self, Duration};
23use all_is_cubes::transaction::{self, Transaction as _};
24use all_is_cubes::universe::{
25    self, Handle, ReadTicket, StrongHandle, Universe, UniverseId, UniverseStepInfo,
26    UniverseTransaction,
27};
28use all_is_cubes::util::{
29    ConciseDebug, Fmt, Refmt as _, ShowStatus, StatusText, YieldProgress, YieldProgressBuilder,
30};
31use all_is_cubes_render::camera::{
32    GraphicsOptions, Layers, StandardCameras, UiViewState, Viewport,
33};
34
35use crate::apps::{FpsCounter, FrameClock, InputProcessor, InputTargets, Settings};
36use crate::ui_content::Vui;
37use crate::ui_content::notification::{self, Notification};
38use crate::vui::widgets::ProgressBarState;
39
40const LOG_FIRST_FRAMES: bool = false;
41
42const SHUTTLE_PANIC_MSG: &str = "Shuttle not returned to Session; \
43    this indicates something went wrong with main task execution";
44
45/// A game session; a bundle of a [`Universe`] and supporting elements such as
46/// a [`FrameClock`] and UI state.
47///
48/// Once we have multiplayer / client-server support, this will become the client-side
49/// structure.
50pub struct Session {
51    /// Determines the timing of simulation and drawing. The caller must arrange
52    /// to advance time in the clock.
53    pub frame_clock: FrameClock,
54
55    /// Handles (some) user input. The caller must provide input events/state to this.
56    /// [`Session`] will handle applying it to the game state.
57    pub input_processor: InputProcessor,
58
59    /// The game universe and other parts of the session that can be mutated by the
60    /// main task. See [`Shuttle`]'s documentation.
61    ///
62    /// Boxed to make the move a cheap pointer move, since `Shuttle` is a large struct.
63    shuttle: Option<Box<Shuttle>>,
64
65    /// If present, a future that is polled at the beginning of stepping,
66    /// which may read or write parts of the session state via the context it was given.
67    ///
68    /// The `SyncWrapper` ensures that `Session: Sync` even though this future need not be
69    /// (which is sound because the future is only polled with `&mut Session`).
70    ///
71    main_task: Option<SyncWrapper<BoxFuture<'static, ExitMainTask>>>,
72
73    /// Jointly owned by the main task.
74    /// The `Option` is filled only when the main task is executing.
75    /// The `RwLock` is never blocked on.
76    task_context_inner: Arc<RwLock<Option<Box<Shuttle>>>>,
77
78    paused: listen::Cell<bool>,
79
80    ambient_sound_source: listen::DynSource<sound::SpatialAmbient>,
81
82    /// Messages for controlling the state that aren't via [`InputProcessor`].
83    ///
84    /// TODO: This is originally a quick kludge to make onscreen UI buttons work.
85    /// Not sure whether it is a good strategy overall.
86    ///
87    /// Design note: Using `flume` not because we want MPMC, but because its receiver is
88    /// `Send + Sync`, unlike the `std` one.
89    /// Our choice of `flume` in particular is just because our other crates use it.
90    control_channel: flume::Receiver<ControlMessage>,
91    control_channel_sender: flume::Sender<ControlMessage>,
92
93    last_step_info: UniverseStepInfo,
94
95    tick_counter_for_logging: u8,
96}
97
98/// Data abstractly belonging to [`Session`] whose ownership is temporarily moved as needed.
99///
100/// Currently, this is between the `Session` and its [`MainTaskContext`], but in the future it
101/// might also be moved to a background task to allow the session stepping to occur independent
102/// of the event loop or other owner of the `Session`.
103struct Shuttle {
104    settings: Settings,
105
106    game_universe: Universe,
107
108    /// Subset of information from `game_universe` that is largely immutable and can be
109    /// meaningfully listened to.
110    game_universe_info: listen::Cell<SessionUniverseInfo>,
111
112    /// Character we're designating as “the player character”.
113    /// Always a member of `game_universe`.
114    game_character: listen::CellWithLocal<Option<StrongHandle<Character>>>,
115
116    ui: Option<Vui>,
117
118    space_watch_state: Layers<SpaceWatchState>,
119
120    control_channel_sender: flume::Sender<ControlMessage>,
121
122    /// Last cursor raycast result.
123    cursor_result: Option<Cursor>,
124
125    /// Outputs [`Fluff`] from the game character's viewpoint and also the session UI.
126    //---
127    // TODO: should include spatial information and source information
128    fluff_notifier: Arc<listen::Notifier<Fluff>>,
129
130    ambient_sound_state: listen::CellWithLocal<sound::SpatialAmbient>,
131
132    /// Notifies when the session transitions to particular states.
133    session_event_notifier: Arc<listen::Notifier<Event>>,
134
135    quit_fn: Option<QuitFn>,
136
137    custom_commands: listen::CellWithLocal<Arc<[crate::ui_content::Command]>>,
138}
139
140impl fmt::Debug for Session {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        let Self {
143            frame_clock,
144            input_processor,
145            main_task,
146            task_context_inner: _,
147            paused,
148            ambient_sound_source: _,
149            control_channel: _,
150            control_channel_sender: _,
151            last_step_info,
152            tick_counter_for_logging,
153
154            shuttle,
155        } = self;
156
157        let Some(shuttle) = shuttle else {
158            write!(f, "Session(is in the middle of an operation)")?;
159            return Ok(());
160        };
161        let Shuttle {
162            settings,
163            game_universe,
164            game_universe_info,
165            game_character,
166            space_watch_state,
167            ui,
168            control_channel_sender: _,
169            cursor_result,
170            quit_fn,
171            fluff_notifier,
172            ambient_sound_state,
173            session_event_notifier,
174            custom_commands,
175        } = &**shuttle;
176
177        f.debug_struct("Session")
178            .field("frame_clock", frame_clock)
179            .field("input_processor", input_processor)
180            .field("settings", settings)
181            .field("game_universe", game_universe)
182            .field("game_universe_info", game_universe_info)
183            .field("game_character", game_character)
184            .field("space_watch_state", space_watch_state)
185            .field("main_task", &main_task.as_ref().map(|_| "..."))
186            .field("session_event_notifier", session_event_notifier)
187            .field("fluff_notifier", fluff_notifier)
188            .field("ambient_sound_state", ambient_sound_state)
189            .field("paused", &paused)
190            .field("ui", &ui)
191            .field(
192                "quit_fn",
193                &quit_fn.as_ref().map(|_cant_print_a_function| ()),
194            )
195            .field("custom_commands", &custom_commands)
196            .field("cursor_result", &cursor_result)
197            .field("last_step_info", &last_step_info)
198            .field("tick_counter_for_logging", &tick_counter_for_logging)
199            .finish_non_exhaustive()
200    }
201}
202
203impl Session {
204    /// Returns a [`SessionBuilder`] with which to construct a new [`Session`].
205    pub fn builder() -> SessionBuilder {
206        SessionBuilder::default()
207    }
208
209    #[track_caller]
210    fn shuttle(&self) -> &Shuttle {
211        self.shuttle.as_ref().expect(SHUTTLE_PANIC_MSG)
212    }
213    #[track_caller]
214    fn shuttle_mut(&mut self) -> &mut Shuttle {
215        self.shuttle.as_mut().expect(SHUTTLE_PANIC_MSG)
216    }
217
218    /// Returns a source for the [`Character`] that should be shown to the user.
219    pub fn character(&self) -> listen::DynSource<Option<StrongHandle<Character>>> {
220        self.shuttle().game_character.as_source()
221    }
222
223    /// Replaces the game universe, such as for initial setup or because the player
224    /// chose to load a new one.
225    /// This also resets the character to be the new universe's default character.
226    pub fn set_universe(&mut self, universe: Box<Universe>) {
227        self.shuttle_mut().set_universe(universe);
228    }
229
230    /// Set the character which this session is “looking through the eyes of”.
231    /// It must be from the universe previously set with `set_universe()`.
232    pub fn set_character(&mut self, character: Option<StrongHandle<Character>>) {
233        let shuttle = self.shuttle_mut();
234        if let Some(character) = &character {
235            assert!(character.universe_id() == Some(shuttle.game_universe.universe_id()));
236        }
237
238        shuttle.game_character.set(character);
239        shuttle.sync_universe_and_character_derived();
240    }
241
242    /// Install a main task in the session, replacing any existing one.
243    ///
244    /// The main task is an `async` task (that is, a [`Future`] that will be polled without further
245    /// intervention) which executes interleaved with this session's routine operations done by
246    /// [`Session::maybe_step_universe()`], and therefore has the opportunity to intervene in them.
247    ///
248    ///
249    /// The main task is given a [`MainTaskContext`] by which it can manipulate the session.
250    /// The context will panic if it is used at times when the main task is not running.
251    pub fn set_main_task<F>(&mut self, task_ctor: F)
252    where
253        F: async_fn_traits::AsyncFnOnce1<
254                MainTaskContext,
255                Output = ExitMainTask,
256                OutputFuture: Send + 'static,
257            >,
258    {
259        let context = MainTaskContext {
260            shuttle: self.task_context_inner.clone(),
261            session_event_notifier: Arc::downgrade(&self.shuttle().session_event_notifier),
262        };
263        self.main_task = Some(SyncWrapper::new(Box::pin(task_ctor(context))));
264    }
265
266    /// Returns the current game universe owned by this session.
267    pub fn universe(&self) -> &Universe {
268        &self.shuttle().game_universe
269    }
270
271    /// Returns a mutable reference to the [`Universe`].
272    ///
273    /// Note: Replacing the universe will not update the UI and character state.
274    /// Use [`Self::set_universe()`] instead.
275    pub fn universe_mut(&mut self) -> &mut Universe {
276        &mut self.shuttle_mut().game_universe
277    }
278
279    /// Allows observing replacement of the current universe in this session, or updates to its
280    /// [`WhenceUniverse`].
281    pub fn universe_info(&self) -> listen::DynSource<SessionUniverseInfo> {
282        self.shuttle().game_universe_info.as_source()
283    }
284
285    /// What the renderer should be displaying on screen for the UI.
286    pub fn ui_view(&self) -> listen::DynSource<Arc<UiViewState>> {
287        self.shuttle().ui_view()
288    }
289
290    /// Returns [`ReadTicket`]s for the current game universe and UI universe, suitable for passing to
291    /// a renderer.
292    pub fn read_tickets(&self) -> Layers<ReadTicket<'_>> {
293        self.shuttle().read_tickets()
294    }
295
296    /// Allows reading, and observing changes to, the current graphics options.
297    pub fn graphics_options(&self) -> listen::DynSource<Arc<GraphicsOptions>> {
298        self.shuttle().settings.as_source()
299    }
300
301    /// Allows changing the settings associated with this session.
302    ///
303    /// Note that these settings may be shared with other sessions.
304    pub fn settings(&self) -> &Settings {
305        &self.shuttle().settings
306    }
307
308    /// Returns a [`StandardCameras`] which may be used in rendering a view of this session,
309    /// including following changes to the current character or universe.
310    ///
311    /// They will be freshly [updated][StandardCameras::update].
312    pub fn create_cameras(&self, viewport_source: listen::DynSource<Viewport>) -> StandardCameras {
313        let mut c = StandardCameras::new(
314            self.graphics_options(),
315            viewport_source,
316            self.character(),
317            self.ui_view(),
318        );
319        c.update(self.read_tickets());
320        c
321    }
322
323    /// Listen for [`Fluff`] events from this session. Fluff constitutes short-duration
324    /// sound or particle effects.
325    pub fn listen_fluff(&self, listener: impl listen::Listener<Fluff> + Send + Sync + 'static) {
326        self.shuttle().fluff_notifier.listen(listener)
327    }
328
329    /// Source of the current ambient sound environment from the game world.
330    pub fn ambient_sound(&self) -> listen::DynSource<sound::SpatialAmbient> {
331        self.ambient_sound_source.clone()
332    }
333
334    /// Steps the universe if the [`FrameClock`] says it's time to do so.
335    /// Also may update other session state from any incoming events.
336    ///
337    /// Always returns info for the last step even if multiple steps were taken.
338    pub fn maybe_step_universe(&mut self) -> Option<UniverseStepInfo> {
339        self.process_control_messages_and_stuff();
340
341        // Let main task do things before the step due to external inputs.
342        self.poll_main_task();
343
344        // Process any messages generated by the main task
345        self.process_control_messages_and_stuff();
346
347        let mut step_result = None;
348        // TODO: Catch-up implementation should probably live in FrameClock.
349        for _ in 0..FrameClock::CATCH_UP_STEPS {
350            if self.frame_clock.should_step() {
351                let shuttle = self.shuttle.as_mut().expect(SHUTTLE_PANIC_MSG);
352                let u_clock = shuttle.game_universe.clock();
353                let paused = self.paused.get();
354                let ui_tick = u_clock.next_tick(false);
355                let game_tick = u_clock.next_tick(paused);
356
357                self.frame_clock.did_step(u_clock.schedule());
358
359                self.input_processor
360                    .apply_input(
361                        InputTargets {
362                            universe: Some(&mut shuttle.game_universe),
363                            character: shuttle
364                                .game_character
365                                .get()
366                                .as_ref()
367                                .map(StrongHandle::as_ref),
368                            paused: Some(&self.paused),
369                            settings: Some(&shuttle.settings),
370                            control_channel: Some(&self.control_channel_sender),
371                            ui: shuttle.ui.as_ref(),
372                        },
373                        game_tick,
374                    )
375                    .expect("applying input failed");
376                // TODO: switch from FrameClock tick to asking the universe for its tick
377                self.input_processor.step(game_tick);
378
379                // TODO(time-budget): better timing policy that explicitly trades off with time spent
380                // on rendering, event handling, etc.
381                // (That policy should probably live in `frame_clock`.)
382                // TODO(time-budget): give UI a time that reflects how much time it needs, rather
383                // than arbitrarily partitioning the delta_t
384                let step_start_time = time::Instant::now();
385                let dt = game_tick.delta_t();
386                let deadlines = Layers {
387                    world: time::Deadline::At(step_start_time + dt / 2),
388                    ui: time::Deadline::At(step_start_time + dt / 2 + dt / 4),
389                };
390
391                let mut info = shuttle.game_universe.step(paused, deadlines.world);
392                if let Some(ui) = &mut shuttle.ui {
393                    info += ui.step(ui_tick, deadlines.ui, shuttle.game_universe.read_ticket());
394                }
395
396                if LOG_FIRST_FRAMES && self.tick_counter_for_logging <= 10 {
397                    self.tick_counter_for_logging = self.tick_counter_for_logging.saturating_add(1);
398                    log::debug!(
399                        "tick={} step {}",
400                        self.tick_counter_for_logging,
401                        info.computation_time.refmt(&ConciseDebug)
402                    );
403                }
404                self.last_step_info = info.clone();
405                step_result = Some(info);
406
407                // --- Post-step activities ---
408
409                shuttle.session_event_notifier.notify(&Event::Stepped);
410
411                // Let main task do things triggered by the step.
412                // Note that we do this once per step even with catch-up.
413                self.poll_main_task();
414
415                // Process any messages generated by stepping (from a behavior or main task).
416                self.process_control_messages_and_stuff();
417            }
418        }
419        step_result
420    }
421
422    /// Call this each time something happens the main task might care about.
423    fn poll_main_task(&mut self) {
424        // TODO: for efficiency, use a waker
425        if let Some(sync_wrapped_future) = self.main_task.as_mut() {
426            {
427                let mut shuttle_guard = match self.task_context_inner.write() {
428                    Ok(g) => g,
429                    Err(poisoned) => poisoned.into_inner(),
430                };
431                *shuttle_guard = Some(
432                    self.shuttle
433                        .take()
434                        .expect("Session lost its shuttle before polling the main task"),
435                );
436            }
437            // Reset on drop even if the future panics
438            let _reset_on_drop = scopeguard::guard((), |()| {
439                let mut shuttle_guard = match self.task_context_inner.write() {
440                    Ok(g) => g,
441                    Err(poisoned) => poisoned.into_inner(),
442                };
443                if let Some(shuttle) = shuttle_guard.take() {
444                    self.shuttle = Some(shuttle);
445                } else {
446                    log::error!(
447                        "Session lost its shuttle while polling the main task and will be unusable"
448                    );
449                }
450                *shuttle_guard = None;
451            });
452
453            let future: Pin<&mut dyn Future<Output = ExitMainTask>> =
454                sync_wrapped_future.get_mut().as_mut();
455            match future.poll(&mut Context::from_waker(Waker::noop())) {
456                Poll::Pending => {}
457                Poll::Ready(ExitMainTask) => {
458                    self.main_task = None;
459                }
460            }
461        }
462    }
463
464    fn process_control_messages_and_stuff(&mut self) {
465        loop {
466            match self.control_channel.try_recv() {
467                Ok(msg) => match msg {
468                    ControlMessage::Back => {
469                        // TODO: error reporting … ? hm.
470                        if let Some(ui) = &mut self.shuttle_mut().ui {
471                            ui.back();
472                        }
473                    }
474                    ControlMessage::ShowModal(message) => {
475                        self.show_modal_message(message);
476                    }
477                    ControlMessage::EnterDebug => {
478                        let shuttle = self.shuttle_mut();
479                        if let Some(ui) = &mut shuttle.ui {
480                            if let Some(cursor) = &shuttle.cursor_result {
481                                ui.enter_debug(shuttle.game_universe.read_ticket(), cursor);
482                            } else {
483                                // TODO: not actually a click
484                                ui.show_click_result(usize::MAX, Err(ToolError::NothingSelected));
485                            }
486                        }
487                    }
488                    ControlMessage::Save => {
489                        // TODO: Make this asynchronous. We will need to suspend normal
490                        // stepping during that period.
491                        let u = &self.shuttle().game_universe;
492                        let fut = u.whence.save(
493                            u,
494                            YieldProgressBuilder::new()
495                                .yield_using(|_| async {}) // noop yield
496                                .build(),
497                        );
498                        match futures_util::FutureExt::now_or_never(fut) {
499                            Some(Ok(())) => {
500                                // TODO: show a momentary "Saved!" message
501                            }
502                            Some(Err(e)) => {
503                                self.show_modal_message(arcstr::format!(
504                                    "{}",
505                                    all_is_cubes::util::ErrorChain(&*e)
506                                ));
507                            }
508                            None => {
509                                self.show_modal_message(
510                                    "unsupported: saving did not complete synchronously".into(),
511                                );
512                            }
513                        }
514                    }
515                    ControlMessage::TogglePause => {
516                        self.paused.set(!self.paused.get());
517                    }
518                    ControlMessage::ToggleMouselook => {
519                        self.input_processor.toggle_mouselook_mode();
520                    }
521                    ControlMessage::ModifySettings(function) => {
522                        function(&self.shuttle().settings);
523                    }
524                },
525                Err(TryRecvError::Empty) => break,
526                Err(TryRecvError::Disconnected) => {
527                    // Lack of whatever control sources is non-fatal.
528                    break;
529                }
530            }
531        }
532
533        self.shuttle_mut().sync_universe_and_character_derived();
534    }
535
536    /// Call this once per frame to update the cursor raycast.
537    ///
538    /// TODO: bad API; revisit general cursor handling logic.
539    /// We'd like to not have too much dependencies on the rendering, but also
540    /// not obligate each platform/renderer layer to have too much boilerplate.
541    pub fn update_cursor(&mut self, cameras: &StandardCameras) {
542        let cursor_result = self.input_processor.cursor_ndc_position().and_then(|ndc_pos| {
543            cameras
544                .project_cursor(self.read_tickets(), ndc_pos)
545                .expect("shouldn't happen: read error in update_cursor()")
546        });
547        self.shuttle_mut().cursor_result = cursor_result;
548    }
549
550    /// Returns the [`Cursor`] computed by the last call to [`Session::update_cursor()`].
551    pub fn cursor_result(&self) -> Option<&Cursor> {
552        self.shuttle().cursor_result.as_ref()
553    }
554
555    /// Returns the suggested mouse-pointer/cursor appearance for the current [`Cursor`]
556    /// as computed by the last call to [`Session::update_cursor()`].
557    ///
558    /// Note that this does not report any information about whether the pointer should be
559    /// *hidden*. (TODO: Should we change that?)
560    pub fn cursor_icon(&self) -> &CursorIcon {
561        match self.shuttle().cursor_result {
562            // TODO: add more distinctions.
563            // * Non-clickable UI should get normal arrow cursor.
564            // * Maybe a lack-of-world should be indicated with a disabled cursor.
565            None => &CursorIcon::Crosshair,
566            Some(_) => &CursorIcon::PointingHand,
567        }
568    }
569
570    /// Display a dialog box with a message. The user can exit the dialog box to return
571    /// to the previous UI page.
572    ///
573    /// The message may contain newlines and will be word-wrapped.
574    ///
575    /// If this session was constructed without UI then the message will be logged instead.
576    ///
577    /// Caution: calling this repeatedly will currently result in stacking up arbitrary
578    /// numbers of dialogs. Avoid using it for situations not in response to user action.
579    pub fn show_modal_message(&mut self, message: ArcStr) {
580        if let Some(ui) = &mut self.shuttle_mut().ui {
581            ui.show_modal_message(message);
582        } else {
583            log::info!("UI message not shown: {message}");
584        }
585    }
586
587    /// Display a notification to the user. Notifications persist until dismissed or the returned
588    /// [`Notification`] handle is dropped, and their content may be updated through that handle.
589    ///
590    /// Returns an error if there is no UI to display notifications or if there are too many.
591    pub fn show_notification(
592        &mut self,
593        content: impl Into<notification::NotificationContent>,
594    ) -> Result<Notification, notification::Error> {
595        // TODO: stop requiring mut by using a dedicated channel...?
596        match &mut self.shuttle_mut().ui {
597            Some(ui) => Ok(ui.show_notification(content)),
598            None => Err(notification::Error::NoUi),
599        }
600    }
601
602    /// Handle a mouse-click event, at the position specified by the last
603    /// [`Self::update_cursor()`].
604    ///
605    /// TODO: Clicks should be passed through `InputProcessor` instead of being an entirely separate path.
606    pub fn click(&mut self, button: usize) {
607        // TODO: This function has no tests.
608
609        let result = self.shuttle_mut().click_impl(button);
610
611        // Now, do all the _reporting_ of the tool's success or failure.
612        // (The architectural reason this isn't inside of the use_tool() itself is so that
613        // it is possible to use a tool more silently. That may or may not be a good idea.)
614
615        if let Err(error @ ToolError::Internal(_)) = &result {
616            // Log the message because the UI text field currently doesn't
617            // fit long errors at all.
618            log::error!(
619                "Error applying tool: {error}",
620                error = all_is_cubes::util::ErrorChain(&error)
621            );
622        }
623
624        if let Err(error) = &result {
625            let shuttle = self.shuttle();
626            for fluff in error.fluff() {
627                shuttle.fluff_notifier.notify(&fluff);
628            }
629        } else {
630            // success effects should come from the tool's transaction
631        }
632
633        if let Some(ui) = &self.shuttle_mut().ui {
634            ui.show_click_result(button, result);
635        }
636    }
637
638    /// Invoke the [`SessionBuilder::quit()`] callback as if the user clicked a quit button inside
639    /// our UI.
640    ///
641    /// This may be used in response to a window's close button, for example.
642    ///
643    /// The session state *may* decline to actually call the callback, such as if there are
644    /// user-visible unsaved changes.
645    ///
646    /// The returned future will produce a [`QuitCancelled`] value if quitting was unsuccessful for
647    /// any reason. If it is successful, the future never resolves. It is not necessary to poll
648    /// the future if the result value is not wanted.
649    pub fn quit(&self) -> impl Future<Output = QuitResult> + Send + 'static + use<> {
650        self.shuttle().quit()
651    }
652
653    /// Returns textual information intended to be overlaid as a HUD on top of the rendered scene
654    /// containing diagnostic information about rendering and stepping.
655    pub fn info_text<T: Fmt<StatusText>>(&self, render: T) -> InfoText<'_, T> {
656        let fopt = StatusText {
657            show: self.shuttle().settings.get_graphics_options().debug_info_text_contents,
658        };
659
660        if LOG_FIRST_FRAMES && self.tick_counter_for_logging <= 10 {
661            log::debug!(
662                "tick={} draw {}",
663                self.tick_counter_for_logging,
664                render.refmt(&fopt)
665            )
666        }
667
668        InfoText {
669            session: self,
670            render,
671            fopt,
672        }
673    }
674
675    #[doc(hidden)] // TODO: Decide whether we want FpsCounter in our public API
676    pub fn draw_fps_counter(&self) -> &FpsCounter {
677        self.frame_clock.draw_fps_counter()
678    }
679}
680
681/// Methods on `Shuttle` are those operations that can be called from both [`Session`] and
682/// [ `MainTaskContext`].
683impl Shuttle {
684    fn set_universe(&mut self, universe: Box<Universe>) {
685        self.game_universe = *universe;
686        self.game_character
687            .set(self.game_universe.get_default_character().map(StrongHandle::new));
688
689        self.sync_universe_and_character_derived();
690        // TODO: Need to sync FrameClock's schedule with the universe in case it is different
691    }
692
693    fn quit(&self) -> impl Future<Output = QuitResult> + Send + 'static + use<> {
694        let fut: BoxFuture<'static, QuitResult> = match (&self.ui, &self.quit_fn) {
695            (Some(ui), _) => Box::pin(ui.quit()),
696            (None, Some(quit_fn)) => Box::pin(std::future::ready(quit_fn())),
697            (None, None) => Box::pin(std::future::ready(Err(QuitCancelled::Unsupported))),
698        };
699        fut
700    }
701
702    /// What the renderer should be displaying on screen for the UI.
703    fn ui_view(&self) -> listen::DynSource<Arc<UiViewState>> {
704        match &self.ui {
705            Some(ui) => ui.view(),
706            None => listen::constant(Arc::new(UiViewState::default())), // TODO: cache this to allocate less
707        }
708    }
709
710    /// Implementation of click interpretation logic, called by [`Self::click`].
711    /// TODO: This function needs tests.
712    fn click_impl(&mut self, button: usize) -> Result<(), ToolError> {
713        let cursor_space = self.cursor_result.as_ref().map(|c| c.space());
714        // TODO: A better condition for this would be "is one of the spaces in the UI universe"
715        if let Some(ui) = self
716            .ui
717            .as_mut()
718            .filter(|ui| cursor_space.is_some() && cursor_space == ui.view().get().space.as_ref())
719        {
720            ui.click(button, self.cursor_result.clone())
721        } else {
722            // Otherwise, it's a click inside the game world (even if the cursor hit nothing at all).
723            // Character::click will validate against being a click in the wrong space.
724            if let Some(character_handle) = self.game_character.get() {
725                let transaction = Character::click(
726                    self.game_universe.read_ticket(),
727                    character_handle.to_weak(),
728                    self.cursor_result.as_ref(),
729                    button,
730                )?;
731                transaction
732                    .execute(&mut self.game_universe, (), &mut transaction::no_outputs)
733                    .map_err(|e| ToolError::Internal(e.to_string()))?;
734
735                // Spend a little time doing light updates, to ensure that changes right in front of
736                // the player are clean (and not flashes of blackness).
737                if let Some(space_handle) = self.cursor_result.as_ref().map(Cursor::space) {
738                    let _ = self.game_universe.mutate_space(space_handle, |m| {
739                        m.evaluate_light_for_time(Duration::from_millis(1));
740                    });
741                }
742
743                Ok(())
744            } else {
745                Err(ToolError::NoTool)
746            }
747        }
748    }
749
750    /// Update derived information that might have changed.
751    ///
752    /// * Check if the current game character's current space differs from the current
753    ///   `SpaceWatchState`, and update the latter if so.
754    /// * Check if the universe or the universe's `WhenceUniverse` have changed.
755    /// * Sync sound state.
756    fn sync_universe_and_character_derived(&mut self) {
757        // Sync game_universe_info. The WhenceUniverse might in principle be overwritten any time.
758        self.game_universe_info.set_if_unequal(SessionUniverseInfo {
759            id: self.game_universe.universe_id(),
760            whence: Arc::clone(&self.game_universe.whence),
761        });
762
763        // Sync space_watch_state.world and ambient_sound_state.
764        {
765            let character: Option<character::Read<'_>> =
766                self.game_character.get().as_ref().map(|cref| {
767                    cref.read(self.game_universe.read_ticket())
768                        .expect("TODO: decide how to handle error")
769                });
770            let space: Option<&Handle<Space>> = character.map(|ch| ch.space());
771
772            if space != self.space_watch_state.world.space.as_ref() {
773                self.space_watch_state.world = SpaceWatchState::new(
774                    self.game_universe.read_ticket(),
775                    space.cloned(),
776                    &self.fluff_notifier,
777                )
778                .expect("TODO: decide how to handle error");
779            }
780
781            let new_sound_state: &sound::SpatialAmbient = if let Some(character) = &character {
782                character.ambient_sound()
783            } else {
784                &sound::SpatialAmbient::SILENT
785            };
786            if *new_sound_state != self.ambient_sound_state.get() {
787                self.ambient_sound_state.set(new_sound_state.clone());
788            }
789        }
790
791        // Sync space_watch_state.ui.
792        {
793            // TODO: don't get() and clone every time, add a dirty flag
794            let (read_ticket, space) = match self.ui.as_ref() {
795                Some(ui) => (ui.read_ticket(), ui.view().get().space.clone()),
796                None => (ReadTicket::stub(), None),
797            };
798            if space != self.space_watch_state.ui.space {
799                self.space_watch_state.ui =
800                    SpaceWatchState::new(read_ticket, space, &self.fluff_notifier)
801                        .expect("TODO: decide how to handle error");
802            }
803        }
804    }
805
806    fn read_tickets(&self) -> Layers<ReadTicket<'_>> {
807        Layers {
808            world: self.game_universe.read_ticket(),
809            ui: match &self.ui {
810                Some(ui) => ui.read_ticket(),
811                None => ReadTicket::stub(),
812            },
813        }
814    }
815}
816
817/// Builder for providing the configuration of a new [`Session`].
818#[derive(Clone)]
819#[must_use]
820#[expect(missing_debug_implementations)]
821pub struct SessionBuilder {
822    viewport_for_ui: Option<listen::DynSource<Viewport>>,
823
824    fullscreen_state: listen::DynSource<FullscreenState>,
825    set_fullscreen: FullscreenSetter,
826
827    settings: Option<Settings>,
828
829    quit: Option<QuitFn>,
830}
831
832impl Default for SessionBuilder {
833    fn default() -> Self {
834        Self {
835            viewport_for_ui: None,
836            fullscreen_state: listen::constant(None),
837            set_fullscreen: None,
838            settings: None,
839            quit: None,
840        }
841    }
842}
843
844impl SessionBuilder {
845    /// Create the [`Session`] with configuration from this builder.
846    ///
847    /// This is an async function for the sake of cancellation and optional cooperative
848    /// multitasking, while constructing the initial state. It may safely be blocked on
849    /// from a synchronous context.
850    pub async fn build(self) -> Session {
851        let Self {
852            viewport_for_ui,
853            fullscreen_state,
854            set_fullscreen,
855            settings,
856            quit: quit_fn,
857        } = self;
858
859        let settings = settings.unwrap_or_else(|| Settings::new(Default::default()));
860        let game_character = listen::CellWithLocal::new(None);
861        let input_processor = InputProcessor::new();
862        let paused = listen::Cell::new(false);
863        let ambient_sound_state = listen::CellWithLocal::new(sound::SpatialAmbient::SILENT);
864        let (control_send, control_recv) = flume::bounded(100);
865        let custom_commands = listen::CellWithLocal::new([].into());
866
867        let space_watch_state = Layers {
868            world: SpaceWatchState::empty(),
869            ui: SpaceWatchState::empty(),
870        };
871
872        let ui = match viewport_for_ui {
873            Some(viewport) => Some(
874                Vui::new(crate::ui_content::UiTargets {
875                    mouselook_mode: input_processor.mouselook_mode(),
876                    character_source: game_character.as_source(),
877                    paused: paused.as_source(),
878                    graphics_options: settings.as_source(),
879                    app_control_channel: control_send.clone(),
880                    viewport_source: viewport,
881                    fullscreen_mode: fullscreen_state,
882                    set_fullscreen,
883                    quit: quit_fn.clone(),
884                    custom_commands: custom_commands.as_source(),
885                })
886                .await,
887            ),
888            None => None,
889        };
890
891        // Note: Putting this after the last .await avoids the future needing to be big enough to
892        // own the `Universe` (which is quite a large struct).
893        let game_universe = Universe::new();
894
895        Session {
896            frame_clock: FrameClock::new(game_universe.clock().schedule()),
897            ambient_sound_source: Arc::new(ambient_sound_state.as_source().map({
898                // TODO: need to properly register `paused` as a dependency
899                let paused = paused.as_source();
900                move |sound| {
901                    if paused.get() {
902                        sound::SpatialAmbient::SILENT
903                    } else {
904                        sound
905                    }
906                }
907            })),
908
909            shuttle: Some(Box::new(Shuttle {
910                ui,
911                settings,
912                game_character,
913                game_universe_info: listen::Cell::new(SessionUniverseInfo {
914                    id: game_universe.universe_id(),
915                    whence: game_universe.whence.clone(),
916                }),
917                game_universe: *game_universe,
918                space_watch_state,
919                cursor_result: None,
920                session_event_notifier: Arc::new(listen::Notifier::new()),
921                fluff_notifier: Arc::new(listen::Notifier::new()),
922                ambient_sound_state,
923                control_channel_sender: control_send.clone(),
924                quit_fn,
925                custom_commands,
926            })),
927            input_processor,
928            main_task: None,
929            task_context_inner: Arc::new(RwLock::new(None)),
930            paused,
931            control_channel: control_recv,
932            control_channel_sender: control_send,
933            last_step_info: UniverseStepInfo::default(),
934            tick_counter_for_logging: 0,
935        }
936    }
937
938    /// Enable graphical user interface.
939    ///
940    /// Requires knowing the expected viewport so that UI can be laid out to fit the aspect
941    /// ratio.
942    ///
943    /// If this is not called, then the session will simulate a world but not present any
944    /// controls for it other than those provided directly by the [`InputProcessor`].
945    pub fn ui(mut self, viewport: listen::DynSource<Viewport>) -> Self {
946        self.viewport_for_ui = Some(viewport);
947        self
948    }
949
950    /// Enable awareness of whether the session is being displayed full-screen.
951    ///
952    /// * `state` should report the current state (`true` = is full screen).
953    ///   A `None` value means the state is unknown.
954    /// * `setter` is a function which attempts to change the fullscreen state.
955    pub fn fullscreen(
956        mut self,
957        state: listen::DynSource<Option<bool>>,
958        setter: Option<Arc<dyn Fn(bool) + Send + Sync>>,
959    ) -> Self {
960        self.fullscreen_state = state;
961        self.set_fullscreen = setter;
962        self
963    }
964
965    /// Enable reading and writing user settings.
966    ///
967    /// The session will get a new [`Settings`] object which inherits the given settings.
968    ///
969    /// If this is not called, then the session will have all default settings,
970    /// and they will not be persisted.
971    pub fn settings_from(mut self, settings: Settings) -> Self {
972        self.settings = Some(Settings::inherit(settings));
973        self
974    }
975
976    /// Enable a “quit”/“exit” command in the session's user interface.
977    ///
978    /// This does not cause the session to self-destruct; rather, the provided callback
979    /// function should cause the session’s owner to stop presenting it to the user (and
980    /// be dropped). It may also report that the quit was cancelled for whatever reason.
981    pub fn quit(mut self, quit_fn: QuitFn) -> Self {
982        self.quit = Some(quit_fn);
983        self
984    }
985}
986
987// TODO: these should be in one struct or something.
988pub(crate) type FullscreenState = Option<bool>;
989pub(crate) type FullscreenSetter = Option<Arc<dyn Fn(bool) + Send + Sync>>;
990
991/// A message sent to the [`Session`], such as from a user interface element.
992#[non_exhaustive]
993pub(crate) enum ControlMessage {
994    /// Perform the conventional escape/back/pause function:
995    /// * navigate towards the root of a menu tree
996    /// * if at the root, return to game
997    /// * if in-game, pause and open menu
998    Back,
999
1000    /// Save the game universe back to its [`WhenceUniverse`].
1001    Save,
1002
1003    ShowModal(ArcStr),
1004
1005    EnterDebug,
1006
1007    TogglePause,
1008
1009    ToggleMouselook,
1010
1011    ModifySettings(Box<dyn FnOnce(&Settings) + Send>),
1012}
1013
1014impl fmt::Debug for ControlMessage {
1015    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1016        // Manual implementation required due to contained function.
1017        match self {
1018            Self::Back => write!(f, "Back"),
1019            Self::Save => write!(f, "Save"),
1020            Self::ShowModal(_msg) => f.debug_struct("ShowModal").finish_non_exhaustive(),
1021            Self::EnterDebug => write!(f, "EnterDebug"),
1022            Self::TogglePause => write!(f, "TogglePause"),
1023            Self::ToggleMouselook => write!(f, "ToggleMouselook"),
1024            Self::ModifySettings(_func) => f.debug_struct("ModifySettings").finish_non_exhaustive(),
1025        }
1026    }
1027}
1028
1029/// Bundle of things relating to the [`Session`] watching a particular [`Space`] that
1030/// its [`Character`] is in.
1031#[derive(Debug)]
1032struct SpaceWatchState {
1033    /// Which space this relates to watching.
1034    space: Option<Handle<Space>>,
1035
1036    /// Gates the message forwarding from the `space` to `Session::fluff_notifier`.
1037    #[expect(dead_code, reason = "acts upon being dropped")]
1038    fluff_gate: listen::Gate,
1039    // /// Camera state copied from the character, for use by fluff forwarder.
1040    // camera: Camera,
1041}
1042
1043impl SpaceWatchState {
1044    fn new(
1045        read_ticket: ReadTicket<'_>,
1046        space: Option<Handle<Space>>,
1047        fluff_notifier: &Arc<listen::Notifier<Fluff>>,
1048    ) -> Result<Self, universe::HandleError> {
1049        if let Some(space) = space {
1050            let space_read = space.read(read_ticket)?;
1051            let (fluff_gate, fluff_forwarder) =
1052                listen::Notifier::forwarder(Arc::downgrade(fluff_notifier))
1053                    .filter(|sf: &space::SpaceFluff| {
1054                        // TODO: do not discard spatial information; and add source information
1055                        Some(sf.fluff.clone())
1056                    })
1057                    .with_stack_buffer::<10>() // TODO: non-arbitrary number
1058                    .gate();
1059            space_read.fluff().listen(fluff_forwarder);
1060            Ok(Self {
1061                space: Some(space),
1062                fluff_gate,
1063            })
1064        } else {
1065            Ok(Self::empty())
1066        }
1067    }
1068
1069    fn empty() -> SpaceWatchState {
1070        Self {
1071            space: None,
1072            fluff_gate: listen::Gate::default(),
1073        }
1074    }
1075}
1076
1077/// Information about the [`Universe`] currently owned by a [`Session`].
1078///
1079/// TODO: This suspiciously resembles a struct that should be part of the universe itself...
1080#[derive(Clone, Debug)]
1081#[non_exhaustive]
1082pub struct SessionUniverseInfo {
1083    /// The [`Universe::universe_id()`] of the universe.
1084    pub id: UniverseId,
1085    /// The [`Universe::whence`] of the universe.
1086    pub whence: Arc<dyn WhenceUniverse>,
1087}
1088
1089impl PartialEq for SessionUniverseInfo {
1090    fn eq(&self, other: &Self) -> bool {
1091        self.id == other.id && Arc::ptr_eq(&self.whence, &other.whence)
1092    }
1093}
1094
1095/// Displayable data returned by [`Session::info_text()`].
1096#[derive(Copy, Clone, Debug)]
1097pub struct InfoText<'a, T> {
1098    session: &'a Session,
1099    render: T,
1100    fopt: StatusText,
1101}
1102
1103impl<T: Fmt<StatusText>> fmt::Display for InfoText<'_, T> {
1104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1105        let fopt = self.fopt;
1106        let mut empty = true;
1107        if fopt.show.contains(ShowStatus::CHARACTER)
1108            && let Some(shuttle) = self.session.shuttle.as_ref()
1109            && let Some(character_handle) = shuttle.game_character.get().as_ref()
1110        {
1111            empty = false;
1112            match character_handle.read(shuttle.game_universe.read_ticket()) {
1113                Ok(character_read) => {
1114                    write!(f, "{}", character_read.refmt(&fopt))?;
1115                }
1116                Err(_) => write!(f, "<error reading character>")?,
1117            }
1118        }
1119        if fopt.show.contains(ShowStatus::STEP) {
1120            if !mem::take(&mut empty) {
1121                write!(f, "\n\n")?;
1122            }
1123            write!(
1124                f,
1125                "{:#?}\n\nFPS: {:2.1}\n{:#?}",
1126                self.session.last_step_info.refmt(&fopt),
1127                self.session.frame_clock.draw_fps_counter().frames_per_second(),
1128                self.render.refmt(&fopt),
1129            )?;
1130        }
1131        if fopt.show.contains(ShowStatus::CURSOR) {
1132            if !mem::take(&mut empty) {
1133                write!(f, "\n\n")?;
1134            }
1135            match self.session.cursor_result() {
1136                Some(cursor) => write!(f, "{cursor}"),
1137                None => write!(f, "No block"),
1138            }?;
1139        }
1140        Ok(())
1141    }
1142}
1143
1144/// Suggested mouse pointer appearance for a given [`Cursor`] state.
1145///
1146/// Obtain this from [`Session::cursor_icon()`].
1147#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1148#[non_exhaustive]
1149pub enum CursorIcon {
1150    /// The platform's default appearance; often an arrowhead.
1151    Normal,
1152    /// A crosshair “┼”, suggesting aiming/positioning/selecting.
1153    Crosshair,
1154    /// A hand with finger extended as if to press a button.
1155    PointingHand,
1156}
1157
1158/// TODO: this should be an async fn
1159pub(crate) type QuitFn = Arc<dyn Fn() -> QuitResult + Send + Sync>;
1160pub(crate) type QuitResult = Result<QuitSucceeded, QuitCancelled>;
1161
1162/// Return type of a [`SessionBuilder::quit()`] callback on successful quit.
1163/// This is uninhabited (cannot happen) since the callback should never be observed to
1164/// finish if it successfully quits.
1165pub type QuitSucceeded = std::convert::Infallible;
1166
1167/// Return type of a [`SessionBuilder::quit()`] callback if other considerations cancelled
1168/// the quit operation. In this case, the session will return to normal operation.
1169#[derive(Clone, Copy, Debug, PartialEq)]
1170#[non_exhaustive]
1171pub enum QuitCancelled {
1172    /// Quitting is not a supported operation.
1173    ///
1174    /// [`SessionBuilder::quit()`] callback functions should not usually return this; this value is
1175    /// primarily for when there is no callback to call.
1176    Unsupported,
1177
1178    /// Some user interaction occurred before quitting, and the user indicated that the quit
1179    /// should be cancelled (for example, due to unsaved changes).
1180    UserCancelled,
1181}
1182
1183/// Indicates that a [`Session`]'s main task wishes to exit, leaving the session
1184/// only controlled externally.
1185///
1186/// This type carries no information and merely exists to distinguish an intentional exit
1187/// from accidentally returning early.
1188#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1189#[expect(clippy::exhaustive_structs)]
1190pub struct ExitMainTask;
1191
1192#[derive(Debug)]
1193enum Event {
1194    /// The session has just completed a step.
1195    Stepped,
1196}
1197
1198/// Given to the task of a [`Session::set_main_task()`] to allow manipulating the session.
1199pub struct MainTaskContext {
1200    shuttle: Arc<RwLock<Option<Box<Shuttle>>>>,
1201    session_event_notifier: Weak<listen::Notifier<Event>>,
1202}
1203
1204impl fmt::Debug for MainTaskContext {
1205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1206        f.debug_struct("MainTaskContext").finish_non_exhaustive()
1207    }
1208}
1209
1210impl MainTaskContext {
1211    /// Returns a [`StandardCameras`] which may be used in rendering a view of this session,
1212    /// including following changes to the current character or universe.
1213    ///
1214    /// They will be freshly [updated][StandardCameras::update].
1215    pub fn create_cameras(&self, viewport_source: listen::DynSource<Viewport>) -> StandardCameras {
1216        self.with_ref(|shuttle| {
1217            let mut c = StandardCameras::new(
1218                shuttle.settings.as_source(),
1219                viewport_source,
1220                shuttle.game_character.as_source(),
1221                shuttle.ui_view(),
1222            );
1223            c.update(shuttle.read_tickets());
1224            c
1225        })
1226    }
1227
1228    /// Provides a reference to the current game universe of this session.
1229    pub fn with_universe<R>(&self, f: impl FnOnce(&Universe) -> R) -> R {
1230        self.with_ref(|shuttle| f(&shuttle.game_universe))
1231    }
1232
1233    /// Provides `ReadTicket`s for access to the game and UI universes.
1234    pub fn with_read_tickets<R>(&self, f: impl FnOnce(Layers<ReadTicket<'_>>) -> R) -> R {
1235        self.with_ref(|shuttle| f(shuttle.read_tickets()))
1236    }
1237
1238    /// Executes a transaction on the game universe of this session..
1239    pub fn execute(
1240        &mut self,
1241        transaction: UniverseTransaction,
1242    ) -> Result<(), transaction::ExecuteError> {
1243        self.with_mut(|shuttle| {
1244            transaction.execute(&mut shuttle.game_universe, (), &mut transaction::no_outputs)
1245        })
1246    }
1247
1248    /// Replaces the game universe, such as for initial setup or because the player
1249    /// chose to load a new one.
1250    /// This also resets the character to be the new universe's default character.
1251    ///
1252    /// Panics if called while the main task is suspended.
1253    pub fn set_universe(&mut self, universe: Box<Universe>) {
1254        self.with_mut(|shuttle| {
1255            shuttle.set_universe(universe);
1256        })
1257    }
1258
1259    /// Run the given async task which will obtain a new [`Universe`] and replace the current one.
1260    /// Display a progress notification during this period.
1261    ///
1262    /// If possible, the produced future should do its work on a background thread
1263    /// to minimize impact on the session execution.
1264    ///
1265    /// Panics if called while the main task is suspended.
1266    //---
1267    // TODO: Additional UI features:
1268    // * Optional confirmation with preview after loading and before actually replacing
1269    // * Optional fade to black transition
1270    // Also add a test for this
1271    pub async fn set_universe_async<F>(&mut self, f: F)
1272    where
1273        F: async_fn_traits::AsyncFnOnce1<
1274                YieldProgress,
1275                // TODO: we need to choose some standard "rich error to report to the user" format
1276                Output = Result<Box<Universe>, ArcStr>,
1277                OutputFuture: Send + 'static,
1278            > + Send
1279            + 'static,
1280    {
1281        let notification = self
1282            .show_notification(notification::NotificationContent::Progress {
1283                title: literal!("Loading..."),
1284                progress: ProgressBarState::new(0.0),
1285                part: literal!(""),
1286            })
1287            .ok();
1288
1289        let progress = YieldProgressBuilder::new()
1290            .progress_using(move |info| {
1291                if let Some(notification) = &notification {
1292                    notification.set_content(notification::NotificationContent::Progress {
1293                        title: literal!("Loading..."),
1294                        progress: ProgressBarState::new(info.fraction().into()),
1295                        part: info.label_str().into(),
1296                    });
1297                }
1298            })
1299            .build();
1300
1301        match f(progress).await {
1302            Ok(universe) => {
1303                self.set_universe(universe);
1304            }
1305            Err(error_message) => {
1306                self.show_modal_message(error_message);
1307            }
1308        }
1309
1310        // TODO: Force the notification to go away even if somebody cloned our YieldProgress
1311    }
1312
1313    /// Allows reading or changing the settings of this session.
1314    ///
1315    /// Note that these settings may be shared with other sessions.
1316    pub fn settings(&self) -> Settings {
1317        self.with_ref(|shuttle| shuttle.settings.clone())
1318    }
1319
1320    /// Invoke the [`SessionBuilder::quit()`] callback as if the user clicked a quit button inside
1321    /// our UI.
1322    ///
1323    /// This may be used in response to a window's close button, for example.
1324    ///
1325    /// The session state *may* decline to actually call the callback, such as if there are
1326    /// user-visible unsaved changes.
1327    ///
1328    /// The returned future will produce a [`QuitCancelled`] value if quitting was unsuccessful for
1329    /// any reason. If it is successful, the future never resolves. It is not necessary to poll
1330    /// the future if the result value is not wanted.
1331    pub fn quit(&self) -> impl Future<Output = QuitResult> + Send + 'static + use<> {
1332        self.with_ref(|shuttle| shuttle.quit())
1333    }
1334
1335    /// Add a custom command button, which will be displayed in the pause menu.
1336    ///
1337    /// Future versions may allow such commands to be bound to keys or displayed elsewhere.
1338    ///
1339    /// Panics if called while the main task is suspended.
1340    //---
1341    // TODO: make it so that this returns a handle, or rather a "command was invoked" channel
1342    // in lieu of an arbitrary function, that deletes the command if dropped.
1343    pub fn add_custom_command(&mut self, command: crate::ui_content::Command) {
1344        self.with_mut(|shuttle| {
1345            shuttle
1346                .custom_commands
1347                .set(shuttle.custom_commands.get().iter().cloned().chain([command]).collect())
1348        });
1349    }
1350
1351    /// Display a dialog box with a message. The user can exit the dialog box to return
1352    /// to the previous UI page.
1353    ///
1354    /// The message may contain newlines and will be word-wrapped.
1355    ///
1356    /// If this session was constructed without UI then the message will be logged instead.
1357    ///
1358    /// Caution: calling this repeatedly will currently result in stacking up arbitrary
1359    /// numbers of dialogs. Avoid using it for situations not in response to user action.
1360    //---
1361    // TODO: Replace this entirely with `show_notification`.
1362    pub fn show_modal_message(&self, message: ArcStr) {
1363        self.with_ref(|shuttle| {
1364            if let Err(e) = shuttle.control_channel_sender.send(ControlMessage::ShowModal(message))
1365            {
1366                log::warn!("could not send modal message for display: {e:?}");
1367            }
1368        })
1369    }
1370
1371    /// Display a notification to the user. Notifications persist until dismissed or the returned
1372    /// [`Notification`] handle is dropped, and their content may be updated through that handle.
1373    ///
1374    /// Returns an error if there is no UI to display notifications or if there are too many.
1375    pub fn show_notification(
1376        &mut self,
1377        content: impl Into<notification::NotificationContent>,
1378    ) -> Result<Notification, notification::Error> {
1379        // TODO: stop requiring mut by using a dedicated channel...?
1380        self.with_mut(|shuttle| match &mut shuttle.ui {
1381            Some(ui) => Ok(ui.show_notification(content)),
1382            None => Err(notification::Error::NoUi),
1383        })
1384    }
1385
1386    /// Waits until exactly one step has completed that had not already happened when it was
1387    /// called, then immediately completes. The universe members will be available for reading
1388    /// at that point.
1389    ///
1390    /// Calling this in a loop is thus a means of observing the outcome of every step, such as
1391    /// for a renderer/recorder.
1392    ///
1393    /// Panics if called while the main task is suspended.
1394    pub async fn yield_to_step(&self) {
1395        self.with_ref(|_| {});
1396        let notifier = self
1397            .session_event_notifier
1398            .upgrade()
1399            .expect("can't happen: session_event_notifier dead");
1400        let (mut wake_flag, listener) = listen::WakeFlag::new(false);
1401        notifier.listen(listener.filter(|event| match event {
1402            Event::Stepped => Some(()),
1403        }));
1404        let _alive = wake_flag.wait().await;
1405    }
1406
1407    #[track_caller]
1408    fn with_ref<R>(&self, f: impl FnOnce(&Shuttle) -> R) -> R {
1409        f(self
1410            .shuttle
1411            .try_read()
1412            .expect("MainTaskContext lock misused somehow (could not read)")
1413            .as_ref()
1414            .expect(
1415                "MainTaskContext operations may not be called while the main task is not executing",
1416            ))
1417    }
1418
1419    /// Note: By accepting `&mut self` even though it is unnecessary, we prevent run-time
1420    /// read/write or write/write conflicts with the lock.
1421    #[track_caller]
1422    fn with_mut<R>(&mut self, f: impl FnOnce(&mut Shuttle) -> R) -> R {
1423        f(self
1424            .shuttle
1425            .try_write()
1426            .expect("MainTaskContext lock misused somehow (could not write)")
1427            .as_mut()
1428            .expect(
1429                "MainTaskContext operations may not be called while the main task is not executing",
1430            ))
1431    }
1432}
1433
1434#[cfg(test)]
1435mod tests {
1436    use super::*;
1437    use crate::apps::Key;
1438    use all_is_cubes::character::CharacterTransaction;
1439    use all_is_cubes::math::Cube;
1440    use all_is_cubes::universe::Name;
1441    use all_is_cubes::util::assert_send_sync;
1442    use core::sync::atomic::{AtomicUsize, Ordering};
1443    use futures_channel::oneshot;
1444
1445    fn advance_time(session: &mut Session) {
1446        session.frame_clock.advance_by(session.universe().clock().schedule().delta_t());
1447        let step = session.maybe_step_universe();
1448        assert_ne!(step, None);
1449    }
1450
1451    #[test]
1452    fn is_send_sync() {
1453        assert_send_sync::<Session>();
1454    }
1455
1456    #[macro_rules_attribute::apply(smol_macros::test)]
1457    async fn fluff_forwarding_following() {
1458        // Create universe members
1459        let mut u = Universe::new();
1460        let space1 = u.insert_anonymous(Space::empty_positive(1, 1, 1));
1461        let space2 = u.insert_anonymous(Space::empty_positive(1, 1, 1));
1462        let character = StrongHandle::from(
1463            u.insert_anonymous(Character::spawn_default(u.read_ticket(), space1.clone()).unwrap()),
1464        );
1465        let st = space::CubeTransaction::fluff(Fluff::Happened).at(Cube::ORIGIN);
1466
1467        // Create session
1468        let mut session = Session::builder().build().await;
1469        session.set_universe(u);
1470        session.set_character(Some(character.clone()));
1471        let log = listen::Log::<Fluff>::new();
1472        session.listen_fluff(log.listener());
1473
1474        // Try some fluff with the initial state (we haven't even stepped the session)
1475        session.universe_mut().execute_1(&space1, st.clone()).unwrap();
1476        assert_eq!(log.drain(), vec![Fluff::Happened]);
1477
1478        // Change spaces
1479        session
1480            .universe_mut()
1481            .execute_1(
1482                &character,
1483                CharacterTransaction::move_to_space(space2.clone()),
1484            )
1485            .unwrap();
1486        session.maybe_step_universe();
1487
1488        // Check we're now listening to the new space only
1489        session.universe_mut().execute_1(&space1, st.clone()).unwrap();
1490        assert_eq!(log.drain(), vec![]);
1491        session.universe_mut().execute_1(&space2, st).unwrap();
1492        assert_eq!(log.drain(), vec![Fluff::Happened]);
1493    }
1494
1495    #[macro_rules_attribute::apply(smol_macros::test)]
1496    async fn main_task() {
1497        let old_marker = Name::from("old");
1498        let new_marker = Name::from("new");
1499        let noticed_step = Arc::new(AtomicUsize::new(0));
1500        let mut session = Session::builder().build().await;
1501        session
1502            .universe_mut()
1503            .insert(old_marker.clone(), Space::empty_positive(1, 1, 1))
1504            .unwrap();
1505
1506        // Set up task (that won't do anything until it's polled as part of stepping)
1507        let (send, recv) = oneshot::channel::<Box<Universe>>();
1508        let mut cameras = session.create_cameras(listen::constant(Viewport::ARBITRARY));
1509        session.set_main_task({
1510            let noticed_step = noticed_step.clone();
1511            async move |mut ctx: MainTaskContext| {
1512                eprintln!("main task: waiting for new universe");
1513                let new_universe = recv.await.unwrap();
1514                ctx.set_universe(new_universe);
1515                eprintln!("main task: have set new universe");
1516
1517                ctx.with_read_tickets(|read_tickets| cameras.update(read_tickets));
1518                assert!(cameras.character().is_some(), "has character");
1519
1520                // Now try noticing steps
1521                for _ in 0..2 {
1522                    ctx.yield_to_step().await;
1523                    eprintln!("main task: got yield_to_step()");
1524                    noticed_step.fetch_add(1, Ordering::Relaxed);
1525                }
1526
1527                eprintln!("main task: exiting");
1528                ExitMainTask
1529            }
1530        });
1531
1532        // Existing universe should still be present.
1533        session.maybe_step_universe();
1534        assert!(session.universe_mut().get::<Space>(&old_marker).is_some());
1535
1536        // Deliver new universe.
1537        let mut new_universe = Universe::new();
1538        let new_space =
1539            new_universe.insert(new_marker.clone(), Space::empty_positive(1, 1, 1)).unwrap();
1540        new_universe
1541            .insert(
1542                Name::from("character"),
1543                Character::spawn_default(new_universe.read_ticket(), new_space).unwrap(),
1544            )
1545            .unwrap();
1546        send.send(new_universe).unwrap();
1547
1548        // Receive it.
1549        assert_eq!(noticed_step.load(Ordering::Relaxed), 0);
1550        session.maybe_step_universe();
1551        assert!(session.universe_mut().get::<Space>(&new_marker).is_some());
1552        assert!(session.universe_mut().get::<Space>(&old_marker).is_none());
1553        assert_eq!(noticed_step.load(Ordering::Relaxed), 0);
1554
1555        // Try stepping.
1556        advance_time(&mut session);
1557        assert_eq!(noticed_step.load(Ordering::Relaxed), 1);
1558        advance_time(&mut session);
1559        assert_eq!(noticed_step.load(Ordering::Relaxed), 2);
1560
1561        // Verify cleanup (that the next step can succeed even though the task exited).
1562        advance_time(&mut session);
1563        assert_eq!(noticed_step.load(Ordering::Relaxed), 2);
1564    }
1565
1566    #[macro_rules_attribute::apply(smol_macros::test)]
1567    async fn input_is_processed_even_without_character() {
1568        let mut session =
1569            Session::builder().ui(listen::constant(Viewport::ARBITRARY)).build().await;
1570        assert!(!session.paused.get());
1571
1572        session.input_processor.key_momentary(Key::Escape); // need to not just use control_channel
1573        advance_time(&mut session);
1574
1575        assert!(session.paused.get());
1576    }
1577}