all_is_cubes_ui/apps/
session.rs

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