Skip to main content

repame_shell/
lib.rs

1//! App wiring: Repose platform runners plus sim and gamepad helpers.
2/// Games provide the root view; `run_*` runners mount only, stepping
3/// stays game-side via [`Sim::step`].
4use std::collections::HashSet;
5use web_time::Duration;
6
7use anyhow::Result;
8pub use repame_sim::{Sim, SimTime};
9pub use repame_sprite::{Camera2d, FrameInput, PickEvent, SpriteInstance};
10use repose_core::View;
11pub use repose_core::input::{GamepadEvent, GamepadId};
12use repose_platform::gamepad::{GamepadBackend, create_backend};
13
14mod audio;
15mod levels;
16mod pads;
17mod shortcuts;
18mod staging;
19pub use audio::drain_cues;
20pub use levels::apply_scheduler_levels;
21pub use pads::{PadBank, PadBridge, TRIGGER_HELD};
22pub use shortcuts::{
23    SharedEdges, ShortcutEdges, game_shortcut_map, install_game_shortcuts, install_handler_into,
24    install_map, shared_edges, shortcut_handler, take as take_shortcut_edges,
25};
26pub use staging::Staging;
27
28/// Per-frame hooks the game implements.
29pub trait ShellHooks {
30    /// Build this frame's viewport snapshot from sim and UI state.
31    fn frame_input(&mut self, sim: &Sim) -> FrameInput;
32    /// Variable-rate hook (tweens, audio, autosave timers).
33    fn on_frame(&mut self, _sim: &mut Sim, _dt: Duration) {}
34}
35
36pub struct RetainedRoot {
37    key: String,
38    generation: u64,
39}
40
41impl RetainedRoot {
42    pub fn new() -> Self {
43        Self {
44            key: format!(
45                "repame.retained_root.{}",
46                repose_core::unique_component_id()
47            ),
48            generation: 0,
49        }
50    }
51
52    pub fn call<F>(&mut self, sched: &mut repose_core::runtime::Scheduler, build: F) -> View
53    where
54        F: FnOnce(&mut repose_core::runtime::Scheduler) -> View,
55    {
56        let key = &self.key;
57        let epoch = repose_core::timer::frame_count();
58        let generation = self.generation;
59        repose_core::scope!(key, sched, [epoch, generation], { build(sched) })
60    }
61
62    pub fn clear(&mut self) {
63        self.generation = self.generation.wrapping_add(1);
64    }
65}
66
67impl Default for RetainedRoot {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73/// Desktop entry point. Mounts `root` with title and size.
74/// The root closure owns stepping and requests frames for continuity.
75/// The closure receives the live [`Scheduler`] each frame, whose
76/// `held_keys` / `window_focused` / `mouse_*` fields are the single
77/// polled hardware snapshot (GML `keyboard_check` /
78/// `mouse_check_button` parity).
79#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
80pub fn run_desktop(
81    title: &str,
82    size: (u32, u32),
83    mut root: impl FnMut(
84        &mut repose_core::runtime::Scheduler,
85        &repose_core::RenderContext,
86    ) -> repose_core::View
87    + 'static,
88) -> Result<()> {
89    let config = repose_app::AppConfig {
90        window_title: title.to_string(),
91        window_size: size,
92        ..Default::default()
93    };
94    let mut retained = RetainedRoot::new();
95    repose_platform::run_desktop_app_with_config(
96        move |sched, ctx| retained.call(sched, |sched| root(sched, ctx)),
97        config,
98    )?;
99    Ok(())
100}
101
102/// Web entry point. Mounts `root` with default options.
103#[cfg(target_arch = "wasm32")]
104pub fn run_web(
105    mut root: impl FnMut(
106        &mut repose_core::runtime::Scheduler,
107        &repose_core::RenderContext,
108    ) -> repose_core::View
109    + 'static,
110) -> Result<(), wasm_bindgen::JsValue> {
111    let mut options = repose_platform::web::WebOptions::new(None);
112    options.set_prevent_default(true);
113    let mut retained = RetainedRoot::new();
114    repose_platform::web::run_web_app(
115        move |sched, ctx| retained.call(sched, |sched| root(sched, ctx)),
116        options,
117    )
118}
119
120/// Android entry point. Mounts `root` with default options.
121#[cfg(target_os = "android")]
122pub fn run_android(
123    app: winit::platform::android::activity::AndroidApp,
124    mut root: impl FnMut(
125        &mut repose_core::runtime::Scheduler,
126        &repose_core::RenderContext,
127    ) -> repose_core::View
128    + 'static,
129) -> Result<()> {
130    let mut retained = RetainedRoot::new();
131    repose_platform::android::run_android_app_with_options(
132        app,
133        move |sched, ctx| retained.call(sched, |sched| root(sched, ctx)),
134        repose_app::AndroidOptions::default(),
135    )
136}
137
138/// Gamepad polling on the shared platform backend. Events feed
139/// `ReposeRuntime::handle_gamepad` (UI nav) and `rt.gamepads` (gameplay).
140pub struct GamepadPoller {
141    backend: Option<Box<dyn GamepadBackend>>,
142    connected: HashSet<u32>,
143}
144
145impl GamepadPoller {
146    pub fn new() -> Self {
147        Self {
148            backend: create_backend().map(|b| Box::new(b) as Box<dyn GamepadBackend>),
149            // Boot-plugged pads report on first poll; no press needed.
150            connected: HashSet::new(),
151        }
152    }
153
154    /// Drain hardware events since the last call. Tracks connection state
155    /// so `connected_ids` stays live.
156    pub fn poll(&mut self) -> Vec<GamepadEvent> {
157        let Some(backend) = &mut self.backend else {
158            return Vec::new();
159        };
160        let events = backend.poll();
161        for ev in &events {
162            match ev {
163                GamepadEvent::Connected { id, .. } => {
164                    self.connected.insert(id.0);
165                }
166                GamepadEvent::Disconnected { id } => {
167                    self.connected.remove(&id.0);
168                }
169                GamepadEvent::Button { id, .. } | GamepadEvent::Axis { id, .. } => {
170                    self.connected.insert(id.0);
171                }
172            }
173        }
174        events
175    }
176
177    pub fn connected_ids(&self) -> Vec<GamepadId> {
178        let mut ids: Vec<GamepadId> = self.connected.iter().copied().map(GamepadId).collect();
179        ids.sort_by_key(|id| id.0);
180        ids
181    }
182
183    pub fn connected_count(&self) -> usize {
184        self.connected.len()
185    }
186}
187
188impl Default for GamepadPoller {
189    fn default() -> Self {
190        Self::new()
191    }
192}