game_toolkit_core/
context.rs1use std::path::Path;
2use std::sync::Arc;
3
4use anyhow::Result;
5use game_toolkit_audio::SoundId;
6use game_toolkit_gfx::TextureId;
7use winit::event_loop::ActiveEventLoop;
8use winit::window::Window;
9
10use crate::Rng;
11use crate::app::AppConfig;
12use crate::time::Time;
13
14#[non_exhaustive]
15pub enum GameEvent {
16 Resized {
17 width: u32,
18 height: u32,
19 },
20 FocusChanged(bool),
21 CloseRequested,
22 CanvasAttached(bool),
29}
30
31pub struct Context {
32 pub gfx: game_toolkit_gfx::Graphics,
33 pub audio: Option<game_toolkit_audio::Audio>,
34 pub input: game_toolkit_input::Input,
35 pub assets: game_toolkit_assets::Assets,
36 pub time: Time,
37 pub rng: Rng,
38 pub window: Arc<Window>,
39 pub(crate) quit_requested: bool,
40 pub(crate) canvas_attached: bool,
41 #[cfg(target_arch = "wasm32")]
43 pub(crate) autoplay: Option<crate::web::AutoplayProbe>,
44}
45
46pub(crate) struct Parts {
49 gfx: game_toolkit_gfx::Graphics,
50 assets: game_toolkit_assets::Assets,
51}
52
53pub(crate) fn create_window(
56 event_loop: &ActiveEventLoop,
57 config: &AppConfig,
58) -> Result<Arc<Window>> {
59 let attrs = Window::default_attributes()
60 .with_title(&config.title)
61 .with_inner_size(winit::dpi::LogicalSize::new(config.width, config.height));
62 #[cfg(target_arch = "wasm32")]
63 let attrs = crate::web::canvas_attributes(attrs, config.canvas_id.as_deref())?;
64 Ok(Arc::new(event_loop.create_window(attrs)?))
65}
66
67pub(crate) async fn init_parts(window: Arc<Window>, config: AppConfig) -> Result<Parts> {
70 let gfx = game_toolkit_gfx::Graphics::new(
71 window,
72 config.vsync,
73 config.depth_format,
74 config.msaa_samples,
75 )
76 .await?;
77 let assets = open_assets(&config).await?;
78 Ok(Parts { gfx, assets })
79}
80
81#[cfg(not(target_arch = "wasm32"))]
82#[allow(clippy::unused_async)]
83async fn open_assets(config: &AppConfig) -> Result<game_toolkit_assets::Assets> {
84 use game_toolkit_assets::Assets;
85 let mut assets = Assets::new(&config.asset_root).or_else(|e| {
86 log::warn!(
87 "asset root {} unusable ({e}); falling back to cwd",
88 config.asset_root.display()
89 );
90 Assets::new(".")
91 })?;
92 if let Some(paths) = &config.web_preload {
94 assets.set_web_preload(paths);
95 }
96 Ok(assets)
97}
98
99#[cfg(target_arch = "wasm32")]
100async fn open_assets(config: &AppConfig) -> Result<game_toolkit_assets::Assets> {
101 let mut assets = game_toolkit_assets::Assets::new(&config.asset_root)?;
102 let paths = match &config.web_preload {
103 Some(paths) => paths.clone(),
104 None => assets.fetch_manifest().await?,
105 };
106 log::info!("preloading {} asset(s)", paths.len());
107 assets.preload(paths).await?;
108 Ok(assets)
109}
110
111impl Context {
112 pub(crate) fn from_parts(window: Arc<Window>, parts: Parts, config: &AppConfig) -> Self {
113 let audio = match game_toolkit_audio::Audio::new() {
114 Ok(a) => Some(a),
115 Err(e) => {
116 log::warn!("audio init failed, continuing muted: {e}");
117 None
118 }
119 };
120 let mut input = game_toolkit_input::Input::new();
121 input.set_touch_emulates_mouse(config.touch_emulates_mouse);
122 Self {
123 gfx: parts.gfx,
124 audio,
125 input,
126 assets: parts.assets,
127 time: Time::new(),
128 rng: Rng::new(config.random_seed),
129 window,
130 quit_requested: false,
131 canvas_attached: true,
132 #[cfg(target_arch = "wasm32")]
133 autoplay: None,
134 }
135 }
136
137 pub fn quit(&mut self) {
138 self.quit_requested = true;
139 }
140
141 pub fn canvas_attached(&self) -> bool {
144 self.canvas_attached
145 }
146
147 pub fn load_texture(&mut self, rel: impl AsRef<Path>) -> Result<TextureId> {
151 let rel = rel.as_ref();
152 #[cfg(not(target_arch = "wasm32"))]
153 if !self.assets.is_loaded(rel) {
154 let id = self.gfx.load_texture(self.assets.resolve(rel))?;
155 self.assets.check_web_listed(rel);
156 return Ok(id);
157 }
158 let bytes = self.assets.read(rel)?;
159 self.gfx
160 .load_texture_bytes(&bytes, Some(&rel.to_string_lossy()))
161 }
162
163 pub fn load_sound(&mut self, rel: impl AsRef<Path>) -> Result<Option<SoundId>> {
166 let Some(audio) = self.audio.as_mut() else {
167 return Ok(None);
168 };
169 let bytes = self.assets.read(rel)?;
170 audio.load_sound_bytes(bytes).map(Some)
171 }
172
173 pub(crate) fn start_audio(&mut self) {
177 if let Some(audio) = self.audio.as_mut()
178 && !audio.is_started()
179 && let Err(e) = audio.start()
180 {
181 log::warn!("audio start failed, continuing muted: {e}");
182 self.audio = None;
183 }
184 }
185
186 pub fn start_audio_if_allowed(&mut self) {
196 #[cfg(target_arch = "wasm32")]
197 if self.audio.as_ref().is_some_and(|a| !a.is_started()) && self.autoplay.is_none() {
198 self.autoplay = crate::web::AutoplayProbe::start();
199 self.poll_autoplay();
200 }
201 }
202
203 #[cfg(target_arch = "wasm32")]
205 pub(crate) fn poll_autoplay(&mut self) {
206 use crate::web::Autoplay;
207 let Some(probe) = &self.autoplay else {
208 return;
209 };
210 let started = self
212 .audio
213 .as_ref()
214 .is_none_or(game_toolkit_audio::Audio::is_started);
215 let answer = if started {
216 Autoplay::Denied
217 } else {
218 probe.poll()
219 };
220 match answer {
221 Autoplay::Pending => {}
222 Autoplay::Denied => self.autoplay = None,
223 Autoplay::Allowed => {
224 self.autoplay = None;
225 log::info!("audio allowed without a gesture, starting it now");
226 self.start_audio();
227 }
228 }
229 }
230}