use gasm_host::{assets, audio, gfx, host, keymap, net, script, storage};
use std::collections::{HashMap, HashSet};
use std::process::ExitCode;
use std::sync::Arc;
use std::time::{Duration, Instant};
use gilrs::{Button, Gilrs};
use winit::application::ApplicationHandler;
use winit::dpi::LogicalSize;
use winit::event::{DeviceEvent, DeviceId, ElementState, MouseButton, MouseScrollDelta, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::keyboard::{Key, KeyCode, NamedKey, PhysicalKey};
use winit::window::{CursorGrabMode, Window, WindowId};
use gfx::Gfx;
use host::{Game, Gamepad, Host, KEY_STATE_BYTES, Pointer, RawInput, Stop};
use net::Net;
use storage::Storage;
const USAGE: &str = "\
usage: gasm-run <game.wasm|game.cwasm> [options]
options:
--rom <path> shorthand for --asset rom=<path>
--asset <name>=<path> expose a file to the guest as asset <name> (read on demand, not preloaded)
--asset-dir [<prefix>=]<dir>
expose every file under <dir> (repeatable); names are relative paths
like ART/ART.CAR (or <prefix>/ART/ART.CAR), looked up case-insensitively;
symlinks and hidden files are skipped; --asset wins over folder entries
--param <name>=<value> launch parameter for the guest (repeatable)
--allow-net allow the guest to open network connections (gasm:net)
--storage-dir <dir> where the game's saves live (default: <data dir>/gasm/<game>;
headless runs use memory unless this is given)
--storage-id <id> storage namespace (default: the game file's name)
--window <W>x<H> initial window size in logical pixels (default 960x720)
--keymap <file> keyboard layout (default: <data dir>/gasm/keymap.txt if it exists,
else the built-in two-player layout below)
--print-keymap print the active keyboard layout (a starting point for --keymap) and exit
--mute no audio output
--compile <out.cwasm> AOT-compile the game to native code and exit
(then run the .cwasm instead of the .wasm)
--headless <frames> run N frames without window/audio, print hashes
--screenshot <out.png> (headless) write the last frame as PNG (renders gfx on the GPU)
--input <script> (headless) scripted input, FRAMES:ACTION,... with FRAMES = N or FROM-TO:
A+B+START (pad 1: A B X Y L R SELECT START UP DOWN LEFT RIGHT),
\"text\" (text_input; escapes \\n \\b), KEY(ShiftLeft+ArrowLeft) (raw keys),
PTR(x,y[,L+R+M]) (pointer, drawable px), MOVE(dx,dy), WHEEL(x,y),
GP0(B0+B9+A1=0.5) (gamepad slot 0-3: buttons, axes)
--realtime (headless) pace frames at the guest's rate
--no-hash (headless) skip hashing, for benchmarking
default keys (change with --keymap; one binding per line: <pad 1-4> <button> <key code>...):
pad 1: arrows = d-pad, X = A, Z = B, S = X, A = Y, Q/W = L/R,
Enter = Start, Right Shift = Select
pad 2: I/J/K/L = d-pad, . = A, , = B, M = X, N = Y, U/O = L/R,
Right Ctrl or keypad Enter = Start, Backspace = Select
Keyboard bindings for pad N >= 2 apply while fewer than N gamepads are connected;
gamepads take pads in connection order.
Esc = quit";
struct Args {
wasm: String,
assets: HashMap<String, String>,
asset_dirs: Vec<(Option<String>, String)>,
params: HashMap<String, String>,
allow_net: bool,
storage_dir: Option<String>,
storage_id: Option<String>,
window: (u32, u32),
keymap: Option<String>,
print_keymap: bool,
headless: Option<u64>,
screenshot: Option<String>,
compile: Option<String>,
script: script::Script,
realtime: bool,
mute: bool,
no_hash: bool,
}
fn parse_args() -> Result<Args, String> {
let mut it = std::env::args().skip(1);
let mut args = Args {
wasm: String::new(),
assets: HashMap::new(),
asset_dirs: Vec::new(),
params: HashMap::new(),
allow_net: false,
storage_dir: None,
storage_id: None,
window: (960, 720),
keymap: None,
print_keymap: false,
headless: None,
screenshot: None,
compile: None,
script: script::Script::default(),
realtime: false,
mute: false,
no_hash: false,
};
while let Some(a) = it.next() {
let mut val = |name: &str| it.next().ok_or(format!("{name} needs a value"));
match a.as_str() {
"--rom" => {
args.assets.insert("rom".into(), val("--rom")?);
}
"--asset" => {
let v = val("--asset")?;
let (k, p) = v.split_once('=').ok_or("--asset expects name=path")?;
args.assets.insert(k.into(), p.into());
}
"--asset-dir" => {
let v = val("--asset-dir")?;
let dir = match v.split_once('=') {
Some((p, d)) if !p.is_empty() && !p.contains(['/', '\\']) => (Some(p.to_owned()), d.to_owned()),
_ => (None, v),
};
args.asset_dirs.push(dir);
}
"--param" => {
let v = val("--param")?;
let (k, p) = v.split_once('=').ok_or("--param expects name=value")?;
args.params.insert(k.into(), p.into());
}
"--allow-net" => args.allow_net = true,
"--storage-dir" => args.storage_dir = Some(val("--storage-dir")?),
"--storage-id" => args.storage_id = Some(val("--storage-id")?),
"--window" => {
let v = val("--window")?;
let (w, h) = v.split_once('x').ok_or("--window expects WxH")?;
args.window = (w.parse().map_err(|_| "bad width")?, h.parse().map_err(|_| "bad height")?);
}
"--keymap" => args.keymap = Some(val("--keymap")?),
"--print-keymap" => args.print_keymap = true,
"--headless" => {
args.headless = Some(val("--headless")?.parse().map_err(|_| "--headless expects a number")?)
}
"--screenshot" => args.screenshot = Some(val("--screenshot")?),
"--compile" => args.compile = Some(val("--compile")?),
"--input" => args.script = script::Script::parse(&val("--input")?)?,
"--realtime" => args.realtime = true,
"--mute" => args.mute = true,
"--no-hash" => args.no_hash = true,
"-h" | "--help" => return Err(String::new()),
s if s.starts_with("--") => return Err(format!("unknown option {s}")),
s => args.wasm = s.into(),
}
}
if args.wasm.is_empty() && !args.print_keymap {
return Err("missing <game.wasm>".into());
}
Ok(args)
}
fn main() -> ExitCode {
let args = match parse_args() {
Ok(a) => a,
Err(e) => {
if !e.is_empty() {
eprintln!("error: {e}\n");
}
eprintln!("{USAGE}");
return ExitCode::from(2);
}
};
match run(args) {
Ok(code) => ExitCode::from(code as u8),
Err(e) => {
eprintln!("error: {e}");
ExitCode::FAILURE
}
}
}
struct Loaded {
wasm: Vec<u8>,
}
fn open_assets(args: &Args) -> Result<assets::Assets, String> {
let mut a = assets::Assets::new();
for (name, path) in &args.assets {
a.insert_file(name, std::path::Path::new(path))?;
}
for (prefix, dir) in &args.asset_dirs {
let n = a.add_dir(prefix.as_deref(), std::path::Path::new(dir))?;
eprintln!("[gasm] assets: {n} files from {dir}{}", prefix.as_ref().map_or(String::new(), |p| format!(" as {p}/")));
}
a.finish();
Ok(a)
}
fn load_keymap(args: &Args) -> Result<(keymap::Keymap, String, String), String> {
let (text, source) = match &args.keymap {
Some(p) => (std::fs::read_to_string(p).map_err(|e| format!("{p}: {e}"))?, p.clone()),
None => match storage::data_root().map(|r| r.join("keymap.txt")).filter(|p| p.is_file()) {
Some(p) => (std::fs::read_to_string(&p).map_err(|e| format!("{}: {e}", p.display()))?, p.display().to_string()),
None => (keymap::DEFAULT_KEYMAP.to_owned(), "built-in".to_owned()),
},
};
let map = keymap::parse(&text).map_err(|e| format!("keymap {source}:\n{e}"))?;
Ok((map, text, source))
}
fn run(args: Args) -> Result<i32, String> {
if args.print_keymap {
print!("{}", load_keymap(&args)?.1);
return Ok(0);
}
let wasm = std::fs::read(&args.wasm).map_err(|e| format!("{}: {e}", args.wasm))?;
if let Some(out) = &args.compile {
let t0 = Instant::now();
let native = host::precompile(&wasm).map_err(|e| format!("{e:?}"))?;
std::fs::write(out, &native).map_err(|e| format!("{out}: {e}"))?;
eprintln!("[gasm] compiled {} -> {out} ({} bytes) in {:.0} ms", args.wasm, native.len(), t0.elapsed().as_secs_f64() * 1000.0);
return Ok(0);
}
open_assets(&args)?; let loaded = Loaded { wasm };
match args.headless {
Some(frames) => headless(&args, loaded, frames),
None => windowed(args, loaded),
}
}
fn open_storage(args: &Args) -> Result<Storage, Stop> {
let id = args.storage_id.clone().unwrap_or_else(|| {
std::path::Path::new(&args.wasm).file_stem().map_or("game".into(), |s| s.to_string_lossy().into_owned())
});
if !storage::valid_key(&id) {
return Err(Stop::Trap(format!("invalid storage id {id:?} (use [A-Za-z0-9._-])")));
}
let dir = match (&args.storage_dir, args.headless) {
(Some(d), _) => Some(std::path::PathBuf::from(d)),
(None, Some(_)) => None,
(None, None) => storage::default_dir(&id),
};
let s = match dir {
Some(d) => Storage::open(d).map_err(Stop::Trap)?,
None => Storage::memory(),
};
eprintln!("[gasm] storage: {}", s.location());
Ok(s)
}
fn load(args: &Args, loaded: &Loaded, audio: Option<audio::AudioSink>, gfx: Gfx) -> Result<Game, Stop> {
let storage = open_storage(args)?;
let assets = open_assets(args).map_err(Stop::Trap)?;
let mut host = Host::new(assets, args.params.clone(), audio, gfx, Net::new(args.allow_net), storage);
if args.headless.is_some() {
host.virtual_time_ms = Some(0.0);
}
host.hashing = args.headless.is_some() && !args.no_hash;
let t0 = Instant::now();
let game = Game::load(&loaded.wasm, host)?;
eprintln!("[gasm] loaded {} in {:.0} ms", args.wasm, t0.elapsed().as_secs_f64() * 1000.0);
Ok(game)
}
fn headless(args: &Args, loaded: Loaded, frames: u64) -> Result<i32, String> {
let gfx = match &args.screenshot {
Some(_) => Gfx::offscreen(1280, 720).unwrap_or_else(|e| {
eprintln!("[gasm] no GPU for screenshots ({e}); gfx output will be blank");
Gfx::null()
}),
None => Gfx::null(),
};
let mut game = match load(args, &loaded, None, gfx) {
Ok(g) => g,
Err(Stop::Exit(code)) => return Ok(code),
Err(Stop::Trap(e)) => return Err(e),
};
let t0 = Instant::now();
let mut exit = None;
let mut ran = 0;
let mut script_state = script::ScriptState::default();
for i in 0..frames {
let rate = game.host().frame_rate;
let host = game.host_mut();
host.virtual_time_ms = Some(i as f64 * 1000.0 / rate);
host.pads = [args.script.pad(i), 0, 0, 0];
host.text = Some(args.script.text(i));
let (w, h) = host.gfx.size();
let mode = host.input_mode;
host.input = args.script.raw(i, &mut script_state, (w as f32, h as f32), mode);
host.show_frame = args.screenshot.is_some() && i + 1 == frames;
ran = i + 1;
match game.frame() {
Ok(()) => {}
Err(Stop::Exit(code)) => {
exit = Some(code);
break;
}
Err(Stop::Trap(e)) => return Err(format!("frame {i}: {e}")),
}
if args.realtime {
let due = t0 + Duration::from_secs_f64((i + 1) as f64 / rate);
if let Some(wait) = due.checked_duration_since(Instant::now()) {
std::thread::sleep(wait);
}
}
}
if exit.is_none() {
game.exit();
}
let secs = t0.elapsed().as_secs_f64();
let h = game.host();
println!("frames={ran} presented={} size={}x{}", h.frames_presented, h.width, h.height);
println!("video_fnv32={:08x} audio_fnv32={:08x} audio_frames={}", h.video_hash.0, h.audio_hash.0, h.audio_frames);
eprintln!(
"[gasm] {:.1} guest frames/s ({:.1}x realtime at {:.2} Hz)",
ran as f64 / secs,
ran as f64 / secs / h.frame_rate,
h.frame_rate
);
if let Some(path) = &args.screenshot {
let (w, hgt, rgba) = match h.gfx.read_offscreen() {
Some(img) if h.gfx.used => img,
_ => (h.width as u32, h.height as u32, h.rgba.clone()),
};
if w > 0 {
write_png(path, w, hgt, &rgba)?;
eprintln!("[gasm] wrote {path}");
}
}
if let Some(code) = exit {
eprintln!("[gasm] guest exited with code {code}");
return Ok(code);
}
Ok(0)
}
fn write_png(path: &str, w: u32, h: u32, rgba: &[u8]) -> Result<(), String> {
let file = std::fs::File::create(path).map_err(|e| e.to_string())?;
let mut enc = png::Encoder::new(std::io::BufWriter::new(file), w, h);
enc.set_color(png::ColorType::Rgba);
enc.set_depth(png::BitDepth::Eight);
let mut writer = enc.write_header().map_err(|e| e.to_string())?;
writer.write_image_data(rgba).map_err(|e| e.to_string())
}
fn gamepad_pads(gilrs: &mut Gilrs) -> ([u32; 4], usize) {
const MAP: &[(Button, u32)] = &[
(Button::East, 1 << 0),
(Button::South, 1 << 1),
(Button::North, 1 << 2),
(Button::West, 1 << 3),
(Button::LeftTrigger, 1 << 4),
(Button::RightTrigger, 1 << 5),
(Button::Select, 1 << 6),
(Button::Start, 1 << 7),
(Button::DPadUp, 1 << 8),
(Button::DPadDown, 1 << 9),
(Button::DPadLeft, 1 << 10),
(Button::DPadRight, 1 << 11),
];
while gilrs.next_event().is_some() {}
let mut pads = [0u32; 4];
let connected = gilrs.gamepads().count();
for (i, (_, pad)) in gilrs.gamepads().take(4).enumerate() {
pads[i] = MAP.iter().filter(|(b, _)| pad.is_pressed(*b)).fold(0, |m, (_, bit)| m | bit);
let (x, y) = (pad.value(gilrs::Axis::LeftStickX), pad.value(gilrs::Axis::LeftStickY));
if x < -0.5 { pads[i] |= 1 << 10 }
if x > 0.5 { pads[i] |= 1 << 11 }
if y > 0.5 { pads[i] |= 1 << 8 }
if y < -0.5 { pads[i] |= 1 << 9 }
}
(pads, connected)
}
#[derive(Default)]
struct MouseState {
x: f32,
y: f32,
inside: bool,
buttons: u32,
pressed: u32,
released: u32,
dx: f32,
dy: f32,
wheel_x: f32,
wheel_y: f32,
}
const ESC_HOLD: Duration = Duration::from_millis(1000);
fn mouse_bit(b: MouseButton) -> u32 {
match b {
MouseButton::Left => 1,
MouseButton::Right => 2,
MouseButton::Middle => 4,
MouseButton::Back => 8,
MouseButton::Forward => 16,
MouseButton::Other(_) => 0,
}
}
fn gamepads_raw(gilrs: &Gilrs) -> [Gamepad; 4] {
use gilrs::{Axis, MappingSource};
const BUTTONS: [Button; 17] = [
Button::South, Button::East, Button::West, Button::North, Button::LeftTrigger, Button::RightTrigger,
Button::LeftTrigger2, Button::RightTrigger2, Button::Select, Button::Start, Button::LeftThumb,
Button::RightThumb, Button::DPadUp, Button::DPadDown, Button::DPadLeft, Button::DPadRight, Button::Mode,
];
let mut out: [Gamepad; 4] = Default::default();
for (slot, (_, pad)) in gilrs.gamepads().take(4).enumerate() {
let g = &mut out[slot];
g.connected = true;
g.name = pad.name().to_owned();
if pad.mapping_source() != MappingSource::None {
g.standard = true;
g.buttons = BUTTONS.iter().map(|b| pad.button_data(*b).map_or(0.0, |d| d.value())).collect();
g.axes = vec![pad.value(Axis::LeftStickX), -pad.value(Axis::LeftStickY), pad.value(Axis::RightStickX), -pad.value(Axis::RightStickY)];
} else {
let mut b: Vec<_> = pad.state().buttons().map(|(c, d)| (c.into_u32(), d.value())).collect();
let mut a: Vec<_> = pad.state().axes().map(|(c, d)| (c.into_u32(), d.value())).collect();
b.sort_by_key(|x| x.0);
a.sort_by_key(|x| x.0);
g.buttons = b.into_iter().map(|x| x.1).take(host::GAMEPAD_BUTTONS).collect();
g.axes = a.into_iter().map(|x| x.1).take(host::GAMEPAD_AXES).collect();
}
}
out
}
struct App {
args: Args,
keymap: keymap::Keymap,
loaded: Loaded,
window: Option<Arc<Window>>,
game: Option<Game>,
keys: HashSet<KeyCode>,
typed: String,
key_events: Vec<(u16, bool)>,
esc_down: Option<Instant>,
mouse: MouseState,
applied_mode: u32,
pointer_flags: u32,
gilrs: Option<Gilrs>,
next: Instant,
fps_t: Instant,
fps_n: u32,
result: Result<i32, String>,
}
impl App {
fn stop(&mut self, el: &ActiveEventLoop, result: Result<i32, String>) {
self.result = result;
el.exit();
}
fn apply_input_mode(&mut self) {
let (Some(game), Some(w)) = (&self.game, &self.window) else { return };
let want = game.host().input_mode & 6;
if want == self.applied_mode {
return;
}
self.applied_mode = want;
let locked = want & 4 != 0
&& w.set_cursor_grab(CursorGrabMode::Locked).or_else(|_| w.set_cursor_grab(CursorGrabMode::Confined)).is_ok();
if want & 4 == 0 {
let _ = w.set_cursor_grab(CursorGrabMode::None);
}
w.set_cursor_visible(want == 0);
self.pointer_flags = if want != 0 { 2 } else { 0 } | if locked { 4 } else { 0 };
}
fn quit(&mut self, el: &ActiveEventLoop) {
if let Some(g) = &mut self.game {
g.exit();
}
self.stop(el, Ok(0));
}
fn tick(&mut self, el: &ActiveEventLoop) {
let Some(game) = &mut self.game else { return };
let period = Duration::from_secs_f64(1.0 / game.host().frame_rate);
let now = Instant::now();
if now >= self.next {
let behind = ((now - self.next).as_secs_f64() / period.as_secs_f64()) as u32 + 1;
let steps = behind.min(4);
if self.esc_down.is_some_and(|t| t.elapsed() >= ESC_HOLD) {
return self.quit(el);
}
let mut keys = [0u8; KEY_STATE_BYTES];
for &c in &self.keys {
let k = keymap::gasm_key(c);
if k != 0 {
keys[k as usize / 8] |= 1 << (k % 8);
}
}
let raw_pads = self.gilrs.as_ref().map(gamepads_raw);
let drawable = game.host().gfx.size();
for k in 0..steps {
let (mut pads, gamepads) = self.gilrs.as_mut().map(gamepad_pads).unwrap_or_default();
if game.host().input_mode & 1 == 0 {
let kb = keymap::pads(&self.keymap, &self.keys, gamepads);
for (p, k) in pads.iter_mut().zip(kb) {
*p |= k;
}
}
let host = game.host_mut();
host.pads = pads;
let first = k == 0;
host.text = Some(if first { std::mem::take(&mut self.typed) } else { String::new() });
let m = &mut self.mouse;
let pointer = Pointer {
x: m.x,
y: m.y,
dx: if first { m.dx } else { 0.0 },
dy: if first { m.dy } else { 0.0 },
wheel_x: if first { m.wheel_x } else { 0.0 },
wheel_y: if first { m.wheel_y } else { 0.0 },
buttons: m.buttons,
pressed: if first { m.pressed } else { 0 },
released: if first { m.released } else { 0 },
flags: m.inside as u32 | self.pointer_flags,
drawable: (drawable.0 as f32, drawable.1 as f32),
};
if first {
(m.dx, m.dy, m.wheel_x, m.wheel_y, m.pressed, m.released) = (0.0, 0.0, 0.0, 0.0, 0, 0);
}
host.input = RawInput {
keys: Some(keys),
key_events: if first { std::mem::take(&mut self.key_events) } else { Vec::new() },
pointer: Some(pointer),
gamepads: Some(raw_pads.clone().unwrap_or_default()),
};
host.show_frame = k + 1 == steps;
host.gfx.used = false;
match game.frame() {
Ok(()) => {}
Err(Stop::Exit(code)) => return self.stop(el, Ok(code)),
Err(Stop::Trap(e)) => return self.stop(el, Err(e)),
}
self.next += period;
self.fps_n += 1;
}
if behind > 4 {
self.next = Instant::now() + period;
}
let host = game.host_mut();
if !host.gfx.used && host.width > 0 {
let (w, h) = (host.width as u32, host.height as u32);
let rgba = std::mem::take(&mut host.rgba);
host.gfx.present_video(&rgba, w, h);
host.rgba = rgba;
}
}
self.apply_input_mode();
if self.fps_t.elapsed() >= Duration::from_secs(1) {
if let Some(w) = &self.window {
w.set_title(&format!("gasm — {} — {} fps", self.args.wasm, self.fps_n));
}
self.fps_n = 0;
self.fps_t = Instant::now();
}
el.set_control_flow(ControlFlow::WaitUntil(self.next));
}
}
impl ApplicationHandler for App {
fn resumed(&mut self, el: &ActiveEventLoop) {
if self.window.is_some() {
return;
}
let attrs = Window::default_attributes()
.with_title("gasm")
.with_inner_size(LogicalSize::new(self.args.window.0, self.args.window.1));
let window = match el.create_window(attrs) {
Ok(w) => Arc::new(w),
Err(e) => return self.stop(el, Err(format!("cannot create window: {e}"))),
};
let gfx = match Gfx::for_window(window.clone()) {
Ok(g) => g,
Err(e) => return self.stop(el, Err(format!("cannot initialise GPU: {e}"))),
};
let audio = if self.args.mute {
None
} else {
match audio::AudioSink::open() {
Ok(a) => {
eprintln!("[gasm] audio: {} Hz", a.device_rate());
Some(a)
}
Err(e) => {
eprintln!("[gasm] audio disabled: {e}");
None
}
}
};
match load(&self.args, &self.loaded, audio, gfx) {
Ok(g) => self.game = Some(g),
Err(Stop::Exit(code)) => return self.stop(el, Ok(code)),
Err(Stop::Trap(e)) => return self.stop(el, Err(e)),
}
self.window = Some(window);
self.next = Instant::now();
el.set_control_flow(ControlFlow::WaitUntil(self.next));
}
fn window_event(&mut self, el: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
match event {
WindowEvent::CloseRequested => self.quit(el),
WindowEvent::Resized(size) => {
if let Some(g) = &mut self.game {
g.host_mut().gfx.resize(size.width, size.height);
}
}
WindowEvent::Focused(false) => {
for c in self.keys.drain() {
let k = keymap::gasm_key(c);
if k != 0 {
self.key_events.push((k, false));
}
}
self.esc_down = None;
self.mouse.inside = false;
}
WindowEvent::CursorMoved { position, .. } => {
self.mouse.x = position.x as f32;
self.mouse.y = position.y as f32;
self.mouse.inside = true;
}
WindowEvent::CursorEntered { .. } => self.mouse.inside = true,
WindowEvent::CursorLeft { .. } => self.mouse.inside = false,
WindowEvent::MouseInput { state, button, .. } => {
let bit = mouse_bit(button);
match state {
ElementState::Pressed => {
self.mouse.buttons |= bit;
self.mouse.pressed |= bit;
}
ElementState::Released => {
self.mouse.buttons &= !bit;
self.mouse.released |= bit;
}
}
}
WindowEvent::MouseWheel { delta, .. } => {
let (x, y) = match delta {
MouseScrollDelta::LineDelta(x, y) => (x, y),
MouseScrollDelta::PixelDelta(p) => (p.x as f32 / 100.0, p.y as f32 / 100.0),
};
self.mouse.wheel_x -= x;
self.mouse.wheel_y -= y;
}
WindowEvent::KeyboardInput { event, .. } => {
if let PhysicalKey::Code(code) = event.physical_key {
if code == KeyCode::Escape && !event.repeat {
self.esc_down = (event.state == ElementState::Pressed).then(Instant::now);
}
let k = keymap::gasm_key(code);
if !event.repeat && k != 0 {
self.key_events.push((k, event.state == ElementState::Pressed));
}
match event.state {
ElementState::Pressed => self.keys.insert(code),
ElementState::Released => self.keys.remove(&code),
};
}
if event.state == ElementState::Pressed {
match &event.logical_key {
Key::Named(NamedKey::Enter) => self.typed.push('\n'),
Key::Named(NamedKey::Backspace) => self.typed.push('\u{8}'),
_ => {
if let Some(t) = &event.text {
self.typed.extend(t.chars().filter(|c| !c.is_control()));
}
}
}
}
}
WindowEvent::RedrawRequested => self.tick(el),
_ => {}
}
}
fn device_event(&mut self, _el: &ActiveEventLoop, _id: DeviceId, event: DeviceEvent) {
if let DeviceEvent::MouseMotion { delta } = event
&& self.window.as_ref().is_some_and(|w| w.has_focus())
{
self.mouse.dx += delta.0 as f32;
self.mouse.dy += delta.1 as f32;
}
}
fn about_to_wait(&mut self, el: &ActiveEventLoop) {
self.tick(el);
}
}
fn windowed(args: Args, loaded: Loaded) -> Result<i32, String> {
let (keymap, _, source) = load_keymap(&args)?;
eprintln!("[gasm] keyboard layout: {source}");
let event_loop = EventLoop::new().map_err(|e| e.to_string())?;
let mut app = App {
keymap,
args,
loaded,
window: None,
game: None,
keys: HashSet::new(),
typed: String::new(),
key_events: Vec::new(),
esc_down: None,
mouse: MouseState::default(),
applied_mode: 0,
pointer_flags: 0,
gilrs: Gilrs::new().ok(),
next: Instant::now(),
fps_t: Instant::now(),
fps_n: 0,
result: Ok(0),
};
event_loop.run_app(&mut app).map_err(|e| e.to_string())?;
app.result
}