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
49pub struct Session<I> {
58 pub frame_clock: FrameClock<I>,
61
62 pub input_processor: InputProcessor,
65
66 shuttle: Option<Box<Shuttle>>,
71
72 main_task: Option<SyncWrapper<BoxFuture<'static, ExitMainTask>>>,
79
80 task_context_inner: Arc<RwLock<Option<Box<Shuttle>>>>,
84
85 paused: listen::Cell<bool>,
86
87 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
103struct Shuttle {
109 graphics_options: listen::Cell<Arc<GraphicsOptions>>,
110
111 game_universe: Universe,
112
113 game_universe_info: listen::Cell<SessionUniverseInfo>,
116
117 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 cursor_result: Option<Cursor>,
129
130 fluff_notifier: Arc<listen::Notifier<Fluff>>,
134
135 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 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 pub fn character(&self) -> listen::DynSource<Option<Handle<Character>>> {
216 self.shuttle().game_character.as_source()
217 }
218
219 pub fn set_universe(&mut self, universe: Universe) {
223 self.shuttle_mut().set_universe(universe);
224 }
225
226 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 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 pub fn universe(&self) -> &Universe {
264 &self.shuttle().game_universe
265 }
266
267 pub fn universe_mut(&mut self) -> &mut Universe {
272 &mut self.shuttle_mut().game_universe
273 }
274
275 pub fn universe_info(&self) -> listen::DynSource<SessionUniverseInfo> {
278 self.shuttle().game_universe_info.as_source()
279 }
280
281 pub fn ui_view(&self) -> listen::DynSource<Arc<UiViewState>> {
283 self.shuttle().ui_view()
284 }
285
286 pub fn graphics_options(&self) -> listen::DynSource<Arc<GraphicsOptions>> {
288 self.shuttle().graphics_options.as_source()
289 }
290
291 pub fn graphics_options_mut(&self) -> &listen::Cell<Arc<GraphicsOptions>> {
293 &self.shuttle().graphics_options
294 }
295
296 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 pub fn listen_fluff(&self, listener: impl listen::Listener<Fluff> + Send + Sync + 'static) {
310 self.shuttle().fluff_notifier.listen(listener)
311 }
312
313 pub fn maybe_step_universe(&mut self) -> Option<UniverseStepInfo> {
318 self.process_control_messages_and_stuff();
319
320 self.poll_main_task();
322
323 self.process_control_messages_and_stuff();
325
326 let mut step_result = None;
327 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 self.input_processor.step(game_tick);
351
352 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 shuttle.session_event_notifier.notify(&Event::Stepped);
383
384 self.poll_main_task();
387
388 self.process_control_messages_and_stuff();
390 }
391 }
392 step_result
393 }
394
395 fn poll_main_task(&mut self) {
397 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 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 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 ui.show_click_result(usize::MAX, Err(ToolError::NothingSelected));
458 }
459 }
460 }
461 ControlMessage::Save => {
462 let u = &self.shuttle().game_universe;
465 let fut = u.whence.save(
466 u,
467 YieldProgressBuilder::new()
468 .yield_using(|_| async {}) .build(),
470 );
471 match futures_util::FutureExt::now_or_never(fut) {
472 Some(Ok(())) => {
473 }
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 break;
507 }
508 }
509 }
510
511 self.shuttle_mut().sync_universe_and_character_derived();
512 }
513
514 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 pub fn cursor_result(&self) -> Option<&Cursor> {
528 self.shuttle().cursor_result.as_ref()
529 }
530
531 pub fn cursor_icon(&self) -> &CursorIcon {
537 match self.shuttle().cursor_result {
538 None => &CursorIcon::Crosshair,
542 Some(_) => &CursorIcon::PointingHand,
543 }
544 }
545
546 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 pub fn show_notification(
568 &mut self,
569 content: impl Into<notification::NotificationContent>,
570 ) -> Result<Notification, notification::Error> {
571 match &mut self.shuttle_mut().ui {
573 Some(ui) => Ok(ui.show_notification(content)),
574 None => Err(notification::Error::NoUi),
575 }
576 }
577
578 pub fn click(&mut self, button: usize) {
583 let result = self.shuttle_mut().click_impl::<I>(button);
586
587 if let Err(error @ ToolError::Internal(_)) = &result {
592 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 }
608
609 if let Some(ui) = &self.shuttle_mut().ui {
610 ui.show_click_result(button, result);
611 }
612 }
613
614 pub fn quit(&self) -> impl Future<Output = QuitResult> + Send + 'static + use<I> {
626 self.shuttle().quit()
627 }
628
629 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)] pub fn draw_fps_counter(&self) -> &FpsCounter<I> {
657 self.frame_clock.draw_fps_counter()
658 }
659}
660
661impl 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 }
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 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())), }
688 }
689
690 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 if cursor_space == Option::as_ref(&self.ui_view().get().space) {
696 self.ui
698 .as_mut()
699 .unwrap()
700 .click(button, self.cursor_result.clone())
701 } else {
702 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 if let Some(space_handle) = self.cursor_result.as_ref().map(Cursor::space) {
717 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 fn sync_universe_and_character_derived(&mut self) {
736 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 {
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#[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 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 pub fn ui(mut self, viewport: listen::DynSource<Viewport>) -> Self {
864 self.viewport_for_ui = Some(viewport);
865 self
866 }
867
868 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 pub fn quit(mut self, quit_fn: QuitFn) -> Self {
889 self.quit = Some(quit_fn);
890 self
891 }
892}
893
894pub(crate) type FullscreenState = Option<bool>;
896pub(crate) type FullscreenSetter = Option<Arc<dyn Fn(bool) + Send + Sync>>;
897
898#[non_exhaustive]
900pub(crate) enum ControlMessage {
901 Back,
906
907 Save,
909
910 ShowModal(ArcStr),
911
912 EnterDebug,
913
914 TogglePause,
915
916 ToggleMouselook,
917
918 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 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#[derive(Debug)]
942struct SpaceWatchState {
943 space: Option<Handle<Space>>,
945
946 #[expect(dead_code, reason = "acts upon being dropped")]
948 fluff_gate: listen::Gate,
949 }
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 Some(sf.fluff.clone())
965 })
966 .with_stack_buffer::<10>() .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#[derive(Clone, Debug)]
990#[non_exhaustive]
991pub struct SessionUniverseInfo {
992 pub id: UniverseId,
994 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#[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#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1059#[non_exhaustive]
1060pub enum CursorIcon {
1061 Normal,
1063 Crosshair,
1065 PointingHand,
1067}
1068
1069pub(crate) type QuitFn = Arc<dyn Fn() -> QuitResult + Send + Sync>;
1071pub(crate) type QuitResult = Result<QuitSucceeded, QuitCancelled>;
1072
1073pub type QuitSucceeded = std::convert::Infallible;
1077
1078#[derive(Clone, Copy, Debug, PartialEq)]
1081#[non_exhaustive]
1082pub enum QuitCancelled {
1083 Unsupported,
1088
1089 UserCancelled,
1092}
1093
1094#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1100#[expect(clippy::exhaustive_structs)]
1101pub struct ExitMainTask;
1102
1103#[derive(Debug)]
1104enum Event {
1105 Stepped,
1107}
1108
1109pub 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 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 pub fn with_universe<R>(&self, f: impl FnOnce(&Universe) -> R) -> R {
1137 self.with_ref(|shuttle| f(&shuttle.game_universe))
1138 }
1139
1140 pub fn set_universe(&mut self, universe: Universe) {
1146 self.with_mut(|shuttle| {
1147 shuttle.set_universe(universe);
1148 })
1149 }
1150
1151 pub fn quit(&self) -> impl Future<Output = QuitResult> + Send + 'static + use<> {
1163 self.with_ref(|shuttle| shuttle.quit())
1164 }
1165
1166 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 pub fn show_notification(
1191 &mut self,
1192 content: impl Into<notification::NotificationContent>,
1193 ) -> Result<Notification, notification::Error> {
1194 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 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 #[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
1249fn 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 !inner.notified.load(Ordering::Relaxed)
1272 } else {
1273 inner.notified.store(true, Ordering::Relaxed);
1274 inner.waker.wake();
1275
1276 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 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 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 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 space1.execute(&st).unwrap();
1349 assert_eq!(sink.drain(), vec![Fluff::Happened]);
1350
1351 character
1353 .execute(&CharacterTransaction::move_to_space(space2.clone()))
1354 .unwrap();
1355 session.maybe_step_universe();
1356
1357 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 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 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 session.maybe_step_universe();
1403 assert!(session.universe_mut().get::<Space>(&old_marker).is_some());
1404
1405 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 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 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 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); advance_time(&mut session);
1443
1444 assert!(session.paused.get());
1445 }
1446}