0.1.0-0-pre.8
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: Android/README.md -->
|
<!-- file: Android/README.md -->
|
||||||
<!-- version: 4 -->
|
<!-- version: 5 -->
|
||||||
|
|
||||||
# Android
|
# Android
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ Chaque module `game-*` est une application Android indépendante ; il dépend du
|
|||||||
|
|
||||||
## Toolchain de référence
|
## Toolchain de référence
|
||||||
|
|
||||||
Pour la baseline `0.1.0-0-pre.6` :
|
Pour la baseline `0.1.0-0-pre.8` :
|
||||||
|
|
||||||
- Android Gradle Plugin `9.4.0` ;
|
- Android Gradle Plugin `9.4.0` ;
|
||||||
- Gradle `9.6.0` attendu par AGP 9.4 ;
|
- Gradle `9.6.0` attendu par AGP 9.4 ;
|
||||||
@@ -70,3 +70,9 @@ python3 ../scripts/build_android_rust.py snake
|
|||||||
```
|
```
|
||||||
|
|
||||||
Le script extrait temporairement `libSDL3.so` de l'AAR uniquement pour fournir le chemin de linkage à `rustc`; l'AAR reste responsable du packaging SDL3 dans l'APK.
|
Le script extrait temporairement `libSDL3.so` de l'AAR uniquement pour fournir le chemin de linkage à `rustc`; l'AAR reste responsable du packaging SDL3 dans l'APK.
|
||||||
|
|
||||||
|
## Bridge JNI et tactile
|
||||||
|
|
||||||
|
À partir de `0.1.0-0-pre.8`, `SaseGameActivity` vérifie au démarrage la version du bridge Java/JNI chargé depuis la bibliothèque Rust.
|
||||||
|
|
||||||
|
Les touch events standards restent traités via SDL3 et ne transitent pas par JNI.
|
||||||
|
|||||||
@@ -1,19 +1,37 @@
|
|||||||
// file: Android/common/src/main/java/com/sasedev/games/common/NativeBridge.java
|
// file: Android/common/src/main/java/com/sasedev/games/common/NativeBridge.java
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
package com.sasedev.games.common;
|
package com.sasedev.games.common;
|
||||||
|
|
||||||
/** Stable Java side of the future Java/JNI platform bridge. */
|
/** Stable Java/JNI platform bridge contract shared by Android game applications. */
|
||||||
public final class NativeBridge {
|
public final class NativeBridge {
|
||||||
|
private static final int CONTRACT_VERSION = 1;
|
||||||
|
|
||||||
private NativeBridge() {
|
private NativeBridge() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the bridge contract version.
|
* Returns the Java-side bridge contract version.
|
||||||
*
|
*
|
||||||
* @return bridge contract version
|
* @return bridge contract version
|
||||||
*/
|
*/
|
||||||
public static int contractVersion() {
|
public static int contractVersion() {
|
||||||
return 1;
|
return CONTRACT_VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Rust-side bridge contract version.
|
||||||
|
*
|
||||||
|
* @return native bridge contract version
|
||||||
|
*/
|
||||||
|
public static native int nativeContractVersion();
|
||||||
|
|
||||||
|
/** Verifies that the loaded native bridge matches the Java contract. */
|
||||||
|
public static void requireCompatibleContract() {
|
||||||
|
final int nativeVersion = nativeContractVersion();
|
||||||
|
if (nativeVersion != CONTRACT_VERSION) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Native bridge contract mismatch: java=" + CONTRACT_VERSION + ", native=" + nativeVersion);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// file: Android/common/src/main/java/com/sasedev/games/common/SaseGameActivity.java
|
// file: Android/common/src/main/java/com/sasedev/games/common/SaseGameActivity.java
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
package com.sasedev.games.common;
|
package com.sasedev.games.common;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Common Android activity boundary for games.sasedev applications.
|
* Common Android activity boundary for games.sasedev applications.
|
||||||
*
|
*
|
||||||
@@ -10,6 +12,13 @@ package com.sasedev.games.common;
|
|||||||
*/
|
*/
|
||||||
public abstract class SaseGameActivity extends org.libsdl.app.SDLActivity {
|
public abstract class SaseGameActivity extends org.libsdl.app.SDLActivity {
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
NativeBridge.requireCompatibleContract();
|
||||||
|
}
|
||||||
|
|
||||||
/** {@inheritDoc} */
|
/** {@inheritDoc} */
|
||||||
@Override
|
@Override
|
||||||
protected final String[] getLibraries() {
|
protected final String[] getLibraries() {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: Android/game-reflex-poc/build.gradle
|
// file: Android/game-reflex-poc/build.gradle
|
||||||
// version: 5
|
// version: 6
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
@@ -16,7 +16,7 @@ android {
|
|||||||
minSdk 21
|
minSdk 21
|
||||||
targetSdk 36
|
targetSdk 36
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName '0.1.0-0-pre.7.fix.1'
|
versionName '0.1.0-0-pre.8'
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: Android/game-snake-poc/build.gradle
|
// file: Android/game-snake-poc/build.gradle
|
||||||
// version: 5
|
// version: 6
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
@@ -16,7 +16,7 @@ android {
|
|||||||
minSdk 21
|
minSdk 21
|
||||||
targetSdk 36
|
targetSdk 36
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName '0.1.0-0-pre.7.fix.1'
|
versionName '0.1.0-0-pre.8'
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 15
|
# version: 16
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
@@ -16,7 +16,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.0-0-pre.7.fix.1"
|
version = "0.1.0-0-pre.8"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/games"
|
repository = "https://git.sasedev.com/Sasedev/games"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/apps/game-android-entrypoint/src/lib.rs
|
// file: crates/apps/game-android-entrypoint/src/lib.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -19,6 +19,11 @@ extern "C" fn sdl_main(_argc: core::ffi::c_int, _argv: *mut *mut core::ffi::c_ch
|
|||||||
return run_selected_game();
|
return run_selected_game();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[unsafe(export_name = "Java_com_sasedev_games_common_NativeBridge_nativeContractVersion")]
|
||||||
|
extern "C" fn native_contract_version(_environment: *mut core::ffi::c_void, _class: *mut core::ffi::c_void) -> core::ffi::c_int {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "reflex")]
|
#[cfg(feature = "reflex")]
|
||||||
fn run_selected_game() -> core::ffi::c_int {
|
fn run_selected_game() -> core::ffi::c_int {
|
||||||
let runtime = engine_v1_sdl::SdlRuntime::new("Reflex POC", 405, 720, std::time::Duration::from_millis(16));
|
let runtime = engine_v1_sdl::SdlRuntime::new("Reflex POC", 405, 720, std::time::Duration::from_millis(16));
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
// file: crates/apps/game-android-entrypoint/unit_tests/selection.rs
|
// file: crates/apps/game-android-entrypoint/unit_tests/selection.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn no_feature_entrypoint_has_failure_status() {
|
fn no_feature_entrypoint_has_failure_status() {
|
||||||
#[cfg(not(any(feature = "reflex", feature = "snake")))]
|
#[cfg(not(any(feature = "reflex", feature = "snake")))]
|
||||||
assert_eq!(crate::run_selected_game(), 2);
|
assert_eq!(crate::run_selected_game(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn native_bridge_contract_version_is_one() {
|
||||||
|
assert_eq!(crate::native_contract_version(core::ptr::null_mut(), core::ptr::null_mut()), 1);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// file: crates/engines/engine-v1-common/src/input.rs
|
// file: crates/engines/engine-v1-common/src/input.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
/// Platform-independent snapshot of logical input actions for one update.
|
/// Platform-independent snapshot of logical input actions for one update.
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
pub struct InputState {
|
pub struct InputState {
|
||||||
left: bool,
|
left: bool,
|
||||||
right: bool,
|
right: bool,
|
||||||
@@ -11,13 +11,23 @@ pub struct InputState {
|
|||||||
primary: bool,
|
primary: bool,
|
||||||
secondary: bool,
|
secondary: bool,
|
||||||
pause: bool,
|
pause: bool,
|
||||||
|
pointer: crate::PointerState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputState {
|
impl InputState {
|
||||||
/// Creates a snapshot with no active action.
|
/// Creates a snapshot with no active action.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn none() -> Self {
|
pub const fn none() -> Self {
|
||||||
return Self { left: false, right: false, up: false, down: false, primary: false, secondary: false, pause: false };
|
return Self {
|
||||||
|
left: false,
|
||||||
|
right: false,
|
||||||
|
up: false,
|
||||||
|
down: false,
|
||||||
|
primary: false,
|
||||||
|
secondary: false,
|
||||||
|
pause: false,
|
||||||
|
pointer: crate::PointerState::inactive(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a copy with the selected logical action set to the requested state.
|
/// Returns a copy with the selected logical action set to the requested state.
|
||||||
@@ -35,6 +45,13 @@ impl InputState {
|
|||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a copy carrying the selected normalized primary pointer state.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn with_pointer(mut self, pointer: crate::PointerState) -> Self {
|
||||||
|
self.pointer = pointer;
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
/// Reports whether the selected logical action is active.
|
/// Reports whether the selected logical action is active.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn is_active(self, action: crate::GameAction) -> bool {
|
pub const fn is_active(self, action: crate::GameAction) -> bool {
|
||||||
@@ -48,6 +65,12 @@ impl InputState {
|
|||||||
crate::GameAction::Pause => self.pause,
|
crate::GameAction::Pause => self.pause,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the primary pointer snapshot.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn pointer(self) -> crate::PointerState {
|
||||||
|
return self.pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/engines/engine-v1-common/src/lib.rs
|
// file: crates/engines/engine-v1-common/src/lib.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -11,6 +11,7 @@ mod action;
|
|||||||
mod frame;
|
mod frame;
|
||||||
mod game_loop;
|
mod game_loop;
|
||||||
mod input;
|
mod input;
|
||||||
|
mod pointer;
|
||||||
|
|
||||||
/// Re-export of the canonical logical input action used by engine V1 games.
|
/// Re-export of the canonical logical input action used by engine V1 games.
|
||||||
pub use self::action::GameAction;
|
pub use self::action::GameAction;
|
||||||
@@ -22,3 +23,5 @@ pub use self::game_loop::EngineGame;
|
|||||||
pub use self::game_loop::FixedStepRunner;
|
pub use self::game_loop::FixedStepRunner;
|
||||||
/// Re-export of the platform-independent logical input snapshot.
|
/// Re-export of the platform-independent logical input snapshot.
|
||||||
pub use self::input::InputState;
|
pub use self::input::InputState;
|
||||||
|
/// Re-export of the normalized primary pointer snapshot.
|
||||||
|
pub use self::pointer::PointerState;
|
||||||
|
|||||||
52
crates/engines/engine-v1-common/src/pointer.rs
Normal file
52
crates/engines/engine-v1-common/src/pointer.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// file: crates/engines/engine-v1-common/src/pointer.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
/// Platform-independent normalized primary pointer state for one engine update.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct PointerState {
|
||||||
|
active: bool,
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PointerState {
|
||||||
|
/// Creates an inactive pointer at the normalized origin.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn inactive() -> Self {
|
||||||
|
return Self { active: false, x: 0.0, y: 0.0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a pointer state from normalized coordinates.
|
||||||
|
#[must_use]
|
||||||
|
pub fn normalized(active: bool, x: f32, y: f32) -> Self {
|
||||||
|
return Self { active, x: x.clamp(0.0, 1.0), y: y.clamp(0.0, 1.0) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reports whether the pointer is currently active.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn active(self) -> bool {
|
||||||
|
return self.active;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the normalized horizontal coordinate.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn x(self) -> f32 {
|
||||||
|
return self.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the normalized vertical coordinate.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn y(self) -> f32 {
|
||||||
|
return self.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PointerState {
|
||||||
|
fn default() -> Self {
|
||||||
|
return Self::inactive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/pointer.rs"]
|
||||||
|
mod tests;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/engines/engine-v1-common/unit_tests/input.rs
|
// file: crates/engines/engine-v1-common/unit_tests/input.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn action_state_is_independent() {
|
fn action_state_is_independent() {
|
||||||
@@ -10,3 +10,11 @@ fn action_state_is_independent() {
|
|||||||
assert!(!input.is_active(crate::GameAction::Left));
|
assert!(!input.is_active(crate::GameAction::Left));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pointer_state_is_carried_independently_from_actions() {
|
||||||
|
let pointer = crate::PointerState::normalized(true, 0.25, 0.75);
|
||||||
|
let input = crate::InputState::none().with_pointer(pointer);
|
||||||
|
assert_eq!(input.pointer(), pointer);
|
||||||
|
assert!(!input.is_active(crate::GameAction::Primary));
|
||||||
|
}
|
||||||
|
|||||||
10
crates/engines/engine-v1-common/unit_tests/pointer.rs
Normal file
10
crates/engines/engine-v1-common/unit_tests/pointer.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// file: crates/engines/engine-v1-common/unit_tests/pointer.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalized_pointer_clamps_coordinates() {
|
||||||
|
let pointer = crate::PointerState::normalized(true, -0.5, 1.5);
|
||||||
|
assert!(pointer.active());
|
||||||
|
assert_eq!(pointer.x(), 0.0);
|
||||||
|
assert_eq!(pointer.y(), 1.0);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/engines/engine-v1-sdl/src/runtime.rs
|
// file: crates/engines/engine-v1-sdl/src/runtime.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
/// Minimal SDL3 runtime used by Desktop POC runners.
|
/// Minimal SDL3 runtime used by Desktop POC runners.
|
||||||
pub struct SdlRuntime {
|
pub struct SdlRuntime {
|
||||||
@@ -39,9 +39,10 @@ impl SdlRuntime {
|
|||||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||||
};
|
};
|
||||||
let mut runner = engine_v1_common::FixedStepRunner::new(self.frame_duration);
|
let mut runner = engine_v1_common::FixedStepRunner::new(self.frame_duration);
|
||||||
|
let mut pointer = engine_v1_common::PointerState::inactive();
|
||||||
tracing::info!(title = %self.title, width = self.width, height = self.height, "SDL3 runtime started");
|
tracing::info!(title = %self.title, width = self.width, height = self.height, "SDL3 runtime started");
|
||||||
'running: loop {
|
'running: loop {
|
||||||
let mut input = engine_v1_common::InputState::none();
|
let mut input = engine_v1_common::InputState::none().with_pointer(pointer);
|
||||||
for event in events.poll_iter() {
|
for event in events.poll_iter() {
|
||||||
match event {
|
match event {
|
||||||
sdl3::event::Event::Quit { .. } => break 'running,
|
sdl3::event::Event::Quit { .. } => break 'running,
|
||||||
@@ -49,9 +50,20 @@ impl SdlRuntime {
|
|||||||
sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::Space), .. } => {
|
sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::Space), .. } => {
|
||||||
input = input.with_action(engine_v1_common::GameAction::Primary, true);
|
input = input.with_action(engine_v1_common::GameAction::Primary, true);
|
||||||
},
|
},
|
||||||
|
sdl3::event::Event::FingerDown { x, y, .. } | sdl3::event::Event::FingerMotion { x, y, .. } => {
|
||||||
|
pointer = engine_v1_common::PointerState::normalized(true, x, y);
|
||||||
|
input = input.with_pointer(pointer).with_action(engine_v1_common::GameAction::Primary, true);
|
||||||
|
},
|
||||||
|
sdl3::event::Event::FingerUp { x, y, .. } | sdl3::event::Event::FingerCanceled { x, y, .. } => {
|
||||||
|
pointer = engine_v1_common::PointerState::normalized(false, x, y);
|
||||||
|
input = input.with_pointer(pointer);
|
||||||
|
},
|
||||||
_ => {},
|
_ => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if pointer.active() {
|
||||||
|
input = input.with_pointer(pointer).with_action(engine_v1_common::GameAction::Primary, true);
|
||||||
|
}
|
||||||
runner.tick(game, input);
|
runner.tick(game, input);
|
||||||
canvas.set_draw_color(sdl3::pixels::Color::RGB(18, 18, 24));
|
canvas.set_draw_color(sdl3::pixels::Color::RGB(18, 18, 24));
|
||||||
canvas.clear();
|
canvas.clear();
|
||||||
|
|||||||
128
deltas/0.1.0/0-pre.8.md
Normal file
128
deltas/0.1.0/0-pre.8.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<!-- file: deltas/0.1.0/0-pre.8.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta 0.1.0-0-pre.8
|
||||||
|
|
||||||
|
## Base
|
||||||
|
|
||||||
|
Base validée : `0.1.0-0-pre.7.fix.1`.
|
||||||
|
|
||||||
|
## Validation du delta précédent
|
||||||
|
|
||||||
|
`0.1.0-0-pre.7.fix.1` a été validée le 2026-09-16.
|
||||||
|
|
||||||
|
Les gates légères, les deux builds Rust Android, la présence des bibliothèques natives et les deux builds APK sont propres.
|
||||||
|
|
||||||
|
`adb` est installé et fonctionnel, mais aucun appareil ou émulateur n'était attaché. Le smoke runtime Android n'était donc pas requis pour accepter ce jalon.
|
||||||
|
|
||||||
|
## Objectifs
|
||||||
|
|
||||||
|
- introduire un input tactile portable dans le moteur ;
|
||||||
|
- utiliser les événements SDL3 Android plutôt qu'un bridge JNI pour les touch events ;
|
||||||
|
- rendre le bridge Java/JNI minimal réellement vérifiable ;
|
||||||
|
- conserver une exception FFI Rust strictement bornée.
|
||||||
|
|
||||||
|
## Input tactile
|
||||||
|
|
||||||
|
`engine-v1-common` introduit `PointerState` :
|
||||||
|
|
||||||
|
- état actif/inactif ;
|
||||||
|
- coordonnées normalisées `x` et `y` ;
|
||||||
|
- bornage dans `[0.0, 1.0]`.
|
||||||
|
|
||||||
|
`InputState` transporte désormais ce pointeur.
|
||||||
|
|
||||||
|
`engine-v1-sdl` maintient l'état tactile entre les frames :
|
||||||
|
|
||||||
|
- `FingerDown` et `FingerMotion` activent/mettent à jour le pointeur ;
|
||||||
|
- `FingerUp` et `FingerCanceled` le désactivent ;
|
||||||
|
- un pointeur actif active également `GameAction::Primary` dans cette baseline.
|
||||||
|
|
||||||
|
## Bridge Java/JNI
|
||||||
|
|
||||||
|
`NativeBridge` expose :
|
||||||
|
|
||||||
|
```java
|
||||||
|
public static native int nativeContractVersion();
|
||||||
|
```
|
||||||
|
|
||||||
|
La bibliothèque Rust exporte le symbole JNI correspondant et retourne `1`.
|
||||||
|
|
||||||
|
`SaseGameActivity.onCreate()` appelle `NativeBridge.requireCompatibleContract()` après l'initialisation SDL.
|
||||||
|
|
||||||
|
Le bridge est ainsi réellement vérifié au premier lancement Android disponible.
|
||||||
|
|
||||||
|
## Exception FFI
|
||||||
|
|
||||||
|
L'exception Rust du crate `game-android-entrypoint` autorise désormais exactement deux exports :
|
||||||
|
|
||||||
|
- `SDL_main` ;
|
||||||
|
- `Java_com_sasedev_games_common_NativeBridge_nativeContractVersion`.
|
||||||
|
|
||||||
|
Les blocs `unsafe`, fonctions `unsafe`, déréférencements de pointeurs bruts et autres attributs unsafe restent interdits.
|
||||||
|
|
||||||
|
## Validation Rust à exécuter
|
||||||
|
|
||||||
|
Le delta modifie du Rust :
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests ciblés :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test -p engine-v1-common --all-targets --all-features
|
||||||
|
cargo test -p engine-v1-sdl --all-targets --all-features
|
||||||
|
cargo test -p game-android-entrypoint --no-default-features
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation Android
|
||||||
|
|
||||||
|
Reconstruire les bibliothèques natives :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/build_android_rust.py reflex
|
||||||
|
python3 scripts/build_android_rust.py snake
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis les APK :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Android
|
||||||
|
gradle :game-reflex-poc:assembleDebug
|
||||||
|
gradle :game-snake-poc:assembleDebug
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
## Smoke appareil
|
||||||
|
|
||||||
|
Si un appareil ou émulateur est attaché :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb devices
|
||||||
|
```
|
||||||
|
|
||||||
|
installer puis lancer les deux applications.
|
||||||
|
|
||||||
|
Critères supplémentaires de ce delta :
|
||||||
|
|
||||||
|
- aucun `UnsatisfiedLinkError` sur `NativeBridge.nativeContractVersion` ;
|
||||||
|
- aucune erreur de contrat JNI ;
|
||||||
|
- un contact tactile ne provoque aucun crash ;
|
||||||
|
- SDL3 continue à recevoir les événements tactiles.
|
||||||
|
|
||||||
|
Si aucun device n'est disponible, les builds peuvent valider le delta mais le smoke runtime reste explicitement en attente dans l'historique.
|
||||||
|
|
||||||
|
## Règle de transition
|
||||||
|
|
||||||
|
Si les gates Rust et Android sont propres, passer automatiquement à `0.1.0-0-pre.9`.
|
||||||
|
|
||||||
|
En cas d'échec imputable au projet, produire `0.1.0-0-pre.8.fix.1`.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/000-README.md -->
|
<!-- file: docs/000-README.md -->
|
||||||
<!-- version: 9 -->
|
<!-- version: 10 -->
|
||||||
|
|
||||||
# Documentation games.sasedev
|
# Documentation games.sasedev
|
||||||
|
|
||||||
@@ -51,6 +51,7 @@ Voir [`../RULES.md`](../RULES.md), notamment [`rules/RULES_COMMANDS.md`](rules/R
|
|||||||
- [`development/004-SDL3_DESKTOP_PREREQUISITES.md`](development/004-SDL3_DESKTOP_PREREQUISITES.md) — prérequis SDL3 Desktop, stratégie de liaison système et vérification `pkg-config`.
|
- [`development/004-SDL3_DESKTOP_PREREQUISITES.md`](development/004-SDL3_DESKTOP_PREREQUISITES.md) — prérequis SDL3 Desktop, stratégie de liaison système et vérification `pkg-config`.
|
||||||
- [`development/005-DESKTOP_WINDOW_POLICY.md`](development/005-DESKTOP_WINDOW_POLICY.md) — taille initiale, redimensionnement et séparation future entre fenêtre physique et résolution virtuelle.
|
- [`development/005-DESKTOP_WINDOW_POLICY.md`](development/005-DESKTOP_WINDOW_POLICY.md) — taille initiale, redimensionnement et séparation future entre fenêtre physique et résolution virtuelle.
|
||||||
- [`development/006-ANDROID_RUST_NATIVE_BUILD.md`](development/006-ANDROID_RUST_NATIVE_BUILD.md) — build `cdylib` Rust Android, cargo-ndk, packaging `jniLibs` et symbole `SDL_main`.
|
- [`development/006-ANDROID_RUST_NATIVE_BUILD.md`](development/006-ANDROID_RUST_NATIVE_BUILD.md) — build `cdylib` Rust Android, cargo-ndk, packaging `jniLibs` et symbole `SDL_main`.
|
||||||
|
- [`development/007-ANDROID_JNI_BRIDGE.md`](development/007-ANDROID_JNI_BRIDGE.md) — frontière Java/JNI minimale, version de contrat et séparation avec l'input SDL3.
|
||||||
|
|
||||||
## Historique validé
|
## Historique validé
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/architecture/004-INPUT_AND_CONTROLS.md -->
|
<!-- file: docs/architecture/004-INPUT_AND_CONTROLS.md -->
|
||||||
<!-- version: 1 -->
|
<!-- version: 2 -->
|
||||||
|
|
||||||
# Abstraction des entrées et contrôles
|
# Abstraction des entrées et contrôles
|
||||||
|
|
||||||
@@ -53,3 +53,17 @@ Cette structure n'est pas imposée comme API définitive à la baseline ; elle i
|
|||||||
## Orientation d'écran
|
## Orientation d'écran
|
||||||
|
|
||||||
Portrait convient particulièrement aux jeux one-button, merge, puzzle, idle, stacking et climber. Paysage convient mieux aux shooters, runners latéraux, survivor-like, tower defense et jeux utilisant deux zones de contrôle.
|
Portrait convient particulièrement aux jeux one-button, merge, puzzle, idle, stacking et climber. Paysage convient mieux aux shooters, runners latéraux, survivor-like, tower defense et jeux utilisant deux zones de contrôle.
|
||||||
|
|
||||||
|
## Pointeur primaire normalisé
|
||||||
|
|
||||||
|
Le moteur V1 expose un `PointerState` indépendant de la plateforme :
|
||||||
|
|
||||||
|
- `active` indique qu'un pointeur primaire est maintenu ;
|
||||||
|
- `x` et `y` sont normalisés dans `[0.0, 1.0]` ;
|
||||||
|
- la plateforme borne les coordonnées avant de les transmettre au gameplay.
|
||||||
|
|
||||||
|
SDL3 fournit cette abstraction sur Android avec les événements `FingerDown`, `FingerMotion`, `FingerUp` et `FingerCanceled`.
|
||||||
|
|
||||||
|
La baseline `0.1.0-0-pre.8` mappe également un doigt actif vers `GameAction::Primary`. Cette association est volontairement minimale et pourra être remplacée par des zones tactiles ou boutons virtuels par jeu.
|
||||||
|
|
||||||
|
Le gameplay ne dépend d'aucun type Android, Java ou SDL3.
|
||||||
|
|||||||
47
docs/development/007-ANDROID_JNI_BRIDGE.md
Normal file
47
docs/development/007-ANDROID_JNI_BRIDGE.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<!-- file: docs/development/007-ANDROID_JNI_BRIDGE.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Bridge Java/JNI Android
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Le bridge Java/JNI sert uniquement aux services réellement Android qui ne sont pas déjà fournis de manière portable par SDL3.
|
||||||
|
|
||||||
|
L'input tactile de base ne passe pas par JNI : SDL3 fournit directement les événements de doigt et le moteur les convertit en `PointerState`.
|
||||||
|
|
||||||
|
## Contrat V1
|
||||||
|
|
||||||
|
La baseline expose un unique appel Java vers Rust :
|
||||||
|
|
||||||
|
```text
|
||||||
|
NativeBridge.nativeContractVersion() -> 1
|
||||||
|
```
|
||||||
|
|
||||||
|
`SaseGameActivity` vérifie ce contrat après `SDLActivity.onCreate()`.
|
||||||
|
|
||||||
|
Cette vérification détecte au démarrage :
|
||||||
|
|
||||||
|
- bibliothèque Rust non chargée ;
|
||||||
|
- symbole JNI absent ;
|
||||||
|
- divergence de version de bridge.
|
||||||
|
|
||||||
|
## Extension future
|
||||||
|
|
||||||
|
Les futurs services pourront ajouter des opérations dédiées pour :
|
||||||
|
|
||||||
|
- haptique Android ;
|
||||||
|
- partage natif ;
|
||||||
|
- facturation ;
|
||||||
|
- publicité ;
|
||||||
|
- notifications ;
|
||||||
|
- autres services dépendants du framework Android.
|
||||||
|
|
||||||
|
Ils ne doivent pas servir à réimplémenter les entrées, fenêtres ou événements déjà couverts par SDL3.
|
||||||
|
|
||||||
|
## Sécurité Rust
|
||||||
|
|
||||||
|
L'export JNI utilise un symbole nommé explicitement et reçoit deux pointeurs opaques JNI qui ne sont pas déréférencés.
|
||||||
|
|
||||||
|
Aucun bloc `unsafe` ni fonction `unsafe` n'est autorisé.
|
||||||
|
|
||||||
|
Si un futur service nécessite de manipuler réellement `JNIEnv`, il devra faire l'objet d'une tranche distincte avec une politique FFI réévaluée plutôt que d'élargir implicitement l'exception actuelle.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/rules/RULES_RUST.md -->
|
<!-- file: docs/rules/RULES_RUST.md -->
|
||||||
<!-- version: 4 -->
|
<!-- version: 5 -->
|
||||||
|
|
||||||
# Règles Rust générales
|
# Règles Rust générales
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
- **RUST-BASE-001** — L'édition Rust est Rust 2024.
|
- **RUST-BASE-001** — L'édition Rust est Rust 2024.
|
||||||
- **RUST-BASE-002** — Chaque `lib.rs` et `main.rs` active `missing_docs`, `unreachable_pub` et interdit `unsafe_code`, sauf exception FFI explicitement définie par cette règle.
|
- **RUST-BASE-002** — Chaque `lib.rs` et `main.rs` active `missing_docs`, `unreachable_pub` et interdit `unsafe_code`, sauf exception FFI explicitement définie par cette règle.
|
||||||
- **RUST-BASE-003** — Les lints communs sont déclarés au workspace et hérités par les crates ; une crate FFI exceptée peut déclarer localement le même profil avec uniquement le niveau `unsafe_code` ajusté.
|
- **RUST-BASE-003** — Les lints communs sont déclarés au workspace et hérités par les crates ; une crate FFI exceptée peut déclarer localement le même profil avec uniquement le niveau `unsafe_code` ajusté.
|
||||||
- **RUST-BASE-004** — Les blocs `unsafe` et fonctions `unsafe` sont interdits. L’unique exception actuelle est `crates/apps/game-android-entrypoint/src/lib.rs`, autorisé à porter exactement un attribut `#[unsafe(export_name = "SDL_main")]` afin d’exposer le symbole exigé par SDL Android. Cette exception n’autorise aucun déréférencement de pointeur brut ni autre attribut unsafe.
|
- **RUST-BASE-004** — Les blocs `unsafe` et fonctions `unsafe` sont interdits. L’unique exception actuelle est `crates/apps/game-android-entrypoint/src/lib.rs`, autorisé à porter uniquement les attributs `#[unsafe(export_name = "SDL_main")]` et `#[unsafe(export_name = "Java_com_sasedev_games_common_NativeBridge_nativeContractVersion")]` nécessaires aux frontières SDL Android et JNI. Cette exception n’autorise aucun déréférencement de pointeur brut, aucun autre attribut unsafe, aucun bloc `unsafe` et aucune fonction `unsafe`.
|
||||||
- **RUST-BASE-005** — Tout fichier Rust possède les en-têtes `// file: ...` et `// version: N`.
|
- **RUST-BASE-005** — Tout fichier Rust possède les en-têtes `// file: ...` et `// version: N`.
|
||||||
- **RUST-DEP-001** — Une dépendance tierce partagée déclare uniquement sa contrainte de version canonique sous `[workspace.dependencies]`, sauf exception normative explicitement documentée.
|
- **RUST-DEP-001** — Une dépendance tierce partagée déclare uniquement sa contrainte de version canonique sous `[workspace.dependencies]`, sauf exception normative explicitement documentée.
|
||||||
- **RUST-DEP-002** — Les features d'une dépendance tierce sont activées dans le `Cargo.toml` de la crate qui en a réellement besoin, via `workspace = true`; elles ne sont pas activées globalement au workspace par commodité.
|
- **RUST-DEP-002** — Les features d'une dépendance tierce sont activées dans le `Cargo.toml` de la crate qui en a réellement besoin, via `workspace = true`; elles ne sont pas activées globalement au workspace par commodité.
|
||||||
|
|||||||
32
history/0.1.0/0-pre.7.fix.1.md
Normal file
32
history/0.1.0/0-pre.7.fix.1.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<!-- file: history/0.1.0/0-pre.7.fix.1.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Historique 0.1.0-0-pre.7.fix.1
|
||||||
|
|
||||||
|
## Statut
|
||||||
|
|
||||||
|
Validé par l'utilisateur le 2026-09-16.
|
||||||
|
|
||||||
|
## Gates validées
|
||||||
|
|
||||||
|
- `cargo fmt --all -- --check` ;
|
||||||
|
- audits Python du workspace et des tableaux Markdown ;
|
||||||
|
- `cargo check --workspace` ;
|
||||||
|
- build Rust Android Reflex ;
|
||||||
|
- build Rust Android Snake ;
|
||||||
|
- présence de `libSDL3.so` et `libgame_android_entrypoint.so` pour les deux applications ;
|
||||||
|
- assemblage Gradle Reflex ;
|
||||||
|
- assemblage Gradle Snake.
|
||||||
|
|
||||||
|
## Toolchain observée
|
||||||
|
|
||||||
|
- `cargo-ndk 4.1.2` ;
|
||||||
|
- target Rust `aarch64-linux-android` ;
|
||||||
|
- NDK r28c installé ensuite ;
|
||||||
|
- `adb 1.0.41 / platform-tools 37.0.1`.
|
||||||
|
|
||||||
|
Aucun appareil ou émulateur n'était attaché lors de la validation ; le smoke runtime Android reste donc à exécuter lorsqu'un device sera disponible.
|
||||||
|
|
||||||
|
## Portée durable
|
||||||
|
|
||||||
|
Ce jalon valide la production du `cdylib` Rust Android, le packaging de SDL3 et de la bibliothèque applicative dans les APK et la chaîne de build Android native.
|
||||||
@@ -520,11 +520,18 @@ def audit_file(root: pathlib.Path, path: pathlib.Path) -> list[Violation]:
|
|||||||
if attribute not in lines[:24]:
|
if attribute not in lines[:24]:
|
||||||
violations.append(Violation("RUST-BASE-103", relative, 1, f"missing `{attribute}`"))
|
violations.append(Violation("RUST-BASE-103", relative, 1, f"missing `{attribute}`"))
|
||||||
if ffi_export_exception:
|
if ffi_export_exception:
|
||||||
export_attribute = '#[unsafe(export_name = "SDL_main")]'
|
required_export_attributes = (
|
||||||
|
'#[unsafe(export_name = "Java_com_sasedev_games_common_NativeBridge_nativeContractVersion")]',
|
||||||
|
'#[unsafe(export_name = "SDL_main")]',
|
||||||
|
)
|
||||||
|
for export_attribute in required_export_attributes:
|
||||||
if text.count(export_attribute) != 1:
|
if text.count(export_attribute) != 1:
|
||||||
violations.append(Violation("RUST-FFI-101", relative, 1, "Android entrypoint must contain exactly one SDL_main export attribute"))
|
violations.append(Violation("RUST-FFI-101", relative, 1, f"Android entrypoint must contain exactly one required export attribute: {export_attribute}"))
|
||||||
|
unsafe_export_count = text.count("#[unsafe(export_name = ")
|
||||||
|
if unsafe_export_count != len(required_export_attributes):
|
||||||
|
violations.append(Violation("RUST-FFI-103", relative, 1, "Android entrypoint contains an unexpected unsafe export attribute"))
|
||||||
if "unsafe {" in text or re.search(r"\bunsafe\s+fn\b", text) is not None or re.search(r"\bunsafe\s+extern\b", text) is not None:
|
if "unsafe {" in text or re.search(r"\bunsafe\s+fn\b", text) is not None or re.search(r"\bunsafe\s+extern\b", text) is not None:
|
||||||
violations.append(Violation("RUST-FFI-102", relative, 1, "Android entrypoint exception permits an unsafe export attribute only; unsafe blocks/functions remain forbidden"))
|
violations.append(Violation("RUST-FFI-102", relative, 1, "Android entrypoint exception permits named unsafe export attributes only; unsafe blocks/functions remain forbidden"))
|
||||||
exports: list[tuple[int, str]] = []
|
exports: list[tuple[int, str]] = []
|
||||||
for idx, line in enumerate(lines, 1):
|
for idx, line in enumerate(lines, 1):
|
||||||
stripped = line.strip()
|
stripped = line.strip()
|
||||||
|
|||||||
Reference in New Issue
Block a user