use core::num::NonZeroU32;
use core::time::Duration;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use winit::application::ApplicationHandler;
use winit::event::{DeviceEvent, DeviceId, ElementState, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::window::{Window, WindowId};
use crate::assets::Assets;
use crate::context::{Engine, Recording, Run, Simulating};
use crate::gpu::Gpu;
use crate::input::{Input, Pads, WheelRate};
use crate::math::UVec2;
use crate::renderer::Renderer;
use crate::save::Saved;
use crate::sound::{Audio, Output};
use crate::time::Clock;
use crate::ui::Overlay;
use crate::{Camera, Config, Error, Game, InitContext};
#[cfg(not(target_arch = "wasm32"))]
use native as sys;
#[cfg(target_arch = "wasm32")]
use web as sys;
pub use sys::Instant;
pub(crate) use sys::{AFTER_LOSS, AFTER_OUT_OF_MEMORY, PLATFORM, install_diagnostics};
use sys::PointerHold;
pub(crate) const WHEEL_RATE: WheelRate = PLATFORM.wheel_rate();
pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400);
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum Platform {
Desktop,
Browser,
}
impl Platform {
pub(crate) const fn wheel_rate(self) -> WheelRate {
match self {
Self::Desktop => WheelRate::DESKTOP,
Self::Browser => WheelRate::BROWSER,
}
}
}
pub(crate) fn double_click_interval(config: &Config) -> Duration {
config
.double_click_interval()
.or_else(sys::double_click_interval)
.unwrap_or(DOUBLE_CLICK_INTERVAL)
}
#[cfg(test)]
const GARBAGE: [char; 8] = [' ', '\n', '\\', 's', '#', '\u{0}', '\u{7f}', 'é'];
pub(crate) struct Store(Option<String>);
impl Store {
pub(crate) fn bindings(title: Option<&str>) -> Self {
Self(title.map(|title| format!("{}-bindings", named(title))))
}
pub(crate) fn saves(title: Option<&str>) -> Self {
Self(title.map(|title| format!("{}-saves", named(title))))
}
pub(crate) fn read(&self) -> Option<String> {
sys::store_read(self.0.as_deref()?)
}
pub(crate) fn write(&self, text: &str) {
let Some(title) = self.0.as_deref() else {
return;
};
sys::store_write(title, text);
}
}
#[cfg(test)]
pub(crate) fn manglings(kept: &str) -> Vec<String> {
let letters: Vec<char> = kept.chars().collect();
let lines: Vec<&str> = kept.lines().collect();
let mut out = Vec::with_capacity((letters.len() + 1) * (GARBAGE.len() + 1) + lines.len());
for at in 0..=letters.len() {
out.push(letters[..at].iter().collect());
out.extend(GARBAGE.iter().map(|&dropped| {
let mut with = letters.clone();
with.insert(at, dropped);
with.iter().collect()
}));
}
out.extend((0..lines.len()).map(|at| {
let mut twice = lines.clone();
twice.insert(at, lines[at]);
twice.join("\n")
}));
out
}
fn named(title: &str) -> String {
let plain: String = title
.chars()
.map(|letter| match letter.is_ascii_alphanumeric() {
true => letter.to_ascii_lowercase(),
false => '-',
})
.collect();
match plain.trim_matches('-') {
"" => "game".to_owned(),
trimmed => trimmed.to_owned(),
}
}
type Init<G> = Box<dyn FnOnce(&mut InitContext<'_, G>) -> Result<G, Error>>;
pub(crate) fn run<G: Game>(
config: Config,
init: impl FnOnce(&mut InitContext<'_, G>) -> Result<G, Error> + 'static,
) {
install_diagnostics();
let event_loop = match EventLoop::new() {
Ok(event_loop) => event_loop,
Err(error) => {
sys::report_fatal(&Error::msg(format!("no event loop: {error}")));
return;
}
};
event_loop.set_control_flow(ControlFlow::Wait);
sys::run_app(event_loop, App::<G>::new(config, Box::new(init)));
}
struct Booted<G: Game> {
gpu: Gpu,
assets: Rc<Assets>,
renderer: Renderer<G::Meshes, G::Skyboxes>,
}
type Handed<G> = Rc<RefCell<Option<Result<Booted<G>, Error>>>>;
struct BootHandoff<G: Game>(Handed<G>);
impl<G: Game> BootHandoff<G> {
fn publish(&self, booted: Result<Booted<G>, Error>) {
*self.0.borrow_mut() = Some(booted);
}
fn collect(&self) -> Option<Result<Booted<G>, Error>> {
self.0.borrow_mut().take()
}
}
impl<G: Game> Clone for BootHandoff<G> {
fn clone(&self) -> Self {
Self(Rc::clone(&self.0))
}
}
impl<G: Game> Default for BootHandoff<G> {
fn default() -> Self {
Self(Rc::default())
}
}
pub(crate) async fn load_assets(sources: &[String]) -> Result<Assets, Error> {
let mut files = Vec::with_capacity(sources.len());
for source in sources {
files.push((source.clone(), sys::read(source).await?));
}
Assets::load(files)
}
async fn boot<G: Game>(
instance: wgpu::Instance,
window: Arc<Window>,
config: Config,
) -> Result<Booted<G>, Error> {
let gpu = Gpu::new(instance, window, sys::adapter_facts().await).await?;
let assets = Rc::new(load_assets(config.asset_sources()).await?);
let renderer = Renderer::new::<G::SurfaceStyles, G::PostEffects>(
gpu.device(),
gpu.queue(),
gpu.target_format(),
&config,
Rc::clone(&assets),
)
.await?;
Ok(Booted {
gpu,
assets,
renderer,
})
}
enum Stage<G: Game> {
Unstarted,
Booting,
Running(Box<Running<G>>),
}
struct Running<G: Game> {
gpu: Gpu,
renderer: Renderer<G::Meshes, G::Skyboxes>,
overlay: Overlay,
hold: PointerHold,
audio: Audio<G::Sounds>,
input: Input,
saves: Saved,
game: G,
clock: Clock,
run: Run,
last_camera: Camera,
}
struct App<G: Game> {
config: Config,
handoff: BootHandoff<G>,
stage: Stage<G>,
init: Option<Init<G>>,
}
impl<G: Game> App<G> {
fn new(config: Config, init: Init<G>) -> Self {
Self {
config,
handoff: BootHandoff::default(),
stage: Stage::Unstarted,
init: Some(init),
}
}
fn boot(&mut self, event_loop: &ActiveEventLoop) {
let attributes = match sys::window_attributes(&self.config) {
Ok(attributes) => attributes,
Err(error) => return end_run(event_loop, &error),
};
let window = match event_loop.create_window(attributes) {
Ok(window) => Arc::new(window),
Err(error) => {
return end_run(event_loop, &Error::msg(format!("no window: {error}")));
}
};
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_with_display_handle(
Box::new(event_loop.owned_display_handle()),
));
self.stage = Stage::Booting;
let handoff = self.handoff.clone();
let config = self.config.clone();
sys::spawn(async move {
handoff.publish(boot::<G>(instance, Arc::clone(&window), config).await);
wake_event_loop(&window);
});
}
fn start_game_if_booted(&mut self, event_loop: &ActiveEventLoop) {
let Some(booted) = self.handoff.collect() else {
return;
};
let Some(init) = self.init.take() else {
return;
};
let Booted {
gpu,
assets,
mut renderer,
} = match booted {
Ok(booted) => booted,
Err(error) => return end_run(event_loop, &error),
};
let overlay = Overlay::windowed(gpu.device(), gpu.overlay_format(), gpu.window());
let mut audio = Audio::<G::Sounds>::new(Rc::clone(&assets), Output::open());
let input = Input::new::<G::InputActions>(
Pads::open(),
Store::bindings(Some(self.config.title())),
double_click_interval(&self.config),
);
let saves = Saved::new(Store::saves(Some(self.config.title())));
audio.build_catalog();
renderer.build_catalog(gpu.device(), gpu.queue());
if let Some(error) = assets.unresolved() {
return end_run(event_loop, &error);
}
let engine = Engine::new(
gpu.device(),
gpu.queue(),
gpu.physical_size(),
&self.config,
Camera::default(),
);
let started = init(&mut InitContext::new(
engine,
&mut renderer,
&mut audio,
&saves,
&assets,
&overlay,
));
match started {
Ok(game) => {
let clock = Clock::new(Instant::now());
self.stage = Stage::Running(Box::new(Running {
gpu,
renderer,
overlay,
hold: PointerHold::released(),
audio,
input,
saves,
game,
clock,
run: Run::new(self.config.tick_interval()),
last_camera: Camera::default(),
}));
}
Err(error) => end_run(event_loop, &error),
}
}
}
impl<G: Game> ApplicationHandler for App<G> {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if matches!(self.stage, Stage::Unstarted) {
self.boot(event_loop);
self.start_game_if_booted(event_loop);
}
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
_window: WindowId,
event: WindowEvent,
) {
self.start_game_if_booted(event_loop);
let Stage::Running(running) = &mut self.stage else {
return;
};
let Running {
gpu,
renderer,
overlay,
hold,
audio,
input,
saves,
game,
clock,
run,
last_camera,
} = running.as_mut();
overlay.fold(&event);
input.see(&event);
if is_gesture(&event) {
audio.unlock();
hold.see_gesture(&gpu.window());
}
if matches!(event, WindowEvent::Focused(_)) {
hold.see_focus_change();
}
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::Resized(size) => {
gpu.resize(UVec2::new(size.width, size.height));
gpu.request_frame();
}
WindowEvent::RedrawRequested => {
if let Some(fault) = gpu.fault() {
log::error!("{fault}");
return end_run(event_loop, &Error::msg(fault.told(gpu.adapter())));
}
let config = &self.config;
let size = gpu.physical_size();
let engine = || Engine::new(gpu.device(), gpu.queue(), size, config, *last_camera);
let dt = run.start_ticks();
let owed = clock.frame_at(Instant::now(), dt);
input.sample(clock.elapsed());
if let Some(ticks) = NonZeroU32::new(owed) {
let elapsed = clock.elapsed();
input.ticks(ticks, |ticking| {
let simulating = Simulating { audio, saves, run };
game.tick(&mut simulating.tick_context(
engine(),
renderer.meshes(),
ticking,
dt,
elapsed,
overlay.claims(),
));
});
}
let time = clock.frame_time(run.tick_interval());
let shown = overlay.frame(gpu.device(), gpu.queue(), |layer| {
let recording = Recording {
renderer,
input,
simulating: Simulating { audio, saves, run },
};
game.frame(&mut recording.frame_context(engine(), time, layer));
});
input.hold_pointer(hold.set(&gpu.window(), shown.holds_pointer()));
*last_camera = renderer.camera();
audio.flush(last_camera.view());
input.flush();
saves.flush();
renderer.render(gpu, overlay);
if run.closing() {
sys::close(&gpu.window(), config);
event_loop.exit();
} else {
gpu.request_frame();
}
}
_ => {}
}
}
fn device_event(
&mut self,
_event_loop: &ActiveEventLoop,
_device: DeviceId,
event: DeviceEvent,
) {
let Stage::Running(running) = &mut self.stage else {
return;
};
running.input.see_device(&event);
}
}
fn is_gesture(event: &WindowEvent) -> bool {
matches!(
event,
WindowEvent::KeyboardInput { .. }
| WindowEvent::Touch(_)
| WindowEvent::MouseInput {
state: ElementState::Pressed,
..
}
)
}
fn wake_event_loop(window: &Window) {
window.request_redraw();
}
fn end_run(event_loop: &ActiveEventLoop, error: &Error) {
sys::report_fatal(error);
event_loop.exit();
}
#[cfg(not(target_arch = "wasm32"))]
mod native;
#[cfg(target_arch = "wasm32")]
mod web;
#[cfg(test)]
mod tests {
use super::{Store, named};
#[test]
fn a_title_becomes_a_name_a_file_system_and_a_page_both_take() {
assert_eq!(named("Mirage Breakout"), "mirage-breakout");
assert_eq!(named("../../etc/passwd"), "etc-passwd");
assert_eq!(named(" "), "game", "and there is always a name");
}
#[test]
fn the_two_kinds_of_kept_text_never_name_one_place() {
assert_eq!(
Store::bindings(Some("Mirage Breakout")).0.as_deref(),
Some("mirage-breakout-bindings")
);
assert_eq!(
Store::saves(Some("Mirage Breakout")).0.as_deref(),
Some("mirage-breakout-saves")
);
assert_ne!(
Store::bindings(Some("Escape Saves")).0,
Store::saves(Some("Escape")).0,
"whatever a title ends in"
);
assert_eq!(Store::saves(None).0, None, "and a titleless run keeps none");
}
}