From 919775818424d048791b66b3c984444a159d2cfd Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Wed, 16 Sep 2026 22:58:48 +0200 Subject: [PATCH] 0.1.0-0-pre.11-fix.5 --- Cargo.toml | 4 +- .../engines/engine-v1-common/src/game_loop.rs | 10 +- crates/engines/engine-v1-common/src/lib.rs | 9 +- crates/engines/engine-v1-common/src/quit.rs | 42 +++++++++ .../engine-v1-common/unit_tests/game_loop.rs | 19 +++- crates/engines/engine-v1-sdl/src/runtime.rs | 91 +++++-------------- .../engines/engine-v1-sdl/unit_tests/swipe.rs | 28 ++++-- deltas/0.1.0/0-pre.11.fix.5.md | 46 ++++++++++ docs/architecture/004-INPUT_AND_CONTROLS.md | 22 ++--- 9 files changed, 181 insertions(+), 90 deletions(-) create mode 100644 crates/engines/engine-v1-common/src/quit.rs create mode 100644 deltas/0.1.0/0-pre.11.fix.5.md diff --git a/Cargo.toml b/Cargo.toml index 37b676d..1e9353e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ # file: Cargo.toml -# version: 25 +# version: 26 [workspace] resolver = "3" @@ -17,7 +17,7 @@ members = [ ] [workspace.package] -version = "0.1.0-0-pre.11.fix.4" +version = "0.1.0-0-pre.11.fix.5" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/games" diff --git a/crates/engines/engine-v1-common/src/game_loop.rs b/crates/engines/engine-v1-common/src/game_loop.rs index 469a8ae..6c86958 100644 --- a/crates/engines/engine-v1-common/src/game_loop.rs +++ b/crates/engines/engine-v1-common/src/game_loop.rs @@ -1,11 +1,19 @@ // file: crates/engines/engine-v1-common/src/game_loop.rs -// version: 3 +// version: 4 /// Minimal update contract implemented by a game state consumed by engine V1. pub trait EngineGame { /// Advances the game by one engine update using platform-independent input. fn update(&mut self, frame: crate::EngineFrame, input: crate::InputState); + /// Handles a platform-independent request to leave the current runtime. + /// + /// Games may mutate their state and return [`crate::QuitDecision::Continue`] to implement + /// pause/confirmation screens, navigation, score submission, saving, or other workflows. + fn quit_requested(&mut self, _request: crate::QuitRequest) -> crate::QuitDecision { + return crate::QuitDecision::Exit; + } + /// Produces the platform-independent scene rendered after the update. fn scene(&self) -> crate::EngineScene { return crate::EngineScene::empty(crate::RenderColor::rgb(18, 18, 24)); diff --git a/crates/engines/engine-v1-common/src/lib.rs b/crates/engines/engine-v1-common/src/lib.rs index 21671ab..ddd16cf 100644 --- a/crates/engines/engine-v1-common/src/lib.rs +++ b/crates/engines/engine-v1-common/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/engines/engine-v1-common/src/lib.rs -// version: 4 +// version: 5 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -12,6 +12,7 @@ mod frame; mod game_loop; mod input; mod pointer; +mod quit; mod render; /// Re-export of the canonical logical input action used by engine V1 games. @@ -26,6 +27,12 @@ pub use self::game_loop::FixedStepRunner; pub use self::input::InputState; /// Re-export of the normalized primary pointer snapshot. pub use self::pointer::PointerState; +/// Re-export of a game's response to a quit request. +pub use self::quit::QuitDecision; +/// Re-export of the platform-independent quit request. +pub use self::quit::QuitRequest; +/// Re-export of the platform-level source of a quit request. +pub use self::quit::QuitSource; /// Re-export of the maximum engine scene rectangle capacity. pub use self::render::ENGINE_SCENE_RECT_CAPACITY; /// Re-export of the platform-independent scene snapshot. diff --git a/crates/engines/engine-v1-common/src/quit.rs b/crates/engines/engine-v1-common/src/quit.rs new file mode 100644 index 0000000..f58eaaa --- /dev/null +++ b/crates/engines/engine-v1-common/src/quit.rs @@ -0,0 +1,42 @@ +// file: crates/engines/engine-v1-common/src/quit.rs +// version: 1 + +/// Origin of a platform-independent request to leave the current game runtime. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum QuitSource { + /// The operating-system or window manager requested the window to close. + WindowClose, + /// The Desktop Escape key requested leaving the runtime. + Escape, + /// The platform-native Back action requested leaving the runtime. + PlatformBack, +} + +/// Decision returned by a game when the runtime requests to leave. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum QuitDecision { + /// Leave the current runtime immediately. + Exit, + /// Keep the runtime active after the game handled the request. + Continue, +} + +/// Platform-independent quit request delivered to a game. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct QuitRequest { + source: QuitSource, +} + +impl QuitRequest { + /// Creates a quit request from the supplied source. + #[must_use] + pub const fn new(source: QuitSource) -> Self { + return Self { source }; + } + + /// Returns the physical or platform-level source of the request. + #[must_use] + pub const fn source(self) -> QuitSource { + return self.source; + } +} diff --git a/crates/engines/engine-v1-common/unit_tests/game_loop.rs b/crates/engines/engine-v1-common/unit_tests/game_loop.rs index e4d54be..8183eba 100644 --- a/crates/engines/engine-v1-common/unit_tests/game_loop.rs +++ b/crates/engines/engine-v1-common/unit_tests/game_loop.rs @@ -1,5 +1,5 @@ // file: crates/engines/engine-v1-common/unit_tests/game_loop.rs -// version: 1 +// version: 2 #[derive(Default)] struct CountingGame { @@ -28,3 +28,20 @@ fn fixed_step_runner_advances_index_and_elapsed_time() { assert_eq!(game.last_frame.map(crate::EngineFrame::index), std::option::Option::Some(1)); }); } + +#[test] +fn default_quit_policy_exits() { + game_logging_lib::with_test_tracing("default_quit_policy_exits", || { + let mut game = CountingGame::default(); + let request = crate::QuitRequest::new(crate::QuitSource::Escape); + assert_eq!(crate::EngineGame::quit_requested(&mut game, request), crate::QuitDecision::Exit); + }); +} + +#[test] +fn quit_request_preserves_its_source() { + game_logging_lib::with_test_tracing("quit_request_preserves_its_source", || { + let request = crate::QuitRequest::new(crate::QuitSource::PlatformBack); + assert_eq!(request.source(), crate::QuitSource::PlatformBack); + }); +} diff --git a/crates/engines/engine-v1-sdl/src/runtime.rs b/crates/engines/engine-v1-sdl/src/runtime.rs index 46f1c83..1904a00 100644 --- a/crates/engines/engine-v1-sdl/src/runtime.rs +++ b/crates/engines/engine-v1-sdl/src/runtime.rs @@ -1,5 +1,5 @@ // file: crates/engines/engine-v1-sdl/src/runtime.rs -// version: 10 +// version: 11 /// Minimal SDL3 runtime used by Desktop and Android POC runners. pub struct SdlRuntime { @@ -50,9 +50,22 @@ impl SdlRuntime { let mut input = engine_v1_common::InputState::none().with_pointer(pointer); for event in events.poll_iter() { match event { - sdl3::event::Event::Quit { .. } => break 'running, - sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::Escape), .. } => break 'running, - sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::AcBack), .. } => break 'running, + sdl3::event::Event::Quit { .. } => { + if quit_requested(game, engine_v1_common::QuitSource::WindowClose) { + break 'running; + } + }, + sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::Escape), .. } => { + if quit_requested(game, engine_v1_common::QuitSource::Escape) { + break 'running; + } + }, + sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::AcBack), .. } + | sdl3::event::Event::KeyDown { scancode: Some(sdl3::keyboard::Scancode::AcBack), .. } => { + if quit_requested(game, engine_v1_common::QuitSource::PlatformBack) { + break 'running; + } + }, sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::Left), repeat: false, .. } => { input = input.with_action(engine_v1_common::GameAction::Left, true); }, @@ -67,9 +80,6 @@ impl SdlRuntime { }, sdl3::event::Event::MouseButtonDown { mouse_btn: sdl3::mouse::MouseButton::Left, x, y, .. } => { pointer = normalize_mouse_pointer(&canvas, true, x, y); - if platform_quit_control_hit(pointer) { - break 'running; - } gesture_start = Some(pointer); input = input.with_pointer(pointer).with_action(engine_v1_common::GameAction::Primary, true); }, @@ -86,9 +96,6 @@ impl SdlRuntime { }, sdl3::event::Event::FingerDown { x, y, .. } => { pointer = engine_v1_common::PointerState::normalized(true, x, y); - if platform_quit_control_hit(pointer) { - break 'running; - } gesture_start = Some(pointer); input = input.with_pointer(pointer).with_action(engine_v1_common::GameAction::Primary, true); }, @@ -153,70 +160,18 @@ fn render_scene(canvas: &mut sdl3::render::WindowCanvas, scene: engine_v1_common std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), } } - match render_platform_controls(canvas, width, height) { - std::result::Result::Ok(()) => {}, - std::result::Result::Err(error) => return std::result::Result::Err(error), - } canvas.present(); return std::result::Result::Ok(()); } const SWIPE_DIRECTION_THRESHOLD: f32 = 0.04; -fn platform_quit_control_hit(pointer: engine_v1_common::PointerState) -> bool { - #[cfg(target_os = "android")] - { - return quit_control_hit(pointer); - } - #[cfg(not(target_os = "android"))] - { - let _ = pointer; - return false; - } -} - -fn quit_control_hit(pointer: engine_v1_common::PointerState) -> bool { - return pointer.x() >= 0.86 && pointer.x() <= 0.99 && pointer.y() >= 0.015 && pointer.y() <= 0.105; -} - -fn render_platform_controls(canvas: &mut sdl3::render::WindowCanvas, width: u32, height: u32) -> std::result::Result<(), std::string::String> { - #[cfg(target_os = "android")] - { - let button = sdl3::render::FRect::new(0.86 * width as f32, 0.015 * height as f32, 0.13 * width as f32, 0.09 * height as f32); - canvas.set_draw_color(sdl3::pixels::Color::RGBA(176, 48, 48, 232)); - match canvas.fill_rect(button) { - std::result::Result::Ok(()) => {}, - std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), - } - - canvas.set_draw_color(sdl3::pixels::Color::RGBA(255, 255, 255, 255)); - let marks = [ - (0.885_f32, 0.032_f32), - (0.905_f32, 0.044_f32), - (0.925_f32, 0.056_f32), - (0.945_f32, 0.068_f32), - (0.965_f32, 0.080_f32), - (0.965_f32, 0.032_f32), - (0.945_f32, 0.044_f32), - (0.925_f32, 0.056_f32), - (0.905_f32, 0.068_f32), - (0.885_f32, 0.080_f32), - ]; - for (x, y) in marks { - let mark = sdl3::render::FRect::new(x * width as f32, y * height as f32, 0.012 * width as f32, 0.012 * height as f32); - match canvas.fill_rect(mark) { - std::result::Result::Ok(()) => {}, - std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), - } - } - } - #[cfg(not(target_os = "android"))] - { - let _ = canvas; - let _ = width; - let _ = height; - } - return std::result::Result::Ok(()); +fn quit_requested(game: &mut G, source: engine_v1_common::QuitSource) -> bool +where + G: engine_v1_common::EngineGame, +{ + let request = engine_v1_common::QuitRequest::new(source); + return game.quit_requested(request) == engine_v1_common::QuitDecision::Exit; } fn apply_swipe_direction( diff --git a/crates/engines/engine-v1-sdl/unit_tests/swipe.rs b/crates/engines/engine-v1-sdl/unit_tests/swipe.rs index ad21463..befcf06 100644 --- a/crates/engines/engine-v1-sdl/unit_tests/swipe.rs +++ b/crates/engines/engine-v1-sdl/unit_tests/swipe.rs @@ -1,5 +1,5 @@ // file: crates/engines/engine-v1-sdl/unit_tests/swipe.rs -// version: 2 +// version: 3 fn pointer(x: f32, y: f32) -> engine_v1_common::PointerState { return engine_v1_common::PointerState::normalized(true, x, y); @@ -26,9 +26,25 @@ fn vertical_swipe_uses_dominant_axis() { assert!(input.is_active(engine_v1_common::GameAction::Up)); } -#[test] -fn quit_control_hit_is_limited_to_top_right_region() { - assert!(super::quit_control_hit(pointer(0.92, 0.05))); - assert!(!super::quit_control_hit(pointer(0.50, 0.05))); - assert!(!super::quit_control_hit(pointer(0.92, 0.50))); +#[derive(Default)] +struct QuitAwareGame { + requests: u64, +} + +impl engine_v1_common::EngineGame for QuitAwareGame { + fn update(&mut self, _frame: engine_v1_common::EngineFrame, _input: engine_v1_common::InputState) { + return; + } + + fn quit_requested(&mut self, _request: engine_v1_common::QuitRequest) -> engine_v1_common::QuitDecision { + self.requests = self.requests.saturating_add(1); + return engine_v1_common::QuitDecision::Continue; + } +} + +#[test] +fn runtime_respects_game_quit_decision() { + let mut game = QuitAwareGame::default(); + assert!(!super::quit_requested(&mut game, engine_v1_common::QuitSource::PlatformBack)); + assert_eq!(game.requests, 1); } diff --git a/deltas/0.1.0/0-pre.11.fix.5.md b/deltas/0.1.0/0-pre.11.fix.5.md new file mode 100644 index 0000000..20a3242 --- /dev/null +++ b/deltas/0.1.0/0-pre.11.fix.5.md @@ -0,0 +1,46 @@ + + + +# Delta 0.1.0-0-pre.11.fix.5 + +## Base + +Base complète Git : `0.1.0-0-pre.11.fix.4`. + +## Motifs + +`0-pre.11.fix.4` n'est pas validée : audit `RUST-FMT-101`, dead code Desktop et contrôle de sortie Android spécifique au renderer insuffisant. + +## Contrat de sortie moteur + +`engine-v1-common` introduit `QuitSource`, `QuitRequest` et `QuitDecision`. + +`EngineGame::quit_requested` retourne `Exit` par défaut. Un jeu peut retourner `Continue` après avoir adapté son état pour implémenter confirmation, pause, navigation, sauvegarde, Hall of Fame ou traitement préalable. + +## SDL3 + +Le runtime transmet fermeture de fenêtre, Escape et Back Android au jeu. Android conserve `SDL_ANDROID_TRAP_BACK_BUTTON=1` et reconnaît Back via `Keycode::AcBack` ou `Scancode::AcBack`. + +Le `X` SDL spécifique Android de `fix.4` est retiré. + +## Validation + +```bash +cargo fmt --all +cargo fmt --all -- --check +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates Android deltas history +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test -p engine-v1-common --all-targets --all-features +cargo test -p engine-v1-sdl --all-targets --all-features +cargo test -p game-snake-poc --all-targets --all-features +``` + +Smoke Desktop : `Escape -> QuitRequest(Escape) -> Exit`. + +Smoke Android AVD : `Back -> QuitRequest(PlatformBack) -> Exit`. + +Le Galaxy S9+ détecté en `arm64-v8a` peut servir de test physique après rebuild ARM64 et déploiement APK. + +Le POC WASM + Tauri reste volontairement reporté après cette stabilisation. diff --git a/docs/architecture/004-INPUT_AND_CONTROLS.md b/docs/architecture/004-INPUT_AND_CONTROLS.md index 438cb58..732f0fc 100644 --- a/docs/architecture/004-INPUT_AND_CONTROLS.md +++ b/docs/architecture/004-INPUT_AND_CONTROLS.md @@ -1,5 +1,5 @@ - + # Abstraction des entrées et contrôles @@ -97,18 +97,18 @@ Pour souris et tactile : Reflex conserve son impulsion `Primary` sur `Down` et reste donc indépendant de la reconnaissance du swipe. -## Sortie utilisateur +## Requête de sortie -Desktop : +La plateforme ne décide plus directement de terminer un jeu. Elle traduit l'événement physique en `QuitRequest` : -- `Escape` quitte proprement la boucle SDL ; -- la fermeture de fenêtre continue d'émettre `Quit`. +- fermeture de fenêtre → `QuitSource::WindowClose` ; +- `Escape` Desktop → `QuitSource::Escape` ; +- Back Android → `QuitSource::PlatformBack`. -Android : +`EngineGame::quit_requested` retourne un `QuitDecision`. Le comportement par défaut est `QuitDecision::Exit`. -- `SDL_ANDROID_TRAP_BACK_BUTTON=1` permet de traiter `AcBack` comme sortie propre ; -- un contrôle visuel de fermeture est rendu en haut à droite ; -- un tap/clic dans cette zone quitte la boucle avant d'alimenter le gameplay ; -- la boucle retourne ensuite normalement vers `SDL_main`. +Un jeu peut retourner `QuitDecision::Continue` après avoir modifié son état afin de mettre la partie en pause, demander confirmation, revenir à un écran précédent, préparer un Hall of Fame, sauvegarder ou commencer une action avant une sortie ultérieure. -Le gameplay ne dépend pas de cette logique de cycle de vie. +Sur Android, `SDL_ANDROID_TRAP_BACK_BUTTON=1` reste activé. Le runtime reconnaît le Back via `Keycode::AcBack` ou `Scancode::AcBack`. + +Le contrôle `X` SDL spécifique Android de `0-pre.11.fix.4` est supprimé : le contrôle système Back est la frontière plateforme de référence. Une future UI de confirmation ou navigation appartient au jeu ou à une couche UI moteur.