1use alloc::boxed::Box;
2use alloc::string::ToString as _;
3use alloc::sync::{Arc, Weak};
4use core::fmt;
5use core::mem;
6use core::pin::Pin;
7use core::task::{Context, Poll, Waker};
8use std::sync::RwLock;
9
10use flume::TryRecvError;
11use futures_core::future::BoxFuture;
12use sync_wrapper::SyncWrapper;
13
14use all_is_cubes::arcstr::{self, ArcStr, literal};
15use all_is_cubes::character::{self, Character, Cursor};
16use all_is_cubes::fluff::Fluff;
17use all_is_cubes::inv::ToolError;
18use all_is_cubes::listen::{self, Listen as _, Listener as _, Source as _};
19use all_is_cubes::save::WhenceUniverse;
20use all_is_cubes::sound;
21use all_is_cubes::space::{self, Space};
22use all_is_cubes::time::{self, Duration};
23use all_is_cubes::transaction::{self, Transaction as _};
24use all_is_cubes::universe::{
25 self, Handle, ReadTicket, StrongHandle, Universe, UniverseId, UniverseStepInfo,
26 UniverseTransaction,
27};
28use all_is_cubes::util::{
29 ConciseDebug, Fmt, Refmt as _, ShowStatus, StatusText, YieldProgress, YieldProgressBuilder,
30};
31use all_is_cubes_render::camera::{
32 GraphicsOptions, Layers, StandardCameras, UiViewState, Viewport,
33};
34
35use crate::apps::{FpsCounter, FrameClock, InputProcessor, InputTargets, Settings};
36use crate::ui_content::Vui;
37use crate::ui_content::notification::{self, Notification};
38use crate::vui::widgets::ProgressBarState;
39
40const LOG_FIRST_FRAMES: bool = false;
41
42const SHUTTLE_PANIC_MSG: &str = "Shuttle not returned to Session; \
43 this indicates something went wrong with main task execution";
44
45pub struct Session {
51 pub frame_clock: FrameClock,
54
55 pub input_processor: InputProcessor,
58
59 shuttle: Option<Box<Shuttle>>,
64
65 main_task: Option<SyncWrapper<BoxFuture<'static, ExitMainTask>>>,
72
73 task_context_inner: Arc<RwLock<Option<Box<Shuttle>>>>,
77
78 paused: listen::Cell<bool>,
79
80 ambient_sound_source: listen::DynSource<sound::SpatialAmbient>,
81
82 control_channel: flume::Receiver<ControlMessage>,
91 control_channel_sender: flume::Sender<ControlMessage>,
92
93 last_step_info: UniverseStepInfo,
94
95 tick_counter_for_logging: u8,
96}
97
98struct Shuttle {
104 settings: Settings,
105
106 game_universe: Universe,
107
108 game_universe_info: listen::Cell<SessionUniverseInfo>,
111
112 game_character: listen::CellWithLocal<Option<StrongHandle<Character>>>,
115
116 ui: Option<Vui>,
117
118 space_watch_state: Layers<SpaceWatchState>,
119
120 control_channel_sender: flume::Sender<ControlMessage>,
121
122 cursor_result: Option<Cursor>,
124
125 fluff_notifier: Arc<listen::Notifier<Fluff>>,
129
130 ambient_sound_state: listen::CellWithLocal<sound::SpatialAmbient>,
131
132 session_event_notifier: Arc<listen::Notifier<Event>>,
134
135 quit_fn: Option<QuitFn>,
136
137 custom_commands: listen::CellWithLocal<Arc<[crate::ui_content::Command]>>,
138}
139
140impl fmt::Debug for Session {
141 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142 let Self {
143 frame_clock,
144 input_processor,
145 main_task,
146 task_context_inner: _,
147 paused,
148 ambient_sound_source: _,
149 control_channel: _,
150 control_channel_sender: _,
151 last_step_info,
152 tick_counter_for_logging,
153
154 shuttle,
155 } = self;
156
157 let Some(shuttle) = shuttle else {
158 write!(f, "Session(is in the middle of an operation)")?;
159 return Ok(());
160 };
161 let Shuttle {
162 settings,
163 game_universe,
164 game_universe_info,
165 game_character,
166 space_watch_state,
167 ui,
168 control_channel_sender: _,
169 cursor_result,
170 quit_fn,
171 fluff_notifier,
172 ambient_sound_state,
173 session_event_notifier,
174 custom_commands,
175 } = &**shuttle;
176
177 f.debug_struct("Session")
178 .field("frame_clock", frame_clock)
179 .field("input_processor", input_processor)
180 .field("settings", settings)
181 .field("game_universe", game_universe)
182 .field("game_universe_info", game_universe_info)
183 .field("game_character", game_character)
184 .field("space_watch_state", space_watch_state)
185 .field("main_task", &main_task.as_ref().map(|_| "..."))
186 .field("session_event_notifier", session_event_notifier)
187 .field("fluff_notifier", fluff_notifier)
188 .field("ambient_sound_state", ambient_sound_state)
189 .field("paused", &paused)
190 .field("ui", &ui)
191 .field(
192 "quit_fn",
193 &quit_fn.as_ref().map(|_cant_print_a_function| ()),
194 )
195 .field("custom_commands", &custom_commands)
196 .field("cursor_result", &cursor_result)
197 .field("last_step_info", &last_step_info)
198 .field("tick_counter_for_logging", &tick_counter_for_logging)
199 .finish_non_exhaustive()
200 }
201}
202
203impl Session {
204 pub fn builder() -> SessionBuilder {
206 SessionBuilder::default()
207 }
208
209 #[track_caller]
210 fn shuttle(&self) -> &Shuttle {
211 self.shuttle.as_ref().expect(SHUTTLE_PANIC_MSG)
212 }
213 #[track_caller]
214 fn shuttle_mut(&mut self) -> &mut Shuttle {
215 self.shuttle.as_mut().expect(SHUTTLE_PANIC_MSG)
216 }
217
218 pub fn character(&self) -> listen::DynSource<Option<StrongHandle<Character>>> {
220 self.shuttle().game_character.as_source()
221 }
222
223 pub fn set_universe(&mut self, universe: Box<Universe>) {
227 self.shuttle_mut().set_universe(universe);
228 }
229
230 pub fn set_character(&mut self, character: Option<StrongHandle<Character>>) {
233 let shuttle = self.shuttle_mut();
234 if let Some(character) = &character {
235 assert!(character.universe_id() == Some(shuttle.game_universe.universe_id()));
236 }
237
238 shuttle.game_character.set(character);
239 shuttle.sync_universe_and_character_derived();
240 }
241
242 pub fn set_main_task<F>(&mut self, task_ctor: F)
252 where
253 F: async_fn_traits::AsyncFnOnce1<
254 MainTaskContext,
255 Output = ExitMainTask,
256 OutputFuture: Send + 'static,
257 >,
258 {
259 let context = MainTaskContext {
260 shuttle: self.task_context_inner.clone(),
261 session_event_notifier: Arc::downgrade(&self.shuttle().session_event_notifier),
262 };
263 self.main_task = Some(SyncWrapper::new(Box::pin(task_ctor(context))));
264 }
265
266 pub fn universe(&self) -> &Universe {
268 &self.shuttle().game_universe
269 }
270
271 pub fn universe_mut(&mut self) -> &mut Universe {
276 &mut self.shuttle_mut().game_universe
277 }
278
279 pub fn universe_info(&self) -> listen::DynSource<SessionUniverseInfo> {
282 self.shuttle().game_universe_info.as_source()
283 }
284
285 pub fn ui_view(&self) -> listen::DynSource<Arc<UiViewState>> {
287 self.shuttle().ui_view()
288 }
289
290 pub fn read_tickets(&self) -> Layers<ReadTicket<'_>> {
293 self.shuttle().read_tickets()
294 }
295
296 pub fn graphics_options(&self) -> listen::DynSource<Arc<GraphicsOptions>> {
298 self.shuttle().settings.as_source()
299 }
300
301 pub fn settings(&self) -> &Settings {
305 &self.shuttle().settings
306 }
307
308 pub fn create_cameras(&self, viewport_source: listen::DynSource<Viewport>) -> StandardCameras {
313 let mut c = StandardCameras::new(
314 self.graphics_options(),
315 viewport_source,
316 self.character(),
317 self.ui_view(),
318 );
319 c.update(self.read_tickets());
320 c
321 }
322
323 pub fn listen_fluff(&self, listener: impl listen::Listener<Fluff> + Send + Sync + 'static) {
326 self.shuttle().fluff_notifier.listen(listener)
327 }
328
329 pub fn ambient_sound(&self) -> listen::DynSource<sound::SpatialAmbient> {
331 self.ambient_sound_source.clone()
332 }
333
334 pub fn maybe_step_universe(&mut self) -> Option<UniverseStepInfo> {
339 self.process_control_messages_and_stuff();
340
341 self.poll_main_task();
343
344 self.process_control_messages_and_stuff();
346
347 let mut step_result = None;
348 for _ in 0..FrameClock::CATCH_UP_STEPS {
350 if self.frame_clock.should_step() {
351 let shuttle = self.shuttle.as_mut().expect(SHUTTLE_PANIC_MSG);
352 let u_clock = shuttle.game_universe.clock();
353 let paused = self.paused.get();
354 let ui_tick = u_clock.next_tick(false);
355 let game_tick = u_clock.next_tick(paused);
356
357 self.frame_clock.did_step(u_clock.schedule());
358
359 self.input_processor
360 .apply_input(
361 InputTargets {
362 universe: Some(&mut shuttle.game_universe),
363 character: shuttle
364 .game_character
365 .get()
366 .as_ref()
367 .map(StrongHandle::as_ref),
368 paused: Some(&self.paused),
369 settings: Some(&shuttle.settings),
370 control_channel: Some(&self.control_channel_sender),
371 ui: shuttle.ui.as_ref(),
372 },
373 game_tick,
374 )
375 .expect("applying input failed");
376 self.input_processor.step(game_tick);
378
379 let step_start_time = time::Instant::now();
385 let dt = game_tick.delta_t();
386 let deadlines = Layers {
387 world: time::Deadline::At(step_start_time + dt / 2),
388 ui: time::Deadline::At(step_start_time + dt / 2 + dt / 4),
389 };
390
391 let mut info = shuttle.game_universe.step(paused, deadlines.world);
392 if let Some(ui) = &mut shuttle.ui {
393 info += ui.step(ui_tick, deadlines.ui, shuttle.game_universe.read_ticket());
394 }
395
396 if LOG_FIRST_FRAMES && self.tick_counter_for_logging <= 10 {
397 self.tick_counter_for_logging = self.tick_counter_for_logging.saturating_add(1);
398 log::debug!(
399 "tick={} step {}",
400 self.tick_counter_for_logging,
401 info.computation_time.refmt(&ConciseDebug)
402 );
403 }
404 self.last_step_info = info.clone();
405 step_result = Some(info);
406
407 shuttle.session_event_notifier.notify(&Event::Stepped);
410
411 self.poll_main_task();
414
415 self.process_control_messages_and_stuff();
417 }
418 }
419 step_result
420 }
421
422 fn poll_main_task(&mut self) {
424 if let Some(sync_wrapped_future) = self.main_task.as_mut() {
426 {
427 let mut shuttle_guard = match self.task_context_inner.write() {
428 Ok(g) => g,
429 Err(poisoned) => poisoned.into_inner(),
430 };
431 *shuttle_guard = Some(
432 self.shuttle
433 .take()
434 .expect("Session lost its shuttle before polling the main task"),
435 );
436 }
437 let _reset_on_drop = scopeguard::guard((), |()| {
439 let mut shuttle_guard = match self.task_context_inner.write() {
440 Ok(g) => g,
441 Err(poisoned) => poisoned.into_inner(),
442 };
443 if let Some(shuttle) = shuttle_guard.take() {
444 self.shuttle = Some(shuttle);
445 } else {
446 log::error!(
447 "Session lost its shuttle while polling the main task and will be unusable"
448 );
449 }
450 *shuttle_guard = None;
451 });
452
453 let future: Pin<&mut dyn Future<Output = ExitMainTask>> =
454 sync_wrapped_future.get_mut().as_mut();
455 match future.poll(&mut Context::from_waker(Waker::noop())) {
456 Poll::Pending => {}
457 Poll::Ready(ExitMainTask) => {
458 self.main_task = None;
459 }
460 }
461 }
462 }
463
464 fn process_control_messages_and_stuff(&mut self) {
465 loop {
466 match self.control_channel.try_recv() {
467 Ok(msg) => match msg {
468 ControlMessage::Back => {
469 if let Some(ui) = &mut self.shuttle_mut().ui {
471 ui.back();
472 }
473 }
474 ControlMessage::ShowModal(message) => {
475 self.show_modal_message(message);
476 }
477 ControlMessage::EnterDebug => {
478 let shuttle = self.shuttle_mut();
479 if let Some(ui) = &mut shuttle.ui {
480 if let Some(cursor) = &shuttle.cursor_result {
481 ui.enter_debug(shuttle.game_universe.read_ticket(), cursor);
482 } else {
483 ui.show_click_result(usize::MAX, Err(ToolError::NothingSelected));
485 }
486 }
487 }
488 ControlMessage::Save => {
489 let u = &self.shuttle().game_universe;
492 let fut = u.whence.save(
493 u,
494 YieldProgressBuilder::new()
495 .yield_using(|_| async {}) .build(),
497 );
498 match futures_util::FutureExt::now_or_never(fut) {
499 Some(Ok(())) => {
500 }
502 Some(Err(e)) => {
503 self.show_modal_message(arcstr::format!(
504 "{}",
505 all_is_cubes::util::ErrorChain(&*e)
506 ));
507 }
508 None => {
509 self.show_modal_message(
510 "unsupported: saving did not complete synchronously".into(),
511 );
512 }
513 }
514 }
515 ControlMessage::TogglePause => {
516 self.paused.set(!self.paused.get());
517 }
518 ControlMessage::ToggleMouselook => {
519 self.input_processor.toggle_mouselook_mode();
520 }
521 ControlMessage::ModifySettings(function) => {
522 function(&self.shuttle().settings);
523 }
524 },
525 Err(TryRecvError::Empty) => break,
526 Err(TryRecvError::Disconnected) => {
527 break;
529 }
530 }
531 }
532
533 self.shuttle_mut().sync_universe_and_character_derived();
534 }
535
536 pub fn update_cursor(&mut self, cameras: &StandardCameras) {
542 let cursor_result = self.input_processor.cursor_ndc_position().and_then(|ndc_pos| {
543 cameras
544 .project_cursor(self.read_tickets(), ndc_pos)
545 .expect("shouldn't happen: read error in update_cursor()")
546 });
547 self.shuttle_mut().cursor_result = cursor_result;
548 }
549
550 pub fn cursor_result(&self) -> Option<&Cursor> {
552 self.shuttle().cursor_result.as_ref()
553 }
554
555 pub fn cursor_icon(&self) -> &CursorIcon {
561 match self.shuttle().cursor_result {
562 None => &CursorIcon::Crosshair,
566 Some(_) => &CursorIcon::PointingHand,
567 }
568 }
569
570 pub fn show_modal_message(&mut self, message: ArcStr) {
580 if let Some(ui) = &mut self.shuttle_mut().ui {
581 ui.show_modal_message(message);
582 } else {
583 log::info!("UI message not shown: {message}");
584 }
585 }
586
587 pub fn show_notification(
592 &mut self,
593 content: impl Into<notification::NotificationContent>,
594 ) -> Result<Notification, notification::Error> {
595 match &mut self.shuttle_mut().ui {
597 Some(ui) => Ok(ui.show_notification(content)),
598 None => Err(notification::Error::NoUi),
599 }
600 }
601
602 pub fn click(&mut self, button: usize) {
607 let result = self.shuttle_mut().click_impl(button);
610
611 if let Err(error @ ToolError::Internal(_)) = &result {
616 log::error!(
619 "Error applying tool: {error}",
620 error = all_is_cubes::util::ErrorChain(&error)
621 );
622 }
623
624 if let Err(error) = &result {
625 let shuttle = self.shuttle();
626 for fluff in error.fluff() {
627 shuttle.fluff_notifier.notify(&fluff);
628 }
629 } else {
630 }
632
633 if let Some(ui) = &self.shuttle_mut().ui {
634 ui.show_click_result(button, result);
635 }
636 }
637
638 pub fn quit(&self) -> impl Future<Output = QuitResult> + Send + 'static + use<> {
650 self.shuttle().quit()
651 }
652
653 pub fn info_text<T: Fmt<StatusText>>(&self, render: T) -> InfoText<'_, T> {
656 let fopt = StatusText {
657 show: self.shuttle().settings.get_graphics_options().debug_info_text_contents,
658 };
659
660 if LOG_FIRST_FRAMES && self.tick_counter_for_logging <= 10 {
661 log::debug!(
662 "tick={} draw {}",
663 self.tick_counter_for_logging,
664 render.refmt(&fopt)
665 )
666 }
667
668 InfoText {
669 session: self,
670 render,
671 fopt,
672 }
673 }
674
675 #[doc(hidden)] pub fn draw_fps_counter(&self) -> &FpsCounter {
677 self.frame_clock.draw_fps_counter()
678 }
679}
680
681impl Shuttle {
684 fn set_universe(&mut self, universe: Box<Universe>) {
685 self.game_universe = *universe;
686 self.game_character
687 .set(self.game_universe.get_default_character().map(StrongHandle::new));
688
689 self.sync_universe_and_character_derived();
690 }
692
693 fn quit(&self) -> impl Future<Output = QuitResult> + Send + 'static + use<> {
694 let fut: BoxFuture<'static, QuitResult> = match (&self.ui, &self.quit_fn) {
695 (Some(ui), _) => Box::pin(ui.quit()),
696 (None, Some(quit_fn)) => Box::pin(std::future::ready(quit_fn())),
697 (None, None) => Box::pin(std::future::ready(Err(QuitCancelled::Unsupported))),
698 };
699 fut
700 }
701
702 fn ui_view(&self) -> listen::DynSource<Arc<UiViewState>> {
704 match &self.ui {
705 Some(ui) => ui.view(),
706 None => listen::constant(Arc::new(UiViewState::default())), }
708 }
709
710 fn click_impl(&mut self, button: usize) -> Result<(), ToolError> {
713 let cursor_space = self.cursor_result.as_ref().map(|c| c.space());
714 if let Some(ui) = self
716 .ui
717 .as_mut()
718 .filter(|ui| cursor_space.is_some() && cursor_space == ui.view().get().space.as_ref())
719 {
720 ui.click(button, self.cursor_result.clone())
721 } else {
722 if let Some(character_handle) = self.game_character.get() {
725 let transaction = Character::click(
726 self.game_universe.read_ticket(),
727 character_handle.to_weak(),
728 self.cursor_result.as_ref(),
729 button,
730 )?;
731 transaction
732 .execute(&mut self.game_universe, (), &mut transaction::no_outputs)
733 .map_err(|e| ToolError::Internal(e.to_string()))?;
734
735 if let Some(space_handle) = self.cursor_result.as_ref().map(Cursor::space) {
738 let _ = self.game_universe.mutate_space(space_handle, |m| {
739 m.evaluate_light_for_time(Duration::from_millis(1));
740 });
741 }
742
743 Ok(())
744 } else {
745 Err(ToolError::NoTool)
746 }
747 }
748 }
749
750 fn sync_universe_and_character_derived(&mut self) {
757 self.game_universe_info.set_if_unequal(SessionUniverseInfo {
759 id: self.game_universe.universe_id(),
760 whence: Arc::clone(&self.game_universe.whence),
761 });
762
763 {
765 let character: Option<character::Read<'_>> =
766 self.game_character.get().as_ref().map(|cref| {
767 cref.read(self.game_universe.read_ticket())
768 .expect("TODO: decide how to handle error")
769 });
770 let space: Option<&Handle<Space>> = character.map(|ch| ch.space());
771
772 if space != self.space_watch_state.world.space.as_ref() {
773 self.space_watch_state.world = SpaceWatchState::new(
774 self.game_universe.read_ticket(),
775 space.cloned(),
776 &self.fluff_notifier,
777 )
778 .expect("TODO: decide how to handle error");
779 }
780
781 let new_sound_state: &sound::SpatialAmbient = if let Some(character) = &character {
782 character.ambient_sound()
783 } else {
784 &sound::SpatialAmbient::SILENT
785 };
786 if *new_sound_state != self.ambient_sound_state.get() {
787 self.ambient_sound_state.set(new_sound_state.clone());
788 }
789 }
790
791 {
793 let (read_ticket, space) = match self.ui.as_ref() {
795 Some(ui) => (ui.read_ticket(), ui.view().get().space.clone()),
796 None => (ReadTicket::stub(), None),
797 };
798 if space != self.space_watch_state.ui.space {
799 self.space_watch_state.ui =
800 SpaceWatchState::new(read_ticket, space, &self.fluff_notifier)
801 .expect("TODO: decide how to handle error");
802 }
803 }
804 }
805
806 fn read_tickets(&self) -> Layers<ReadTicket<'_>> {
807 Layers {
808 world: self.game_universe.read_ticket(),
809 ui: match &self.ui {
810 Some(ui) => ui.read_ticket(),
811 None => ReadTicket::stub(),
812 },
813 }
814 }
815}
816
817#[derive(Clone)]
819#[must_use]
820#[expect(missing_debug_implementations)]
821pub struct SessionBuilder {
822 viewport_for_ui: Option<listen::DynSource<Viewport>>,
823
824 fullscreen_state: listen::DynSource<FullscreenState>,
825 set_fullscreen: FullscreenSetter,
826
827 settings: Option<Settings>,
828
829 quit: Option<QuitFn>,
830}
831
832impl Default for SessionBuilder {
833 fn default() -> Self {
834 Self {
835 viewport_for_ui: None,
836 fullscreen_state: listen::constant(None),
837 set_fullscreen: None,
838 settings: None,
839 quit: None,
840 }
841 }
842}
843
844impl SessionBuilder {
845 pub async fn build(self) -> Session {
851 let Self {
852 viewport_for_ui,
853 fullscreen_state,
854 set_fullscreen,
855 settings,
856 quit: quit_fn,
857 } = self;
858
859 let settings = settings.unwrap_or_else(|| Settings::new(Default::default()));
860 let game_character = listen::CellWithLocal::new(None);
861 let input_processor = InputProcessor::new();
862 let paused = listen::Cell::new(false);
863 let ambient_sound_state = listen::CellWithLocal::new(sound::SpatialAmbient::SILENT);
864 let (control_send, control_recv) = flume::bounded(100);
865 let custom_commands = listen::CellWithLocal::new([].into());
866
867 let space_watch_state = Layers {
868 world: SpaceWatchState::empty(),
869 ui: SpaceWatchState::empty(),
870 };
871
872 let ui = match viewport_for_ui {
873 Some(viewport) => Some(
874 Vui::new(crate::ui_content::UiTargets {
875 mouselook_mode: input_processor.mouselook_mode(),
876 character_source: game_character.as_source(),
877 paused: paused.as_source(),
878 graphics_options: settings.as_source(),
879 app_control_channel: control_send.clone(),
880 viewport_source: viewport,
881 fullscreen_mode: fullscreen_state,
882 set_fullscreen,
883 quit: quit_fn.clone(),
884 custom_commands: custom_commands.as_source(),
885 })
886 .await,
887 ),
888 None => None,
889 };
890
891 let game_universe = Universe::new();
894
895 Session {
896 frame_clock: FrameClock::new(game_universe.clock().schedule()),
897 ambient_sound_source: Arc::new(ambient_sound_state.as_source().map({
898 let paused = paused.as_source();
900 move |sound| {
901 if paused.get() {
902 sound::SpatialAmbient::SILENT
903 } else {
904 sound
905 }
906 }
907 })),
908
909 shuttle: Some(Box::new(Shuttle {
910 ui,
911 settings,
912 game_character,
913 game_universe_info: listen::Cell::new(SessionUniverseInfo {
914 id: game_universe.universe_id(),
915 whence: game_universe.whence.clone(),
916 }),
917 game_universe: *game_universe,
918 space_watch_state,
919 cursor_result: None,
920 session_event_notifier: Arc::new(listen::Notifier::new()),
921 fluff_notifier: Arc::new(listen::Notifier::new()),
922 ambient_sound_state,
923 control_channel_sender: control_send.clone(),
924 quit_fn,
925 custom_commands,
926 })),
927 input_processor,
928 main_task: None,
929 task_context_inner: Arc::new(RwLock::new(None)),
930 paused,
931 control_channel: control_recv,
932 control_channel_sender: control_send,
933 last_step_info: UniverseStepInfo::default(),
934 tick_counter_for_logging: 0,
935 }
936 }
937
938 pub fn ui(mut self, viewport: listen::DynSource<Viewport>) -> Self {
946 self.viewport_for_ui = Some(viewport);
947 self
948 }
949
950 pub fn fullscreen(
956 mut self,
957 state: listen::DynSource<Option<bool>>,
958 setter: Option<Arc<dyn Fn(bool) + Send + Sync>>,
959 ) -> Self {
960 self.fullscreen_state = state;
961 self.set_fullscreen = setter;
962 self
963 }
964
965 pub fn settings_from(mut self, settings: Settings) -> Self {
972 self.settings = Some(Settings::inherit(settings));
973 self
974 }
975
976 pub fn quit(mut self, quit_fn: QuitFn) -> Self {
982 self.quit = Some(quit_fn);
983 self
984 }
985}
986
987pub(crate) type FullscreenState = Option<bool>;
989pub(crate) type FullscreenSetter = Option<Arc<dyn Fn(bool) + Send + Sync>>;
990
991#[non_exhaustive]
993pub(crate) enum ControlMessage {
994 Back,
999
1000 Save,
1002
1003 ShowModal(ArcStr),
1004
1005 EnterDebug,
1006
1007 TogglePause,
1008
1009 ToggleMouselook,
1010
1011 ModifySettings(Box<dyn FnOnce(&Settings) + Send>),
1012}
1013
1014impl fmt::Debug for ControlMessage {
1015 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1016 match self {
1018 Self::Back => write!(f, "Back"),
1019 Self::Save => write!(f, "Save"),
1020 Self::ShowModal(_msg) => f.debug_struct("ShowModal").finish_non_exhaustive(),
1021 Self::EnterDebug => write!(f, "EnterDebug"),
1022 Self::TogglePause => write!(f, "TogglePause"),
1023 Self::ToggleMouselook => write!(f, "ToggleMouselook"),
1024 Self::ModifySettings(_func) => f.debug_struct("ModifySettings").finish_non_exhaustive(),
1025 }
1026 }
1027}
1028
1029#[derive(Debug)]
1032struct SpaceWatchState {
1033 space: Option<Handle<Space>>,
1035
1036 #[expect(dead_code, reason = "acts upon being dropped")]
1038 fluff_gate: listen::Gate,
1039 }
1042
1043impl SpaceWatchState {
1044 fn new(
1045 read_ticket: ReadTicket<'_>,
1046 space: Option<Handle<Space>>,
1047 fluff_notifier: &Arc<listen::Notifier<Fluff>>,
1048 ) -> Result<Self, universe::HandleError> {
1049 if let Some(space) = space {
1050 let space_read = space.read(read_ticket)?;
1051 let (fluff_gate, fluff_forwarder) =
1052 listen::Notifier::forwarder(Arc::downgrade(fluff_notifier))
1053 .filter(|sf: &space::SpaceFluff| {
1054 Some(sf.fluff.clone())
1056 })
1057 .with_stack_buffer::<10>() .gate();
1059 space_read.fluff().listen(fluff_forwarder);
1060 Ok(Self {
1061 space: Some(space),
1062 fluff_gate,
1063 })
1064 } else {
1065 Ok(Self::empty())
1066 }
1067 }
1068
1069 fn empty() -> SpaceWatchState {
1070 Self {
1071 space: None,
1072 fluff_gate: listen::Gate::default(),
1073 }
1074 }
1075}
1076
1077#[derive(Clone, Debug)]
1081#[non_exhaustive]
1082pub struct SessionUniverseInfo {
1083 pub id: UniverseId,
1085 pub whence: Arc<dyn WhenceUniverse>,
1087}
1088
1089impl PartialEq for SessionUniverseInfo {
1090 fn eq(&self, other: &Self) -> bool {
1091 self.id == other.id && Arc::ptr_eq(&self.whence, &other.whence)
1092 }
1093}
1094
1095#[derive(Copy, Clone, Debug)]
1097pub struct InfoText<'a, T> {
1098 session: &'a Session,
1099 render: T,
1100 fopt: StatusText,
1101}
1102
1103impl<T: Fmt<StatusText>> fmt::Display for InfoText<'_, T> {
1104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1105 let fopt = self.fopt;
1106 let mut empty = true;
1107 if fopt.show.contains(ShowStatus::CHARACTER)
1108 && let Some(shuttle) = self.session.shuttle.as_ref()
1109 && let Some(character_handle) = shuttle.game_character.get().as_ref()
1110 {
1111 empty = false;
1112 match character_handle.read(shuttle.game_universe.read_ticket()) {
1113 Ok(character_read) => {
1114 write!(f, "{}", character_read.refmt(&fopt))?;
1115 }
1116 Err(_) => write!(f, "<error reading character>")?,
1117 }
1118 }
1119 if fopt.show.contains(ShowStatus::STEP) {
1120 if !mem::take(&mut empty) {
1121 write!(f, "\n\n")?;
1122 }
1123 write!(
1124 f,
1125 "{:#?}\n\nFPS: {:2.1}\n{:#?}",
1126 self.session.last_step_info.refmt(&fopt),
1127 self.session.frame_clock.draw_fps_counter().frames_per_second(),
1128 self.render.refmt(&fopt),
1129 )?;
1130 }
1131 if fopt.show.contains(ShowStatus::CURSOR) {
1132 if !mem::take(&mut empty) {
1133 write!(f, "\n\n")?;
1134 }
1135 match self.session.cursor_result() {
1136 Some(cursor) => write!(f, "{cursor}"),
1137 None => write!(f, "No block"),
1138 }?;
1139 }
1140 Ok(())
1141 }
1142}
1143
1144#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1148#[non_exhaustive]
1149pub enum CursorIcon {
1150 Normal,
1152 Crosshair,
1154 PointingHand,
1156}
1157
1158pub(crate) type QuitFn = Arc<dyn Fn() -> QuitResult + Send + Sync>;
1160pub(crate) type QuitResult = Result<QuitSucceeded, QuitCancelled>;
1161
1162pub type QuitSucceeded = std::convert::Infallible;
1166
1167#[derive(Clone, Copy, Debug, PartialEq)]
1170#[non_exhaustive]
1171pub enum QuitCancelled {
1172 Unsupported,
1177
1178 UserCancelled,
1181}
1182
1183#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1189#[expect(clippy::exhaustive_structs)]
1190pub struct ExitMainTask;
1191
1192#[derive(Debug)]
1193enum Event {
1194 Stepped,
1196}
1197
1198pub struct MainTaskContext {
1200 shuttle: Arc<RwLock<Option<Box<Shuttle>>>>,
1201 session_event_notifier: Weak<listen::Notifier<Event>>,
1202}
1203
1204impl fmt::Debug for MainTaskContext {
1205 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1206 f.debug_struct("MainTaskContext").finish_non_exhaustive()
1207 }
1208}
1209
1210impl MainTaskContext {
1211 pub fn create_cameras(&self, viewport_source: listen::DynSource<Viewport>) -> StandardCameras {
1216 self.with_ref(|shuttle| {
1217 let mut c = StandardCameras::new(
1218 shuttle.settings.as_source(),
1219 viewport_source,
1220 shuttle.game_character.as_source(),
1221 shuttle.ui_view(),
1222 );
1223 c.update(shuttle.read_tickets());
1224 c
1225 })
1226 }
1227
1228 pub fn with_universe<R>(&self, f: impl FnOnce(&Universe) -> R) -> R {
1230 self.with_ref(|shuttle| f(&shuttle.game_universe))
1231 }
1232
1233 pub fn with_read_tickets<R>(&self, f: impl FnOnce(Layers<ReadTicket<'_>>) -> R) -> R {
1235 self.with_ref(|shuttle| f(shuttle.read_tickets()))
1236 }
1237
1238 pub fn execute(
1240 &mut self,
1241 transaction: UniverseTransaction,
1242 ) -> Result<(), transaction::ExecuteError> {
1243 self.with_mut(|shuttle| {
1244 transaction.execute(&mut shuttle.game_universe, (), &mut transaction::no_outputs)
1245 })
1246 }
1247
1248 pub fn set_universe(&mut self, universe: Box<Universe>) {
1254 self.with_mut(|shuttle| {
1255 shuttle.set_universe(universe);
1256 })
1257 }
1258
1259 pub async fn set_universe_async<F>(&mut self, f: F)
1272 where
1273 F: async_fn_traits::AsyncFnOnce1<
1274 YieldProgress,
1275 Output = Result<Box<Universe>, ArcStr>,
1277 OutputFuture: Send + 'static,
1278 > + Send
1279 + 'static,
1280 {
1281 let notification = self
1282 .show_notification(notification::NotificationContent::Progress {
1283 title: literal!("Loading..."),
1284 progress: ProgressBarState::new(0.0),
1285 part: literal!(""),
1286 })
1287 .ok();
1288
1289 let progress = YieldProgressBuilder::new()
1290 .progress_using(move |info| {
1291 if let Some(notification) = ¬ification {
1292 notification.set_content(notification::NotificationContent::Progress {
1293 title: literal!("Loading..."),
1294 progress: ProgressBarState::new(info.fraction().into()),
1295 part: info.label_str().into(),
1296 });
1297 }
1298 })
1299 .build();
1300
1301 match f(progress).await {
1302 Ok(universe) => {
1303 self.set_universe(universe);
1304 }
1305 Err(error_message) => {
1306 self.show_modal_message(error_message);
1307 }
1308 }
1309
1310 }
1312
1313 pub fn settings(&self) -> Settings {
1317 self.with_ref(|shuttle| shuttle.settings.clone())
1318 }
1319
1320 pub fn quit(&self) -> impl Future<Output = QuitResult> + Send + 'static + use<> {
1332 self.with_ref(|shuttle| shuttle.quit())
1333 }
1334
1335 pub fn add_custom_command(&mut self, command: crate::ui_content::Command) {
1344 self.with_mut(|shuttle| {
1345 shuttle
1346 .custom_commands
1347 .set(shuttle.custom_commands.get().iter().cloned().chain([command]).collect())
1348 });
1349 }
1350
1351 pub fn show_modal_message(&self, message: ArcStr) {
1363 self.with_ref(|shuttle| {
1364 if let Err(e) = shuttle.control_channel_sender.send(ControlMessage::ShowModal(message))
1365 {
1366 log::warn!("could not send modal message for display: {e:?}");
1367 }
1368 })
1369 }
1370
1371 pub fn show_notification(
1376 &mut self,
1377 content: impl Into<notification::NotificationContent>,
1378 ) -> Result<Notification, notification::Error> {
1379 self.with_mut(|shuttle| match &mut shuttle.ui {
1381 Some(ui) => Ok(ui.show_notification(content)),
1382 None => Err(notification::Error::NoUi),
1383 })
1384 }
1385
1386 pub async fn yield_to_step(&self) {
1395 self.with_ref(|_| {});
1396 let notifier = self
1397 .session_event_notifier
1398 .upgrade()
1399 .expect("can't happen: session_event_notifier dead");
1400 let (mut wake_flag, listener) = listen::WakeFlag::new(false);
1401 notifier.listen(listener.filter(|event| match event {
1402 Event::Stepped => Some(()),
1403 }));
1404 let _alive = wake_flag.wait().await;
1405 }
1406
1407 #[track_caller]
1408 fn with_ref<R>(&self, f: impl FnOnce(&Shuttle) -> R) -> R {
1409 f(self
1410 .shuttle
1411 .try_read()
1412 .expect("MainTaskContext lock misused somehow (could not read)")
1413 .as_ref()
1414 .expect(
1415 "MainTaskContext operations may not be called while the main task is not executing",
1416 ))
1417 }
1418
1419 #[track_caller]
1422 fn with_mut<R>(&mut self, f: impl FnOnce(&mut Shuttle) -> R) -> R {
1423 f(self
1424 .shuttle
1425 .try_write()
1426 .expect("MainTaskContext lock misused somehow (could not write)")
1427 .as_mut()
1428 .expect(
1429 "MainTaskContext operations may not be called while the main task is not executing",
1430 ))
1431 }
1432}
1433
1434#[cfg(test)]
1435mod tests {
1436 use super::*;
1437 use crate::apps::Key;
1438 use all_is_cubes::character::CharacterTransaction;
1439 use all_is_cubes::math::Cube;
1440 use all_is_cubes::universe::Name;
1441 use all_is_cubes::util::assert_send_sync;
1442 use core::sync::atomic::{AtomicUsize, Ordering};
1443 use futures_channel::oneshot;
1444
1445 fn advance_time(session: &mut Session) {
1446 session.frame_clock.advance_by(session.universe().clock().schedule().delta_t());
1447 let step = session.maybe_step_universe();
1448 assert_ne!(step, None);
1449 }
1450
1451 #[test]
1452 fn is_send_sync() {
1453 assert_send_sync::<Session>();
1454 }
1455
1456 #[macro_rules_attribute::apply(smol_macros::test)]
1457 async fn fluff_forwarding_following() {
1458 let mut u = Universe::new();
1460 let space1 = u.insert_anonymous(Space::empty_positive(1, 1, 1));
1461 let space2 = u.insert_anonymous(Space::empty_positive(1, 1, 1));
1462 let character = StrongHandle::from(
1463 u.insert_anonymous(Character::spawn_default(u.read_ticket(), space1.clone()).unwrap()),
1464 );
1465 let st = space::CubeTransaction::fluff(Fluff::Happened).at(Cube::ORIGIN);
1466
1467 let mut session = Session::builder().build().await;
1469 session.set_universe(u);
1470 session.set_character(Some(character.clone()));
1471 let log = listen::Log::<Fluff>::new();
1472 session.listen_fluff(log.listener());
1473
1474 session.universe_mut().execute_1(&space1, st.clone()).unwrap();
1476 assert_eq!(log.drain(), vec![Fluff::Happened]);
1477
1478 session
1480 .universe_mut()
1481 .execute_1(
1482 &character,
1483 CharacterTransaction::move_to_space(space2.clone()),
1484 )
1485 .unwrap();
1486 session.maybe_step_universe();
1487
1488 session.universe_mut().execute_1(&space1, st.clone()).unwrap();
1490 assert_eq!(log.drain(), vec![]);
1491 session.universe_mut().execute_1(&space2, st).unwrap();
1492 assert_eq!(log.drain(), vec![Fluff::Happened]);
1493 }
1494
1495 #[macro_rules_attribute::apply(smol_macros::test)]
1496 async fn main_task() {
1497 let old_marker = Name::from("old");
1498 let new_marker = Name::from("new");
1499 let noticed_step = Arc::new(AtomicUsize::new(0));
1500 let mut session = Session::builder().build().await;
1501 session
1502 .universe_mut()
1503 .insert(old_marker.clone(), Space::empty_positive(1, 1, 1))
1504 .unwrap();
1505
1506 let (send, recv) = oneshot::channel::<Box<Universe>>();
1508 let mut cameras = session.create_cameras(listen::constant(Viewport::ARBITRARY));
1509 session.set_main_task({
1510 let noticed_step = noticed_step.clone();
1511 async move |mut ctx: MainTaskContext| {
1512 eprintln!("main task: waiting for new universe");
1513 let new_universe = recv.await.unwrap();
1514 ctx.set_universe(new_universe);
1515 eprintln!("main task: have set new universe");
1516
1517 ctx.with_read_tickets(|read_tickets| cameras.update(read_tickets));
1518 assert!(cameras.character().is_some(), "has character");
1519
1520 for _ in 0..2 {
1522 ctx.yield_to_step().await;
1523 eprintln!("main task: got yield_to_step()");
1524 noticed_step.fetch_add(1, Ordering::Relaxed);
1525 }
1526
1527 eprintln!("main task: exiting");
1528 ExitMainTask
1529 }
1530 });
1531
1532 session.maybe_step_universe();
1534 assert!(session.universe_mut().get::<Space>(&old_marker).is_some());
1535
1536 let mut new_universe = Universe::new();
1538 let new_space =
1539 new_universe.insert(new_marker.clone(), Space::empty_positive(1, 1, 1)).unwrap();
1540 new_universe
1541 .insert(
1542 Name::from("character"),
1543 Character::spawn_default(new_universe.read_ticket(), new_space).unwrap(),
1544 )
1545 .unwrap();
1546 send.send(new_universe).unwrap();
1547
1548 assert_eq!(noticed_step.load(Ordering::Relaxed), 0);
1550 session.maybe_step_universe();
1551 assert!(session.universe_mut().get::<Space>(&new_marker).is_some());
1552 assert!(session.universe_mut().get::<Space>(&old_marker).is_none());
1553 assert_eq!(noticed_step.load(Ordering::Relaxed), 0);
1554
1555 advance_time(&mut session);
1557 assert_eq!(noticed_step.load(Ordering::Relaxed), 1);
1558 advance_time(&mut session);
1559 assert_eq!(noticed_step.load(Ordering::Relaxed), 2);
1560
1561 advance_time(&mut session);
1563 assert_eq!(noticed_step.load(Ordering::Relaxed), 2);
1564 }
1565
1566 #[macro_rules_attribute::apply(smol_macros::test)]
1567 async fn input_is_processed_even_without_character() {
1568 let mut session =
1569 Session::builder().ui(listen::constant(Viewport::ARBITRARY)).build().await;
1570 assert!(!session.paused.get());
1571
1572 session.input_processor.key_momentary(Key::Escape); advance_time(&mut session);
1574
1575 assert!(session.paused.get());
1576 }
1577}