use std::path::Path;
use std::sync::Arc;
use anyhow::Result;
use game_toolkit_audio::SoundId;
use game_toolkit_gfx::TextureId;
use winit::event_loop::ActiveEventLoop;
use winit::window::Window;
use crate::Rng;
use crate::app::AppConfig;
use crate::time::Time;
#[non_exhaustive]
pub enum GameEvent {
Resized {
width: u32,
height: u32,
},
FocusChanged(bool),
CloseRequested,
CanvasAttached(bool),
}
pub struct Context {
pub gfx: game_toolkit_gfx::Graphics,
pub audio: Option<game_toolkit_audio::Audio>,
pub input: game_toolkit_input::Input,
pub assets: game_toolkit_assets::Assets,
pub time: Time,
pub rng: Rng,
pub window: Arc<Window>,
pub(crate) quit_requested: bool,
pub(crate) canvas_attached: bool,
#[cfg(target_arch = "wasm32")]
pub(crate) autoplay: Option<crate::web::AutoplayProbe>,
}
pub(crate) struct Parts {
gfx: game_toolkit_gfx::Graphics,
assets: game_toolkit_assets::Assets,
}
pub(crate) fn create_window(
event_loop: &ActiveEventLoop,
config: &AppConfig,
) -> Result<Arc<Window>> {
let attrs = Window::default_attributes()
.with_title(&config.title)
.with_inner_size(winit::dpi::LogicalSize::new(config.width, config.height));
#[cfg(target_arch = "wasm32")]
let attrs = crate::web::canvas_attributes(attrs, config.canvas_id.as_deref())?;
Ok(Arc::new(event_loop.create_window(attrs)?))
}
pub(crate) async fn init_parts(window: Arc<Window>, config: AppConfig) -> Result<Parts> {
let gfx = game_toolkit_gfx::Graphics::new(window, &config.graphics_config()).await?;
let assets = open_assets(&config).await?;
Ok(Parts { gfx, assets })
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(clippy::unused_async)]
async fn open_assets(config: &AppConfig) -> Result<game_toolkit_assets::Assets> {
use game_toolkit_assets::Assets;
let mut assets = Assets::new(&config.asset_root).or_else(|e| {
log::warn!(
"asset root {} unusable ({e}); falling back to cwd",
config.asset_root.display()
);
Assets::new(".")
})?;
if let Some(paths) = &config.web_preload {
assets.set_web_preload(paths);
}
Ok(assets)
}
#[cfg(target_arch = "wasm32")]
async fn open_assets(config: &AppConfig) -> Result<game_toolkit_assets::Assets> {
let mut assets = game_toolkit_assets::Assets::new(&config.asset_root)?;
let paths = match &config.web_preload {
Some(paths) => paths.clone(),
None => assets.fetch_manifest().await?,
};
log::info!("preloading {} asset(s)", paths.len());
assets.preload(paths).await?;
Ok(assets)
}
impl Context {
pub(crate) fn from_parts(window: Arc<Window>, parts: Parts, config: &AppConfig) -> Self {
let audio = match game_toolkit_audio::Audio::new() {
Ok(a) => Some(a),
Err(e) => {
log::warn!("audio init failed, continuing muted: {e}");
None
}
};
let mut input = game_toolkit_input::Input::new();
input.set_touch_emulates_mouse(config.touch_emulates_mouse);
Self {
gfx: parts.gfx,
audio,
input,
assets: parts.assets,
time: Time::new(),
rng: Rng::new(config.random_seed),
window,
quit_requested: false,
canvas_attached: true,
#[cfg(target_arch = "wasm32")]
autoplay: None,
}
}
pub(crate) fn sync_pointer_mapping(&mut self) {
let mapping = pointer_mapping(self.gfx.canvas_rect(), self.gfx.size());
self.input.set_pointer_mapping(mapping);
}
pub fn quit(&mut self) {
self.quit_requested = true;
}
pub fn canvas_attached(&self) -> bool {
self.canvas_attached
}
pub fn load_texture(&mut self, rel: impl AsRef<Path>) -> Result<TextureId> {
let rel = rel.as_ref();
#[cfg(not(target_arch = "wasm32"))]
if !self.assets.is_loaded(rel) {
let id = self.gfx.load_texture(self.assets.resolve(rel))?;
self.assets.check_web_listed(rel);
return Ok(id);
}
let bytes = self.assets.read(rel)?;
self.gfx
.load_texture_bytes(&bytes, Some(&rel.to_string_lossy()))
}
pub fn load_bitmap_font(
&mut self,
rel: impl AsRef<Path>,
layout: &game_toolkit_gfx::GridLayout,
) -> Result<game_toolkit_gfx::BitmapFont> {
let bytes = self.assets.read(rel)?;
game_toolkit_gfx::BitmapFont::from_grid(&mut self.gfx, &bytes, layout)
}
pub fn load_bmfont(&mut self, rel: impl AsRef<Path>) -> Result<game_toolkit_gfx::BitmapFont> {
let rel = rel.as_ref();
let fnt = String::from_utf8(self.assets.read(rel)?)
.map_err(|e| anyhow::anyhow!("{}: not UTF-8 ({e})", rel.display()))?;
let page = game_toolkit_gfx::BitmapFont::bmfont_page_file(&fnt)
.ok_or_else(|| anyhow::anyhow!("{}: no page file", rel.display()))?;
let page_path = Path::new(&page);
if page_path.is_absolute()
|| page_path.components().any(|c| {
!matches!(
c,
std::path::Component::Normal(_) | std::path::Component::CurDir
)
})
{
anyhow::bail!(
"{}: page file {page:?} must be a relative path without `..`",
rel.display()
);
}
let page = rel.parent().unwrap_or(Path::new("")).join(page_path);
let png = self.assets.read(&page)?;
game_toolkit_gfx::BitmapFont::from_bmfont(&mut self.gfx, &fnt, &png)
}
#[cfg(feature = "indexed")]
pub fn load_indexed_texture(
&mut self,
rel: impl AsRef<Path>,
) -> Result<(TextureId, game_toolkit_gfx::Palette)> {
let rel = rel.as_ref();
let bytes = self.assets.read(rel)?;
self.gfx
.load_indexed_texture(&bytes, Some(&rel.to_string_lossy()))
}
pub fn load_sound(&mut self, rel: impl AsRef<Path>) -> Result<Option<SoundId>> {
let Some(audio) = self.audio.as_mut() else {
return Ok(None);
};
let bytes = self.assets.read(rel)?;
audio.load_sound_bytes(bytes).map(Some)
}
pub(crate) fn start_audio(&mut self) {
if let Some(audio) = self.audio.as_mut()
&& !audio.is_started()
&& let Err(e) = audio.start()
{
log::warn!("audio start failed, continuing muted: {e}");
self.audio = None;
}
}
pub fn start_audio_if_allowed(&mut self) {
#[cfg(target_arch = "wasm32")]
if self.audio.as_ref().is_some_and(|a| !a.is_started()) && self.autoplay.is_none() {
self.autoplay = crate::web::AutoplayProbe::start();
self.poll_autoplay();
}
}
#[cfg(target_arch = "wasm32")]
pub(crate) fn poll_autoplay(&mut self) {
use crate::web::Autoplay;
let Some(probe) = &self.autoplay else {
return;
};
let started = self
.audio
.as_ref()
.is_none_or(game_toolkit_audio::Audio::is_started);
let answer = if started {
Autoplay::Denied
} else {
probe.poll()
};
match answer {
Autoplay::Pending => {}
Autoplay::Denied => self.autoplay = None,
Autoplay::Allowed => {
self.autoplay = None;
log::info!("audio allowed without a gesture, starting it now");
self.start_audio();
}
}
}
}
fn pointer_mapping(
rect: Option<game_toolkit_gfx::CanvasRect>,
size: (u32, u32),
) -> Option<game_toolkit_input::PointerMapping> {
rect.map(|rect| {
let scale = rect.canvas_per_window([size.0, size.1]);
game_toolkit_input::PointerMapping {
origin: (rect.origin[0], rect.origin[1]),
scale: (scale[0], scale[1]),
}
})
}
#[cfg(test)]
mod tests {
use game_toolkit_gfx::CanvasRect;
use game_toolkit_input::Input;
use super::pointer_mapping;
fn close(a: (f32, f32), b: (f32, f32)) -> bool {
(a.0 - b.0).abs() < 1e-3 && (a.1 - b.1).abs() < 1e-3
}
#[test]
fn pointer_follows_a_runtime_resolution_change() {
let mut input = Input::new();
input.handle_window_event(&winit::event::WindowEvent::CursorMoved {
device_id: winit::event::DeviceId::dummy(),
position: winit::dpi::PhysicalPosition::new(960.0, 540.0),
});
let rect = CanvasRect {
origin: [0.0, 0.0],
size: [1920.0, 1080.0],
};
input.set_pointer_mapping(pointer_mapping(Some(rect), (640, 360)));
assert!(close(input.mouse_pos(), (320.0, 180.0)));
let rect = CanvasRect {
origin: [240.0, 0.0],
size: [1440.0, 1080.0],
};
input.set_pointer_mapping(pointer_mapping(Some(rect), (800, 600)));
assert!(
close(input.mouse_pos(), (400.0, 300.0)),
"{:?}",
input.mouse_pos()
);
input.set_pointer_mapping(pointer_mapping(None, (1920, 1080)));
assert_eq!(input.mouse_pos(), (960.0, 540.0));
}
}